Compare commits
39 Commits
v4.0.0-bet
...
v4.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c3f5431ba | ||
|
|
d98cf16a4c | ||
|
|
2c3c3ae546 | ||
|
|
905eef48e3 | ||
|
|
b31b520c7c | ||
|
|
17aee086a3 | ||
|
|
c1756e5767 | ||
|
|
2920279c64 | ||
|
|
1f0f985b01 | ||
|
|
0762c81633 | ||
|
|
28ef301ccc | ||
|
|
26c6a2950f | ||
|
|
5082876de3 | ||
|
|
e50e7ad3d5 | ||
|
|
45a4a6b6da | ||
|
|
02918b7267 | ||
|
|
6c662a36c1 | ||
|
|
b78fe3822a | ||
|
|
35eda37e83 | ||
|
|
176a8e7067 | ||
|
|
61d4f1fd4b | ||
|
|
121b68995e | ||
|
|
d11f1d8dae | ||
|
|
c0ef2b5064 | ||
|
|
2a7308363e | ||
|
|
dc0c556f96 | ||
|
|
ba2ee1c0aa | ||
|
|
0f8b550d68 | ||
|
|
ed1fc98821 | ||
|
|
fa53b468fd | ||
|
|
4e2533d320 | ||
|
|
388ae49e55 | ||
|
|
f3f347dcba | ||
|
|
655be3519c | ||
|
|
06df2940af | ||
|
|
4149549e42 | ||
|
|
da351991f8 | ||
|
|
3305152e50 | ||
|
|
bea7bae674 |
31
.github/workflows/docker-image.yml
vendored
31
.github/workflows/docker-image.yml
vendored
@@ -27,6 +27,33 @@ jobs:
|
|||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: git checkout ${{ steps.get-latest-tag.outputs.latest_tag }}
|
run: git checkout ${{ steps.get-latest-tag.outputs.latest_tag }}
|
||||||
|
|
||||||
|
- name: Check if version is pre-release
|
||||||
|
id: check-prerelease
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
version="${{ steps.get-latest-tag.outputs.latest_tag }}"
|
||||||
|
else
|
||||||
|
version="${{ github.ref_name }}"
|
||||||
|
fi
|
||||||
|
if [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]]; then
|
||||||
|
echo "is_prerelease=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version $version is a pre-release, will not push latest tag"
|
||||||
|
else
|
||||||
|
echo "is_prerelease=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version $version is a stable release, will push latest tag"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build Dashboard
|
||||||
|
run: |
|
||||||
|
cd dashboard
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
mkdir -p dist/assets
|
||||||
|
echo $(git rev-parse HEAD) > dist/assets/version
|
||||||
|
cd ..
|
||||||
|
mkdir -p data
|
||||||
|
cp -r dashboard/dist data/
|
||||||
|
|
||||||
- name: Set QEMU
|
- name: Set QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
@@ -53,9 +80,9 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
|
${{ steps.check-prerelease.outputs.is_prerelease == 'false' && format('{0}/astrbot:latest', secrets.DOCKER_HUB_USERNAME) || '' }}
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
|
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
|
||||||
ghcr.io/soulter/astrbot:latest
|
${{ steps.check-prerelease.outputs.is_prerelease == 'false' && 'ghcr.io/soulter/astrbot:latest' || '' }}
|
||||||
ghcr.io/soulter/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
|
ghcr.io/soulter/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
|
||||||
|
|
||||||
- name: Post build notifications
|
- name: Post build notifications
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -6,8 +6,6 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
_✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
|
|
||||||
|
|
||||||
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://github.com/Soulter/AstrBot/releases/latest)
|
[](https://github.com/Soulter/AstrBot/releases/latest)
|
||||||
@@ -27,7 +25,7 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
|
|||||||
|
|
||||||
AstrBot 是一个开源的一站式 Agentic 聊天机器人平台及开发框架。
|
AstrBot 是一个开源的一站式 Agentic 聊天机器人平台及开发框架。
|
||||||
|
|
||||||
## ✨ 主要功能
|
## 主要功能
|
||||||
|
|
||||||
1. **大模型对话**。支持接入多种大模型服务。支持多模态、工具调用、MCP、原生知识库、人设等功能。
|
1. **大模型对话**。支持接入多种大模型服务。支持多模态、工具调用、MCP、原生知识库、人设等功能。
|
||||||
2. **多消息平台支持**。支持接入 QQ、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK 等平台。支持速率限制、白名单、百度内容审核。
|
2. **多消息平台支持**。支持接入 QQ、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK 等平台。支持速率限制、白名单、百度内容审核。
|
||||||
@@ -35,7 +33,7 @@ AstrBot 是一个开源的一站式 Agentic 聊天机器人平台及开发框架
|
|||||||
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,社区插件生态丰富。
|
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,社区插件生态丰富。
|
||||||
5. **WebUI**。可视化配置和管理机器人,功能齐全。
|
5. **WebUI**。可视化配置和管理机器人,功能齐全。
|
||||||
|
|
||||||
## ✨ 使用方式
|
## 部署方式
|
||||||
|
|
||||||
#### Docker 部署
|
#### Docker 部署
|
||||||
|
|
||||||
@@ -79,9 +77,7 @@ AstrBot 已由雨云官方上架至云应用平台,可一键部署。
|
|||||||
|
|
||||||
#### 手动部署
|
#### 手动部署
|
||||||
|
|
||||||
> 推荐使用 `uv`。
|
首先安装 uv:
|
||||||
|
|
||||||
首先,安装 uv:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install uv
|
pip install uv
|
||||||
@@ -96,6 +92,26 @@ uv run main.py
|
|||||||
|
|
||||||
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
|
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
|
||||||
|
|
||||||
|
## 🌍 社区
|
||||||
|
|
||||||
|
### QQ 群组
|
||||||
|
|
||||||
|
- 1 群:322154837
|
||||||
|
- 3 群:630166526
|
||||||
|
- 5 群:822130018
|
||||||
|
- 6 群:753075035
|
||||||
|
- 开发者群:753075035
|
||||||
|
- 开发者群(备份):295657329
|
||||||
|
|
||||||
|
### Telegram 群组
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
### Discord 群组
|
||||||
|
|
||||||
|
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
||||||
|
|
||||||
|
|
||||||
## ⚡ 消息平台支持情况
|
## ⚡ 消息平台支持情况
|
||||||
|
|
||||||
| 平台 | 支持性 |
|
| 平台 | 支持性 |
|
||||||
@@ -112,22 +128,18 @@ uv run main.py
|
|||||||
| Discord | ✔ |
|
| Discord | ✔ |
|
||||||
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | ✔ |
|
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | ✔ |
|
||||||
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | ✔ |
|
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | ✔ |
|
||||||
| 微信对话开放平台 | 🚧 |
|
|
||||||
| WhatsApp | 🚧 |
|
|
||||||
| 小爱音响 | 🚧 |
|
|
||||||
|
|
||||||
## ⚡ 提供商支持情况
|
## ⚡ 提供商支持情况
|
||||||
|
|
||||||
| 名称 | 支持性 | 类型 | 备注 |
|
| 名称 | 支持性 | 类型 | 备注 |
|
||||||
| -------- | ------- | ------- | ------- |
|
| -------- | ------- | ------- | ------- |
|
||||||
| OpenAI API | ✔ | 文本生成 | 也支持 DeepSeek、Gemini、Kimi、xAI 等兼容 OpenAI API 的服务 |
|
| OpenAI | ✔ | 文本生成 | 支持任何兼容 OpenAI API 的服务 |
|
||||||
| Claude API | ✔ | 文本生成 | |
|
| Anthropic | ✔ | 文本生成 | |
|
||||||
| Google Gemini API | ✔ | 文本生成 | |
|
| Google Gemini | ✔ | 文本生成 | |
|
||||||
| Dify | ✔ | LLMOps | |
|
| Dify | ✔ | LLMOps | |
|
||||||
| 阿里云百炼应用 | ✔ | LLMOps | |
|
| 阿里云百炼应用 | ✔ | LLMOps | |
|
||||||
| Ollama | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
|
| Ollama | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
|
||||||
| LM Studio | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
|
| LM Studio | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
|
||||||
| LLMTuner | ✔ | 模型加载器 | 本地加载 lora 等微调模型 |
|
|
||||||
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | ✔ | 模型 API 及算力服务平台 | |
|
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | ✔ | 模型 API 及算力服务平台 | |
|
||||||
| [302.AI](https://share.302.ai/rr1M3l) | ✔ | 模型 API 服务平台 | |
|
| [302.AI](https://share.302.ai/rr1M3l) | ✔ | 模型 API 服务平台 | |
|
||||||
| 硅基流动 | ✔ | 模型 API 服务平台 | |
|
| 硅基流动 | ✔ | 模型 API 服务平台 | |
|
||||||
@@ -143,7 +155,6 @@ uv run main.py
|
|||||||
| 阿里云百炼 TTS | ✔ | 文本转语音 | |
|
| 阿里云百炼 TTS | ✔ | 文本转语音 | |
|
||||||
| Azure TTS | ✔ | 文本转语音 | Microsoft Azure TTS |
|
| Azure TTS | ✔ | 文本转语音 | Microsoft Azure TTS |
|
||||||
|
|
||||||
|
|
||||||
## ❤️ 贡献
|
## ❤️ 贡献
|
||||||
|
|
||||||
欢迎任何 Issues/Pull Requests!只需要将你的更改提交到此项目 :)
|
欢迎任何 Issues/Pull Requests!只需要将你的更改提交到此项目 :)
|
||||||
@@ -162,38 +173,6 @@ pip install pre-commit
|
|||||||
pre-commit install
|
pre-commit install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌟 支持
|
|
||||||
|
|
||||||
- Star 这个项目!
|
|
||||||
- 在[爱发电](https://afdian.com/a/soulter)支持我!
|
|
||||||
|
|
||||||
## ✨ Demo
|
|
||||||
|
|
||||||
<details><summary>👉 点击展开多张 Demo 截图 👈</summary>
|
|
||||||
|
|
||||||
<div align='center'>
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/4ee688d9-467d-45c8-99d6-368f9a8a92d8" width="600">
|
|
||||||
|
|
||||||
_✨基于 Docker 的沙箱化代码执行器(Beta 测试)✨_
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/0378f407-6079-4f64-ae4c-e97ab20611d2" height=500>
|
|
||||||
|
|
||||||
_✨ 多模态、网页搜索、长文本转图片(可配置) ✨_
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/e137a9e1-340a-4bf2-bb2b-771132780735" height=150>
|
|
||||||
<img src="https://github.com/user-attachments/assets/480f5e82-cf6a-4955-a869-0d73137aa6e1" height=150>
|
|
||||||
|
|
||||||
_✨ 插件系统——部分插件展示 ✨_
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/0cdbf564-2f59-4da5-b524-ce0e7ef3d978" width=600>
|
|
||||||
|
|
||||||
_✨ WebUI ✨_
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
## ❤️ Special Thanks
|
## ❤️ Special Thanks
|
||||||
|
|
||||||
@@ -219,7 +198,8 @@ _✨ WebUI ✨_
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||

|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
_私は、高性能ですから!_
|
_私は、高性能ですから!_
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ async def check_dashboard(astrbot_root: Path) -> None:
|
|||||||
):
|
):
|
||||||
click.echo("正在安装管理面板...")
|
click.echo("正在安装管理面板...")
|
||||||
await download_dashboard(
|
await download_dashboard(
|
||||||
path="data/dashboard.zip", extract_path=str(astrbot_root)
|
path="data/dashboard.zip",
|
||||||
|
extract_path=str(astrbot_root),
|
||||||
|
version=f"v{VERSION}",
|
||||||
|
latest=False,
|
||||||
)
|
)
|
||||||
click.echo("管理面板安装完成")
|
click.echo("管理面板安装完成")
|
||||||
|
|
||||||
@@ -50,7 +53,10 @@ async def check_dashboard(astrbot_root: Path) -> None:
|
|||||||
version = dashboard_version.split("v")[1]
|
version = dashboard_version.split("v")[1]
|
||||||
click.echo(f"管理面板版本: {version}")
|
click.echo(f"管理面板版本: {version}")
|
||||||
await download_dashboard(
|
await download_dashboard(
|
||||||
path="data/dashboard.zip", extract_path=str(astrbot_root)
|
path="data/dashboard.zip",
|
||||||
|
extract_path=str(astrbot_root),
|
||||||
|
version=f"v{VERSION}",
|
||||||
|
latest=False,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(f"下载管理面板失败: {e}")
|
click.echo(f"下载管理面板失败: {e}")
|
||||||
@@ -59,7 +65,10 @@ async def check_dashboard(astrbot_root: Path) -> None:
|
|||||||
click.echo("初始化管理面板目录...")
|
click.echo("初始化管理面板目录...")
|
||||||
try:
|
try:
|
||||||
await download_dashboard(
|
await download_dashboard(
|
||||||
path=str(astrbot_root / "dashboard.zip"), extract_path=str(astrbot_root)
|
path=str(astrbot_root / "dashboard.zip"),
|
||||||
|
extract_path=str(astrbot_root),
|
||||||
|
version=f"v{VERSION}",
|
||||||
|
latest=False,
|
||||||
)
|
)
|
||||||
click.echo("管理面板初始化完成")
|
click.echo("管理面板初始化完成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -36,13 +36,21 @@ class AstrBotConfigManager:
|
|||||||
self.confs: dict[str, AstrBotConfig] = {}
|
self.confs: dict[str, AstrBotConfig] = {}
|
||||||
"""uuid / "default" -> AstrBotConfig"""
|
"""uuid / "default" -> AstrBotConfig"""
|
||||||
self.confs["default"] = default_config
|
self.confs["default"] = default_config
|
||||||
|
self.abconf_data = None
|
||||||
self._load_all_configs()
|
self._load_all_configs()
|
||||||
|
|
||||||
|
def _get_abconf_data(self) -> dict:
|
||||||
|
"""获取所有的 abconf 数据"""
|
||||||
|
if self.abconf_data is None:
|
||||||
|
self.abconf_data = self.sp.get(
|
||||||
|
"abconf_mapping", {}, scope="global", scope_id="global"
|
||||||
|
)
|
||||||
|
return self.abconf_data
|
||||||
|
|
||||||
def _load_all_configs(self):
|
def _load_all_configs(self):
|
||||||
"""Load all configurations from the shared preferences."""
|
"""Load all configurations from the shared preferences."""
|
||||||
abconf_data = self.sp.get(
|
abconf_data = self._get_abconf_data()
|
||||||
"abconf_mapping", {}, scope="global", scope_id="global"
|
self.abconf_data = abconf_data
|
||||||
)
|
|
||||||
for uuid_, meta in abconf_data.items():
|
for uuid_, meta in abconf_data.items():
|
||||||
filename = meta["path"]
|
filename = meta["path"]
|
||||||
conf_path = os.path.join(get_astrbot_config_path(), filename)
|
conf_path = os.path.join(get_astrbot_config_path(), filename)
|
||||||
@@ -72,9 +80,7 @@ class AstrBotConfigManager:
|
|||||||
ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型
|
ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型
|
||||||
"""
|
"""
|
||||||
# uuid -> { "umop": list, "path": str, "name": str }
|
# uuid -> { "umop": list, "path": str, "name": str }
|
||||||
abconf_data = self.sp.get(
|
abconf_data = self._get_abconf_data()
|
||||||
"abconf_mapping", {}, scope="global", scope_id="global"
|
|
||||||
)
|
|
||||||
if isinstance(umo, MessageSession):
|
if isinstance(umo, MessageSession):
|
||||||
umo = str(umo)
|
umo = str(umo)
|
||||||
else:
|
else:
|
||||||
@@ -115,6 +121,7 @@ class AstrBotConfigManager:
|
|||||||
"name": random_word,
|
"name": random_word,
|
||||||
}
|
}
|
||||||
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
||||||
|
self.abconf_data = abconf_data
|
||||||
|
|
||||||
def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig:
|
def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig:
|
||||||
"""获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。"""
|
"""获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。"""
|
||||||
@@ -147,9 +154,7 @@ class AstrBotConfigManager:
|
|||||||
"""获取所有配置文件的元数据列表"""
|
"""获取所有配置文件的元数据列表"""
|
||||||
conf_list = []
|
conf_list = []
|
||||||
conf_list.append(DEFAULT_CONFIG_CONF_INFO)
|
conf_list.append(DEFAULT_CONFIG_CONF_INFO)
|
||||||
abconf_mapping = self.sp.get(
|
abconf_mapping = self._get_abconf_data()
|
||||||
"abconf_mapping", {}, scope="global", scope_id="global"
|
|
||||||
)
|
|
||||||
for uuid_, meta in abconf_mapping.items():
|
for uuid_, meta in abconf_mapping.items():
|
||||||
conf_list.append(ConfInfo(**meta, id=uuid_))
|
conf_list.append(ConfInfo(**meta, id=uuid_))
|
||||||
return conf_list
|
return conf_list
|
||||||
@@ -218,6 +223,7 @@ class AstrBotConfigManager:
|
|||||||
# 从映射中移除
|
# 从映射中移除
|
||||||
del abconf_data[conf_id]
|
del abconf_data[conf_id]
|
||||||
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
||||||
|
self.abconf_data = abconf_data
|
||||||
|
|
||||||
logger.info(f"成功删除配置文件 {conf_id}")
|
logger.info(f"成功删除配置文件 {conf_id}")
|
||||||
return True
|
return True
|
||||||
@@ -263,6 +269,7 @@ class AstrBotConfigManager:
|
|||||||
|
|
||||||
# 保存更新
|
# 保存更新
|
||||||
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
||||||
|
self.abconf_data = abconf_data
|
||||||
logger.info(f"成功更新配置文件 {conf_id} 的信息")
|
logger.info(f"成功更新配置文件 {conf_id} 的信息")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import os
|
|||||||
|
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
VERSION = "4.0.0-beta.2"
|
VERSION = "4.0.0"
|
||||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||||
|
|
||||||
# 默认配置
|
# 默认配置
|
||||||
@@ -103,6 +103,7 @@ DEFAULT_CONFIG = {
|
|||||||
"t2i_strategy": "remote",
|
"t2i_strategy": "remote",
|
||||||
"t2i_endpoint": "",
|
"t2i_endpoint": "",
|
||||||
"t2i_use_file_service": False,
|
"t2i_use_file_service": False,
|
||||||
|
"t2i_active_template": "base",
|
||||||
"http_proxy": "",
|
"http_proxy": "",
|
||||||
"no_proxy": ["localhost", "127.0.0.1", "::1"],
|
"no_proxy": ["localhost", "127.0.0.1", "::1"],
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
@@ -246,8 +247,49 @@ CONFIG_METADATA_2 = {
|
|||||||
"slack_webhook_port": 6197,
|
"slack_webhook_port": 6197,
|
||||||
"slack_webhook_path": "/astrbot-slack-webhook/callback",
|
"slack_webhook_path": "/astrbot-slack-webhook/callback",
|
||||||
},
|
},
|
||||||
|
"Satori": {
|
||||||
|
"id": "satori",
|
||||||
|
"type": "satori",
|
||||||
|
"enable": False,
|
||||||
|
"satori_api_base_url": "http://localhost:5140/satori/v1",
|
||||||
|
"satori_endpoint": "ws://127.0.0.1:5140/satori/v1/events",
|
||||||
|
"satori_token": "",
|
||||||
|
"satori_auto_reconnect": True,
|
||||||
|
"satori_heartbeat_interval": 10,
|
||||||
|
"satori_reconnect_delay": 5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
|
"satori_api_base_url": {
|
||||||
|
"description": "Satori API Base URL",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "The base URL for the Satori API.",
|
||||||
|
},
|
||||||
|
"satori_endpoint": {
|
||||||
|
"description": "Satori WebSocket Endpoint",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "The WebSocket endpoint for Satori events.",
|
||||||
|
},
|
||||||
|
"satori_token": {
|
||||||
|
"description": "Satori Token",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "The token used for authenticating with the Satori API.",
|
||||||
|
},
|
||||||
|
"satori_auto_reconnect": {
|
||||||
|
"description": "Enable Auto Reconnect",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "Whether to automatically reconnect the WebSocket on disconnection.",
|
||||||
|
},
|
||||||
|
"satori_heartbeat_interval": {
|
||||||
|
"description": "Satori Heartbeat Interval",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "The interval (in seconds) for sending heartbeat messages.",
|
||||||
|
},
|
||||||
|
"satori_reconnect_delay": {
|
||||||
|
"description": "Satori Reconnect Delay",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "The delay (in seconds) before attempting to reconnect.",
|
||||||
|
},
|
||||||
"slack_connection_mode": {
|
"slack_connection_mode": {
|
||||||
"description": "Slack Connection Mode",
|
"description": "Slack Connection Mode",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -866,6 +908,9 @@ CONFIG_METADATA_2 = {
|
|||||||
"provider_type": "text_to_speech",
|
"provider_type": "text_to_speech",
|
||||||
"enable": False,
|
"enable": False,
|
||||||
"edge-tts-voice": "zh-CN-XiaoxiaoNeural",
|
"edge-tts-voice": "zh-CN-XiaoxiaoNeural",
|
||||||
|
"rate": "+0%",
|
||||||
|
"volume": "+0%",
|
||||||
|
"pitch": "+0Hz",
|
||||||
"timeout": 20,
|
"timeout": 20,
|
||||||
},
|
},
|
||||||
"GSV TTS(本地加载)": {
|
"GSV TTS(本地加载)": {
|
||||||
@@ -1913,7 +1958,7 @@ CONFIG_METADATA_3 = {
|
|||||||
"type": "bool",
|
"type": "bool",
|
||||||
},
|
},
|
||||||
"provider_settings.identifier": {
|
"provider_settings.identifier": {
|
||||||
"description": "用户感知",
|
"description": "用户识别",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
},
|
},
|
||||||
"provider_settings.datetime_system_prompt": {
|
"provider_settings.datetime_system_prompt": {
|
||||||
@@ -1926,7 +1971,7 @@ CONFIG_METADATA_3 = {
|
|||||||
},
|
},
|
||||||
"provider_settings.max_agent_step": {
|
"provider_settings.max_agent_step": {
|
||||||
"description": "工具调用轮数上限",
|
"description": "工具调用轮数上限",
|
||||||
"type": "bool",
|
"type": "int",
|
||||||
},
|
},
|
||||||
"provider_settings.streaming_response": {
|
"provider_settings.streaming_response": {
|
||||||
"description": "流式回复",
|
"description": "流式回复",
|
||||||
@@ -2288,7 +2333,13 @@ CONFIG_METADATA_3_SYSTEM = {
|
|||||||
"condition": {
|
"condition": {
|
||||||
"t2i_strategy": "remote",
|
"t2i_strategy": "remote",
|
||||||
},
|
},
|
||||||
"_special": "t2i_template"
|
"_special": "t2i_template",
|
||||||
|
},
|
||||||
|
"t2i_active_template": {
|
||||||
|
"description": "当前应用的文转图渲染模板",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "此处的值由文转图模板管理页面进行维护。",
|
||||||
|
"invisible": True,
|
||||||
},
|
},
|
||||||
"log_level": {
|
"log_level": {
|
||||||
"description": "控制台日志级别",
|
"description": "控制台日志级别",
|
||||||
@@ -2321,6 +2372,11 @@ CONFIG_METADATA_3_SYSTEM = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
|
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
|
||||||
},
|
},
|
||||||
|
"no_proxy": {
|
||||||
|
"description": "直连地址列表",
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|||||||
from astrbot.core.utils.io import download_file, download_image_by_url, file_to_base64
|
from astrbot.core.utils.io import download_file, download_image_by_url, file_to_base64
|
||||||
|
|
||||||
|
|
||||||
class ComponentType(Enum):
|
class ComponentType(str, Enum):
|
||||||
Plain = "Plain" # 纯文本消息
|
Plain = "Plain" # 纯文本消息
|
||||||
Face = "Face" # QQ表情
|
Face = "Face" # QQ表情
|
||||||
Record = "Record" # 语音
|
Record = "Record" # 语音
|
||||||
@@ -108,7 +108,7 @@ class BaseMessageComponent(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Plain(BaseMessageComponent):
|
class Plain(BaseMessageComponent):
|
||||||
type: ComponentType = "Plain"
|
type = ComponentType.Plain
|
||||||
text: str
|
text: str
|
||||||
convert: T.Optional[bool] = True # 若为 False 则直接发送未转换 CQ 码的消息
|
convert: T.Optional[bool] = True # 若为 False 则直接发送未转换 CQ 码的消息
|
||||||
|
|
||||||
@@ -128,8 +128,9 @@ class Plain(BaseMessageComponent):
|
|||||||
async def to_dict(self):
|
async def to_dict(self):
|
||||||
return {"type": "text", "data": {"text": self.text}}
|
return {"type": "text", "data": {"text": self.text}}
|
||||||
|
|
||||||
|
|
||||||
class Face(BaseMessageComponent):
|
class Face(BaseMessageComponent):
|
||||||
type: ComponentType = "Face"
|
type = ComponentType.Face
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
@@ -137,7 +138,7 @@ class Face(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Record(BaseMessageComponent):
|
class Record(BaseMessageComponent):
|
||||||
type: ComponentType = "Record"
|
type = ComponentType.Record
|
||||||
file: T.Optional[str] = ""
|
file: T.Optional[str] = ""
|
||||||
magic: T.Optional[bool] = False
|
magic: T.Optional[bool] = False
|
||||||
url: T.Optional[str] = ""
|
url: T.Optional[str] = ""
|
||||||
@@ -164,19 +165,24 @@ class Record(BaseMessageComponent):
|
|||||||
return Record(file=url, **_)
|
return Record(file=url, **_)
|
||||||
raise Exception("not a valid url")
|
raise Exception("not a valid url")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromBase64(bs64_data: str, **_):
|
||||||
|
return Record(file=f"base64://{bs64_data}", **_)
|
||||||
|
|
||||||
async def convert_to_file_path(self) -> str:
|
async def convert_to_file_path(self) -> str:
|
||||||
"""将这个语音统一转换为本地文件路径。这个方法避免了手动判断语音数据类型,直接返回语音数据的本地路径(如果是网络 URL, 则会自动进行下载)。
|
"""将这个语音统一转换为本地文件路径。这个方法避免了手动判断语音数据类型,直接返回语音数据的本地路径(如果是网络 URL, 则会自动进行下载)。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: 语音的本地路径,以绝对路径表示。
|
str: 语音的本地路径,以绝对路径表示。
|
||||||
"""
|
"""
|
||||||
if self.file and self.file.startswith("file:///"):
|
if not self.file:
|
||||||
file_path = self.file[8:]
|
raise Exception(f"not a valid file: {self.file}")
|
||||||
return file_path
|
if self.file.startswith("file:///"):
|
||||||
elif self.file and self.file.startswith("http"):
|
return self.file[8:]
|
||||||
|
elif self.file.startswith("http"):
|
||||||
file_path = await download_image_by_url(self.file)
|
file_path = await download_image_by_url(self.file)
|
||||||
return os.path.abspath(file_path)
|
return os.path.abspath(file_path)
|
||||||
elif self.file and self.file.startswith("base64://"):
|
elif self.file.startswith("base64://"):
|
||||||
bs64_data = self.file.removeprefix("base64://")
|
bs64_data = self.file.removeprefix("base64://")
|
||||||
image_bytes = base64.b64decode(bs64_data)
|
image_bytes = base64.b64decode(bs64_data)
|
||||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||||
@@ -185,8 +191,7 @@ class Record(BaseMessageComponent):
|
|||||||
f.write(image_bytes)
|
f.write(image_bytes)
|
||||||
return os.path.abspath(file_path)
|
return os.path.abspath(file_path)
|
||||||
elif os.path.exists(self.file):
|
elif os.path.exists(self.file):
|
||||||
file_path = self.file
|
return os.path.abspath(self.file)
|
||||||
return os.path.abspath(file_path)
|
|
||||||
else:
|
else:
|
||||||
raise Exception(f"not a valid file: {self.file}")
|
raise Exception(f"not a valid file: {self.file}")
|
||||||
|
|
||||||
@@ -197,12 +202,14 @@ class Record(BaseMessageComponent):
|
|||||||
str: 语音的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。
|
str: 语音的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。
|
||||||
"""
|
"""
|
||||||
# convert to base64
|
# convert to base64
|
||||||
if self.file and self.file.startswith("file:///"):
|
if not self.file:
|
||||||
|
raise Exception(f"not a valid file: {self.file}")
|
||||||
|
if self.file.startswith("file:///"):
|
||||||
bs64_data = file_to_base64(self.file[8:])
|
bs64_data = file_to_base64(self.file[8:])
|
||||||
elif self.file and self.file.startswith("http"):
|
elif self.file.startswith("http"):
|
||||||
file_path = await download_image_by_url(self.file)
|
file_path = await download_image_by_url(self.file)
|
||||||
bs64_data = file_to_base64(file_path)
|
bs64_data = file_to_base64(file_path)
|
||||||
elif self.file and self.file.startswith("base64://"):
|
elif self.file.startswith("base64://"):
|
||||||
bs64_data = self.file
|
bs64_data = self.file
|
||||||
elif os.path.exists(self.file):
|
elif os.path.exists(self.file):
|
||||||
bs64_data = file_to_base64(self.file)
|
bs64_data = file_to_base64(self.file)
|
||||||
@@ -236,7 +243,7 @@ class Record(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Video(BaseMessageComponent):
|
class Video(BaseMessageComponent):
|
||||||
type: ComponentType = "Video"
|
type = ComponentType.Video
|
||||||
file: str
|
file: str
|
||||||
cover: T.Optional[str] = ""
|
cover: T.Optional[str] = ""
|
||||||
c: T.Optional[int] = 2
|
c: T.Optional[int] = 2
|
||||||
@@ -322,7 +329,7 @@ class Video(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class At(BaseMessageComponent):
|
class At(BaseMessageComponent):
|
||||||
type: ComponentType = "At"
|
type = ComponentType.At
|
||||||
qq: T.Union[int, str] # 此处str为all时代表所有人
|
qq: T.Union[int, str] # 此处str为all时代表所有人
|
||||||
name: T.Optional[str] = ""
|
name: T.Optional[str] = ""
|
||||||
|
|
||||||
@@ -344,28 +351,28 @@ class AtAll(At):
|
|||||||
|
|
||||||
|
|
||||||
class RPS(BaseMessageComponent): # TODO
|
class RPS(BaseMessageComponent): # TODO
|
||||||
type: ComponentType = "RPS"
|
type = ComponentType.RPS
|
||||||
|
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
super().__init__(**_)
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
class Dice(BaseMessageComponent): # TODO
|
class Dice(BaseMessageComponent): # TODO
|
||||||
type: ComponentType = "Dice"
|
type = ComponentType.Dice
|
||||||
|
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
super().__init__(**_)
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
class Shake(BaseMessageComponent): # TODO
|
class Shake(BaseMessageComponent): # TODO
|
||||||
type: ComponentType = "Shake"
|
type = ComponentType.Shake
|
||||||
|
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
super().__init__(**_)
|
super().__init__(**_)
|
||||||
|
|
||||||
|
|
||||||
class Anonymous(BaseMessageComponent): # TODO
|
class Anonymous(BaseMessageComponent): # TODO
|
||||||
type: ComponentType = "Anonymous"
|
type = ComponentType.Anonymous
|
||||||
ignore: T.Optional[bool] = False
|
ignore: T.Optional[bool] = False
|
||||||
|
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
@@ -373,7 +380,7 @@ class Anonymous(BaseMessageComponent): # TODO
|
|||||||
|
|
||||||
|
|
||||||
class Share(BaseMessageComponent):
|
class Share(BaseMessageComponent):
|
||||||
type: ComponentType = "Share"
|
type = ComponentType.Share
|
||||||
url: str
|
url: str
|
||||||
title: str
|
title: str
|
||||||
content: T.Optional[str] = ""
|
content: T.Optional[str] = ""
|
||||||
@@ -384,7 +391,7 @@ class Share(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Contact(BaseMessageComponent): # TODO
|
class Contact(BaseMessageComponent): # TODO
|
||||||
type: ComponentType = "Contact"
|
type = ComponentType.Contact
|
||||||
_type: str # type 字段冲突
|
_type: str # type 字段冲突
|
||||||
id: T.Optional[int] = 0
|
id: T.Optional[int] = 0
|
||||||
|
|
||||||
@@ -393,7 +400,7 @@ class Contact(BaseMessageComponent): # TODO
|
|||||||
|
|
||||||
|
|
||||||
class Location(BaseMessageComponent): # TODO
|
class Location(BaseMessageComponent): # TODO
|
||||||
type: ComponentType = "Location"
|
type = ComponentType.Location
|
||||||
lat: float
|
lat: float
|
||||||
lon: float
|
lon: float
|
||||||
title: T.Optional[str] = ""
|
title: T.Optional[str] = ""
|
||||||
@@ -404,7 +411,7 @@ class Location(BaseMessageComponent): # TODO
|
|||||||
|
|
||||||
|
|
||||||
class Music(BaseMessageComponent):
|
class Music(BaseMessageComponent):
|
||||||
type: ComponentType = "Music"
|
type = ComponentType.Music
|
||||||
_type: str
|
_type: str
|
||||||
id: T.Optional[int] = 0
|
id: T.Optional[int] = 0
|
||||||
url: T.Optional[str] = ""
|
url: T.Optional[str] = ""
|
||||||
@@ -421,7 +428,7 @@ class Music(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Image(BaseMessageComponent):
|
class Image(BaseMessageComponent):
|
||||||
type: ComponentType = "Image"
|
type = ComponentType.Image
|
||||||
file: T.Optional[str] = ""
|
file: T.Optional[str] = ""
|
||||||
_type: T.Optional[str] = ""
|
_type: T.Optional[str] = ""
|
||||||
subType: T.Optional[int] = 0
|
subType: T.Optional[int] = 0
|
||||||
@@ -464,14 +471,15 @@ class Image(BaseMessageComponent):
|
|||||||
Returns:
|
Returns:
|
||||||
str: 图片的本地路径,以绝对路径表示。
|
str: 图片的本地路径,以绝对路径表示。
|
||||||
"""
|
"""
|
||||||
url = self.url if self.url else self.file
|
url = self.url or self.file
|
||||||
if url and url.startswith("file:///"):
|
if not url:
|
||||||
image_file_path = url[8:]
|
raise ValueError("No valid file or URL provided")
|
||||||
return image_file_path
|
if url.startswith("file:///"):
|
||||||
elif url and url.startswith("http"):
|
return url[8:]
|
||||||
|
elif url.startswith("http"):
|
||||||
image_file_path = await download_image_by_url(url)
|
image_file_path = await download_image_by_url(url)
|
||||||
return os.path.abspath(image_file_path)
|
return os.path.abspath(image_file_path)
|
||||||
elif url and url.startswith("base64://"):
|
elif url.startswith("base64://"):
|
||||||
bs64_data = url.removeprefix("base64://")
|
bs64_data = url.removeprefix("base64://")
|
||||||
image_bytes = base64.b64decode(bs64_data)
|
image_bytes = base64.b64decode(bs64_data)
|
||||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||||
@@ -480,8 +488,7 @@ class Image(BaseMessageComponent):
|
|||||||
f.write(image_bytes)
|
f.write(image_bytes)
|
||||||
return os.path.abspath(image_file_path)
|
return os.path.abspath(image_file_path)
|
||||||
elif os.path.exists(url):
|
elif os.path.exists(url):
|
||||||
image_file_path = url
|
return os.path.abspath(url)
|
||||||
return os.path.abspath(image_file_path)
|
|
||||||
else:
|
else:
|
||||||
raise Exception(f"not a valid file: {url}")
|
raise Exception(f"not a valid file: {url}")
|
||||||
|
|
||||||
@@ -492,13 +499,15 @@ class Image(BaseMessageComponent):
|
|||||||
str: 图片的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。
|
str: 图片的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。
|
||||||
"""
|
"""
|
||||||
# convert to base64
|
# convert to base64
|
||||||
url = self.url if self.url else self.file
|
url = self.url or self.file
|
||||||
if url and url.startswith("file:///"):
|
if not url:
|
||||||
|
raise ValueError("No valid file or URL provided")
|
||||||
|
if url.startswith("file:///"):
|
||||||
bs64_data = file_to_base64(url[8:])
|
bs64_data = file_to_base64(url[8:])
|
||||||
elif url and url.startswith("http"):
|
elif url.startswith("http"):
|
||||||
image_file_path = await download_image_by_url(url)
|
image_file_path = await download_image_by_url(url)
|
||||||
bs64_data = file_to_base64(image_file_path)
|
bs64_data = file_to_base64(image_file_path)
|
||||||
elif url and url.startswith("base64://"):
|
elif url.startswith("base64://"):
|
||||||
bs64_data = url
|
bs64_data = url
|
||||||
elif os.path.exists(url):
|
elif os.path.exists(url):
|
||||||
bs64_data = file_to_base64(url)
|
bs64_data = file_to_base64(url)
|
||||||
@@ -532,7 +541,7 @@ class Image(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Reply(BaseMessageComponent):
|
class Reply(BaseMessageComponent):
|
||||||
type: ComponentType = "Reply"
|
type = ComponentType.Reply
|
||||||
id: T.Union[str, int]
|
id: T.Union[str, int]
|
||||||
"""所引用的消息 ID"""
|
"""所引用的消息 ID"""
|
||||||
chain: T.Optional[T.List["BaseMessageComponent"]] = []
|
chain: T.Optional[T.List["BaseMessageComponent"]] = []
|
||||||
@@ -558,7 +567,7 @@ class Reply(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class RedBag(BaseMessageComponent):
|
class RedBag(BaseMessageComponent):
|
||||||
type: ComponentType = "RedBag"
|
type = ComponentType.RedBag
|
||||||
title: str
|
title: str
|
||||||
|
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
@@ -566,7 +575,7 @@ class RedBag(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Poke(BaseMessageComponent):
|
class Poke(BaseMessageComponent):
|
||||||
type: str = ""
|
type: str = ComponentType.Poke
|
||||||
id: T.Optional[int] = 0
|
id: T.Optional[int] = 0
|
||||||
qq: T.Optional[int] = 0
|
qq: T.Optional[int] = 0
|
||||||
|
|
||||||
@@ -576,7 +585,7 @@ class Poke(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Forward(BaseMessageComponent):
|
class Forward(BaseMessageComponent):
|
||||||
type: ComponentType = "Forward"
|
type = ComponentType.Forward
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
@@ -586,7 +595,7 @@ class Forward(BaseMessageComponent):
|
|||||||
class Node(BaseMessageComponent):
|
class Node(BaseMessageComponent):
|
||||||
"""群合并转发消息"""
|
"""群合并转发消息"""
|
||||||
|
|
||||||
type: ComponentType = "Node"
|
type = ComponentType.Node
|
||||||
id: T.Optional[int] = 0 # 忽略
|
id: T.Optional[int] = 0 # 忽略
|
||||||
name: T.Optional[str] = "" # qq昵称
|
name: T.Optional[str] = "" # qq昵称
|
||||||
uin: T.Optional[str] = "0" # qq号
|
uin: T.Optional[str] = "0" # qq号
|
||||||
@@ -638,7 +647,7 @@ class Node(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Nodes(BaseMessageComponent):
|
class Nodes(BaseMessageComponent):
|
||||||
type: ComponentType = "Nodes"
|
type = ComponentType.Nodes
|
||||||
nodes: T.List[Node]
|
nodes: T.List[Node]
|
||||||
|
|
||||||
def __init__(self, nodes: T.List[Node], **_):
|
def __init__(self, nodes: T.List[Node], **_):
|
||||||
@@ -664,7 +673,7 @@ class Nodes(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Xml(BaseMessageComponent):
|
class Xml(BaseMessageComponent):
|
||||||
type: ComponentType = "Xml"
|
type = ComponentType.Xml
|
||||||
data: str
|
data: str
|
||||||
resid: T.Optional[int] = 0
|
resid: T.Optional[int] = 0
|
||||||
|
|
||||||
@@ -673,7 +682,7 @@ class Xml(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Json(BaseMessageComponent):
|
class Json(BaseMessageComponent):
|
||||||
type: ComponentType = "Json"
|
type = ComponentType.Json
|
||||||
data: T.Union[str, dict]
|
data: T.Union[str, dict]
|
||||||
resid: T.Optional[int] = 0
|
resid: T.Optional[int] = 0
|
||||||
|
|
||||||
@@ -684,7 +693,7 @@ class Json(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class CardImage(BaseMessageComponent):
|
class CardImage(BaseMessageComponent):
|
||||||
type: ComponentType = "CardImage"
|
type = ComponentType.CardImage
|
||||||
file: str
|
file: str
|
||||||
cache: T.Optional[bool] = True
|
cache: T.Optional[bool] = True
|
||||||
minwidth: T.Optional[int] = 400
|
minwidth: T.Optional[int] = 400
|
||||||
@@ -703,7 +712,7 @@ class CardImage(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class TTS(BaseMessageComponent):
|
class TTS(BaseMessageComponent):
|
||||||
type: ComponentType = "TTS"
|
type = ComponentType.TTS
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
@@ -711,7 +720,7 @@ class TTS(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class Unknown(BaseMessageComponent):
|
class Unknown(BaseMessageComponent):
|
||||||
type: ComponentType = "Unknown"
|
type = ComponentType.Unknown
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
def toString(self):
|
def toString(self):
|
||||||
@@ -723,7 +732,7 @@ class File(BaseMessageComponent):
|
|||||||
文件消息段
|
文件消息段
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type: ComponentType = "File"
|
type = ComponentType.File
|
||||||
name: T.Optional[str] = "" # 名字
|
name: T.Optional[str] = "" # 名字
|
||||||
file_: T.Optional[str] = "" # 本地路径
|
file_: T.Optional[str] = "" # 本地路径
|
||||||
url: T.Optional[str] = "" # url
|
url: T.Optional[str] = "" # url
|
||||||
@@ -853,7 +862,7 @@ class File(BaseMessageComponent):
|
|||||||
|
|
||||||
|
|
||||||
class WechatEmoji(BaseMessageComponent):
|
class WechatEmoji(BaseMessageComponent):
|
||||||
type: ComponentType = "WechatEmoji"
|
type = ComponentType.WechatEmoji
|
||||||
md5: T.Optional[str] = ""
|
md5: T.Optional[str] = ""
|
||||||
md5_len: T.Optional[int] = 0
|
md5_len: T.Optional[int] = 0
|
||||||
cdnurl: T.Optional[str] = ""
|
cdnurl: T.Optional[str] = ""
|
||||||
|
|||||||
@@ -299,7 +299,9 @@ class LLMRequestSubStage(Stage):
|
|||||||
self.max_context_length - 1,
|
self.max_context_length - 1,
|
||||||
)
|
)
|
||||||
self.streaming_response: bool = settings["streaming_response"]
|
self.streaming_response: bool = settings["streaming_response"]
|
||||||
self.max_step: int = settings.get("max_agent_step", 10)
|
self.max_step: int = settings.get("max_agent_step", 30)
|
||||||
|
if isinstance(self.max_step, bool): # workaround: #2622
|
||||||
|
self.max_step = 30
|
||||||
self.show_tool_use: bool = settings.get("show_tool_use_status", True)
|
self.show_tool_use: bool = settings.get("show_tool_use_status", True)
|
||||||
|
|
||||||
for bwp in self.bot_wake_prefixs:
|
for bwp in self.bot_wake_prefixs:
|
||||||
@@ -434,7 +436,9 @@ class LLMRequestSubStage(Stage):
|
|||||||
provider_cfg = provider.provider_config.get("modalities", ["tool_use"])
|
provider_cfg = provider.provider_config.get("modalities", ["tool_use"])
|
||||||
# 如果模型不支持工具使用,但请求中包含工具列表,则清空。
|
# 如果模型不支持工具使用,但请求中包含工具列表,则清空。
|
||||||
if "tool_use" not in provider_cfg:
|
if "tool_use" not in provider_cfg:
|
||||||
logger.debug(f"用户设置提供商 {provider} 不支持工具使用,清空工具列表。")
|
logger.debug(
|
||||||
|
f"用户设置提供商 {provider} 不支持工具使用,清空工具列表。"
|
||||||
|
)
|
||||||
req.func_tool = None
|
req.func_tool = None
|
||||||
# 插件可用性设置
|
# 插件可用性设置
|
||||||
if event.plugins_name is not None and req.func_tool:
|
if event.plugins_name is not None and req.func_tool:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ResultDecorateStage(Stage):
|
|||||||
self.t2i_word_threshold = 150
|
self.t2i_word_threshold = 150
|
||||||
self.t2i_strategy = ctx.astrbot_config["t2i_strategy"]
|
self.t2i_strategy = ctx.astrbot_config["t2i_strategy"]
|
||||||
self.t2i_use_network = self.t2i_strategy == "remote"
|
self.t2i_use_network = self.t2i_strategy == "remote"
|
||||||
|
self.t2i_active_template = ctx.astrbot_config["t2i_active_template"]
|
||||||
|
|
||||||
self.forward_threshold = ctx.astrbot_config["platform_settings"][
|
self.forward_threshold = ctx.astrbot_config["platform_settings"][
|
||||||
"forward_threshold"
|
"forward_threshold"
|
||||||
@@ -247,7 +248,10 @@ class ResultDecorateStage(Stage):
|
|||||||
render_start = time.time()
|
render_start = time.time()
|
||||||
try:
|
try:
|
||||||
url = await html_renderer.render_t2i(
|
url = await html_renderer.render_t2i(
|
||||||
plain_str, return_url=True, use_network=self.t2i_use_network
|
plain_str,
|
||||||
|
return_url=True,
|
||||||
|
use_network=self.t2i_use_network,
|
||||||
|
template_name=self.t2i_active_template,
|
||||||
)
|
)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.error("文本转图片失败,使用文本发送。")
|
logger.error("文本转图片失败,使用文本发送。")
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ class PlatformManager:
|
|||||||
)
|
)
|
||||||
case "slack":
|
case "slack":
|
||||||
from .sources.slack.slack_adapter import SlackAdapter # noqa: F401
|
from .sources.slack.slack_adapter import SlackAdapter # noqa: F401
|
||||||
|
case "satori":
|
||||||
|
from .sources.satori.satori_adapter import SatoriPlatformAdapter # noqa: F401
|
||||||
except (ImportError, ModuleNotFoundError) as e:
|
except (ImportError, ModuleNotFoundError) as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。"
|
f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。"
|
||||||
|
|||||||
@@ -67,12 +67,19 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
messages: list[dict],
|
messages: list[dict],
|
||||||
):
|
):
|
||||||
if event:
|
# session_id 必须是纯数字字符串
|
||||||
await bot.send(event=event, message=messages)
|
session_id = int(session_id) if session_id.isdigit() else None
|
||||||
elif is_group:
|
|
||||||
|
if is_group and isinstance(session_id, int):
|
||||||
await bot.send_group_msg(group_id=session_id, message=messages)
|
await bot.send_group_msg(group_id=session_id, message=messages)
|
||||||
else:
|
elif not is_group and isinstance(session_id, int):
|
||||||
await bot.send_private_msg(user_id=session_id, message=messages)
|
await bot.send_private_msg(user_id=session_id, message=messages)
|
||||||
|
elif isinstance(event, Event): # 最后兜底
|
||||||
|
await bot.send(event=event, message=messages)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"无法发送消息:缺少有效的数字 session_id({session_id}) 或 event({event})"
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def send_message(
|
async def send_message(
|
||||||
@@ -83,7 +90,15 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|||||||
is_group: bool = False,
|
is_group: bool = False,
|
||||||
session_id: str = None,
|
session_id: str = None,
|
||||||
):
|
):
|
||||||
"""发送消息"""
|
"""发送消息至 QQ 协议端(aiocqhttp)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (CQHttp): aiocqhttp 机器人实例
|
||||||
|
message_chain (MessageChain): 要发送的消息链
|
||||||
|
event (Event | None, optional): aiocqhttp 事件对象.
|
||||||
|
is_group (bool, optional): 是否为群消息.
|
||||||
|
session_id (str | None, optional): 会话 ID(群号或 QQ 号
|
||||||
|
"""
|
||||||
|
|
||||||
# 转发消息、文件消息不能和普通消息混在一起发送
|
# 转发消息、文件消息不能和普通消息混在一起发送
|
||||||
send_one_by_one = any(
|
send_one_by_one = any(
|
||||||
@@ -122,18 +137,15 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|||||||
|
|
||||||
async def send(self, message: MessageChain):
|
async def send(self, message: MessageChain):
|
||||||
"""发送消息"""
|
"""发送消息"""
|
||||||
event = self.message_obj.raw_message
|
event = getattr(self.message_obj, "raw_message", None)
|
||||||
assert isinstance(event, Event), "Event must be an instance of aiocqhttp.Event"
|
|
||||||
is_group = False
|
is_group = bool(self.get_group_id())
|
||||||
if self.get_group_id():
|
session_id = self.get_group_id() if is_group else self.get_sender_id()
|
||||||
is_group = True
|
|
||||||
session_id = self.get_group_id()
|
|
||||||
else:
|
|
||||||
session_id = self.get_sender_id()
|
|
||||||
await self.send_message(
|
await self.send_message(
|
||||||
bot=self.bot,
|
bot=self.bot,
|
||||||
message_chain=message,
|
message_chain=message,
|
||||||
event=event,
|
event=event, # 不强制要求一定是 Event
|
||||||
is_group=is_group,
|
is_group=is_group,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -308,13 +308,20 @@ class AiocqhttpAdapter(Platform):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
at_info = await self.bot.call_action(
|
at_info = await self.bot.call_action(
|
||||||
action="get_stranger_info",
|
action="get_group_member_info",
|
||||||
|
group_id=event.group_id,
|
||||||
user_id=int(m["data"]["qq"]),
|
user_id=int(m["data"]["qq"]),
|
||||||
|
no_cache=False,
|
||||||
)
|
)
|
||||||
if at_info:
|
if at_info:
|
||||||
nickname = at_info.get("nick", "") or at_info.get(
|
nickname = at_info.get("card", "")
|
||||||
"nickname", ""
|
if nickname == "":
|
||||||
)
|
at_info = await self.bot.call_action(
|
||||||
|
action="get_stranger_info",
|
||||||
|
user_id=int(m["data"]["qq"]),
|
||||||
|
no_cache=False,
|
||||||
|
)
|
||||||
|
nickname = at_info.get("nick", "") or at_info.get("nickname", "")
|
||||||
is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"}
|
is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"}
|
||||||
|
|
||||||
abm.message.append(
|
abm.message.append(
|
||||||
|
|||||||
482
astrbot/core/platform/sources/satori/satori_adapter.py
Normal file
482
astrbot/core/platform/sources/satori/satori_adapter.py
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import websockets
|
||||||
|
from websockets.asyncio.client import connect
|
||||||
|
from typing import Optional
|
||||||
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
|
from websockets.asyncio.client import ClientConnection
|
||||||
|
from astrbot.api import logger
|
||||||
|
from astrbot.api.event import MessageChain
|
||||||
|
from astrbot.api.platform import (
|
||||||
|
AstrBotMessage,
|
||||||
|
MessageMember,
|
||||||
|
MessageType,
|
||||||
|
Platform,
|
||||||
|
PlatformMetadata,
|
||||||
|
register_platform_adapter,
|
||||||
|
)
|
||||||
|
from astrbot.core.platform.astr_message_event import MessageSession
|
||||||
|
from astrbot.api.message_components import Plain, Image, At, File, Record
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
@register_platform_adapter(
|
||||||
|
"satori",
|
||||||
|
"Satori 协议适配器",
|
||||||
|
)
|
||||||
|
class SatoriPlatformAdapter(Platform):
|
||||||
|
def __init__(
|
||||||
|
self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
|
||||||
|
) -> None:
|
||||||
|
super().__init__(event_queue)
|
||||||
|
self.config = platform_config
|
||||||
|
self.settings = platform_settings
|
||||||
|
|
||||||
|
self.api_base_url = self.config.get(
|
||||||
|
"satori_api_base_url", "http://localhost:5140/satori/v1"
|
||||||
|
)
|
||||||
|
self.token = self.config.get("satori_token", "")
|
||||||
|
self.endpoint = self.config.get(
|
||||||
|
"satori_endpoint", "ws://127.0.0.1:5140/satori/v1/events"
|
||||||
|
)
|
||||||
|
self.auto_reconnect = self.config.get("satori_auto_reconnect", True)
|
||||||
|
self.heartbeat_interval = self.config.get("satori_heartbeat_interval", 10)
|
||||||
|
self.reconnect_delay = self.config.get("satori_reconnect_delay", 5)
|
||||||
|
|
||||||
|
self.ws: Optional[ClientConnection] = None
|
||||||
|
self.session: Optional[ClientSession] = None
|
||||||
|
self.sequence = 0
|
||||||
|
self.logins = []
|
||||||
|
self.running = False
|
||||||
|
self.heartbeat_task: Optional[asyncio.Task] = None
|
||||||
|
self.ready_received = False
|
||||||
|
|
||||||
|
async def send_by_session(
|
||||||
|
self, session: MessageSession, message_chain: MessageChain
|
||||||
|
):
|
||||||
|
from .satori_event import SatoriPlatformEvent
|
||||||
|
|
||||||
|
await SatoriPlatformEvent.send_with_adapter(
|
||||||
|
self, message_chain, session.session_id
|
||||||
|
)
|
||||||
|
await super().send_by_session(session, message_chain)
|
||||||
|
|
||||||
|
def meta(self) -> PlatformMetadata:
|
||||||
|
return PlatformMetadata(name="satori", description="Satori 通用协议适配器")
|
||||||
|
|
||||||
|
def _is_websocket_closed(self, ws) -> bool:
|
||||||
|
"""检查WebSocket连接是否已关闭"""
|
||||||
|
if not ws:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
if hasattr(ws, "closed"):
|
||||||
|
return ws.closed
|
||||||
|
elif hasattr(ws, "close_code"):
|
||||||
|
return ws.close_code is not None
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
self.running = True
|
||||||
|
self.session = ClientSession(timeout=ClientTimeout(total=30))
|
||||||
|
|
||||||
|
retry_count = 0
|
||||||
|
max_retries = 10
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
await self.connect_websocket()
|
||||||
|
retry_count = 0
|
||||||
|
except websockets.exceptions.ConnectionClosed as e:
|
||||||
|
logger.warning(f"Satori WebSocket 连接关闭: {e}")
|
||||||
|
retry_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori WebSocket 连接失败: {e}")
|
||||||
|
retry_count += 1
|
||||||
|
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
|
|
||||||
|
if retry_count >= max_retries:
|
||||||
|
logger.error(f"达到最大重试次数 ({max_retries}),停止重试")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not self.auto_reconnect:
|
||||||
|
break
|
||||||
|
|
||||||
|
delay = min(self.reconnect_delay * (2 ** (retry_count - 1)), 60)
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
|
if self.session:
|
||||||
|
await self.session.close()
|
||||||
|
|
||||||
|
async def connect_websocket(self):
|
||||||
|
logger.info(f"Satori 适配器正在连接到 WebSocket: {self.endpoint}")
|
||||||
|
logger.info(f"Satori 适配器 HTTP API 地址: {self.api_base_url}")
|
||||||
|
|
||||||
|
if not self.endpoint.startswith(("ws://", "wss://")):
|
||||||
|
logger.error(f"无效的WebSocket URL: {self.endpoint}")
|
||||||
|
raise ValueError(f"WebSocket URL必须以ws://或wss://开头: {self.endpoint}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
websocket = await connect(self.endpoint, additional_headers={})
|
||||||
|
self.ws = websocket
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
await self.send_identify()
|
||||||
|
|
||||||
|
self.heartbeat_task = asyncio.create_task(self.heartbeat_loop())
|
||||||
|
|
||||||
|
async for message in websocket:
|
||||||
|
try:
|
||||||
|
await self.handle_message(message) # type: ignore
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori 处理消息异常: {e}")
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed as e:
|
||||||
|
logger.warning(f"Satori WebSocket 连接关闭: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori WebSocket 连接异常: {e}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if self.heartbeat_task:
|
||||||
|
self.heartbeat_task.cancel()
|
||||||
|
try:
|
||||||
|
await self.heartbeat_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
if self.ws:
|
||||||
|
try:
|
||||||
|
await self.ws.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori WebSocket 关闭异常: {e}")
|
||||||
|
|
||||||
|
async def send_identify(self):
|
||||||
|
if not self.ws:
|
||||||
|
raise Exception("WebSocket连接未建立")
|
||||||
|
|
||||||
|
if self._is_websocket_closed(self.ws):
|
||||||
|
raise Exception("WebSocket连接已关闭")
|
||||||
|
|
||||||
|
identify_payload = {
|
||||||
|
"op": 3, # IDENTIFY
|
||||||
|
"body": {
|
||||||
|
"token": str(self.token) if self.token else "", # 字符串
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 只有在有序列号时才添加sn字段
|
||||||
|
if self.sequence > 0:
|
||||||
|
identify_payload["body"]["sn"] = self.sequence
|
||||||
|
|
||||||
|
try:
|
||||||
|
message_str = json.dumps(identify_payload, ensure_ascii=False)
|
||||||
|
await self.ws.send(message_str)
|
||||||
|
except websockets.exceptions.ConnectionClosed as e:
|
||||||
|
logger.error(f"发送 IDENTIFY 信令时连接关闭: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送 IDENTIFY 信令失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def heartbeat_loop(self):
|
||||||
|
try:
|
||||||
|
while self.running and self.ws:
|
||||||
|
await asyncio.sleep(self.heartbeat_interval)
|
||||||
|
|
||||||
|
if self.ws and not self._is_websocket_closed(self.ws):
|
||||||
|
try:
|
||||||
|
ping_payload = {
|
||||||
|
"op": 1, # PING
|
||||||
|
"body": {},
|
||||||
|
}
|
||||||
|
await self.ws.send(json.dumps(ping_payload, ensure_ascii=False))
|
||||||
|
except websockets.exceptions.ConnectionClosed as e:
|
||||||
|
logger.error(f"Satori WebSocket 连接关闭: {e}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori WebSocket 发送心跳失败: {e}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"心跳任务异常: {e}")
|
||||||
|
|
||||||
|
async def handle_message(self, message: str):
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
op = data.get("op")
|
||||||
|
body = data.get("body", {})
|
||||||
|
|
||||||
|
if op == 4: # READY
|
||||||
|
self.logins = body.get("logins", [])
|
||||||
|
self.ready_received = True
|
||||||
|
|
||||||
|
# 输出连接成功的bot信息
|
||||||
|
if self.logins:
|
||||||
|
for i, login in enumerate(self.logins):
|
||||||
|
platform = login.get("platform", "")
|
||||||
|
user = login.get("user", {})
|
||||||
|
user_id = user.get("id", "")
|
||||||
|
user_name = user.get("name", "")
|
||||||
|
logger.info(
|
||||||
|
f"Satori 连接成功 - Bot {i + 1}: platform={platform}, user_id={user_id}, user_name={user_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "sn" in body:
|
||||||
|
self.sequence = body["sn"]
|
||||||
|
|
||||||
|
elif op == 2: # PONG
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif op == 0: # EVENT
|
||||||
|
await self.handle_event(body)
|
||||||
|
if "sn" in body:
|
||||||
|
self.sequence = body["sn"]
|
||||||
|
|
||||||
|
elif op == 5: # META
|
||||||
|
if "sn" in body:
|
||||||
|
self.sequence = body["sn"]
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"解析 WebSocket 消息失败: {e}, 消息内容: {message}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理 WebSocket 消息异常: {e}")
|
||||||
|
|
||||||
|
async def handle_event(self, event_data: dict):
|
||||||
|
try:
|
||||||
|
event_type = event_data.get("type")
|
||||||
|
sn = event_data.get("sn")
|
||||||
|
if sn:
|
||||||
|
self.sequence = sn
|
||||||
|
|
||||||
|
if event_type == "message-created":
|
||||||
|
message = event_data.get("message", {})
|
||||||
|
user = event_data.get("user", {})
|
||||||
|
channel = event_data.get("channel", {})
|
||||||
|
guild = event_data.get("guild")
|
||||||
|
login = event_data.get("login", {})
|
||||||
|
timestamp = event_data.get("timestamp")
|
||||||
|
|
||||||
|
if user.get("id") == login.get("user", {}).get("id"):
|
||||||
|
return
|
||||||
|
|
||||||
|
abm = await self.convert_satori_message(
|
||||||
|
message, user, channel, guild, login, timestamp
|
||||||
|
)
|
||||||
|
if abm:
|
||||||
|
await self.handle_msg(abm)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理事件失败: {e}")
|
||||||
|
|
||||||
|
async def convert_satori_message(
|
||||||
|
self,
|
||||||
|
message: dict,
|
||||||
|
user: dict,
|
||||||
|
channel: dict,
|
||||||
|
guild: Optional[dict],
|
||||||
|
login: dict,
|
||||||
|
timestamp: Optional[int] = None,
|
||||||
|
) -> Optional[AstrBotMessage]:
|
||||||
|
try:
|
||||||
|
abm = AstrBotMessage()
|
||||||
|
abm.message_id = message.get("id", "")
|
||||||
|
abm.raw_message = {
|
||||||
|
"message": message,
|
||||||
|
"user": user,
|
||||||
|
"channel": channel,
|
||||||
|
"guild": guild,
|
||||||
|
"login": login,
|
||||||
|
}
|
||||||
|
|
||||||
|
if guild and guild.get("id"):
|
||||||
|
abm.type = MessageType.GROUP_MESSAGE
|
||||||
|
abm.group_id = guild.get("id", "")
|
||||||
|
abm.session_id = channel.get("id", "")
|
||||||
|
else:
|
||||||
|
abm.type = MessageType.FRIEND_MESSAGE
|
||||||
|
abm.session_id = channel.get("id", "")
|
||||||
|
|
||||||
|
abm.sender = MessageMember(
|
||||||
|
user_id=user.get("id", ""),
|
||||||
|
nickname=user.get("nick", user.get("name", "")),
|
||||||
|
)
|
||||||
|
|
||||||
|
abm.self_id = login.get("user", {}).get("id", "")
|
||||||
|
|
||||||
|
content = message.get("content", "")
|
||||||
|
abm.message = await self.parse_satori_elements(content)
|
||||||
|
|
||||||
|
# parse message_str
|
||||||
|
abm.message_str = ""
|
||||||
|
for comp in abm.message:
|
||||||
|
if isinstance(comp, Plain):
|
||||||
|
abm.message_str += comp.text
|
||||||
|
|
||||||
|
# 优先使用Satori事件中的时间戳
|
||||||
|
if timestamp is not None:
|
||||||
|
abm.timestamp = timestamp
|
||||||
|
else:
|
||||||
|
abm.timestamp = int(time.time())
|
||||||
|
|
||||||
|
return abm
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"转换 Satori 消息失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def parse_satori_elements(self, content: str) -> list:
|
||||||
|
"""解析 Satori 消息元素"""
|
||||||
|
elements = []
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return elements
|
||||||
|
|
||||||
|
try:
|
||||||
|
wrapped_content = f"<root>{content}</root>"
|
||||||
|
root = ET.fromstring(wrapped_content)
|
||||||
|
await self._parse_xml_node(root, elements)
|
||||||
|
except ET.ParseError as e:
|
||||||
|
raise ValueError(f"解析 Satori 元素时发生解析错误: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# 如果没有解析到任何元素,将整个内容当作纯文本
|
||||||
|
if not elements and content.strip():
|
||||||
|
elements.append(Plain(text=content))
|
||||||
|
|
||||||
|
return elements
|
||||||
|
|
||||||
|
async def _parse_xml_node(self, node: ET.Element, elements: list) -> None:
|
||||||
|
"""递归解析 XML 节点"""
|
||||||
|
if node.text and node.text.strip():
|
||||||
|
elements.append(Plain(text=node.text))
|
||||||
|
|
||||||
|
for child in node:
|
||||||
|
tag_name = child.tag.lower()
|
||||||
|
attrs = child.attrib
|
||||||
|
|
||||||
|
if tag_name == "at":
|
||||||
|
user_id = attrs.get("id") or attrs.get("name", "")
|
||||||
|
elements.append(At(qq=user_id, name=user_id))
|
||||||
|
|
||||||
|
elif tag_name in ("img", "image"):
|
||||||
|
src = attrs.get("src", "")
|
||||||
|
if not src:
|
||||||
|
continue
|
||||||
|
if src.startswith("data:image/"):
|
||||||
|
src = src.split(",")[1]
|
||||||
|
elements.append(Image.fromBase64(src))
|
||||||
|
elif src.startswith("http"):
|
||||||
|
elements.append(Image.fromURL(src))
|
||||||
|
else:
|
||||||
|
logger.error(f"未知的图片 src 格式: {str(src)[:16]}")
|
||||||
|
|
||||||
|
elif tag_name == "file":
|
||||||
|
src = attrs.get("src", "")
|
||||||
|
name = attrs.get("name", "文件")
|
||||||
|
if src:
|
||||||
|
elements.append(File(file=src, name=name))
|
||||||
|
|
||||||
|
elif tag_name in ("audio", "record"):
|
||||||
|
src = attrs.get("src", "")
|
||||||
|
if not src:
|
||||||
|
continue
|
||||||
|
if src.startswith("data:audio/"):
|
||||||
|
src = src.split(",")[1]
|
||||||
|
elements.append(Record.fromBase64(src))
|
||||||
|
elif src.startswith("http"):
|
||||||
|
elements.append(Record.fromURL(src))
|
||||||
|
else:
|
||||||
|
logger.error(f"未知的音频 src 格式: {str(src)[:16]}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 未知标签,递归处理其内容
|
||||||
|
if child.text and child.text.strip():
|
||||||
|
elements.append(Plain(text=child.text))
|
||||||
|
await self._parse_xml_node(child, elements)
|
||||||
|
|
||||||
|
# 处理标签后的文本
|
||||||
|
if child.tail and child.tail.strip():
|
||||||
|
elements.append(Plain(text=child.tail))
|
||||||
|
|
||||||
|
async def handle_msg(self, message: AstrBotMessage):
|
||||||
|
from .satori_event import SatoriPlatformEvent
|
||||||
|
|
||||||
|
message_event = SatoriPlatformEvent(
|
||||||
|
message_str=message.message_str,
|
||||||
|
message_obj=message,
|
||||||
|
platform_meta=self.meta(),
|
||||||
|
session_id=message.session_id,
|
||||||
|
adapter=self,
|
||||||
|
)
|
||||||
|
self.commit_event(message_event)
|
||||||
|
|
||||||
|
async def send_http_request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
data: dict | None = None,
|
||||||
|
platform: str | None = None,
|
||||||
|
user_id: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
if not self.session:
|
||||||
|
raise Exception("HTTP session 未初始化")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.token:
|
||||||
|
headers["Authorization"] = f"Bearer {self.token}"
|
||||||
|
|
||||||
|
if platform and user_id:
|
||||||
|
headers["satori-platform"] = platform
|
||||||
|
headers["satori-user-id"] = user_id
|
||||||
|
elif self.logins:
|
||||||
|
current_login = self.logins[0]
|
||||||
|
headers["satori-platform"] = current_login.get("platform", "")
|
||||||
|
user = current_login.get("user", {})
|
||||||
|
headers["satori-user-id"] = user.get("id", "") if user else ""
|
||||||
|
|
||||||
|
if not path.startswith("/"):
|
||||||
|
path = "/" + path
|
||||||
|
|
||||||
|
# 使用新的API地址配置
|
||||||
|
url = f"{self.api_base_url.rstrip('/')}{path}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self.session.request(
|
||||||
|
method, url, json=data, headers=headers
|
||||||
|
) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
result = await response.json()
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori HTTP 请求异常: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def terminate(self):
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
if self.heartbeat_task:
|
||||||
|
self.heartbeat_task.cancel()
|
||||||
|
|
||||||
|
if self.ws:
|
||||||
|
try:
|
||||||
|
await self.ws.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori WebSocket 关闭异常: {e}")
|
||||||
|
|
||||||
|
if self.session:
|
||||||
|
await self.session.close()
|
||||||
221
astrbot/core/platform/sources/satori/satori_event.py
Normal file
221
astrbot/core/platform/sources/satori/satori_event.py
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from astrbot.api import logger
|
||||||
|
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||||
|
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||||
|
from astrbot.api.message_components import Plain, Image, At, File, Record
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .satori_adapter import SatoriPlatformAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class SatoriPlatformEvent(AstrMessageEvent):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message_str: str,
|
||||||
|
message_obj: AstrBotMessage,
|
||||||
|
platform_meta: PlatformMetadata,
|
||||||
|
session_id: str,
|
||||||
|
adapter: "SatoriPlatformAdapter",
|
||||||
|
):
|
||||||
|
super().__init__(message_str, message_obj, platform_meta, session_id)
|
||||||
|
self.adapter = adapter
|
||||||
|
self.platform = None
|
||||||
|
self.user_id = None
|
||||||
|
if (
|
||||||
|
hasattr(message_obj, "raw_message")
|
||||||
|
and message_obj.raw_message
|
||||||
|
and isinstance(message_obj.raw_message, dict)
|
||||||
|
):
|
||||||
|
login = message_obj.raw_message.get("login", {})
|
||||||
|
self.platform = login.get("platform")
|
||||||
|
user = login.get("user", {})
|
||||||
|
self.user_id = user.get("id") if user else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def send_with_adapter(
|
||||||
|
cls, adapter: "SatoriPlatformAdapter", message: MessageChain, session_id: str
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
content_parts = []
|
||||||
|
|
||||||
|
for component in message.chain:
|
||||||
|
if isinstance(component, Plain):
|
||||||
|
text = (
|
||||||
|
component.text.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
)
|
||||||
|
content_parts.append(text)
|
||||||
|
|
||||||
|
elif isinstance(component, At):
|
||||||
|
if component.qq:
|
||||||
|
content_parts.append(f'<at id="{component.qq}"/>')
|
||||||
|
elif component.name:
|
||||||
|
content_parts.append(f'<at name="{component.name}"/>')
|
||||||
|
|
||||||
|
elif isinstance(component, Image):
|
||||||
|
try:
|
||||||
|
image_base64 = await component.convert_to_base64()
|
||||||
|
if image_base64:
|
||||||
|
content_parts.append(
|
||||||
|
f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"图片转换为base64失败: {e}")
|
||||||
|
|
||||||
|
elif isinstance(component, File):
|
||||||
|
content_parts.append(
|
||||||
|
f'<file src="{component.file}" name="{component.name or "文件"}"/>'
|
||||||
|
)
|
||||||
|
|
||||||
|
elif isinstance(component, Record):
|
||||||
|
try:
|
||||||
|
record_base64 = await component.convert_to_base64()
|
||||||
|
if record_base64:
|
||||||
|
content_parts.append(
|
||||||
|
f'<audio src="data:audio/wav;base64,{record_base64}"/>'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"语音转换为base64失败: {e}")
|
||||||
|
|
||||||
|
content = "".join(content_parts)
|
||||||
|
channel_id = session_id
|
||||||
|
data = {"channel_id": channel_id, "content": content}
|
||||||
|
|
||||||
|
platform = None
|
||||||
|
user_id = None
|
||||||
|
|
||||||
|
if hasattr(adapter, "logins") and adapter.logins:
|
||||||
|
current_login = adapter.logins[0]
|
||||||
|
platform = current_login.get("platform", "")
|
||||||
|
user = current_login.get("user", {})
|
||||||
|
user_id = user.get("id", "") if user else ""
|
||||||
|
|
||||||
|
result = await adapter.send_http_request(
|
||||||
|
"POST", "/message.create", data, platform, user_id
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori 消息发送异常: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def send(self, message: MessageChain):
|
||||||
|
platform = getattr(self, "platform", None)
|
||||||
|
user_id = getattr(self, "user_id", None)
|
||||||
|
|
||||||
|
if not platform or not user_id:
|
||||||
|
if hasattr(self.adapter, "logins") and self.adapter.logins:
|
||||||
|
current_login = self.adapter.logins[0]
|
||||||
|
platform = current_login.get("platform", "")
|
||||||
|
user = current_login.get("user", {})
|
||||||
|
user_id = user.get("id", "") if user else ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_parts = []
|
||||||
|
|
||||||
|
for component in message.chain:
|
||||||
|
if isinstance(component, Plain):
|
||||||
|
text = (
|
||||||
|
component.text.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
)
|
||||||
|
content_parts.append(text)
|
||||||
|
|
||||||
|
elif isinstance(component, At):
|
||||||
|
if component.qq:
|
||||||
|
content_parts.append(f'<at id="{component.qq}"/>')
|
||||||
|
elif component.name:
|
||||||
|
content_parts.append(f'<at name="{component.name}"/>')
|
||||||
|
|
||||||
|
elif isinstance(component, Image):
|
||||||
|
try:
|
||||||
|
image_base64 = await component.convert_to_base64()
|
||||||
|
if image_base64:
|
||||||
|
content_parts.append(
|
||||||
|
f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"图片转换为base64失败: {e}")
|
||||||
|
|
||||||
|
elif isinstance(component, File):
|
||||||
|
content_parts.append(
|
||||||
|
f'<file src="{component.file}" name="{component.name or "文件"}"/>'
|
||||||
|
)
|
||||||
|
|
||||||
|
elif isinstance(component, Record):
|
||||||
|
try:
|
||||||
|
record_base64 = await component.convert_to_base64()
|
||||||
|
if record_base64:
|
||||||
|
content_parts.append(
|
||||||
|
f'<audio src="data:audio/wav;base64,{record_base64}"/>'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"语音转换为base64失败: {e}")
|
||||||
|
|
||||||
|
content = "".join(content_parts)
|
||||||
|
channel_id = self.session_id
|
||||||
|
data = {"channel_id": channel_id, "content": content}
|
||||||
|
|
||||||
|
result = await self.adapter.send_http_request(
|
||||||
|
"POST", "/message.create", data, platform, user_id
|
||||||
|
)
|
||||||
|
if not result:
|
||||||
|
logger.error("Satori 消息发送失败")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori 消息发送异常: {e}")
|
||||||
|
|
||||||
|
await super().send(message)
|
||||||
|
|
||||||
|
async def send_streaming(self, generator, use_fallback: bool = False):
|
||||||
|
try:
|
||||||
|
content_parts = []
|
||||||
|
|
||||||
|
async for chain in generator:
|
||||||
|
if isinstance(chain, MessageChain):
|
||||||
|
if chain.type == "break":
|
||||||
|
if content_parts:
|
||||||
|
content = "".join(content_parts)
|
||||||
|
temp_chain = MessageChain([Plain(text=content)])
|
||||||
|
await self.send(temp_chain)
|
||||||
|
content_parts = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
for component in chain.chain:
|
||||||
|
if isinstance(component, Plain):
|
||||||
|
content_parts.append(component.text)
|
||||||
|
elif isinstance(component, Image):
|
||||||
|
if content_parts:
|
||||||
|
content = "".join(content_parts)
|
||||||
|
temp_chain = MessageChain([Plain(text=content)])
|
||||||
|
await self.send(temp_chain)
|
||||||
|
content_parts = []
|
||||||
|
try:
|
||||||
|
image_base64 = await component.convert_to_base64()
|
||||||
|
if image_base64:
|
||||||
|
img_chain = MessageChain(
|
||||||
|
[
|
||||||
|
Plain(
|
||||||
|
text=f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
await self.send(img_chain)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"图片转换为base64失败: {e}")
|
||||||
|
else:
|
||||||
|
content_parts.append(str(component))
|
||||||
|
|
||||||
|
if content_parts:
|
||||||
|
content = "".join(content_parts)
|
||||||
|
temp_chain = MessageChain([Plain(text=content)])
|
||||||
|
await self.send(temp_chain)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Satori 流式消息发送异常: {e}")
|
||||||
|
|
||||||
|
return await super().send_streaming(generator, use_fallback)
|
||||||
@@ -183,7 +183,6 @@ class TelegramPlatformAdapter(Platform):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if not re.match(r"^[a-z0-9_]+$", cmd_name) or len(cmd_name) > 32:
|
if not re.match(r"^[a-z0-9_]+$", cmd_name) or len(cmd_name) > 32:
|
||||||
logger.debug(f"跳过无法注册的命令: {cmd_name}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Build description.
|
# Build description.
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import json
|
|||||||
from astrbot.core.utils.io import download_image_by_url
|
from astrbot.core.utils.io import download_image_by_url
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List, Dict, Type
|
from typing import List, Dict, Type, Any
|
||||||
from astrbot.core.agent.tool import ToolSet
|
from astrbot.core.agent.tool import ToolSet
|
||||||
from openai.types.chat.chat_completion import ChatCompletion
|
from openai.types.chat.chat_completion import ChatCompletion
|
||||||
|
from google.genai.types import GenerateContentResponse
|
||||||
|
from anthropic.types import Message
|
||||||
from openai.types.chat.chat_completion_message_tool_call import (
|
from openai.types.chat.chat_completion_message_tool_call import (
|
||||||
ChatCompletionMessageToolCall,
|
ChatCompletionMessageToolCall,
|
||||||
)
|
)
|
||||||
@@ -30,11 +32,11 @@ class ProviderMetaData:
|
|||||||
desc: str = ""
|
desc: str = ""
|
||||||
"""提供商适配器描述."""
|
"""提供商适配器描述."""
|
||||||
provider_type: ProviderType = ProviderType.CHAT_COMPLETION
|
provider_type: ProviderType = ProviderType.CHAT_COMPLETION
|
||||||
cls_type: Type = None
|
cls_type: Type | None = None
|
||||||
|
|
||||||
default_config_tmpl: dict = None
|
default_config_tmpl: dict | None = None
|
||||||
"""平台的默认配置模板"""
|
"""平台的默认配置模板"""
|
||||||
provider_display_name: str = None
|
provider_display_name: str | None = None
|
||||||
"""显示在 WebUI 配置页中的提供商名称,如空则是 type"""
|
"""显示在 WebUI 配置页中的提供商名称,如空则是 type"""
|
||||||
|
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ class ToolCallMessageSegment:
|
|||||||
class AssistantMessageSegment:
|
class AssistantMessageSegment:
|
||||||
"""OpenAI 格式的上下文中 role 为 assistant 的消息段。参考: https://platform.openai.com/docs/guides/function-calling"""
|
"""OpenAI 格式的上下文中 role 为 assistant 的消息段。参考: https://platform.openai.com/docs/guides/function-calling"""
|
||||||
|
|
||||||
content: str = None
|
content: str | None = None
|
||||||
tool_calls: List[ChatCompletionMessageToolCall | Dict] = field(default_factory=list)
|
tool_calls: List[ChatCompletionMessageToolCall | Dict] = field(default_factory=list)
|
||||||
role: str = "assistant"
|
role: str = "assistant"
|
||||||
|
|
||||||
@@ -205,17 +207,17 @@ class ProviderRequest:
|
|||||||
class LLMResponse:
|
class LLMResponse:
|
||||||
role: str
|
role: str
|
||||||
"""角色, assistant, tool, err"""
|
"""角色, assistant, tool, err"""
|
||||||
result_chain: MessageChain = None
|
result_chain: MessageChain | None = None
|
||||||
"""返回的消息链"""
|
"""返回的消息链"""
|
||||||
tools_call_args: List[Dict[str, any]] = field(default_factory=list)
|
tools_call_args: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
"""工具调用参数"""
|
"""工具调用参数"""
|
||||||
tools_call_name: List[str] = field(default_factory=list)
|
tools_call_name: List[str] = field(default_factory=list)
|
||||||
"""工具调用名称"""
|
"""工具调用名称"""
|
||||||
tools_call_ids: List[str] = field(default_factory=list)
|
tools_call_ids: List[str] = field(default_factory=list)
|
||||||
"""工具调用 ID"""
|
"""工具调用 ID"""
|
||||||
|
|
||||||
raw_completion: ChatCompletion = None
|
raw_completion: ChatCompletion | GenerateContentResponse | Message | None = None
|
||||||
_new_record: Dict[str, any] = None
|
_new_record: Dict[str, Any] | None = None
|
||||||
|
|
||||||
_completion_text: str = ""
|
_completion_text: str = ""
|
||||||
|
|
||||||
@@ -226,12 +228,12 @@ class LLMResponse:
|
|||||||
self,
|
self,
|
||||||
role: str,
|
role: str,
|
||||||
completion_text: str = "",
|
completion_text: str = "",
|
||||||
result_chain: MessageChain = None,
|
result_chain: MessageChain | None = None,
|
||||||
tools_call_args: List[Dict[str, any]] = None,
|
tools_call_args: List[Dict[str, Any]] | None = None,
|
||||||
tools_call_name: List[str] = None,
|
tools_call_name: List[str] | None = None,
|
||||||
tools_call_ids: List[str] = None,
|
tools_call_ids: List[str] | None = None,
|
||||||
raw_completion: ChatCompletion = None,
|
raw_completion: ChatCompletion | None = None,
|
||||||
_new_record: Dict[str, any] = None,
|
_new_record: Dict[str, Any] | None = None,
|
||||||
is_chunk: bool = False,
|
is_chunk: bool = False,
|
||||||
):
|
):
|
||||||
"""初始化 LLMResponse
|
"""初始化 LLMResponse
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from astrbot import logger
|
|||||||
from astrbot.api.provider import Provider
|
from astrbot.api.provider import Provider
|
||||||
from astrbot.core.message.message_event_result import MessageChain
|
from astrbot.core.message.message_event_result import MessageChain
|
||||||
from astrbot.core.provider.entities import LLMResponse
|
from astrbot.core.provider.entities import LLMResponse
|
||||||
from astrbot.core.provider.func_tool_manager import FuncCall
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
||||||
from astrbot.core.utils.io import download_image_by_url
|
from astrbot.core.utils.io import download_image_by_url
|
||||||
|
|
||||||
from ..register import register_provider_adapter
|
from ..register import register_provider_adapter
|
||||||
@@ -61,7 +61,7 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
default_persona,
|
default_persona,
|
||||||
)
|
)
|
||||||
self.api_keys: list = provider_config.get("key", [])
|
self.api_keys: list = provider_config.get("key", [])
|
||||||
self.chosen_api_key: str = self.api_keys[0] if len(self.api_keys) > 0 else None
|
self.chosen_api_key: str = self.api_keys[0] if len(self.api_keys) > 0 else ""
|
||||||
self.timeout: int = int(provider_config.get("timeout", 180))
|
self.timeout: int = int(provider_config.get("timeout", 180))
|
||||||
|
|
||||||
self.api_base: Optional[str] = provider_config.get("api_base", None)
|
self.api_base: Optional[str] = provider_config.get("api_base", None)
|
||||||
@@ -96,6 +96,9 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
|
|
||||||
async def _handle_api_error(self, e: APIError, keys: list[str]) -> bool:
|
async def _handle_api_error(self, e: APIError, keys: list[str]) -> bool:
|
||||||
"""处理API错误,返回是否需要重试"""
|
"""处理API错误,返回是否需要重试"""
|
||||||
|
if e.message is None:
|
||||||
|
e.message = ""
|
||||||
|
|
||||||
if e.code == 429 or "API key not valid" in e.message:
|
if e.code == 429 or "API key not valid" in e.message:
|
||||||
keys.remove(self.chosen_api_key)
|
keys.remove(self.chosen_api_key)
|
||||||
if len(keys) > 0:
|
if len(keys) > 0:
|
||||||
@@ -119,7 +122,7 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
async def _prepare_query_config(
|
async def _prepare_query_config(
|
||||||
self,
|
self,
|
||||||
payloads: dict,
|
payloads: dict,
|
||||||
tools: Optional[FuncCall] = None,
|
tools: Optional[ToolSet] = None,
|
||||||
system_instruction: Optional[str] = None,
|
system_instruction: Optional[str] = None,
|
||||||
modalities: Optional[list[str]] = None,
|
modalities: Optional[list[str]] = None,
|
||||||
temperature: float = 0.7,
|
temperature: float = 0.7,
|
||||||
@@ -321,11 +324,15 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _process_content_parts(
|
def _process_content_parts(
|
||||||
result: types.GenerateContentResponse, llm_response: LLMResponse
|
candidate: types.Candidate, llm_response: LLMResponse
|
||||||
) -> MessageChain:
|
) -> MessageChain:
|
||||||
"""处理内容部分并构建消息链"""
|
"""处理内容部分并构建消息链"""
|
||||||
finish_reason = result.candidates[0].finish_reason
|
if not candidate.content:
|
||||||
result_parts: Optional[types.Part] = result.candidates[0].content.parts
|
logger.warning(f"收到的 candidate.content 为空: {candidate}")
|
||||||
|
raise Exception("API 返回的 candidate.content 为空。")
|
||||||
|
|
||||||
|
finish_reason = candidate.finish_reason
|
||||||
|
result_parts: list[types.Part] | None = candidate.content.parts
|
||||||
|
|
||||||
if finish_reason == types.FinishReason.SAFETY:
|
if finish_reason == types.FinishReason.SAFETY:
|
||||||
raise Exception("模型生成内容未通过 Gemini 平台的安全检查")
|
raise Exception("模型生成内容未通过 Gemini 平台的安全检查")
|
||||||
@@ -343,22 +350,28 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
raise Exception("模型生成内容违反 Gemini 平台政策")
|
raise Exception("模型生成内容违反 Gemini 平台政策")
|
||||||
|
|
||||||
if not result_parts:
|
if not result_parts:
|
||||||
logger.debug(result.candidates)
|
logger.warning(f"收到的 candidate.content.parts 为空: {candidate}")
|
||||||
raise Exception("API 返回的内容为空。")
|
raise Exception("API 返回的 candidate.content.parts 为空。")
|
||||||
|
|
||||||
chain = []
|
chain = []
|
||||||
part: types.Part
|
part: types.Part
|
||||||
|
|
||||||
# 暂时这样Fallback
|
# 暂时这样Fallback
|
||||||
if all(
|
if all(
|
||||||
part.inline_data and part.inline_data.mime_type.startswith("image/")
|
part.inline_data
|
||||||
|
and part.inline_data.mime_type
|
||||||
|
and part.inline_data.mime_type.startswith("image/")
|
||||||
for part in result_parts
|
for part in result_parts
|
||||||
):
|
):
|
||||||
chain.append(Comp.Plain("这是图片"))
|
chain.append(Comp.Plain("这是图片"))
|
||||||
for part in result_parts:
|
for part in result_parts:
|
||||||
if part.text:
|
if part.text:
|
||||||
chain.append(Comp.Plain(part.text))
|
chain.append(Comp.Plain(part.text))
|
||||||
elif part.function_call:
|
elif (
|
||||||
|
part.function_call
|
||||||
|
and part.function_call.name is not None
|
||||||
|
and part.function_call.args is not None
|
||||||
|
):
|
||||||
llm_response.role = "tool"
|
llm_response.role = "tool"
|
||||||
llm_response.tools_call_name.append(part.function_call.name)
|
llm_response.tools_call_name.append(part.function_call.name)
|
||||||
llm_response.tools_call_args.append(part.function_call.args)
|
llm_response.tools_call_args.append(part.function_call.args)
|
||||||
@@ -366,11 +379,16 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
llm_response.tools_call_ids.append(
|
llm_response.tools_call_ids.append(
|
||||||
part.function_call.id or part.function_call.name
|
part.function_call.id or part.function_call.name
|
||||||
)
|
)
|
||||||
elif part.inline_data and part.inline_data.mime_type.startswith("image/"):
|
elif (
|
||||||
|
part.inline_data
|
||||||
|
and part.inline_data.mime_type
|
||||||
|
and part.inline_data.mime_type.startswith("image/")
|
||||||
|
and part.inline_data.data
|
||||||
|
):
|
||||||
chain.append(Comp.Image.fromBytes(part.inline_data.data))
|
chain.append(Comp.Image.fromBytes(part.inline_data.data))
|
||||||
return MessageChain(chain=chain)
|
return MessageChain(chain=chain)
|
||||||
|
|
||||||
async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
|
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
|
||||||
"""非流式请求 Gemini API"""
|
"""非流式请求 Gemini API"""
|
||||||
system_instruction = next(
|
system_instruction = next(
|
||||||
(msg["content"] for msg in payloads["messages"] if msg["role"] == "system"),
|
(msg["content"] for msg in payloads["messages"] if msg["role"] == "system"),
|
||||||
@@ -396,6 +414,10 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
config=config,
|
config=config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not result.candidates:
|
||||||
|
logger.error(f"请求失败, 返回的 candidates 为空: {result}")
|
||||||
|
raise Exception("请求失败, 返回的 candidates 为空。")
|
||||||
|
|
||||||
if result.candidates[0].finish_reason == types.FinishReason.RECITATION:
|
if result.candidates[0].finish_reason == types.FinishReason.RECITATION:
|
||||||
if temperature > 2:
|
if temperature > 2:
|
||||||
raise Exception("温度参数已超过最大值2,仍然发生recitation")
|
raise Exception("温度参数已超过最大值2,仍然发生recitation")
|
||||||
@@ -408,6 +430,8 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
break
|
break
|
||||||
|
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
|
if e.message is None:
|
||||||
|
e.message = ""
|
||||||
if "Developer instruction is not enabled" in e.message:
|
if "Developer instruction is not enabled" in e.message:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)"
|
f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)"
|
||||||
@@ -432,11 +456,13 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
|
|
||||||
llm_response = LLMResponse("assistant")
|
llm_response = LLMResponse("assistant")
|
||||||
llm_response.raw_completion = result
|
llm_response.raw_completion = result
|
||||||
llm_response.result_chain = self._process_content_parts(result, llm_response)
|
llm_response.result_chain = self._process_content_parts(
|
||||||
|
result.candidates[0], llm_response
|
||||||
|
)
|
||||||
return llm_response
|
return llm_response
|
||||||
|
|
||||||
async def _query_stream(
|
async def _query_stream(
|
||||||
self, payloads: dict, tools: FuncCall
|
self, payloads: dict, tools: ToolSet | None
|
||||||
) -> AsyncGenerator[LLMResponse, None]:
|
) -> AsyncGenerator[LLMResponse, None]:
|
||||||
"""流式请求 Gemini API"""
|
"""流式请求 Gemini API"""
|
||||||
system_instruction = next(
|
system_instruction = next(
|
||||||
@@ -459,6 +485,8 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
|
if e.message is None:
|
||||||
|
e.message = ""
|
||||||
if "Developer instruction is not enabled" in e.message:
|
if "Developer instruction is not enabled" in e.message:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)"
|
f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)"
|
||||||
@@ -478,13 +506,20 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
async for chunk in result:
|
async for chunk in result:
|
||||||
llm_response = LLMResponse("assistant", is_chunk=True)
|
llm_response = LLMResponse("assistant", is_chunk=True)
|
||||||
|
|
||||||
|
if not chunk.candidates:
|
||||||
|
logger.warning(f"收到的 chunk 中 candidates 为空: {chunk}")
|
||||||
|
continue
|
||||||
|
if not chunk.candidates[0].content:
|
||||||
|
logger.warning(f"收到的 chunk 中 content 为空: {chunk}")
|
||||||
|
continue
|
||||||
|
|
||||||
if chunk.candidates[0].content.parts and any(
|
if chunk.candidates[0].content.parts and any(
|
||||||
part.function_call for part in chunk.candidates[0].content.parts
|
part.function_call for part in chunk.candidates[0].content.parts
|
||||||
):
|
):
|
||||||
llm_response = LLMResponse("assistant", is_chunk=False)
|
llm_response = LLMResponse("assistant", is_chunk=False)
|
||||||
llm_response.raw_completion = chunk
|
llm_response.raw_completion = chunk
|
||||||
llm_response.result_chain = self._process_content_parts(
|
llm_response.result_chain = self._process_content_parts(
|
||||||
chunk, llm_response
|
chunk.candidates[0], llm_response
|
||||||
)
|
)
|
||||||
yield llm_response
|
yield llm_response
|
||||||
return
|
return
|
||||||
@@ -500,7 +535,7 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
final_response = LLMResponse("assistant", is_chunk=False)
|
final_response = LLMResponse("assistant", is_chunk=False)
|
||||||
final_response.raw_completion = chunk
|
final_response.raw_completion = chunk
|
||||||
final_response.result_chain = self._process_content_parts(
|
final_response.result_chain = self._process_content_parts(
|
||||||
chunk, final_response
|
chunk.candidates[0], final_response
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -566,6 +601,8 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
|
|
||||||
|
raise Exception("请求失败。")
|
||||||
|
|
||||||
async def text_chat_stream(
|
async def text_chat_stream(
|
||||||
self,
|
self,
|
||||||
prompt,
|
prompt,
|
||||||
@@ -621,7 +658,9 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
return [
|
return [
|
||||||
m.name.replace("models/", "")
|
m.name.replace("models/", "")
|
||||||
for m in models
|
for m in models
|
||||||
if "generateContent" in m.supported_actions
|
if m.supported_actions
|
||||||
|
and "generateContent" in m.supported_actions
|
||||||
|
and m.name
|
||||||
]
|
]
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
raise Exception(f"获取模型列表失败: {e.message}")
|
raise Exception(f"获取模型列表失败: {e.message}")
|
||||||
@@ -636,7 +675,7 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
self.chosen_api_key = key
|
self.chosen_api_key = key
|
||||||
self._init_client()
|
self._init_client()
|
||||||
|
|
||||||
async def assemble_context(self, text: str, image_urls: list[str] = None):
|
async def assemble_context(self, text: str, image_urls: list[str] | None = None):
|
||||||
"""
|
"""
|
||||||
组装上下文。
|
组装上下文。
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
del payloads[key]
|
del payloads[key]
|
||||||
|
|
||||||
model = payloads.get("model", "")
|
model = payloads.get("model", "")
|
||||||
# 针对 qwen3 模型的特殊处理:非流式调用必须设置 enable_thinking=false
|
# 针对 qwen3 非 thinking 模型的特殊处理:非流式调用必须设置 enable_thinking=false
|
||||||
if "qwen3" in model.lower():
|
if "qwen3" in model.lower() and "thinking" not in model.lower():
|
||||||
extra_body["enable_thinking"] = False
|
extra_body["enable_thinking"] = False
|
||||||
# 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
|
# 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
|
||||||
elif model == "deepseek-reasoner" and "tools" in payloads:
|
elif model == "deepseek-reasoner" and "tools" in payloads:
|
||||||
del payloads["tools"]
|
del payloads["tools"]
|
||||||
|
|||||||
@@ -27,14 +27,16 @@ class Star(CommandParserMixin):
|
|||||||
star_map[cls.__module__].star_cls_type = cls
|
star_map[cls.__module__].star_cls_type = cls
|
||||||
star_map[cls.__module__].module_path = cls.__module__
|
star_map[cls.__module__].module_path = cls.__module__
|
||||||
|
|
||||||
@staticmethod
|
async def text_to_image(self, text: str, return_url=True) -> str:
|
||||||
async def text_to_image(text: str, return_url=True) -> str:
|
|
||||||
"""将文本转换为图片"""
|
"""将文本转换为图片"""
|
||||||
return await html_renderer.render_t2i(text, return_url=return_url)
|
return await html_renderer.render_t2i(
|
||||||
|
text,
|
||||||
|
return_url=return_url,
|
||||||
|
template_name=self.context._config.get("t2i_active_template"),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def html_render(
|
async def html_render(
|
||||||
tmpl: str, data: dict, return_url=True, options: dict | None = None
|
self, tmpl: str, data: dict, return_url=True, options: dict | None = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""渲染 HTML"""
|
"""渲染 HTML"""
|
||||||
return await html_renderer.render_custom_template(
|
return await html_renderer.render_custom_template(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class PlatformAdapterType(enum.Flag):
|
|||||||
KOOK = enum.auto()
|
KOOK = enum.auto()
|
||||||
VOCECHAT = enum.auto()
|
VOCECHAT = enum.auto()
|
||||||
WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
|
WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
|
||||||
|
SATORI = enum.auto()
|
||||||
ALL = (
|
ALL = (
|
||||||
AIOCQHTTP
|
AIOCQHTTP
|
||||||
| QQOFFICIAL
|
| QQOFFICIAL
|
||||||
@@ -31,6 +32,7 @@ class PlatformAdapterType(enum.Flag):
|
|||||||
| KOOK
|
| KOOK
|
||||||
| VOCECHAT
|
| VOCECHAT
|
||||||
| WEIXIN_OFFICIAL_ACCOUNT
|
| WEIXIN_OFFICIAL_ACCOUNT
|
||||||
|
| SATORI
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ ADAPTER_NAME_2_TYPE = {
|
|||||||
"wechatpadpro": PlatformAdapterType.WECHATPADPRO,
|
"wechatpadpro": PlatformAdapterType.WECHATPADPRO,
|
||||||
"vocechat": PlatformAdapterType.VOCECHAT,
|
"vocechat": PlatformAdapterType.VOCECHAT,
|
||||||
"weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
|
"weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
|
||||||
|
"satori": PlatformAdapterType.SATORI,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,37 @@
|
|||||||
|
"""
|
||||||
|
插件开发工具集
|
||||||
|
封装了许多常用的操作,方便插件开发者使用
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
主动发送消息: send_message(session, message_chain)
|
||||||
|
根据 session (unified_msg_origin) 主动发送消息, 前提是需要提前获得或构造 session
|
||||||
|
|
||||||
|
根据id直接主动发送消息: send_message_by_id(type, id, message_chain, platform="aiocqhttp")
|
||||||
|
根据 id (例如 qq 号, 群号等) 直接, 主动地发送消息
|
||||||
|
|
||||||
|
以上两种方式需要构造消息链, 也就是消息组件的列表
|
||||||
|
|
||||||
|
构造事件:
|
||||||
|
|
||||||
|
首先需要构造一个 AstrBotMessage 对象, 使用 create_message 方法
|
||||||
|
然后使用 create_event 方法提交事件到指定平台
|
||||||
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Union, Awaitable, List, Optional, ClassVar
|
from typing import Union, Awaitable, List, Optional, ClassVar
|
||||||
from astrbot.core.message.components import BaseMessageComponent
|
from astrbot.core.message.components import BaseMessageComponent
|
||||||
from astrbot.core.message.message_event_result import MessageChain
|
from astrbot.core.message.message_event_result import MessageChain
|
||||||
from astrbot.api.platform import MessageMember, AstrBotMessage
|
from astrbot.api.platform import MessageMember, AstrBotMessage, MessageType
|
||||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||||
from astrbot.core.star.context import Context
|
from astrbot.core.star.context import Context
|
||||||
from astrbot.core.star.star import star_map
|
from astrbot.core.star.star import star_map
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
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
|
||||||
|
|
||||||
class StarTools:
|
class StarTools:
|
||||||
"""
|
"""
|
||||||
@@ -49,42 +71,76 @@ class StarTools:
|
|||||||
Note:
|
Note:
|
||||||
qq_official(QQ官方API平台)不支持此方法
|
qq_official(QQ官方API平台)不支持此方法
|
||||||
"""
|
"""
|
||||||
|
if cls._context is None:
|
||||||
|
raise ValueError("StarTools not initialized")
|
||||||
return await cls._context.send_message(session, message_chain)
|
return await cls._context.send_message(session, message_chain)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def send_message_by_id(
|
||||||
|
cls, type: str, id: str, message_chain: MessageChain, platform: str = "aiocqhttp"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
根据 id(例如qq号, 群号等) 直接, 主动地发送消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (str): 消息类型, 可选: PrivateMessage, GroupMessage
|
||||||
|
id (str): 目标ID, 例如QQ号, 群号等
|
||||||
|
message_chain (MessageChain): 消息链
|
||||||
|
platform (str): 可选的平台名称,默认平台(aiocqhttp), 目前只支持 aiocqhttp
|
||||||
|
"""
|
||||||
|
if cls._context is None:
|
||||||
|
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)
|
||||||
|
if adapter is None:
|
||||||
|
raise ValueError("未找到适配器: AiocqhttpAdapter")
|
||||||
|
await AiocqhttpMessageEvent.send_message(
|
||||||
|
bot=adapter.bot,
|
||||||
|
message_chain=message_chain,
|
||||||
|
is_group=(type == "GroupMessage"),
|
||||||
|
session_id=id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的平台: {platform}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_message(
|
async def create_message(
|
||||||
cls,
|
cls,
|
||||||
type: str,
|
type: str,
|
||||||
self_id: str,
|
self_id: str,
|
||||||
session_id: str,
|
session_id: str,
|
||||||
message_id: str,
|
|
||||||
sender: MessageMember,
|
sender: MessageMember,
|
||||||
message: List[BaseMessageComponent],
|
message: List[BaseMessageComponent],
|
||||||
message_str: str,
|
message_str: str,
|
||||||
raw_message: object,
|
message_id: str = "",
|
||||||
group_id: str = "",
|
raw_message: object = None,
|
||||||
):
|
group_id: str = ""
|
||||||
|
) -> AstrBotMessage:
|
||||||
"""
|
"""
|
||||||
创建一个AstrBot消息对象
|
创建一个AstrBot消息对象
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
type (str): 消息类型
|
type (str): 消息类型, 例如 "GroupMessage" "FriendMessage" "OtherMessage"
|
||||||
self_id (str): 机器人自身ID
|
self_id (str): 机器人自身ID
|
||||||
session_id (str): 会话ID(通常为用户ID)(QQ号, 群号等)
|
session_id (str): 会话ID(通常为用户ID)(QQ号, 群号等)
|
||||||
message_id (str): 消息ID
|
sender (MessageMember): 发送者信息, 例如 MessageMember(user_id="123456", nickname="昵称")
|
||||||
sender (MessageMember): 发送者信息
|
message (List[BaseMessageComponent]): 消息组件列表, 也就是消息链, 这个不会发给 llm, 但是会经过其他处理
|
||||||
message (List[BaseMessageComponent]): 消息组件列表
|
message_str (str): 消息字符串, 也就是纯文本消息, 也就是发送给 llm 的消息, 与消息链一致
|
||||||
message_str (str): 消息字符串
|
|
||||||
raw_message (object): 原始消息对象
|
message_id (str): 消息ID, 构造消息时可以随意填写也可不填
|
||||||
|
raw_message (object): 原始消息对象, 可以随意填写也可不填
|
||||||
group_id (str, optional): 群组ID, 如果为私聊则为空. Defaults to "".
|
group_id (str, optional): 群组ID, 如果为私聊则为空. Defaults to "".
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
AstrBotMessage: 创建的消息对象
|
AstrBotMessage: 创建的消息对象
|
||||||
"""
|
"""
|
||||||
abm = AstrBotMessage()
|
abm = AstrBotMessage()
|
||||||
abm.type = type
|
abm.type = MessageType(type)
|
||||||
abm.self_id = self_id
|
abm.self_id = self_id
|
||||||
abm.session_id = session_id
|
abm.session_id = session_id
|
||||||
|
if message_id == "":
|
||||||
|
message_id = uuid.uuid4().hex
|
||||||
abm.message_id = message_id
|
abm.message_id = message_id
|
||||||
abm.sender = sender
|
abm.sender = sender
|
||||||
abm.message = message
|
abm.message = message
|
||||||
@@ -93,13 +149,38 @@ class StarTools:
|
|||||||
abm.group_id = group_id
|
abm.group_id = group_id
|
||||||
return abm
|
return abm
|
||||||
|
|
||||||
# todo: 添加构造事件的方法
|
@classmethod
|
||||||
# async def create_event(
|
async def create_event(
|
||||||
# self, platform: str, umo: str, sender_id: str, session_id: str
|
cls, abm: AstrBotMessage, platform: str = "aiocqhttp", is_wake: bool = True
|
||||||
# ):
|
|
||||||
# platform = self._context.get_platform(platform)
|
|
||||||
|
|
||||||
# todo: 添加找到对应平台并提交对应事件的方法
|
) -> None:
|
||||||
|
"""
|
||||||
|
创建并提交事件到指定平台
|
||||||
|
当有需要创建一个事件, 触发某些处理流程时, 使用该方法
|
||||||
|
|
||||||
|
Args:
|
||||||
|
abm (AstrBotMessage): 要提交的消息对象, 请先使用 create_message 创建
|
||||||
|
platform (str): 可选的平台名称,默认平台(aiocqhttp), 目前只支持 aiocqhttp
|
||||||
|
is_wake (bool): 是否标记为唤醒事件, 默认为 True, 只有唤醒事件才会被 llm 响应
|
||||||
|
"""
|
||||||
|
if cls._context is None:
|
||||||
|
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)
|
||||||
|
if adapter is None:
|
||||||
|
raise ValueError("未找到适配器: AiocqhttpAdapter")
|
||||||
|
event = AiocqhttpMessageEvent(
|
||||||
|
message_str=abm.message_str,
|
||||||
|
message_obj=abm,
|
||||||
|
platform_meta=adapter.metadata,
|
||||||
|
session_id=abm.session_id,
|
||||||
|
bot=adapter.bot,
|
||||||
|
)
|
||||||
|
event.is_wake = is_wake
|
||||||
|
adapter.commit_event(event)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的平台: {platform}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def activate_llm_tool(cls, name: str) -> bool:
|
def activate_llm_tool(cls, name: str) -> bool:
|
||||||
@@ -110,6 +191,8 @@ class StarTools:
|
|||||||
Args:
|
Args:
|
||||||
name (str): 工具名称
|
name (str): 工具名称
|
||||||
"""
|
"""
|
||||||
|
if cls._context is None:
|
||||||
|
raise ValueError("StarTools not initialized")
|
||||||
return cls._context.activate_llm_tool(name)
|
return cls._context.activate_llm_tool(name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -120,6 +203,8 @@ class StarTools:
|
|||||||
Args:
|
Args:
|
||||||
name (str): 工具名称
|
name (str): 工具名称
|
||||||
"""
|
"""
|
||||||
|
if cls._context is None:
|
||||||
|
raise ValueError("StarTools not initialized")
|
||||||
return cls._context.deactivate_llm_tool(name)
|
return cls._context.deactivate_llm_tool(name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -135,6 +220,8 @@ class StarTools:
|
|||||||
desc (str): 工具描述
|
desc (str): 工具描述
|
||||||
func_obj (Awaitable): 函数对象,必须是异步函数
|
func_obj (Awaitable): 函数对象,必须是异步函数
|
||||||
"""
|
"""
|
||||||
|
if cls._context is None:
|
||||||
|
raise ValueError("StarTools not initialized")
|
||||||
cls._context.register_llm_tool(name, func_args, desc, func_obj)
|
cls._context.register_llm_tool(name, func_args, desc, func_obj)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -146,6 +233,8 @@ class StarTools:
|
|||||||
Args:
|
Args:
|
||||||
name (str): 工具名称
|
name (str): 工具名称
|
||||||
"""
|
"""
|
||||||
|
if cls._context is None:
|
||||||
|
raise ValueError("StarTools not initialized")
|
||||||
cls._context.unregister_llm_tool(name)
|
cls._context.unregister_llm_tool(name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -169,8 +258,11 @@ class StarTools:
|
|||||||
- 创建目录失败(权限不足或其他IO错误)
|
- 创建目录失败(权限不足或其他IO错误)
|
||||||
"""
|
"""
|
||||||
if not plugin_name:
|
if not plugin_name:
|
||||||
frame = inspect.currentframe().f_back
|
frame = inspect.currentframe()
|
||||||
module = inspect.getmodule(frame)
|
module = None
|
||||||
|
if frame:
|
||||||
|
frame = frame.f_back
|
||||||
|
module = inspect.getmodule(frame)
|
||||||
|
|
||||||
if not module:
|
if not module:
|
||||||
raise RuntimeError("无法获取调用者模块信息")
|
raise RuntimeError("无法获取调用者模块信息")
|
||||||
@@ -182,6 +274,9 @@ class StarTools:
|
|||||||
|
|
||||||
plugin_name = metadata.name
|
plugin_name = metadata.name
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ class AstrBotUpdator(RepoZipUpdator):
|
|||||||
try:
|
try:
|
||||||
if "astrbot" in os.path.basename(sys.argv[0]): # 兼容cli
|
if "astrbot" in os.path.basename(sys.argv[0]): # 兼容cli
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
args = [
|
args = [f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]]
|
||||||
f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args)
|
os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args)
|
||||||
@@ -68,9 +66,13 @@ class AstrBotUpdator(RepoZipUpdator):
|
|||||||
logger.error(f"重启失败({py}, {e}),请尝试手动重启。")
|
logger.error(f"重启失败({py}, {e}),请尝试手动重启。")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def check_update(self, url: str, current_version: str) -> ReleaseInfo:
|
async def check_update(
|
||||||
|
self, url: str, current_version: str, consider_prerelease: bool = True
|
||||||
|
) -> ReleaseInfo:
|
||||||
"""检查更新"""
|
"""检查更新"""
|
||||||
return await super().check_update(self.ASTRBOT_RELEASE_API, VERSION)
|
return await super().check_update(
|
||||||
|
self.ASTRBOT_RELEASE_API, VERSION, consider_prerelease
|
||||||
|
)
|
||||||
|
|
||||||
async def get_releases(self) -> list:
|
async def get_releases(self) -> list:
|
||||||
return await self.fetch_release_info(self.ASTRBOT_RELEASE_API)
|
return await self.fetch_release_info(self.ASTRBOT_RELEASE_API)
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ async def download_dashboard(
|
|||||||
path = os.path.join(get_astrbot_data_path(), "dashboard.zip")
|
path = os.path.join(get_astrbot_data_path(), "dashboard.zip")
|
||||||
|
|
||||||
if latest or len(str(version)) != 40:
|
if latest or len(str(version)) != 40:
|
||||||
logger.info("准备下载最新发行版本的 AstrBot WebUI")
|
logger.info(f"准备下载 {version} 发行版本的 AstrBot WebUI 文件")
|
||||||
ver_name = "latest" if latest else version
|
ver_name = "latest" if latest else version
|
||||||
dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip"
|
dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip"
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
|
||||||
import ssl
|
import ssl
|
||||||
import certifi
|
import certifi
|
||||||
import logging
|
import logging
|
||||||
@@ -8,10 +7,9 @@ import random
|
|||||||
from . import RenderStrategy
|
from . import RenderStrategy
|
||||||
from astrbot.core.config import VERSION
|
from astrbot.core.config import VERSION
|
||||||
from astrbot.core.utils.io import download_image_by_url
|
from astrbot.core.utils.io import download_image_by_url
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.t2i.template_manager import TemplateManager
|
||||||
|
|
||||||
ASTRBOT_T2I_DEFAULT_ENDPOINT = "https://t2i.soulter.top/text2img"
|
ASTRBOT_T2I_DEFAULT_ENDPOINT = "https://t2i.soulter.top/text2img"
|
||||||
CUSTOM_T2I_TEMPLATE_PATH = os.path.join(get_astrbot_data_path(), "t2i_template.html")
|
|
||||||
|
|
||||||
logger = logging.getLogger("astrbot")
|
logger = logging.getLogger("astrbot")
|
||||||
|
|
||||||
@@ -23,26 +21,17 @@ class NetworkRenderStrategy(RenderStrategy):
|
|||||||
self.BASE_RENDER_URL = ASTRBOT_T2I_DEFAULT_ENDPOINT
|
self.BASE_RENDER_URL = ASTRBOT_T2I_DEFAULT_ENDPOINT
|
||||||
else:
|
else:
|
||||||
self.BASE_RENDER_URL = self._clean_url(base_url)
|
self.BASE_RENDER_URL = self._clean_url(base_url)
|
||||||
self.TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "template", "base.html")
|
|
||||||
with open(self.TEMPLATE_PATH, "r", encoding="utf-8") as f:
|
|
||||||
self.DEFAULT_TEMPLATE = f.read()
|
|
||||||
|
|
||||||
self.endpoints = [self.BASE_RENDER_URL]
|
self.endpoints = [self.BASE_RENDER_URL]
|
||||||
|
self.template_manager = TemplateManager()
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
if self.BASE_RENDER_URL == ASTRBOT_T2I_DEFAULT_ENDPOINT:
|
if self.BASE_RENDER_URL == ASTRBOT_T2I_DEFAULT_ENDPOINT:
|
||||||
asyncio.create_task(self.get_official_endpoints())
|
asyncio.create_task(self.get_official_endpoints())
|
||||||
|
|
||||||
async def get_template(self) -> str:
|
async def get_template(self, name: str = "base") -> str:
|
||||||
"""获取文转图 HTML 模板
|
"""通过名称获取文转图 HTML 模板"""
|
||||||
|
return self.template_manager.get_template(name)
|
||||||
Returns:
|
|
||||||
str: 文转图 HTML 模板字符串
|
|
||||||
"""
|
|
||||||
if os.path.exists(CUSTOM_T2I_TEMPLATE_PATH):
|
|
||||||
with open(CUSTOM_T2I_TEMPLATE_PATH, "r", encoding="utf-8") as f:
|
|
||||||
return f.read()
|
|
||||||
return self.DEFAULT_TEMPLATE
|
|
||||||
|
|
||||||
async def get_official_endpoints(self):
|
async def get_official_endpoints(self):
|
||||||
"""获取官方的 t2i 端点列表。"""
|
"""获取官方的 t2i 端点列表。"""
|
||||||
@@ -124,11 +113,15 @@ class NetworkRenderStrategy(RenderStrategy):
|
|||||||
logger.error(f"All endpoints failed: {last_exception}")
|
logger.error(f"All endpoints failed: {last_exception}")
|
||||||
raise RuntimeError(f"All endpoints failed: {last_exception}")
|
raise RuntimeError(f"All endpoints failed: {last_exception}")
|
||||||
|
|
||||||
async def render(self, text: str, return_url: bool = False) -> str:
|
async def render(
|
||||||
|
self, text: str, return_url: bool = False, template_name: str | None = "base"
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
返回图像的文件路径
|
返回图像的文件路径
|
||||||
"""
|
"""
|
||||||
tmpl_str = await self.get_template()
|
if not template_name:
|
||||||
|
template_name = "base"
|
||||||
|
tmpl_str = await self.get_template(name=template_name)
|
||||||
text = text.replace("`", "\\`")
|
text = text.replace("`", "\\`")
|
||||||
return await self.render_custom_template(
|
return await self.render_custom_template(
|
||||||
tmpl_str, {"text": text, "version": f"v{VERSION}"}, return_url
|
tmpl_str, {"text": text, "version": f"v{VERSION}"}, return_url
|
||||||
|
|||||||
@@ -34,12 +34,18 @@ class HtmlRenderer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def render_t2i(
|
async def render_t2i(
|
||||||
self, text: str, use_network: bool = True, return_url: bool = False
|
self,
|
||||||
|
text: str,
|
||||||
|
use_network: bool = True,
|
||||||
|
return_url: bool = False,
|
||||||
|
template_name: str | None = None,
|
||||||
):
|
):
|
||||||
"""使用默认文转图模板。"""
|
"""使用默认文转图模板。"""
|
||||||
if use_network:
|
if use_network:
|
||||||
try:
|
try:
|
||||||
return await self.network_strategy.render(text, return_url=return_url)
|
return await self.network_strategy.render(
|
||||||
|
text, return_url=return_url, template_name=template_name
|
||||||
|
)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Failed to render image via AstrBot API: {e}. Falling back to local rendering."
|
f"Failed to render image via AstrBot API: {e}. Falling back to local rendering."
|
||||||
|
|||||||
184
astrbot/core/utils/t2i/template/astrbot_powershell.html
Normal file
184
astrbot/core/utils/t2i/template/astrbot_powershell.html
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>Astrbot PowerShell {{ version }} </title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/common.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>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #010409;
|
||||||
|
--text-color: #e6edf3;
|
||||||
|
--title-bar-color: #161b22;
|
||||||
|
--title-text-color: #e6edf3;
|
||||||
|
--font-family: 'Consolas', 'Microsoft YaHei Mono', 'Dengxian Mono', 'Courier New', monospace;
|
||||||
|
--glow-color: rgba(200, 220, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanline {
|
||||||
|
0% {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 18px;
|
||||||
|
/* The CRT glow effect from the image */
|
||||||
|
text-shadow: 0 0 15px var(--glow-color), 0 0 7px rgba(255, 255, 255, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.3) 50%);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: scanline 8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: var(--title-bar-color);
|
||||||
|
padding: 12px 18px;
|
||||||
|
color: var(--title-text-color);
|
||||||
|
font-size: 16px;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
text-shadow: none; /* No glow for title bar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .version {
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
/* min-width and max-width removed as per request */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Markdown Styles adjusted for terminal look --- */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
h1 { font-size: 2rem; }
|
||||||
|
h2 { font-size: 1.7rem; }
|
||||||
|
h3 { font-size: 1.4rem; }
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
display: block;
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px dashed #30363d;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 90%;
|
||||||
|
background-color: #161b22;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #0d1117;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
text-shadow: none; /* Disable glow inside code blocks for clarity */
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #58a6ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #30363d;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
color: #8b949e;
|
||||||
|
background-color: #161b22;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<span class="title">> Astrbot PowerShell</span>
|
||||||
|
<span class="version">{{ version }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div id="content"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.getElementById('content').innerHTML = marked.parse(`{{ text | safe }}`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
247
astrbot/core/utils/t2i/template/default_template.html.bak
Normal file
247
astrbot/core/utils/t2i/template/default_template.html.bak
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<!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>
|
||||||
95
astrbot/core/utils/t2i/template_manager.py
Normal file
95
astrbot/core/utils/t2i/template_manager.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# astrbot/core/utils/t2i/template_manager.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateManager:
|
||||||
|
"""
|
||||||
|
负责管理 t2i HTML 模板的 CRUD 和重置操作。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 修正路径拼接,加入缺失的 'astrbot' 目录
|
||||||
|
self.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)
|
||||||
|
|
||||||
|
# 检查模板目录中是否有 .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()
|
||||||
|
|
||||||
|
def _get_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")
|
||||||
|
|
||||||
|
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("模板不存在。")
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def create_template(self, name: str, content: str):
|
||||||
|
"""创建一个新的模板文件。"""
|
||||||
|
path = self._get_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("模板不存在。")
|
||||||
|
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)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
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)
|
||||||
@@ -107,16 +107,38 @@ class RepoZipUpdator:
|
|||||||
"""Semver 版本比较"""
|
"""Semver 版本比较"""
|
||||||
return VersionComparator.compare_version(v1, v2)
|
return VersionComparator.compare_version(v1, v2)
|
||||||
|
|
||||||
async def check_update(self, url: str, current_version: str) -> ReleaseInfo | None:
|
async def check_update(
|
||||||
|
self, url: str, current_version: str, consider_prerelease: bool = True
|
||||||
|
) -> ReleaseInfo | None:
|
||||||
update_data = await self.fetch_release_info(url)
|
update_data = await self.fetch_release_info(url)
|
||||||
tag_name = update_data[0]["tag_name"]
|
|
||||||
|
sel_release_data = None
|
||||||
|
if consider_prerelease:
|
||||||
|
tag_name = update_data[0]["tag_name"]
|
||||||
|
sel_release_data = update_data[0]
|
||||||
|
else:
|
||||||
|
for data in update_data:
|
||||||
|
# 跳过带有 alpha、beta 等预发布标签的版本
|
||||||
|
if re.search(
|
||||||
|
r"[\-_.]?(alpha|beta|rc|dev)[\-_.]?\d*$",
|
||||||
|
data["tag_name"],
|
||||||
|
re.IGNORECASE,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
tag_name = data["tag_name"]
|
||||||
|
sel_release_data = data
|
||||||
|
break
|
||||||
|
|
||||||
|
if not sel_release_data or not tag_name:
|
||||||
|
logger.error("未找到合适的发布版本")
|
||||||
|
return None
|
||||||
|
|
||||||
if self.compare_version(current_version, tag_name) >= 0:
|
if self.compare_version(current_version, tag_name) >= 0:
|
||||||
return None
|
return None
|
||||||
return ReleaseInfo(
|
return ReleaseInfo(
|
||||||
version=tag_name,
|
version=tag_name,
|
||||||
published_at=update_data[0]["published_at"],
|
published_at=sel_release_data["published_at"],
|
||||||
body=update_data[0]["body"],
|
body=f"{tag_name}\n\n{sel_release_data['body']}",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def download_from_repo_url(self, target_path: str, repo_url: str, proxy=""):
|
async def download_from_repo_url(self, target_path: str, repo_url: str, proxy=""):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import typing
|
import typing
|
||||||
import traceback
|
import traceback
|
||||||
import os
|
import os
|
||||||
|
import copy
|
||||||
from .route import Route, Response, RouteContext
|
from .route import Route, Response, RouteContext
|
||||||
from astrbot.core.provider.entities import ProviderType
|
from astrbot.core.provider.entities import ProviderType
|
||||||
from quart import request
|
from quart import request
|
||||||
@@ -16,10 +17,10 @@ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|||||||
from astrbot.core.platform.register import platform_registry
|
from astrbot.core.platform.register import platform_registry
|
||||||
from astrbot.core.provider.register import provider_registry
|
from astrbot.core.provider.register import provider_registry
|
||||||
from astrbot.core.star.star import star_registry
|
from astrbot.core.star.star import star_registry
|
||||||
from astrbot.core import logger, html_renderer
|
from astrbot.core import logger
|
||||||
from astrbot.core.provider import Provider
|
from astrbot.core.provider import Provider
|
||||||
|
from astrbot.core.provider.provider import RerankProvider
|
||||||
import asyncio
|
import asyncio
|
||||||
from astrbot.core.utils.t2i.network_strategy import CUSTOM_T2I_TEMPLATE_PATH
|
|
||||||
|
|
||||||
|
|
||||||
def try_cast(value: str, type_: str):
|
def try_cast(value: str, type_: str):
|
||||||
@@ -155,6 +156,7 @@ def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False)
|
|||||||
raise ValueError(f"验证配置时出现异常: {e}")
|
raise ValueError(f"验证配置时出现异常: {e}")
|
||||||
if errors:
|
if errors:
|
||||||
raise ValueError(f"格式校验未通过: {errors}")
|
raise ValueError(f"格式校验未通过: {errors}")
|
||||||
|
|
||||||
config.save_config(post_config)
|
config.save_config(post_config)
|
||||||
|
|
||||||
|
|
||||||
@@ -185,56 +187,9 @@ class ConfigRoute(Route):
|
|||||||
"/config/provider/check_one": ("GET", self.check_one_provider_status),
|
"/config/provider/check_one": ("GET", self.check_one_provider_status),
|
||||||
"/config/provider/list": ("GET", self.get_provider_config_list),
|
"/config/provider/list": ("GET", self.get_provider_config_list),
|
||||||
"/config/provider/model_list": ("GET", self.get_provider_model_list),
|
"/config/provider/model_list": ("GET", self.get_provider_model_list),
|
||||||
"/config/astrbot/t2i-template/get": ("GET", self.get_t2i_template),
|
|
||||||
"/config/astrbot/t2i-template/save": ("POST", self.post_t2i_template),
|
|
||||||
"/config/astrbot/t2i-template/delete": ("DELETE", self.delete_t2i_template),
|
|
||||||
}
|
}
|
||||||
self.register_routes()
|
self.register_routes()
|
||||||
|
|
||||||
async def get_t2i_template(self):
|
|
||||||
"""获取 T2I 模板"""
|
|
||||||
try:
|
|
||||||
template = await html_renderer.network_strategy.get_template()
|
|
||||||
has_custom_template = os.path.exists(CUSTOM_T2I_TEMPLATE_PATH)
|
|
||||||
return (
|
|
||||||
Response()
|
|
||||||
.ok({"template": template, "has_custom_template": has_custom_template})
|
|
||||||
.__dict__
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
return Response().error(f"获取模板失败: {str(e)}").__dict__
|
|
||||||
|
|
||||||
async def post_t2i_template(self):
|
|
||||||
"""保存 T2I 模板"""
|
|
||||||
try:
|
|
||||||
post_data = await request.json
|
|
||||||
if not post_data or "template" not in post_data:
|
|
||||||
return Response().error("缺少模板内容").__dict__
|
|
||||||
|
|
||||||
template_content = post_data["template"]
|
|
||||||
|
|
||||||
# 保存自定义模板到文件
|
|
||||||
with open(CUSTOM_T2I_TEMPLATE_PATH, "w", encoding="utf-8") as f:
|
|
||||||
f.write(template_content)
|
|
||||||
|
|
||||||
return Response().ok(message="模板保存成功").__dict__
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
return Response().error(f"保存模板失败: {str(e)}").__dict__
|
|
||||||
|
|
||||||
async def delete_t2i_template(self):
|
|
||||||
"""删除自定义 T2I 模板,恢复默认模板"""
|
|
||||||
try:
|
|
||||||
if os.path.exists(CUSTOM_T2I_TEMPLATE_PATH):
|
|
||||||
os.remove(CUSTOM_T2I_TEMPLATE_PATH)
|
|
||||||
return Response().ok(message="已恢复默认模板").__dict__
|
|
||||||
else:
|
|
||||||
return Response().ok(message="未找到自定义模板文件").__dict__
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
return Response().error(f"删除模板失败: {str(e)}").__dict__
|
|
||||||
|
|
||||||
async def get_abconf_list(self):
|
async def get_abconf_list(self):
|
||||||
"""获取所有 AstrBot 配置文件的列表"""
|
"""获取所有 AstrBot 配置文件的列表"""
|
||||||
abconf_list = self.acm.get_conf_list()
|
abconf_list = self.acm.get_conf_list()
|
||||||
@@ -481,6 +436,19 @@ class ConfigRoute(Route):
|
|||||||
)
|
)
|
||||||
status_info["status"] = "unavailable"
|
status_info["status"] = "unavailable"
|
||||||
status_info["error"] = f"STT test failed: {str(e)}"
|
status_info["error"] = f"STT test failed: {str(e)}"
|
||||||
|
elif provider_capability_type == ProviderType.RERANK:
|
||||||
|
try:
|
||||||
|
assert isinstance(provider, RerankProvider)
|
||||||
|
await provider.rerank("Apple", documents=["apple", "banana"])
|
||||||
|
status_info["status"] = "available"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error testing rerank provider {provider_name}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
status_info["status"] = "unavailable"
|
||||||
|
status_info["error"] = f"Rerank test failed: {str(e)}"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}"
|
f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}"
|
||||||
@@ -752,6 +720,13 @@ class ConfigRoute(Route):
|
|||||||
if conf_id not in self.acm.confs:
|
if conf_id not in self.acm.confs:
|
||||||
raise ValueError(f"配置文件 {conf_id} 不存在")
|
raise ValueError(f"配置文件 {conf_id} 不存在")
|
||||||
astrbot_config = self.acm.confs[conf_id]
|
astrbot_config = self.acm.confs[conf_id]
|
||||||
|
|
||||||
|
# 保留服务端的 t2i_active_template 值
|
||||||
|
if "t2i_active_template" in astrbot_config:
|
||||||
|
post_configs["t2i_active_template"] = astrbot_config[
|
||||||
|
"t2i_active_template"
|
||||||
|
]
|
||||||
|
|
||||||
save_config(post_configs, astrbot_config, is_core=True)
|
save_config(post_configs, astrbot_config, is_core=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from astrbot.core import logger
|
||||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from quart import Quart
|
from quart import Quart
|
||||||
@@ -15,8 +16,24 @@ class Route:
|
|||||||
self.config = context.config
|
self.config = context.config
|
||||||
|
|
||||||
def register_routes(self):
|
def register_routes(self):
|
||||||
for route, (method, func) in self.routes.items():
|
def _add_rule(path, method, func):
|
||||||
self.app.add_url_rule(f"/api{route}", view_func=func, methods=[method])
|
# 统一添加 /api 前缀
|
||||||
|
full_path = f"/api{path}"
|
||||||
|
self.app.add_url_rule(full_path, view_func=func, methods=[method])
|
||||||
|
|
||||||
|
# 兼容字典和列表两种格式
|
||||||
|
routes_to_register = (
|
||||||
|
self.routes.items() if isinstance(self.routes, dict) else self.routes
|
||||||
|
)
|
||||||
|
|
||||||
|
for route, definition in routes_to_register:
|
||||||
|
# 兼容一个路由多个方法
|
||||||
|
if isinstance(definition, list):
|
||||||
|
for method, func in definition:
|
||||||
|
_add_rule(route, method, func)
|
||||||
|
else:
|
||||||
|
method, func = definition
|
||||||
|
_add_rule(route, method, func)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
232
astrbot/dashboard/routes/t2i.py
Normal file
232
astrbot/dashboard/routes/t2i.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# astrbot/dashboard/routes/t2i.py
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
from quart import jsonify, request
|
||||||
|
|
||||||
|
from astrbot.core import logger
|
||||||
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||||
|
from astrbot.core.utils.t2i.template_manager import TemplateManager
|
||||||
|
from .route import Response, Route, RouteContext
|
||||||
|
|
||||||
|
|
||||||
|
class T2iRoute(Route):
|
||||||
|
def __init__(self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle):
|
||||||
|
super().__init__(context)
|
||||||
|
self.core_lifecycle = core_lifecycle
|
||||||
|
self.config = core_lifecycle.astrbot_config
|
||||||
|
self.manager = TemplateManager()
|
||||||
|
# 使用列表保证路由注册顺序,避免 /<name> 路由优先匹配 /reset_default
|
||||||
|
self.routes = [
|
||||||
|
("/t2i/templates", ("GET", self.list_templates)),
|
||||||
|
("/t2i/templates/active", ("GET", self.get_active_template)),
|
||||||
|
("/t2i/templates/create", ("POST", self.create_template)),
|
||||||
|
("/t2i/templates/reset_default", ("POST", self.reset_default_template)),
|
||||||
|
("/t2i/templates/set_active", ("POST", self.set_active_template)),
|
||||||
|
# 动态路由应该在静态路由之后注册
|
||||||
|
(
|
||||||
|
"/t2i/templates/<name>",
|
||||||
|
[
|
||||||
|
("GET", self.get_template),
|
||||||
|
("PUT", self.update_template),
|
||||||
|
("DELETE", self.delete_template),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 应用启动时,确保备份存在
|
||||||
|
self.manager.backup_default_template_if_not_exist()
|
||||||
|
|
||||||
|
self.register_routes()
|
||||||
|
|
||||||
|
async def list_templates(self):
|
||||||
|
"""获取所有T2I模板列表"""
|
||||||
|
try:
|
||||||
|
templates = self.manager.list_templates()
|
||||||
|
return jsonify(asdict(Response().ok(data=templates)))
|
||||||
|
except Exception as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 500
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def get_active_template(self):
|
||||||
|
"""获取当前激活的T2I模板"""
|
||||||
|
try:
|
||||||
|
active_template = self.config.get("t2i_active_template", "base")
|
||||||
|
return jsonify(
|
||||||
|
asdict(Response().ok(data={"active_template": active_template}))
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error in get_active_template", exc_info=True)
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 500
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def get_template(self, name: str):
|
||||||
|
"""获取指定名称的T2I模板内容"""
|
||||||
|
try:
|
||||||
|
content = self.manager.get_template(name)
|
||||||
|
return jsonify(
|
||||||
|
asdict(Response().ok(data={"name": name, "content": content}))
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
response = jsonify(asdict(Response().error("Template not found")))
|
||||||
|
response.status_code = 404
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 500
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def create_template(self):
|
||||||
|
"""创建一个新的T2I模板"""
|
||||||
|
try:
|
||||||
|
data = await request.json
|
||||||
|
name = data.get("name")
|
||||||
|
content = data.get("content")
|
||||||
|
if not name or not content:
|
||||||
|
response = jsonify(
|
||||||
|
asdict(Response().error("Name and content are required."))
|
||||||
|
)
|
||||||
|
response.status_code = 400
|
||||||
|
return response
|
||||||
|
|
||||||
|
self.manager.create_template(name, content)
|
||||||
|
response = jsonify(
|
||||||
|
asdict(
|
||||||
|
Response().ok(
|
||||||
|
data={"name": name}, message="Template created successfully."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
response.status_code = 201
|
||||||
|
return response
|
||||||
|
except FileExistsError:
|
||||||
|
response = jsonify(
|
||||||
|
asdict(Response().error("Template with this name already exists."))
|
||||||
|
)
|
||||||
|
response.status_code = 409
|
||||||
|
return response
|
||||||
|
except ValueError as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 400
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 500
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def update_template(self, name: str):
|
||||||
|
"""更新一个已存在的T2I模板"""
|
||||||
|
try:
|
||||||
|
data = await request.json
|
||||||
|
content = data.get("content")
|
||||||
|
if content is None:
|
||||||
|
response = jsonify(asdict(Response().error("Content is required.")))
|
||||||
|
response.status_code = 400
|
||||||
|
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
|
||||||
|
except ValueError as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 400
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 500
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def delete_template(self, name: str):
|
||||||
|
"""删除一个T2I模板"""
|
||||||
|
try:
|
||||||
|
self.manager.delete_template(name)
|
||||||
|
return jsonify(
|
||||||
|
asdict(Response().ok(message="Template deleted successfully."))
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
response = jsonify(asdict(Response().error("Template not found.")))
|
||||||
|
response.status_code = 404
|
||||||
|
return response
|
||||||
|
except ValueError as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 400
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 500
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def set_active_template(self):
|
||||||
|
"""设置当前活动的T2I模板"""
|
||||||
|
try:
|
||||||
|
data = await request.json
|
||||||
|
name = data.get("name")
|
||||||
|
if not name:
|
||||||
|
response = jsonify(asdict(Response().error("模板名称(name)不能为空。")))
|
||||||
|
response.status_code = 400
|
||||||
|
return response
|
||||||
|
|
||||||
|
# 验证模板文件是否存在
|
||||||
|
self.manager.get_template(name)
|
||||||
|
|
||||||
|
# 更新配置
|
||||||
|
config = self.config
|
||||||
|
config["t2i_active_template"] = name
|
||||||
|
config.save_config(config)
|
||||||
|
|
||||||
|
# 热重载以应用更改
|
||||||
|
await self.core_lifecycle.reload_pipeline_scheduler("default")
|
||||||
|
|
||||||
|
return jsonify(asdict(Response().ok(message=f"模板 '{name}' 已成功应用。")))
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
response = jsonify(
|
||||||
|
asdict(Response().error(f"模板 '{name}' 不存在,无法应用。"))
|
||||||
|
)
|
||||||
|
response.status_code = 404
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error in set_active_template", exc_info=True)
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 500
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def reset_default_template(self):
|
||||||
|
"""重置默认的'base'模板"""
|
||||||
|
try:
|
||||||
|
self.manager.reset_default_template()
|
||||||
|
|
||||||
|
# 更新配置,将激活模板也重置为'base'
|
||||||
|
config = self.config
|
||||||
|
config["t2i_active_template"] = "base"
|
||||||
|
config.save_config(config)
|
||||||
|
|
||||||
|
# 热重载以应用更改
|
||||||
|
await self.core_lifecycle.reload_pipeline_scheduler("default")
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
asdict(
|
||||||
|
Response().ok(
|
||||||
|
message="Default template has been reset and activated."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 404
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error in reset_default_template", exc_info=True)
|
||||||
|
response = jsonify(asdict(Response().error(str(e))))
|
||||||
|
response.status_code = 500
|
||||||
|
return response
|
||||||
@@ -57,7 +57,7 @@ class UpdateRoute(Route):
|
|||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ret = await self.astrbot_updator.check_update(None, None)
|
ret = await self.astrbot_updator.check_update(None, None, False)
|
||||||
return Response(
|
return Response(
|
||||||
status="success",
|
status="success",
|
||||||
message=str(ret) if ret is not None else "已经是最新版本了。",
|
message=str(ret) if ret is not None else "已经是最新版本了。",
|
||||||
@@ -100,9 +100,7 @@ class UpdateRoute(Route):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await download_dashboard(
|
await download_dashboard(latest=latest, version=version, proxy=proxy)
|
||||||
latest=latest, version=version, proxy=proxy
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"下载管理面板文件失败: {e}。")
|
logger.error(f"下载管理面板文件失败: {e}。")
|
||||||
|
|
||||||
@@ -133,7 +131,7 @@ class UpdateRoute(Route):
|
|||||||
async def update_dashboard(self):
|
async def update_dashboard(self):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
await download_dashboard()
|
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"下载管理面板文件失败: {e}。")
|
logger.error(f"下载管理面板文件失败: {e}。")
|
||||||
return Response().error(f"下载管理面板文件失败: {e}").__dict__
|
return Response().error(f"下载管理面板文件失败: {e}").__dict__
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from astrbot.core.utils.io import get_local_ip_addresses
|
|||||||
from .routes import *
|
from .routes import *
|
||||||
from .routes.route import Response, RouteContext
|
from .routes.route import Response, RouteContext
|
||||||
from .routes.session_management import SessionManagementRoute
|
from .routes.session_management import SessionManagementRoute
|
||||||
|
from .routes.t2i import T2iRoute
|
||||||
|
|
||||||
APP: Quart = None
|
APP: Quart = None
|
||||||
|
|
||||||
@@ -60,9 +61,8 @@ class AstrBotDashboard:
|
|||||||
self.session_management_route = SessionManagementRoute(
|
self.session_management_route = SessionManagementRoute(
|
||||||
self.context, db, core_lifecycle
|
self.context, db, core_lifecycle
|
||||||
)
|
)
|
||||||
self.persona_route = PersonaRoute(
|
self.persona_route = PersonaRoute(self.context, db, core_lifecycle)
|
||||||
self.context, db, core_lifecycle
|
self.t2i_route = T2iRoute(self.context, core_lifecycle)
|
||||||
)
|
|
||||||
|
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
"/api/plug/<path:subpath>",
|
"/api/plug/<path:subpath>",
|
||||||
|
|||||||
5
changelogs/v3.5.27.md
Normal file
5
changelogs/v3.5.27.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# What's Changed
|
||||||
|
|
||||||
|
1. 修复:构建 docker 镜像时同时构建 webui,并放入镜像中。
|
||||||
|
2. 修复:下载 WebUI 文件时,明确版本号,以防止 latest 不一致导致下载的 WebUI 文件版本号与实际所需不符的问题。
|
||||||
|
3. 优化:优化版本检测,考虑预发布版本,移除 `更新到最新版本` 按钮
|
||||||
@@ -1 +0,0 @@
|
|||||||
# What's Changed
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# What's Changed
|
# What's Changed
|
||||||
|
|
||||||
> 请仔细阅读:**这是 v4.0.0 的测试版本(beta.1),功能尚未完全稳定和加入**。v4.0.0 被设计为向前兼容,如有任何插件兼容性问题或者其他异常请在 GitHub 提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues)。在测试版本期间,您可以无缝回退到旧版本的 AstrBot,并且数据不受影响。新版本文档请[从此](https://docs-v4.astrbot.app/)访问,直到第一个 v4.0.0 稳定版本发布。
|
> 请仔细阅读:**这是 v4.0.0 的测试版本(beta.3),功能尚未完全稳定和加入**。v4.0.0 被设计为向前兼容,如有任何插件兼容性问题或者其他异常请在 GitHub 提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues)。在测试版本期间,您可以无缝回退到旧版本的 AstrBot,并且数据不受影响。新版本文档请[从此](https://docs-v4.astrbot.app/)访问,直到第一个 v4.0.0 稳定版本发布。
|
||||||
8
changelogs/v4.0.0-beta.4.md
Normal file
8
changelogs/v4.0.0-beta.4.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# What's Changed
|
||||||
|
|
||||||
|
> 请仔细阅读:**这是 v4.0.0 的测试版本(beta.4),功能尚未完全稳定和加入**。v4.0.0 被设计为向前兼容,如有任何插件兼容性问题或者其他异常请在 GitHub 提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues)。在测试版本期间,您可以无缝回退到旧版本的 AstrBot,并且数据不受影响。新版本文档请[从此](https://docs-v4.astrbot.app/)访问,直到第一个 v4.0.0 稳定版本发布。
|
||||||
|
|
||||||
|
相较于 beta.3:
|
||||||
|
|
||||||
|
1. 修复了主动回复时报错的问题
|
||||||
|
2. 数据迁移完毕之后引导重启程序
|
||||||
15
changelogs/v4.0.0-beta.5.md
Normal file
15
changelogs/v4.0.0-beta.5.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# What's Changed
|
||||||
|
|
||||||
|
> 请仔细阅读:**这是 v4.0.0 的测试版本(beta.4),功能尚未完全稳定和加入**。v4.0.0 被设计为向前兼容,如有任何插件兼容性问题或者其他异常请在 GitHub 提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues)。在测试版本期间,您可以无缝回退到旧版本的 AstrBot,并且数据不受影响。新版本文档请[从此](https://docs-v4.astrbot.app/)访问,直到第一个 v4.0.0 稳定版本发布。
|
||||||
|
|
||||||
|
相较于 beta.4:
|
||||||
|
|
||||||
|
1. ‼️修复:新版本在初次保存配置之后,调用 LLM 无法获得响应,但插件指令仍可以使用的问题
|
||||||
|
2. 修复:部分情况下,Dashboard 内修改配置保存后报错 UnicodeDecodeError
|
||||||
|
3. 修复:构建 docker 镜像时同时构建 webui,并放入镜像中。
|
||||||
|
4. 修复:下载 WebUI 文件时,明确版本号,以防止 latest 不一致导致下载的 WebUI 文件版本号与实际所需不符的问题。
|
||||||
|
5. 优化:优化版本检测,考虑预发布版本,移除 `更新到最新版本` 按钮
|
||||||
|
6. 优化:增加 abconf_data 缓存,优化性能
|
||||||
|
7. 优化: 适配 qwen3 的 thinking 类模型
|
||||||
|
8. 优化: 完善对 rerank model 的可用性检测
|
||||||
|
9. 新增: 给添加 edge_tts 新增 rate, volume, pitch 参数 ([#2625](https://github.com/Soulter/AstrBot/issues/2625))
|
||||||
24
changelogs/v4.0.0.md
Normal file
24
changelogs/v4.0.0.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# What's Changed
|
||||||
|
|
||||||
|
> 新版本介绍和用法请看 AstrBot 官方 Blog [v4.0.0 的新变化](https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/)。
|
||||||
|
|
||||||
|
* Refactor: using sqlmodel(sqlchemy+pydantic) as ORM framework and switch to async-based sqlite operation by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2294
|
||||||
|
* Fix: 当多个相同消息平台实例部署时上下文可能混乱(共享) by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2298
|
||||||
|
* Improve: 引入全新的人格管理模式 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2305
|
||||||
|
* Feature: Add support to sync MCP servers from ModelScope by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2313
|
||||||
|
* Feature: 移除 MCP 市场相关逻辑 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2314
|
||||||
|
* Refactor: 重构配置文件管理,以支持更灵活的、会话粒度的(基于 umo part)配置文件隔离 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2328
|
||||||
|
* Feature: 增加图片转述提供商配置、支持用户自定义模型模态能力 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2422
|
||||||
|
* Feature: 优化 WebSearch 的爬取网页速度并且支持使用 Tavily 作为搜索引擎 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2427
|
||||||
|
* Feature: 添加url转知识库功能 by @RC-CHN in https://github.com/AstrBotDevs/AstrBot/pull/2280
|
||||||
|
* Feature: 添加条件显示逻辑以优化插件配置项的可见性管理 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2433
|
||||||
|
* Feature: 支持在 WebUI 配置文件页中配置默认知识库 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2437
|
||||||
|
* Feature: 重构 Function Tool 管理并初步引入 Multi Agent 及 Agent Handsoff 机制 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2454
|
||||||
|
* feat: 添加数据迁移助手以及相关迁移方法 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2477
|
||||||
|
* Refactor: 重构 SharedPreference 类并采用数据库存储替换 json 存储 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2482
|
||||||
|
* Feature: 支持配置重排序模型(vLLM API 格式)用于 score 任务 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2496
|
||||||
|
* Feature: 支持在配置文件配置可用的插件组 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2505
|
||||||
|
* Feature: llm_tool 装饰器返回值支持 mcp 库的 tool 返回值类型 (mcp.type.CallToolResult) by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2507
|
||||||
|
* Feature: 多 t2i 服务的随机负载均衡 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2529
|
||||||
|
* Improve: 扩大配置文件生效范围的自定义程度到会话粒度 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2532
|
||||||
|
* Feature: 支持可视化自定义 T2I 模版 by @Soulter in https://github.com/AstrBotDevs/AstrBot/pull/2581
|
||||||
BIN
dashboard/src/assets/images/platform_logos/satori.png
Normal file
BIN
dashboard/src/assets/images/platform_logos/satori.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -12,7 +12,13 @@
|
|||||||
<div v-if="migrationCompleted" class="text-center py-8">
|
<div v-if="migrationCompleted" class="text-center py-8">
|
||||||
<v-icon size="64" color="success" class="mb-4">mdi-check-circle</v-icon>
|
<v-icon size="64" color="success" class="mb-4">mdi-check-circle</v-icon>
|
||||||
<h3 class="mb-4">{{ t('features.migration.dialog.completed') }}</h3>
|
<h3 class="mb-4">{{ t('features.migration.dialog.completed') }}</h3>
|
||||||
{{ migrationResult?.message || t('features.migration.dialog.success') }}
|
<p class="mb-4">{{ migrationResult?.message || t('features.migration.dialog.success') }}</p>
|
||||||
|
<v-alert type="info" variant="tonal" class="mb-4">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-information</v-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('features.migration.dialog.restartRecommended') }}
|
||||||
|
</v-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="migrating" class="migration-in-progress">
|
<div v-else-if="migrating" class="migration-in-progress">
|
||||||
@@ -80,8 +86,11 @@
|
|||||||
<v-card-actions class="px-6 py-4">
|
<v-card-actions class="px-6 py-4">
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<template v-if="migrationCompleted">
|
<template v-if="migrationCompleted">
|
||||||
<v-btn color="primary" variant="elevated" @click="handleClose">
|
<v-btn color="grey" variant="text" @click="handleClose">
|
||||||
{{ t('core.common.confirm') }}
|
{{ t('core.common.close') }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="primary" variant="elevated" @click="restartAstrBot">
|
||||||
|
{{ t('features.migration.dialog.restartNow') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -96,6 +105,8 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -103,6 +114,7 @@ import { ref, computed, watch } from 'vue'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useI18n } from '@/i18n/composables'
|
import { useI18n } from '@/i18n/composables'
|
||||||
import ConsoleDisplayer from './ConsoleDisplayer.vue'
|
import ConsoleDisplayer from './ConsoleDisplayer.vue'
|
||||||
|
import WaitingForRestart from './WaitingForRestart.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -114,6 +126,7 @@ const migrationCompleted = ref(false)
|
|||||||
const migrationResult = ref(null)
|
const migrationResult = ref(null)
|
||||||
const platforms = ref([])
|
const platforms = ref([])
|
||||||
const selectedPlatforms = ref({})
|
const selectedPlatforms = ref({})
|
||||||
|
const wfr = ref(null)
|
||||||
|
|
||||||
let resolvePromise = null
|
let resolvePromise = null
|
||||||
|
|
||||||
@@ -244,6 +257,15 @@ const getPlatformLabel = (platform) => {
|
|||||||
return `${name}`
|
return `${name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重启 AstrBot
|
||||||
|
const restartAstrBot = () => {
|
||||||
|
axios.post('/api/stat/restart-core').then(() => {
|
||||||
|
if (wfr.value) {
|
||||||
|
wfr.value.check();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 打开对话框的方法
|
// 打开对话框的方法
|
||||||
const open = () => {
|
const open = () => {
|
||||||
isOpen.value = true
|
isOpen.value = true
|
||||||
|
|||||||
@@ -15,17 +15,59 @@
|
|||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="d-flex align-center justify-space-between">
|
<v-card-title class="d-flex align-center justify-space-between">
|
||||||
<span>自定义文转图 HTML 模板</span>
|
<span>自定义文转图 HTML 模板</span>
|
||||||
<div class="d-flex gap-2">
|
<v-spacer></v-spacer>
|
||||||
<v-btn
|
<div class="d-flex align-center gap-2" style="width: 60%">
|
||||||
v-if="hasCustomTemplate"
|
<v-text-field
|
||||||
|
v-if="isCreatingNew"
|
||||||
|
v-model="editingName"
|
||||||
|
label="输入新模板名称"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="warning"
|
class="flex-grow-1"
|
||||||
size="small"
|
autofocus
|
||||||
@click="resetToDefault"
|
:rules="[v => !!v || '名称不能为空']"
|
||||||
:loading="resetLoading"
|
></v-text-field>
|
||||||
|
<v-select
|
||||||
|
v-else
|
||||||
|
v-model="selectedTemplate"
|
||||||
|
:items="templates"
|
||||||
|
item-title="name"
|
||||||
|
item-value="name"
|
||||||
|
label="选择模板"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
variant="outlined"
|
||||||
|
class="flex-grow-1"
|
||||||
|
:loading="loading"
|
||||||
>
|
>
|
||||||
恢复默认
|
<template v-slot:item="{ props, item }">
|
||||||
</v-btn>
|
<v-list-item v-bind="props" :title="item.raw.name">
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-chip
|
||||||
|
v-if="item.raw.name === activeTemplate"
|
||||||
|
color="success"
|
||||||
|
variant="tonal"
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
已应用
|
||||||
|
</v-chip>
|
||||||
|
<v-btn
|
||||||
|
v-else
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
@click.stop="setActiveTemplate(item.raw.name)"
|
||||||
|
:loading="applyLoading"
|
||||||
|
>
|
||||||
|
应用
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
<v-btn
|
<v-btn
|
||||||
variant="text"
|
variant="text"
|
||||||
icon
|
icon
|
||||||
@@ -41,17 +83,49 @@
|
|||||||
<!-- 左侧编辑器 -->
|
<!-- 左侧编辑器 -->
|
||||||
<v-col cols="6" class="d-flex flex-column">
|
<v-col cols="6" class="d-flex flex-column">
|
||||||
<v-toolbar density="compact" color="surface-variant">
|
<v-toolbar density="compact" color="surface-variant">
|
||||||
<v-toolbar-title class="text-subtitle-2">HTML 模板编辑器</v-toolbar-title>
|
<v-toolbar-title class="text-subtitle-2">模板编辑器</v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn
|
<div class="d-flex align-center pa-1" style="border: 1px solid rgba(0,0,0,0.1); border-radius: 8px;">
|
||||||
variant="text"
|
<v-btn
|
||||||
size="small"
|
variant="text"
|
||||||
@click="saveTemplate"
|
size="small"
|
||||||
:loading="saveLoading"
|
@click="newTemplate"
|
||||||
color="primary"
|
color="success"
|
||||||
>
|
>
|
||||||
保存模板
|
<v-icon left>mdi-plus</v-icon>
|
||||||
</v-btn>
|
新建
|
||||||
|
</v-btn>
|
||||||
|
<v-divider vertical class="mx-1"></v-divider>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="resetToDefault"
|
||||||
|
:loading="resetLoading"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
重置Base
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="promptDelete"
|
||||||
|
color="error"
|
||||||
|
:disabled="isCreatingNew || selectedTemplate === 'base' || !selectedTemplate"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</v-btn>
|
||||||
|
<v-divider vertical class="mx-1"></v-divider>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="saveTemplate"
|
||||||
|
:loading="saveLoading"
|
||||||
|
color="primary"
|
||||||
|
:disabled="(isCreatingNew && !editingName) || (!isCreatingNew && !selectedTemplate)"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<div class="flex-grow-1" style="border-right: 1px solid rgba(0,0,0,0.1);">
|
<div class="flex-grow-1" style="border-right: 1px solid rgba(0,0,0,0.1);">
|
||||||
<VueMonacoEditor
|
<VueMonacoEditor
|
||||||
@@ -106,10 +180,11 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="saveTemplate"
|
@click="promptApplyAndClose"
|
||||||
:loading="saveLoading"
|
:loading="saveLoading"
|
||||||
|
:disabled="isCreatingNew || !selectedTemplate"
|
||||||
>
|
>
|
||||||
保存并应用
|
保存应用当前编辑模板
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@@ -121,7 +196,7 @@
|
|||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title>确认重置</v-card-title>
|
<v-card-title>确认重置</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
确定要恢复默认模板吗?这将删除您的自定义模板,此操作无法撤销。
|
确定要将 'base' 模板恢复为默认内容吗?当前编辑器中的任何未保存更改将丢失。此操作无法撤销。
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
@@ -130,6 +205,37 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<v-dialog v-model="deleteDialog" max-width="400px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>确认删除</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
确定要删除模板 '{{ selectedTemplate }}' 吗?此操作无法撤销。
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn text @click="deleteDialog = false">取消</v-btn>
|
||||||
|
<v-btn color="error" @click="confirmDelete" :loading="saveLoading">确认删除</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 保存并应用确认对话框 -->
|
||||||
|
<v-dialog v-model="applyAndCloseDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>确认操作</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
确定要保存对 '{{ selectedTemplate }}' 的修改,并将其设为新的活动模板吗?
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn text @click="applyAndCloseDialog = false">取消</v-btn>
|
||||||
|
<v-btn color="primary" @click="confirmApplyAndClose" :loading="saveLoading">确认</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -141,18 +247,30 @@ import axios from 'axios'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 响应式数据
|
// --- 响应式数据 ---
|
||||||
const dialog = ref(false)
|
const dialog = ref(false)
|
||||||
const resetDialog = ref(false)
|
const loading = ref(false) // 用于加载模板列表
|
||||||
const loading = ref(false)
|
|
||||||
const saveLoading = ref(false)
|
const saveLoading = ref(false)
|
||||||
const resetLoading = ref(false)
|
const resetLoading = ref(false)
|
||||||
const previewLoading = ref(false)
|
const previewLoading = ref(false)
|
||||||
|
const applyLoading = ref(false)
|
||||||
|
|
||||||
|
// 模板管理
|
||||||
|
const templates = ref([])
|
||||||
|
const activeTemplate = ref('base')
|
||||||
|
const selectedTemplate = ref(null)
|
||||||
|
const editingName = ref('') // 用于新建模式下的名称输入
|
||||||
const templateContent = ref('')
|
const templateContent = ref('')
|
||||||
const hasCustomTemplate = ref(false)
|
const isCreatingNew = ref(false)
|
||||||
|
|
||||||
|
// 对话框状态
|
||||||
|
const resetDialog = ref(false)
|
||||||
|
const deleteDialog = ref(false)
|
||||||
|
const applyAndCloseDialog = ref(false)
|
||||||
|
|
||||||
const previewFrame = ref(null)
|
const previewFrame = ref(null)
|
||||||
|
|
||||||
// 编辑器配置
|
// --- 编辑器配置 ---
|
||||||
const editorTheme = computed(() => 'vs-light')
|
const editorTheme = computed(() => 'vs-light')
|
||||||
const editorOptions = {
|
const editorOptions = {
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
@@ -163,16 +281,13 @@ const editorOptions = {
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 示例数据用于预览
|
// --- 预览逻辑 ---
|
||||||
const previewData = {
|
const previewData = {
|
||||||
text: '这是一个示例文本,用于预览模板效果。\n\n这里可以包含多行文本,支持换行和各种格式。',
|
text: '这是一个示例文本,用于预览模板效果。\n\n这里可以包含多行文本,支持换行和各种格式。',
|
||||||
version: 'v4.0.0'
|
version: 'v4.0.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成预览内容
|
|
||||||
const previewContent = computed(() => {
|
const previewContent = computed(() => {
|
||||||
try {
|
try {
|
||||||
// 简单的模板替换,模拟 Jinja2 渲染
|
|
||||||
let content = templateContent.value
|
let content = templateContent.value
|
||||||
content = content.replace(/\{\{\s*text\s*\|\s*safe\s*\}\}/g, previewData.text)
|
content = content.replace(/\{\{\s*text\s*\|\s*safe\s*\}\}/g, previewData.text)
|
||||||
content = content.replace(/\{\{\s*version\s*\}\}/g, previewData.version)
|
content = content.replace(/\{\{\s*version\s*\}\}/g, previewData.version)
|
||||||
@@ -182,58 +297,128 @@ const previewContent = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 方法
|
// --- API 调用方法 ---
|
||||||
const loadTemplate = async () => {
|
|
||||||
|
const loadInitialData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/config/astrbot/t2i-template/get')
|
const [listRes, activeRes] = await Promise.all([
|
||||||
if (response.data.status === 'ok') {
|
axios.get('/api/t2i/templates'),
|
||||||
templateContent.value = response.data.data.template
|
axios.get('/api/t2i/templates/active')
|
||||||
hasCustomTemplate.value = response.data.data.has_custom_template
|
])
|
||||||
|
|
||||||
|
if (listRes.data.status === 'ok') {
|
||||||
|
templates.value = listRes.data.data
|
||||||
} else {
|
} else {
|
||||||
console.error('加载模板失败:', response.data.message)
|
console.error('加载模板列表失败:', listRes.data.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeRes.data.status === 'ok') {
|
||||||
|
activeTemplate.value = activeRes.data.data.active_template
|
||||||
|
} else {
|
||||||
|
console.error('加载活动模板失败:', activeRes.data.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置初始选中的模板
|
||||||
|
if (templates.value.length > 0) {
|
||||||
|
selectedTemplate.value = activeTemplate.value
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载模板失败:', error)
|
console.error('加载初始数据失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadTemplateContent = async (name) => {
|
||||||
|
if (!name) return
|
||||||
|
previewLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/t2i/templates/${name}`)
|
||||||
|
if (response.data.status === 'ok') {
|
||||||
|
templateContent.value = response.data.data.content
|
||||||
|
} else {
|
||||||
|
console.error(`加载模板 '${name}' 失败:`, response.data.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`加载模板 '${name}' 失败:`, error)
|
||||||
|
} finally {
|
||||||
|
previewLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const saveTemplate = async () => {
|
const saveTemplate = async () => {
|
||||||
saveLoading.value = true
|
saveLoading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/config/astrbot/t2i-template/save', {
|
if (isCreatingNew.value) {
|
||||||
template: templateContent.value
|
// --- 创建新模板 ---
|
||||||
})
|
if (!editingName.value) return
|
||||||
if (response.data.status === 'ok') {
|
const response = await axios.post('/api/t2i/templates/create', {
|
||||||
hasCustomTemplate.value = true
|
name: editingName.value,
|
||||||
closeDialog()
|
content: templateContent.value
|
||||||
|
})
|
||||||
|
await loadInitialData() // 重新加载所有数据
|
||||||
|
selectedTemplate.value = response.data.data.name
|
||||||
|
isCreatingNew.value = false
|
||||||
} else {
|
} else {
|
||||||
console.error('保存模板失败:', response.data.message)
|
// --- 更新现有模板 ---
|
||||||
|
if (!selectedTemplate.value) return
|
||||||
|
await axios.put(`/api/t2i/templates/${selectedTemplate.value}`, {
|
||||||
|
content: templateContent.value
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存模板失败:', error)
|
console.error('保存模板失败:', error)
|
||||||
|
// 可以在此添加错误提示
|
||||||
} finally {
|
} finally {
|
||||||
saveLoading.value = false
|
saveLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetToDefault = () => {
|
const setActiveTemplate = async (name) => {
|
||||||
resetDialog.value = true
|
applyLoading.value = true
|
||||||
|
try {
|
||||||
|
await axios.post('/api/t2i/templates/set_active', { name })
|
||||||
|
activeTemplate.value = name
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`应用模板 '${name}' 失败:`, error)
|
||||||
|
} finally {
|
||||||
|
applyLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
if (!selectedTemplate.value || selectedTemplate.value === 'base') return
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const nameToDelete = selectedTemplate.value
|
||||||
|
await axios.delete(`/api/t2i/templates/${nameToDelete}`)
|
||||||
|
deleteDialog.value = false
|
||||||
|
|
||||||
|
// 如果删除的是当前活动模板,则将活动模板重置为base
|
||||||
|
if (activeTemplate.value === nameToDelete) {
|
||||||
|
await setActiveTemplate('base')
|
||||||
|
}
|
||||||
|
await loadInitialData()
|
||||||
|
selectedTemplate.value = 'base'
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`删除模板 '${selectedTemplate.value}' 失败:`, error)
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmReset = async () => {
|
const confirmReset = async () => {
|
||||||
resetLoading.value = true
|
resetLoading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await axios.delete('/api/config/astrbot/t2i-template/delete')
|
await axios.post('/api/t2i/templates/reset_default')
|
||||||
if (response.data.status === 'ok') {
|
resetDialog.value = false
|
||||||
hasCustomTemplate.value = false
|
if (selectedTemplate.value === 'base') {
|
||||||
resetDialog.value = false
|
await loadTemplateContent('base')
|
||||||
// 重新加载默认模板
|
}
|
||||||
await loadTemplate()
|
if (activeTemplate.value !== 'base') {
|
||||||
} else {
|
await setActiveTemplate('base')
|
||||||
console.error('重置模板失败:', response.data.message)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重置模板失败:', error)
|
console.error('重置模板失败:', error)
|
||||||
@@ -242,15 +427,58 @@ const confirmReset = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- UI 交互方法 ---
|
||||||
|
|
||||||
|
const resetToDefault = () => {
|
||||||
|
resetDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTemplate = () => {
|
||||||
|
isCreatingNew.value = true
|
||||||
|
selectedTemplate.value = null
|
||||||
|
editingName.value = ''
|
||||||
|
templateContent.value = `<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>New Template</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 从这里开始编辑 -->
|
||||||
|
<article>{{ text | safe }}</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptDelete = () => {
|
||||||
|
if (selectedTemplate.value && selectedTemplate.value !== 'base') {
|
||||||
|
deleteDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptApplyAndClose = () => {
|
||||||
|
if (!isCreatingNew.value && selectedTemplate.value) {
|
||||||
|
applyAndCloseDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmApplyAndClose = async () => {
|
||||||
|
if (isCreatingNew.value) return
|
||||||
|
|
||||||
|
await saveTemplate()
|
||||||
|
await setActiveTemplate(selectedTemplate.value)
|
||||||
|
applyAndCloseDialog.value = false
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
const refreshPreview = () => {
|
const refreshPreview = () => {
|
||||||
previewLoading.value = true
|
previewLoading.value = true
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (previewFrame.value) {
|
if (previewFrame.value) {
|
||||||
previewFrame.value.contentWindow.location.reload()
|
previewFrame.value.contentWindow.location.reload()
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => previewLoading.value = false, 500)
|
||||||
previewLoading.value = false
|
|
||||||
}, 500)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,18 +486,29 @@ const closeDialog = () => {
|
|||||||
dialog.value = false
|
dialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 监听器和生命周期 ---
|
||||||
|
|
||||||
watch(dialog, (newVal) => {
|
watch(dialog, (newVal) => {
|
||||||
if (newVal && !templateContent.value) {
|
if (newVal) {
|
||||||
loadTemplate()
|
loadInitialData()
|
||||||
|
} else {
|
||||||
|
// 关闭时重置状态
|
||||||
|
selectedTemplate.value = null
|
||||||
|
templateContent.value = ''
|
||||||
|
isCreatingNew.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedTemplate, (newName) => {
|
||||||
|
if (newName) {
|
||||||
|
isCreatingNew.value = false
|
||||||
|
loadTemplateContent(newName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openDialog: () => {
|
openDialog: () => {
|
||||||
dialog.value = true
|
dialog.value = true
|
||||||
if (!templateContent.value) {
|
|
||||||
loadTemplate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"tip": "💡 TIP:",
|
"tip": "💡 TIP:",
|
||||||
"tipLink": "",
|
"tipLink": "",
|
||||||
"tipContinue": "By default, the corresponding version of the WebUI files will be downloaded when switching versions. The WebUI code is located in the dashboard directory of the project, and you can use npm to build it yourself.",
|
"tipContinue": "By default, the corresponding version of the WebUI files will be downloaded when switching versions. The WebUI code is located in the dashboard directory of the project, and you can use npm to build it yourself.",
|
||||||
"dockerTip": "The `Update to Latest Version` button will try to update both the bot main program and the dashboard. If you are using Docker deployment, you can also re-pull the image or use",
|
"dockerTip": "When switching versions, it will try to update both the bot main program and the dashboard. If you are using Docker deployment, you can also re-pull the image or use",
|
||||||
"dockerTipLink": "watchtower",
|
"dockerTipLink": "watchtower",
|
||||||
"dockerTipContinue": "to automatically monitor and pull.",
|
"dockerTipContinue": "to automatically monitor and pull.",
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"migratingSubtitle": "Please wait patiently, do not close this window during migration",
|
"migratingSubtitle": "Please wait patiently, do not close this window during migration",
|
||||||
"migrationError": "Migration failed",
|
"migrationError": "Migration failed",
|
||||||
"success": "Migration completed successfully!",
|
"success": "Migration completed successfully!",
|
||||||
"completed": "Migration Completed"
|
"completed": "Migration Completed",
|
||||||
|
"restartRecommended": "It is recommended to restart the application for all changes to take effect.",
|
||||||
|
"restartNow": "Restart Now"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
"title": "ID Conflict Warning",
|
"title": "ID Conflict Warning",
|
||||||
"message": "Detected duplicate ID \"{id}\". Please use a new ID.",
|
"message": "Detected duplicate ID \"{id}\". Please use a new ID.",
|
||||||
"confirm": "OK"
|
"confirm": "OK"
|
||||||
|
},
|
||||||
|
"securityWarning": {
|
||||||
|
"title": "Security Warning",
|
||||||
|
"aiocqhttpTokenMissing": "To enhance connection security, it is strongly recommended to set ws_reverse_token. Not setting a token may lead to security risks.",
|
||||||
|
"learnMore": "Learn More"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
},
|
},
|
||||||
"tip": "💡 TIP: ",
|
"tip": "💡 TIP: ",
|
||||||
"tipContinue": "默认在切换版本时会下载对应版本的 WebUI 文件。WebUI 代码位于项目的 dashboard 目录,您可使用 npm 自行构建。",
|
"tipContinue": "默认在切换版本时会下载对应版本的 WebUI 文件。WebUI 代码位于项目的 dashboard 目录,您可使用 npm 自行构建。",
|
||||||
"dockerTip": "`更新到最新版本` 按钮会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用",
|
"dockerTip": "切换版本时,会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用",
|
||||||
"dockerTipLink": "watchtower",
|
"dockerTipLink": "watchtower",
|
||||||
"dockerTipContinue": "来自动监控拉取。",
|
"dockerTipContinue": "来自动监控拉取。",
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"migratingSubtitle": "请耐心等待,迁移过程中请勿关闭此窗口",
|
"migratingSubtitle": "请耐心等待,迁移过程中请勿关闭此窗口",
|
||||||
"migrationError": "迁移失败",
|
"migrationError": "迁移失败",
|
||||||
"success": "迁移成功完成!",
|
"success": "迁移成功完成!",
|
||||||
"completed": "迁移已完成"
|
"completed": "迁移已完成",
|
||||||
|
"restartRecommended": "建议重启应用程序以使所有更改生效。",
|
||||||
|
"restartNow": "立即重启"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
"title": "ID 冲突警告",
|
"title": "ID 冲突警告",
|
||||||
"message": "检测到 ID \"{id}\" 重复。请使用一个新的 ID。",
|
"message": "检测到 ID \"{id}\" 重复。请使用一个新的 ID。",
|
||||||
"confirm": "好的"
|
"confirm": "好的"
|
||||||
|
},
|
||||||
|
"securityWarning": {
|
||||||
|
"title": "安全提醒",
|
||||||
|
"aiocqhttpTokenMissing": "为了增强连接安全性,强烈建议您设置 ws_reverse_token。未设置 Token 可能导致安全风险。",
|
||||||
|
"learnMore": "了解更多"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -378,10 +378,6 @@ commonStore.getStartTime();
|
|||||||
|
|
||||||
<!-- 发行版 -->
|
<!-- 发行版 -->
|
||||||
<v-tabs-window-item key="0" v-show="tab == 0">
|
<v-tabs-window-item key="0" v-show="tab == 0">
|
||||||
<v-btn class="mt-4 mb-4" @click="switchVersion('latest')" color="primary" style="border-radius: 10px;"
|
|
||||||
:disabled="!hasNewVersion">
|
|
||||||
{{ t('core.header.updateDialog.updateToLatest') }}
|
|
||||||
</v-btn>
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<small>{{ t('core.header.updateDialog.dockerTip') }} <a
|
<small>{{ t('core.header.updateDialog.dockerTip') }} <a
|
||||||
href="https://containrrr.dev/watchtower/usage-overview/">{{ t('core.header.updateDialog.dockerTipLink') }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
|
href="https://containrrr.dev/watchtower/usage-overview/">{{ t('core.header.updateDialog.dockerTipLink') }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
</v-select>
|
</v-select>
|
||||||
|
<a style="color: inherit;" href="https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" target="_blank"><v-btn icon="mdi-help-circle" size="small" variant="plain"></v-btn></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-btn-toggle v-model="configType" mandatory color="primary" variant="outlined" density="comfortable"
|
<v-btn-toggle v-model="configType" mandatory color="primary" variant="outlined" density="comfortable"
|
||||||
@@ -481,7 +483,7 @@ export default {
|
|||||||
if (!this.fetched) return;
|
if (!this.fetched) return;
|
||||||
|
|
||||||
const postData = {
|
const postData = {
|
||||||
config: this.config_data
|
config: JSON.parse(JSON.stringify(this.config_data)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.isSystemConfig) {
|
if (this.isSystemConfig) {
|
||||||
|
|||||||
@@ -383,8 +383,7 @@ export default {
|
|||||||
messageType: 'success',
|
messageType: 'success',
|
||||||
personaIdRules: [
|
personaIdRules: [
|
||||||
v => !!v || this.tm('validation.required'),
|
v => !!v || this.tm('validation.required'),
|
||||||
v => (v && v.length >= 2) || this.tm('validation.minLength', { min: 2 }),
|
v => (v && v.length >= 0) || this.tm('validation.minLength', { min: 2 }),
|
||||||
v => /^[a-zA-Z0-9_-]+$/.test(v) || this.tm('validation.alphanumeric')
|
|
||||||
],
|
],
|
||||||
systemPromptRules: [
|
systemPromptRules: [
|
||||||
v => !!v || this.tm('validation.required'),
|
v => !!v || this.tm('validation.required'),
|
||||||
|
|||||||
@@ -168,6 +168,28 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 安全警告对话框 -->
|
||||||
|
<v-dialog v-model="showOneBotEmptyTokenWarnDialog" max-width="600" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
{{ tm('dialog.securityWarning.title') }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="py-4">
|
||||||
|
<p>{{ tm('dialog.securityWarning.aiocqhttpTokenMissing') }}</p>
|
||||||
|
<span><a href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7" target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions class="px-4 pb-4">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="error" @click="handleOneBotEmptyTokenWarningDismiss(true)">
|
||||||
|
无视警告并继续创建
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="primary" @click="handleOneBotEmptyTokenWarningDismiss(false)">
|
||||||
|
重新修改
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -234,17 +256,27 @@ export default {
|
|||||||
conflictId: '',
|
conflictId: '',
|
||||||
idConflictResolve: null,
|
idConflictResolve: null,
|
||||||
|
|
||||||
|
// OneBot Empty Token Warning #2639
|
||||||
|
showOneBotEmptyTokenWarnDialog: false,
|
||||||
|
oneBotEmptyTokenWarningResolve: null,
|
||||||
|
|
||||||
store: useCommonStore()
|
store: useCommonStore()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
showIdConflictDialog(newValue) {
|
showIdConflictDialog(newValue) {
|
||||||
// 当对话框关闭时,如果 Promise 还在等待,则拒绝它以防止内存泄漏
|
|
||||||
if (!newValue && this.idConflictResolve) {
|
if (!newValue && this.idConflictResolve) {
|
||||||
this.idConflictResolve(false);
|
this.idConflictResolve(false);
|
||||||
this.idConflictResolve = null;
|
this.idConflictResolve = null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showOneBotEmptyTokenWarnDialog(newValue) {
|
||||||
|
if (!newValue && this.oneBotEmptyTokenWarningResolve) {
|
||||||
|
this.oneBotEmptyTokenWarningResolve(true);
|
||||||
|
this.oneBotEmptyTokenWarningResolve = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -279,6 +311,8 @@ export default {
|
|||||||
return new URL('@/assets/images/platform_logos/kook.png', import.meta.url).href
|
return new URL('@/assets/images/platform_logos/kook.png', import.meta.url).href
|
||||||
} else if (name === 'vocechat') {
|
} else if (name === 'vocechat') {
|
||||||
return new URL('@/assets/images/platform_logos/vocechat.png', import.meta.url).href
|
return new URL('@/assets/images/platform_logos/vocechat.png', import.meta.url).href
|
||||||
|
} else if (name === 'satori' || name === 'Satori') {
|
||||||
|
return new URL('@/assets/images/platform_logos/satori.png', import.meta.url).href
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -297,6 +331,7 @@ export default {
|
|||||||
"slack": "https://astrbot.app/deploy/platform/slack.html",
|
"slack": "https://astrbot.app/deploy/platform/slack.html",
|
||||||
"kook": "https://astrbot.app/deploy/platform/kook.html",
|
"kook": "https://astrbot.app/deploy/platform/kook.html",
|
||||||
"vocechat": "https://astrbot.app/deploy/platform/vocechat.html",
|
"vocechat": "https://astrbot.app/deploy/platform/vocechat.html",
|
||||||
|
"satori": "https://astrbot.app/deploy/platform/satori.html", // TODO
|
||||||
}
|
}
|
||||||
return tutorial_map[platform_type] || "https://docs.astrbot.app";
|
return tutorial_map[platform_type] || "https://docs.astrbot.app";
|
||||||
},
|
},
|
||||||
@@ -350,24 +385,39 @@ export default {
|
|||||||
newPlatform() {
|
newPlatform() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
if (this.updatingMode) {
|
if (this.updatingMode) {
|
||||||
axios.post('/api/config/platform/update', {
|
if (this.newSelectedPlatformConfig.type === 'aiocqhttp') {
|
||||||
id: this.newSelectedPlatformName,
|
const token = this.newSelectedPlatformConfig.ws_reverse_token;
|
||||||
config: this.newSelectedPlatformConfig
|
if (!token || token.trim() === '') {
|
||||||
}).then((res) => {
|
this.showOneBotEmptyTokenWarning().then((continueWithWarning) => {
|
||||||
this.loading = false;
|
if (continueWithWarning) {
|
||||||
this.showPlatformCfg = false;
|
this.updatePlatform();
|
||||||
this.getConfig();
|
}
|
||||||
this.showSuccess(res.data.message || this.messages.updateSuccess);
|
});
|
||||||
}).catch((err) => {
|
return;
|
||||||
this.loading = false;
|
}
|
||||||
this.showError(err.response?.data?.message || err.message);
|
}
|
||||||
});
|
this.updatePlatform();
|
||||||
this.updatingMode = false;
|
|
||||||
} else {
|
} else {
|
||||||
this.savePlatform();
|
this.savePlatform();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updatePlatform() {
|
||||||
|
axios.post('/api/config/platform/update', {
|
||||||
|
id: this.newSelectedPlatformName,
|
||||||
|
config: this.newSelectedPlatformConfig
|
||||||
|
}).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.showPlatformCfg = false;
|
||||||
|
this.getConfig();
|
||||||
|
this.showSuccess(res.data.message || this.messages.updateSuccess);
|
||||||
|
}).catch((err) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.showError(err.response?.data?.message || err.message);
|
||||||
|
});
|
||||||
|
this.updatingMode = false;
|
||||||
|
},
|
||||||
|
|
||||||
async savePlatform() {
|
async savePlatform() {
|
||||||
// 检查 ID 是否已存在
|
// 检查 ID 是否已存在
|
||||||
const existingPlatform = this.config_data.platform?.find(p => p.id === this.newSelectedPlatformConfig.id);
|
const existingPlatform = this.config_data.platform?.find(p => p.id === this.newSelectedPlatformConfig.id);
|
||||||
@@ -379,6 +429,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查 aiocqhttp 适配器的安全设置
|
||||||
|
if (this.newSelectedPlatformConfig.type === 'aiocqhttp') {
|
||||||
|
const token = this.newSelectedPlatformConfig.ws_reverse_token;
|
||||||
|
if (!token || token.trim() === '') {
|
||||||
|
const continueWithWarning = await this.showOneBotEmptyTokenWarning();
|
||||||
|
if (!continueWithWarning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post('/api/config/platform/new', this.newSelectedPlatformConfig);
|
const res = await axios.post('/api/config/platform/new', this.newSelectedPlatformConfig);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@@ -406,6 +467,25 @@ export default {
|
|||||||
this.showIdConflictDialog = false;
|
this.showIdConflictDialog = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showOneBotEmptyTokenWarning() {
|
||||||
|
this.showOneBotEmptyTokenWarnDialog = true;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.oneBotEmptyTokenWarningResolve = resolve;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOneBotEmptyTokenWarningDismiss(continueWithWarning) {
|
||||||
|
this.showOneBotEmptyTokenWarnDialog = false;
|
||||||
|
if (this.oneBotEmptyTokenWarningResolve) {
|
||||||
|
this.oneBotEmptyTokenWarningResolve(continueWithWarning);
|
||||||
|
this.oneBotEmptyTokenWarningResolve = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!continueWithWarning) {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
deletePlatform(platform) {
|
deletePlatform(platform) {
|
||||||
if (confirm(`${this.messages.deleteConfirm} ${platform.id}?`)) {
|
if (confirm(`${this.messages.deleteConfirm} ${platform.id}?`)) {
|
||||||
axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => {
|
axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => {
|
||||||
@@ -532,4 +612,4 @@ export default {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
6
main.py
6
main.py
@@ -44,10 +44,10 @@ async def check_dashboard_files():
|
|||||||
if v is not None:
|
if v is not None:
|
||||||
# has file
|
# has file
|
||||||
if v == f"v{VERSION}":
|
if v == f"v{VERSION}":
|
||||||
logger.info("管理面板文件已是最新。")
|
logger.info("WebUI 版本已是最新。")
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"检测到管理面板有更新。可以使用 /dashboard_update 命令更新。"
|
f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ async def check_dashboard_files():
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await download_dashboard()
|
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(f"下载管理面板文件失败: {e}。")
|
logger.critical(f"下载管理面板文件失败: {e}。")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -25,14 +25,18 @@ class LongTermMemory:
|
|||||||
def cfg(self, event: AstrMessageEvent):
|
def cfg(self, event: AstrMessageEvent):
|
||||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||||
try:
|
try:
|
||||||
max_cnt = int(cfg["group_message_max_cnt"])
|
max_cnt = int(cfg["provider_ltm_settings"]["group_message_max_cnt"])
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
max_cnt = 300
|
max_cnt = 300
|
||||||
image_caption = True if cfg["image_caption_provider_id"] else False
|
image_caption = (
|
||||||
image_caption_prompt = cfg["image_caption_prompt"]
|
True
|
||||||
image_caption_provider_id = cfg["image_caption_provider_id"]
|
if cfg["provider_settings"]["default_image_caption_provider_id"]
|
||||||
active_reply = cfg["active_reply"]
|
else False
|
||||||
|
)
|
||||||
|
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
|
||||||
|
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)
|
enable_active_reply = active_reply.get("enable", False)
|
||||||
ar_method = active_reply["method"]
|
ar_method = active_reply["method"]
|
||||||
ar_possibility = active_reply["possibility_reply"]
|
ar_possibility = active_reply["possibility_reply"]
|
||||||
@@ -88,7 +92,9 @@ class LongTermMemory:
|
|||||||
|
|
||||||
if cfg["ar_whitelist"] and (
|
if cfg["ar_whitelist"] and (
|
||||||
event.unified_msg_origin not in cfg["ar_whitelist"]
|
event.unified_msg_origin not in cfg["ar_whitelist"]
|
||||||
and (event.get_group_id() and event.get_group_id() not in cfg["ar_whitelist"])
|
and (
|
||||||
|
event.get_group_id() and event.get_group_id() not in cfg["ar_whitelist"]
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -1094,7 +1094,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
@filter.command("dashboard_update")
|
@filter.command("dashboard_update")
|
||||||
async def update_dashboard(self, event: AstrMessageEvent):
|
async def update_dashboard(self, event: AstrMessageEvent):
|
||||||
yield event.plain_result("正在尝试更新管理面板...")
|
yield event.plain_result("正在尝试更新管理面板...")
|
||||||
await download_dashboard()
|
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||||
yield event.plain_result("管理面板更新完成。")
|
yield event.plain_result("管理面板更新完成。")
|
||||||
|
|
||||||
@filter.command("set")
|
@filter.command("set")
|
||||||
@@ -1110,7 +1110,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
@filter.command("unset")
|
@filter.command("unset")
|
||||||
async def unset_variable(self, event: AstrMessageEvent, key: str):
|
async def unset_variable(self, event: AstrMessageEvent, key: str):
|
||||||
uid = event.unified_msg_origin
|
uid = event.unified_msg_origin
|
||||||
session_var = await sp.session_get(umo="uid", key="session_variables", default={})
|
session_var = await sp.session_get(
|
||||||
|
umo="uid", key="session_variables", default={}
|
||||||
|
)
|
||||||
|
|
||||||
if key not in session_var:
|
if key not in session_var:
|
||||||
yield event.plain_result("没有那个变量名。格式 /unset 变量名。")
|
yield event.plain_result("没有那个变量名。格式 /unset 变量名。")
|
||||||
@@ -1176,9 +1178,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
prompt = self.ltm.ar_prompt
|
prompt = event.message_str
|
||||||
if not prompt:
|
|
||||||
prompt = event.message_str
|
|
||||||
|
|
||||||
yield event.request_llm(
|
yield event.request_llm(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
|
|||||||
@@ -463,11 +463,11 @@ class Main(star.Star):
|
|||||||
yield event.image_result(image_path)
|
yield event.image_result(image_path)
|
||||||
elif match.group(1) == "FILE":
|
elif match.group(1) == "FILE":
|
||||||
file_path = os.path.join(workplace_path, match.group(2))
|
file_path = os.path.join(workplace_path, match.group(2))
|
||||||
logger.debug(f"Sending file: {file_path}")
|
# logger.debug(f"Sending file: {file_path}")
|
||||||
file_s3_url = await self.file_upload(file_path)
|
# file_s3_url = await self.file_upload(file_path)
|
||||||
logger.info(f"文件上传到 AstrBot 云节点: {file_s3_url}")
|
# logger.info(f"文件上传到 AstrBot 云节点: {file_s3_url}")
|
||||||
file_name = os.path.basename(file_path)
|
file_name = os.path.basename(file_path)
|
||||||
chain = [File(name=file_name, file=file_s3_url)]
|
chain = [File(name=file_name, file=file_path)]
|
||||||
yield event.set_result(MessageEventResult(chain=chain))
|
yield event.set_result(MessageEventResult(chain=chain))
|
||||||
|
|
||||||
elif "Traceback (most recent call last)" in log or "[Error]: " in log:
|
elif "Traceback (most recent call last)" in log or "[Error]: " in log:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "AstrBot"
|
name = "AstrBot"
|
||||||
version = "4.0.0-beta.2"
|
version = "4.0.0"
|
||||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -21,7 +21,7 @@ dependencies = [
|
|||||||
"deprecated>=1.2.18",
|
"deprecated>=1.2.18",
|
||||||
"dingtalk-stream>=0.22.1",
|
"dingtalk-stream>=0.22.1",
|
||||||
"docstring-parser>=0.16",
|
"docstring-parser>=0.16",
|
||||||
"faiss-cpu>=1.10.0",
|
"faiss-cpu==1.10.0",
|
||||||
"filelock>=3.18.0",
|
"filelock>=3.18.0",
|
||||||
"google-genai>=1.14.0",
|
"google-genai>=1.14.0",
|
||||||
"googlesearch-python>=1.3.0",
|
"googlesearch-python>=1.3.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user