Compare commits

..

43 Commits

Author SHA1 Message Date
Soulter 8b9abc093a refactor: remove unused imports in component panel 2025-12-15 00:50:06 +08:00
Soulter eeec6bcc48 refactor: move mcp and command page to extension page 2025-12-15 00:44:37 +08:00
Soulter bd1c1c7e4f Merge remote-tracking branch 'origin/master' into feature/command-panel 2025-12-14 22:06:13 +08:00
Oscar 2387abe570 refactor(sidebar): 提取侧边栏项目解析逻辑到工具函数复用 2025-12-11 17:14:02 +08:00
Oscar 7e43cca134 perf(db): 优化重构command相关数据库操作 2025-12-11 17:00:15 +08:00
Oscar 89fdb18936 perf(command): 优化指令管理辅助函数和配置绑定逻辑 2025-12-11 16:51:10 +08:00
Oscar e710454d18 perf(command): 优化命令冲突计数逻辑 2025-12-11 15:45:14 +08:00
Oscar 20024cfec9 perf(dashboard): 删除多余的CommandPage.vue文件(已被模块化引用) 2025-12-11 15:36:22 +08:00
Oscar 042c507127 refactor(commandPanel): 移除未使用的 filterState 常量 2025-12-11 15:28:55 +08:00
Oscar 5e83a19ac5 style(builtin_commands): 补充命令描述 2025-12-11 14:48:28 +08:00
Oscar 1f87984133 Merge branch 'master' of https://github.com/ocetars/AstrBot-OscarDev into feature/command-panel 2025-12-11 14:45:01 +08:00
Oscar c6739105c4 refactor(commands): 重构/help指令以动态显示实际命令并补充部分命令描述 2025-12-11 14:34:46 +08:00
Oscar 6fd86eda13 fix(sidebar): 补全新增侧边栏项后的侧边栏位追加逻辑 2025-12-08 17:28:58 +08:00
Ocetars 693f2988be fix(command): 确保新命令配置的事务提交 2025-12-04 16:38:27 +08:00
Ocetars adcffcc466 style(commandPanel): 微调指令面板UI 2025-12-04 16:17:12 +08:00
Ocetars 238aa30331 refactor(commandPanel): 重命名指令模块目录为 commandPanel 2025-12-04 16:05:49 +08:00
Ocetars 26a27776ab refactor(command): 模块化指令管理面板前端代码 2025-12-04 15:58:11 +08:00
Ocetars eb2c88f802 style(extension): 文案修改 2025-12-04 15:23:53 +08:00
Ocetars 81a0e0f28e refactor(command): 移除指令表格内部加载指示器 2025-12-04 15:12:10 +08:00
Ocetars aa61815fcd feat(extension): 添加插件指令冲突检测与提示
- 在插件安装或启用后,自动检测并提示指令冲突。
- 当检测到指令冲突时,显示警告对话框,告知用户冲突数量及可能的影响。
2025-12-03 20:58:53 +08:00
Ocetars f34902574f style(command): 更新空状态描述 2025-12-03 19:55:42 +08:00
Ocetars b1b031077c refactor(command): 更新指令数展示逻辑 2025-12-03 19:48:22 +08:00
Ocetars 7f0e011126 feat(command): 添加系统插件指令过滤与冲突处理 2025-12-03 19:41:27 +08:00
Ocetars 7b7d9f1b8c style(command-page): 优化命令列表UI 2025-12-03 19:19:56 +08:00
Ocetars fe040da7a4 refactor(command): 修改指令列表排序逻辑 2025-12-03 19:10:20 +08:00
Ocetars b98cd1bd72 style(command): 优化指令组子指令数量显示UI 2025-12-03 18:09:12 +08:00
Ocetars 7fa71c538e feat(command-management): 新增指令层级管理与UI展示
- 【后端】
  - `CommandDescriptor` 新增 `parent_group_handler` 和 `sub_commands` 字段,支持指令层级结构定义。
  - `list_commands` 函数重构,实现指令的层级收集与构建,将子指令正确挂载到其父指令组下。
  - 新增 `_collect_all_descriptors` 和 `_find_parent_group_handler` 辅助函数,用于全面收集指令并定位父指令组。
  - `_build_descriptor` 优化指令类型判断逻辑,明确区分普通指令、指令组和子指令。
  - `_descriptor_to_dict` 递归处理子指令,确保 API 返回完整的指令层级数据。
- 【前端】
  - 指令管理页面 (`CommandPage.vue`) 增加指令类型筛选器,并支持指令组的展开/折叠功能。
  - 表格展示优化,为指令组和子指令添加不同的样式和缩进,提升层级结构的视觉可读性。
  - 指令详情对话框新增指令类型、所属指令组和子指令列表的展示。
  - 更新 `CommandItem` 接口,以适配后端提供的层级数据结构。
- 【i18n】
  - 新增指令类型(指令、指令组、子指令)的国际化文本。
  - 更新指令管理相关 UI 文本,包括表格头部、详情对话框字段和筛选器选项。
2025-12-03 17:58:52 +08:00
Ocetars 97c0be85e4 refactor(command): 调整指令管理中的成员权限显示与筛选
- 更新指令筛选逻辑,当选择“所有人”权限筛选时,将同时包含 `everyone` 和 `member` 权限的指令。
2025-12-03 17:12:09 +08:00
Ocetars b1273ff997 style: UI 细节 2025-12-03 15:55:11 +08:00
Ocetars e560f396c5 refactor(command): 优化指令页面布局并更新冲突警告
- 【布局优化】重新组织指令管理页面布局,将筛选器移至顶部独立行
- 【信息展示】将搜索栏与总指令数、已禁用指令数合并显示,提升页面空间利用率
- 【视觉更新】更新指令冲突警告样式
2025-12-03 15:45:12 +08:00
Ocetars 9c842ecd03 style(command-page): 调整命令页面表格样式和图标大小 2025-12-03 15:24:53 +08:00
Ocetars 281ac6dcfe chore(command-page): 禁用命令表格部分列的排序功能 2025-12-03 15:11:36 +08:00
Ocetars 7aa44ba3d8 feat(command): 优化指令冲突显示与提示
- 【功能】新增指令冲突警告提示,当检测到冲突时显示详细信息及解决方案。
- 【优化】调整指令列表排序逻辑,将冲突指令优先显示并分组。
- 【样式】为冲突指令行添加专属高亮样式,提升视觉识别度。
- 【国际化】更新英文和中文多语言文件,增加指令冲突警告相关的翻译文本。
2025-12-03 15:04:30 +08:00
Ocetars 8144b61ae0 fix(command): 排除已禁用指令的冲突检测
- 只有 `effective_command` 存在且 `enabled` 为 `True` 的指令才会被纳入冲突检测范围。
2025-12-03 14:43:18 +08:00
Ocetars 3da0c77e87 fix(command): 修正指令冲突检测逻辑 2025-12-03 14:34:25 +08:00
Ocetars 5e7a0591d9 refactor(command): 移除指令重命名时的别名功能 2025-12-03 14:25:49 +08:00
Ocetars 09d6b715f0 test: 新增命令管理相关测试 2025-12-02 20:57:03 +08:00
Ocetars f0770c5c4d feat: 新增命令管理国际化支持 2025-12-02 20:56:33 +08:00
Ocetars 0858ec4cba feat: 新增命令管理界面页面 2025-12-02 20:56:21 +08:00
Ocetars ae07835da7 feat: 新增命令管理后台 API 2025-12-02 20:56:05 +08:00
Ocetars 6ba1c51cd2 feat: 将命令管理集成到 Star 框架 2025-12-02 20:55:24 +08:00
Ocetars 2dc28eff89 feat: 实现核心命令管理系统 2025-12-02 20:55:14 +08:00
Ocetars 68c1e4ecf9 feat: 新增命令配置数据库模型 2025-12-02 20:53:53 +08:00
119 changed files with 413 additions and 16290 deletions
-79
View File
@@ -1,79 +0,0 @@
name: Build Desktop App
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies (Ubuntu)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install Python dependencies
run: |
pip install uv
uv sync
- name: Build Python backend with Nuitka
run: |
pip install nuitka
python build_nuitka.py
- name: Install Node dependencies
working-directory: ./dashboard
run: npm install
- name: Build Tauri app
working-directory: ./dashboard
run: npm run tauri:build
- name: Upload artifacts (macOS)
if: matrix.platform == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: astrbot-macos
path: dashboard/src-tauri/target/release/bundle/dmg/*.dmg
- name: Upload artifacts (Windows)
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: astrbot-windows
path: dashboard/src-tauri/target/release/bundle/msi/*.msi
- name: Upload artifacts (Linux)
if: matrix.platform == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: astrbot-linux
path: |
dashboard/src-tauri/target/release/bundle/deb/*.deb
dashboard/src-tauri/target/release/bundle/appimage/*.AppImage
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
zip -r dist.zip dist
- name: Archive production artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: dist-without-markdown
path: |
-2
View File
@@ -32,7 +32,6 @@ tests/astrbot_plugin_openai
# Dashboard
dashboard/node_modules/
dashboard/dist/
dashboard/src-tauri/target
package-lock.json
package.json
yarn.lock
@@ -49,6 +48,5 @@ astrbot.lock
chroma
venv/*
pytest.ini
build/
AGENTS.md
IFLOW.md
-287
View File
@@ -1,287 +0,0 @@
# AstrBot 桌面应用构建指南
本指南介绍如何使用 Nuitka 将 Python 后端打包并集成到 Tauri 桌面应用中。
## 前置要求
### 系统要求
- Python 3.10+
- Node.js 20+
- Rust (通过 rustup 安装)
- UV 包管理器
### macOS 额外要求
- Xcode Command Line Tools: `xcode-select --install`
### Linux 额外要求
```bash
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev \
libappindicator3-dev librsvg2-dev patchelf
```
### Windows 额外要求
- Visual Studio 2019+ with C++ build tools
- Windows 10 SDK
## 构建步骤
### 1. 安装 Python 依赖
```bash
pip install uv
uv sync
```
### 2. 安装 Nuitka
```bash
pip install nuitka
```
### 3. 构建 Python 后端
```bash
python build_nuitka.py
```
这会使用 Nuitka 将 `main.py` 编译为独立可执行文件,输出到 `build/nuitka/` 目录。
**注意**: Nuitka 编译过程可能需要 10-30 分钟,取决于您的系统性能。
### 4. 安装前端依赖
```bash
cd dashboard
npm install
```
### 5. 构建 Tauri 应用
```bash
npm run tauri:build
```
构建脚本会自动:
1. 运行 `build_nuitka.py` 编译 Python 后端
2. 将编译好的可执行文件复制到 `src-tauri/resources/` 目录
3. 构建 Tauri 应用并打包所有资源
### 6. 查找构建产物
构建完成后,您可以在以下位置找到安装包:
- **macOS**: `dashboard/src-tauri/target/release/bundle/dmg/AstrBot_*.dmg`
- **Windows**: `dashboard/src-tauri/target/release/bundle/msi/AstrBot_*.msi`
- **Linux**:
- `dashboard/src-tauri/target/release/bundle/deb/astrbot_*.deb`
- `dashboard/src-tauri/target/release/bundle/appimage/astrbot_*.AppImage`
## 开发模式
在开发时,您可能不想每次都完整编译 Python 后端。
### 仅开发 Tauri + Vue
```bash
cd dashboard
npm run tauri:dev
```
这会启动开发服务器,但不会自动启动 Python 后端。您需要手动运行:
```bash
uv run main.py
```
### 测试完整集成
如果您想测试 Tauri 自动启动 Python 后端的功能:
1. 先编译一次 Python 后端:
```bash
python build_nuitka.py
```
2. 手动复制到资源目录:
```bash
# macOS
cp -r build/nuitka/main.app dashboard/src-tauri/resources/astrbot-backend.app
# Windows
copy build\nuitka\main.exe dashboard\src-tauri\resources\astrbot-backend.exe
# Linux
cp build/nuitka/main.bin dashboard/src-tauri/resources/astrbot-backend
```
3. 运行开发模式:
```bash
cd dashboard
npm run tauri:dev
```
## Nuitka 构建选项说明
`build_nuitka.py` 脚本使用以下关键选项:
- `--standalone`: 创建包含所有依赖的独立目录
- `--onefile`: 将所有内容打包到单个可执行文件
- `--follow-imports`: 自动跟踪所有 Python 导入
- `--include-package`: 明确包含特定包
- `--include-data-dir`: 包含数据目录(插件、配置等)
### 自定义构建
如果您需要修改构建选项,编辑 `build_nuitka.py`:
```python
# 添加更多要包含的包
include_packages = [
"astrbot",
"your_custom_package",
# ...
]
# 添加更多数据目录
data_includes = [
"data/config",
"your_custom_data",
# ...
]
```
## 常见问题
### 1. Nuitka 编译失败
**问题**: 编译时出现 "module not found" 错误
**解决方案**: 在 `build_nuitka.py` 中添加缺失的包到 `include_packages` 列表
### 2. 运行时找不到资源文件
**问题**: 应用启动后提示找不到配置文件或插件
**解决方案**: 确保在 `build_nuitka.py` 中使用 `--include-data-dir` 包含了所有必要的数据目录
### 3. macOS 安全警告
**问题**: macOS 提示"应用来自未知开发者"
**解决方案**:
```bash
# 临时解除限制
sudo spctl --master-disable
# 或者为特定应用授权
xattr -cr /Applications/AstrBot.app
```
对于生产发布,您需要:
1. 注册 Apple Developer 账号
2. 对应用进行代码签名
3. 提交公证 (Notarization)
### 4. Windows Defender 报毒
**问题**: Windows Defender 或其他杀毒软件报毒
**解决方案**:
- 这是 Nuitka 打包程序的常见问题
- 可以使用 `--windows-company-name``--windows-product-name` 添加元数据
- 对于生产发布,需要购买代码签名证书
### 5. Linux 依赖问题
**问题**: 在某些 Linux 发行版上缺少共享库
**解决方案**: 使用 AppImage 格式,它包含所有依赖:
```bash
# 构建时会自动生成 AppImage
npm run tauri:build
```
## 优化构建大小
默认的 `--onefile` 模式会生成较大的可执行文件。如果需要减小体积:
1. 移除不需要的包
2. 使用 `--standalone` 而不是 `--onefile`
3. 排除不必要的数据文件
修改 `build_nuitka.py`:
```python
# 移除 --onefile,使用 --standalone
nuitka_cmd = [
sys.executable,
"-m", "nuitka",
"--standalone", # 只使用 standalone
# "--onefile", # 注释掉 onefile
# ...
]
```
## CI/CD 集成
项目已配置 GitHub Actions 工作流 (`.github/workflows/build-app.yml`),可以自动为所有平台构建应用。
推送标签时自动触发:
```bash
git tag v4.5.7
git push origin v4.5.7
```
或手动触发:
在 GitHub Actions 页面选择 "Build Desktop App" 工作流并点击 "Run workflow"
## 发布清单
在发布新版本前:
- [ ] 更新版本号
- `pyproject.toml` - Python 项目版本
- `dashboard/package.json` - Node 项目版本
- `dashboard/src-tauri/Cargo.toml` - Rust 项目版本
- `dashboard/src-tauri/tauri.conf.json` - Tauri 配置版本
- [ ] 运行代码检查
```bash
uv run ruff check .
uv run ruff format .
```
- [ ] 本地测试构建
```bash
python build_nuitka.py
cd dashboard && npm run tauri:build
```
- [ ] 测试安装包
- 安装生成的安装包
- 验证应用启动
- 验证 Python 后端自动启动
- 测试核心功能
- [ ] 创建发布标签
```bash
git tag -a v4.5.7 -m "Release v4.5.7"
git push origin v4.5.7
```
## 技术架构
```
┌─────────────────────────────────────┐
│ Tauri Desktop App │
│ (Rust + WebView) │
│ │
│ ┌─────────────────────────────┐ │
│ │ Vue.js Dashboard │ │
│ │ (Frontend UI) │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Python Backend │ │
│ │ (Nuitka Compiled) │ │
│ │ - AstrBot Core │ │
│ │ - Plugins │ │
│ │ - API Server │ │
│ └─────────────────────────────┘ │
│ │
│ HTTP/WebSocket │
│ localhost:6185 │
└─────────────────────────────────────┘
```
## 参考资源
- [Nuitka 文档](https://nuitka.net/doc/user-manual.html)
- [Tauri 文档](https://tauri.app/v1/guides/)
- [AstrBot 文档](https://astrbot.fun)
+1 -26
View File
@@ -33,20 +33,6 @@
- 请使用英文描述您的 PR。
- 标题请使用 `fix: `, `feat: `, `docs: `, `style: `, `refactor: `, `test: `, `chore: ` 等语义化前缀,并简要描述更改内容。如:`fix: correct login page typo`
#### 代码规范
##### Core
我们使用 Ruff 作为代码格式化和静态分析工具。在提交代码之前,请运行以下命令以确保代码符合规范:
```bash
ruff format .
ruff check .
```
如果您使用 VSCode,可以安装 `Ruff` 插件。
## Contributing Guide
First off, thanks for taking the time to contribute! ❤️
@@ -76,15 +62,4 @@ We use the `fix/` prefix for bug fixes and the `feat/` prefix for new features.
#### PR Description
- Please use English to describe your PR.
- Use semantic prefixes like `fix: `, `feat: `, `docs: `, `style: `, `refactor: `, `test: `, `chore: ` in the title, followed by a brief description of the changes, e.g., `fix: correct login page typo`.
#### Code Style
##### Core
We use Ruff as our code formatter and static analysis tool. Before submitting your code, please run the following commands to ensure your code adheres to the style guidelines:
```bash
ruff format .
ruff check .
```
- Use semantic prefixes like `fix: `, `feat: `, `docs: `, `style: `, `refactor: `, `test: `, `chore: ` in the title, followed by a brief description of the changes, e.g., `fix: correct login page typo`.
-6
View File
@@ -243,10 +243,4 @@ pre-commit install
</details>
<div align="center">
_私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.9.2"
__version__ = "4.9.0"
+4 -6
View File
@@ -3,7 +3,7 @@
from typing import Any, ClassVar, Literal, cast
from pydantic import BaseModel, GetCoreSchemaHandler, model_serializer, model_validator
from pydantic import BaseModel, GetCoreSchemaHandler, model_validator
from pydantic_core import core_schema
@@ -122,12 +122,10 @@ class ToolCall(BaseModel):
extra_content: dict[str, Any] | None = None
"""Extra metadata for the tool call."""
@model_serializer(mode="wrap")
def serialize(self, handler):
data = handler(self)
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
if self.extra_content is None:
data.pop("extra_content", None)
return data
kwargs.setdefault("exclude", set()).add("extra_content")
return super().model_dump(**kwargs)
class ToolCallPart(BaseModel):
+1 -22
View File
@@ -1,8 +1,7 @@
import typing as T
from dataclasses import dataclass, field
from dataclasses import dataclass
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.provider.entities import TokenUsage
class AgentResponseData(T.TypedDict):
@@ -13,23 +12,3 @@ class AgentResponseData(T.TypedDict):
class AgentResponse:
type: str
data: AgentResponseData
@dataclass
class AgentStats:
token_usage: TokenUsage = field(default_factory=TokenUsage)
start_time: float = 0.0
end_time: float = 0.0
time_to_first_token: float = 0.0
@property
def duration(self) -> float:
return self.end_time - self.start_time
def to_dict(self) -> dict:
return {
"token_usage": self.token_usage.__dict__,
"start_time": self.start_time,
"end_time": self.end_time,
"time_to_first_token": self.time_to_first_token,
}
+1 -1
View File
@@ -9,7 +9,7 @@ from .message import Message
TContext = TypeVar("TContext", default=Any)
@dataclass
@dataclass(config={"arbitrary_types_allowed": True})
class ContextWrapper(Generic[TContext]):
"""A context for running an agent, which can be used to pass additional data or state."""
@@ -1,5 +1,4 @@
import sys
import time
import traceback
import typing as T
@@ -13,7 +12,6 @@ from mcp.types import (
)
from astrbot import logger
from astrbot.core.message.components import Json
from astrbot.core.message.message_event_result import (
MessageChain,
)
@@ -26,7 +24,7 @@ from astrbot.core.provider.provider import Provider
from ..hooks import BaseAgentRunHooks
from ..message import AssistantMessageSegment, Message, ToolCallMessageSegment
from ..response import AgentResponseData, AgentStats
from ..response import AgentResponseData
from ..run_context import ContextWrapper, TContext
from ..tool_executor import BaseFunctionToolExecutor
from .base import AgentResponse, AgentState, BaseAgentRunner
@@ -71,9 +69,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
)
self.run_context.messages = messages
self.stats = AgentStats()
self.stats.start_time = time.time()
async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
"""Yields chunks *and* a final LLMResponse."""
if self.streaming:
@@ -103,10 +98,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
async for llm_response in self._iter_llm_responses():
if llm_response.is_chunk:
# update ttft
if self.stats.time_to_first_token == 0:
self.stats.time_to_first_token = time.time() - self.stats.start_time
if llm_response.result_chain:
yield AgentResponse(
type="streaming_delta",
@@ -130,10 +121,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
)
continue
llm_resp_result = llm_response
if not llm_response.is_chunk and llm_response.usage:
# only count the token usage of the final response for computation purpose
self.stats.token_usage += llm_response.usage
break # got final response
if not llm_resp_result:
@@ -145,7 +132,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
if llm_resp.role == "err":
# 如果 LLM 响应错误,转换到错误状态
self.final_llm_resp = llm_resp
self.stats.end_time = time.time()
self._transition_state(AgentState.ERROR)
yield AgentResponse(
type="err",
@@ -160,7 +146,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
# 如果没有工具调用,转换到完成状态
self.final_llm_resp = llm_resp
self._transition_state(AgentState.DONE)
self.stats.end_time = time.time()
# record the final assistant message
self.run_context.messages.append(
Message(
@@ -190,19 +175,22 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
# 如果有工具调用,还需处理工具调用
if llm_resp.tools_call_name:
tool_call_result_blocks = []
for tool_call_name in llm_resp.tools_call_name:
yield AgentResponse(
type="tool_call",
data=AgentResponseData(
chain=MessageChain(type="tool_call").message(
f"🔨 调用工具: {tool_call_name}"
),
),
)
async for result in self._handle_function_tools(self.req, llm_resp):
if isinstance(result, list):
tool_call_result_blocks = result
elif isinstance(result, MessageChain):
if result.type is None:
# should not happen
continue
if result.type == "tool_direct_result":
ar_type = "tool_call_result"
else:
ar_type = result.type
result.type = "tool_call_result"
yield AgentResponse(
type=ar_type,
type="tool_call_result",
data=AgentResponseData(chain=result),
)
# 将结果添加到上下文中
@@ -245,19 +233,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
llm_response.tools_call_args,
llm_response.tools_call_ids,
):
yield MessageChain(
type="tool_call",
chain=[
Json(
data={
"id": func_tool_id,
"name": func_tool_name,
"args": func_tool_args,
"ts": time.time(),
}
)
],
)
try:
if not req.func_tool:
return
@@ -331,6 +306,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
content=res.content[0].text,
),
)
yield MessageChain().message(res.content[0].text)
elif isinstance(res.content[0], ImageContent):
tool_call_result_blocks.append(
ToolCallMessageSegment(
@@ -352,6 +328,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
content=resource.text,
),
)
yield MessageChain().message(resource.text)
elif (
isinstance(resource, BlobResourceContents)
and resource.mimeType
@@ -375,22 +352,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
content="返回的数据类型不受支持",
),
)
# yield the last tool call result
if tool_call_result_blocks:
last_tcr_content = str(tool_call_result_blocks[-1].content)
yield MessageChain(
type="tool_call_result",
chain=[
Json(
data={
"id": func_tool_id,
"ts": time.time(),
"result": last_tcr_content,
}
)
],
)
yield MessageChain().message("返回的数据类型不受支持。")
elif resp is None:
# Tool 直接请求发送消息给用户
@@ -400,7 +362,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
f"{func_tool_name} 没有没有返回值或者将结果直接发送给用户,此工具调用不会被记录到历史中。"
)
self._transition_state(AgentState.DONE)
self.stats.end_time = time.time()
else:
# 不应该出现其他类型
logger.warning(
+1 -3
View File
@@ -6,10 +6,8 @@ from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.star.context import Context
@dataclass
@dataclass(config={"arbitrary_types_allowed": True})
class AstrAgentContext:
__pydantic_config__ = {"arbitrary_types_allowed": True}
context: Context
"""The star context instance"""
event: AstrMessageEvent
+2 -23
View File
@@ -4,7 +4,6 @@ from collections.abc import AsyncGenerator
from astrbot.core import logger
from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner
from astrbot.core.astr_agent_context import AstrAgentContext
from astrbot.core.message.components import Json
from astrbot.core.message.message_event_result import (
MessageChain,
MessageEventResult,
@@ -34,27 +33,16 @@ async def run_agent(
msg_chain = resp.data["chain"]
if msg_chain.type == "tool_direct_result":
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
await astr_event.send(msg_chain)
await astr_event.send(resp.data["chain"])
continue
if astr_event.get_platform_id() == "webchat":
await astr_event.send(msg_chain)
# 对于其他情况,暂时先不处理
continue
elif resp.type == "tool_call":
if agent_runner.streaming:
# 用来标记流式响应需要分节
yield MessageChain(chain=[], type="break")
if astr_event.get_platform_name() == "webchat":
if show_tool_use:
await astr_event.send(resp.data["chain"])
elif show_tool_use:
json_comp = resp.data["chain"].chain[0]
if isinstance(json_comp, Json):
m = f"🔨 调用工具: {json_comp.data.get('name')}"
else:
m = "🔨 调用工具..."
chain = MessageChain(type="tool_call").message(m)
await astr_event.send(chain)
continue
if stream_to_general and resp.type == "streaming_delta":
@@ -81,15 +69,6 @@ async def run_agent(
continue
yield resp.data["chain"] # MessageChain
if agent_runner.done():
# send agent stats to webchat
if astr_event.get_platform_name() == "webchat":
await astr_event.send(
MessageChain(
type="agent_stats",
chain=[Json(data=agent_runner.stats.to_dict())],
)
)
break
except Exception as e:
+10 -32
View File
@@ -4,7 +4,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.9.2"
VERSION = "4.9.0"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
WEBHOOK_SUPPORTED_PLATFORMS = [
@@ -108,7 +108,6 @@ DEFAULT_CONFIG = {
"provider_id": "",
"dual_output": False,
"use_file_service": False,
"trigger_probability": 1.0,
},
"provider_ltm_settings": {
"group_icl_enable": False,
@@ -209,7 +208,7 @@ CONFIG_METADATA_2 = {
"callback_server_host": "0.0.0.0",
"port": 6196,
},
"OneBot v11": {
"QQ 个人号(OneBot v11)": {
"id": "default",
"type": "aiocqhttp",
"enable": False,
@@ -946,7 +945,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
"timeout": 120,
"model_config": {
"model": "gemini-3-flash-preview",
"model": "gemini-1.5-flash",
"temperature": 0.4,
},
"custom_headers": {},
@@ -963,7 +962,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://generativelanguage.googleapis.com/",
"timeout": 120,
"model_config": {
"model": "gemini-3-flash-preview",
"model": "gemini-2.0-flash-exp",
"temperature": 0.4,
},
"gm_resp_image_modal": False,
@@ -976,7 +975,9 @@ CONFIG_METADATA_2 = {
"sexually_explicit": "BLOCK_MEDIUM_AND_ABOVE",
"dangerous_content": "BLOCK_MEDIUM_AND_ABOVE",
},
"gm_thinking_config": {"budget": 0, "level": "HIGH"},
"gm_thinking_config": {
"budget": 0,
},
"modalities": ["text", "image", "tool_use"],
},
"DeepSeek": {
@@ -1817,24 +1818,13 @@ CONFIG_METADATA_2 = {
},
},
"gm_thinking_config": {
"description": "Thinking Config",
"description": "Gemini思考设置",
"type": "object",
"items": {
"budget": {
"description": "Thinking Budget",
"description": "思考预算",
"type": "int",
"hint": "Guides the model on the specific number of thinking tokens to use for reasoning. See: https://ai.google.dev/gemini-api/docs/thinking#set-budget",
},
"level": {
"description": "Thinking Level",
"type": "string",
"hint": "Recommended for Gemini 3 models and onwards, lets you control reasoning behavior.See: https://ai.google.dev/gemini-api/docs/thinking#thinking-levels",
"options": [
"MINIMAL",
"LOW",
"MEDIUM",
"HIGH",
],
"hint": "模型应该生成的思考Token的数量,设为0关闭思考。除gemini-2.5-flash外的模型会静默忽略此参数。",
},
},
},
@@ -2219,9 +2209,6 @@ CONFIG_METADATA_2 = {
"use_file_service": {
"type": "bool",
},
"trigger_probability": {
"type": "float",
},
},
},
"provider_ltm_settings": {
@@ -2432,14 +2419,6 @@ CONFIG_METADATA_3 = {
"provider_tts_settings.enable": True,
},
},
"provider_tts_settings.trigger_probability": {
"description": "TTS 触发概率",
"type": "float",
"slider": {"min": 0, "max": 1, "step": 0.05},
"condition": {
"provider_tts_settings.enable": True,
},
},
"provider_settings.image_caption_prompt": {
"description": "图片转述提示词",
"type": "text",
@@ -3007,7 +2986,6 @@ CONFIG_METADATA_3 = {
"description": "回复概率",
"type": "float",
"hint": "0.0-1.0 之间的数值",
"slider": {"min": 0, "max": 1, "step": 0.05},
"condition": {
"provider_ltm_settings.active_reply.enable": True,
},
-1
View File
@@ -79,7 +79,6 @@ class ConfigMetadataI18n:
"_special",
"invisible",
"options",
"slider",
]:
if attr in field_data:
field_result[attr] = field_data[attr]
+5 -4
View File
@@ -629,11 +629,12 @@ class Nodes(BaseMessageComponent):
class Json(BaseMessageComponent):
type = ComponentType.Json
data: dict
data: str | dict
resid: int | None = 0
def __init__(self, data: str | dict, **_):
if isinstance(data, str):
data = json.loads(data)
def __init__(self, data, **_):
if isinstance(data, dict):
data = json.dumps(data)
super().__init__(data=data, **_)
+1 -5
View File
@@ -119,7 +119,7 @@ class RespondStage(Stage):
if (result := event.get_result()) is None:
return False
if self.only_llm_result and not result.is_llm_result():
if self.only_llm_result and result.is_llm_result():
return False
if event.get_platform_name() in [
@@ -158,11 +158,7 @@ class RespondStage(Stage):
result = event.get_result()
if result is None:
return
if event.get_extra("_streaming_finished", False):
# prevent some plugin make result content type to LLM_RESULT after streaming finished, lead to send again
return
if result.result_content_type == ResultContentType.STREAMING_FINISH:
event.set_extra("_streaming_finished", True)
return
logger.info(
+1 -21
View File
@@ -1,4 +1,3 @@
import random
import re
import time
import traceback
@@ -43,18 +42,6 @@ class ResultDecorateStage(Stage):
"forward_threshold"
]
trigger_probability = ctx.astrbot_config["provider_tts_settings"].get(
"trigger_probability",
1,
)
try:
self.tts_trigger_probability = max(
0.0,
min(float(trigger_probability), 1.0),
)
except (TypeError, ValueError):
self.tts_trigger_probability = 1.0
# 分段回复
self.words_count_threshold = int(
ctx.astrbot_config["platform_settings"]["segmented_reply"][
@@ -259,14 +246,7 @@ class ResultDecorateStage(Stage):
and result.is_llm_result()
and SessionServiceManager.should_process_tts_request(event)
):
should_tts = self.tts_trigger_probability >= 1.0 or (
self.tts_trigger_probability > 0.0
and random.random() <= self.tts_trigger_probability
)
if not should_tts:
logger.debug("跳过 TTS:触发概率未命中。")
elif not tts_provider:
if not tts_provider:
logger.warning(
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。",
)
@@ -200,15 +200,6 @@ class TelegramPlatformEvent(AstrMessageEvent):
if isinstance(chain, MessageChain):
if chain.type == "break":
# 分割符
if message_id:
try:
await self.client.edit_message_text(
text=delta,
chat_id=payload["chat_id"],
message_id=message_id,
)
except Exception as e:
logger.warning(f"编辑消息失败(streaming-break): {e!s}")
message_id = None # 重置消息 ID
delta = "" # 重置 delta
continue
@@ -1,12 +1,11 @@
import base64
import json
import os
import shutil
import uuid
from astrbot.api import logger
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.message_components import File, Image, Json, Plain, Record
from astrbot.api.message_components import File, Image, Plain, Record
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from .webchat_queue_mgr import webchat_queue_mgr
@@ -42,20 +41,12 @@ class WebChatMessageEvent(AstrMessageEvent):
await web_chat_back_queue.put(
{
"type": "plain",
"cid": cid,
"data": data,
"streaming": streaming,
"chain_type": message.type,
},
)
elif isinstance(comp, Json):
await web_chat_back_queue.put(
{
"type": "plain",
"data": json.dumps(comp.data, ensure_ascii=False),
"streaming": streaming,
"chain_type": message.type,
},
)
elif isinstance(comp, Image):
# save image to local
filename = f"{str(uuid.uuid4())}.jpg"
@@ -67,6 +58,7 @@ class WebChatMessageEvent(AstrMessageEvent):
await web_chat_back_queue.put(
{
"type": "image",
"cid": cid,
"data": data,
"streaming": streaming,
},
@@ -82,6 +74,7 @@ class WebChatMessageEvent(AstrMessageEvent):
await web_chat_back_queue.put(
{
"type": "record",
"cid": cid,
"data": data,
"streaming": streaming,
},
@@ -98,6 +91,7 @@ class WebChatMessageEvent(AstrMessageEvent):
await web_chat_back_queue.put(
{
"type": "file",
"cid": cid,
"data": data,
"streaming": streaming,
},
@@ -117,17 +111,18 @@ class WebChatMessageEvent(AstrMessageEvent):
cid = self.session_id.split("!")[-1]
web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
async for chain in generator:
# if chain.type == "break" and final_data:
# # 分割符
# await web_chat_back_queue.put(
# {
# "type": "break", # break means a segment end
# "data": final_data,
# "streaming": True,
# },
# )
# final_data = ""
# continue
if chain.type == "break" and final_data:
# 分割符
await web_chat_back_queue.put(
{
"type": "break", # break means a segment end
"data": final_data,
"streaming": True,
"cid": cid,
},
)
final_data = ""
continue
r = await WebChatMessageEvent._send(
chain,
@@ -147,6 +142,7 @@ class WebChatMessageEvent(AstrMessageEvent):
"data": final_data,
"reasoning": reasoning_content,
"streaming": True,
"cid": cid,
},
)
await super().send_streaming(generator, use_fallback)
-41
View File
@@ -1,5 +1,3 @@
from __future__ import annotations
import base64
import enum
import json
@@ -201,38 +199,6 @@ class ProviderRequest:
return ""
@dataclass
class TokenUsage:
input_other: int = 0
"""The number of input tokens, excluding cached tokens."""
input_cached: int = 0
"""The number of input cached tokens."""
output: int = 0
"""The number of output tokens."""
@property
def total(self) -> int:
return self.input_other + self.input_cached + self.output
@property
def input(self) -> int:
return self.input_other + self.input_cached
def __add__(self, other: TokenUsage) -> TokenUsage:
return TokenUsage(
input_other=self.input_other + other.input_other,
input_cached=self.input_cached + other.input_cached,
output=self.output + other.output,
)
def __sub__(self, other: TokenUsage) -> TokenUsage:
return TokenUsage(
input_other=self.input_other - other.input_other,
input_cached=self.input_cached - other.input_cached,
output=self.output - other.output,
)
@dataclass
class LLMResponse:
role: str
@@ -261,11 +227,6 @@ class LLMResponse:
is_chunk: bool = False
"""Indicates if the response is a chunked response."""
id: str | None = None
"""The ID of the response. For chunked responses, it's the ID of the chunk; for non-chunked responses, it's the ID of the response."""
usage: TokenUsage | None = None
"""The usage of the response. For chunked responses, it's the usage of the chunk; for non-chunked responses, it's the usage of the response."""
def __init__(
self,
role: str,
@@ -280,8 +241,6 @@ class LLMResponse:
| AnthropicMessage
| None = None,
is_chunk: bool = False,
id: str | None = None,
usage: TokenUsage | None = None,
):
"""初始化 LLMResponse
@@ -6,12 +6,10 @@ from mimetypes import guess_type
import anthropic
from anthropic import AsyncAnthropic
from anthropic.types import Message
from anthropic.types.message_delta_usage import MessageDeltaUsage
from anthropic.types.usage import Usage
from astrbot import logger
from astrbot.api.provider import Provider
from astrbot.core.provider.entities import LLMResponse, TokenUsage
from astrbot.core.provider.entities import LLMResponse
from astrbot.core.provider.func_tool_manager import ToolSet
from astrbot.core.utils.io import download_image_by_url
@@ -109,22 +107,6 @@ class ProviderAnthropic(Provider):
return system_prompt, new_messages
def _extract_usage(self, usage: Usage) -> TokenUsage:
# https://docs.claude.com/en/docs/build-with-claude/prompt-caching#tracking-cache-performance
return TokenUsage(
input_other=usage.input_tokens or 0,
input_cached=usage.cache_read_input_tokens or 0,
output=usage.output_tokens,
)
def _update_usage(self, token_usage: TokenUsage, usage: MessageDeltaUsage) -> None:
if usage.input_tokens is not None:
token_usage.input_other = usage.input_tokens
if usage.cache_read_input_tokens is not None:
token_usage.input_cached = usage.cache_read_input_tokens
if usage.output_tokens is not None:
token_usage.output = usage.output_tokens
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
if tools:
if tool_list := tools.get_func_desc_anthropic_style():
@@ -149,10 +131,6 @@ class ProviderAnthropic(Provider):
llm_response.tools_call_args.append(content_block.input)
llm_response.tools_call_name.append(content_block.name)
llm_response.tools_call_ids.append(content_block.id)
llm_response.id = completion.id
llm_response.usage = self._extract_usage(completion.usage)
# TODO(Soulter): 处理 end_turn 情况
if not llm_response.completion_text and not llm_response.tools_call_args:
raise Exception(f"Anthropic API 返回的 completion 无法解析:{completion}")
@@ -174,16 +152,9 @@ class ProviderAnthropic(Provider):
final_text = ""
final_tool_calls = []
id = None
usage = TokenUsage()
async with self.client.messages.stream(**payloads) as stream:
assert isinstance(stream, anthropic.AsyncMessageStream)
async for event in stream:
if event.type == "message_start":
# the usage contains input token usage
id = event.message.id
usage = self._extract_usage(event.message.usage)
if event.type == "content_block_start":
if event.content_block.type == "text":
# 文本块开始
@@ -191,8 +162,6 @@ class ProviderAnthropic(Provider):
role="assistant",
completion_text="",
is_chunk=True,
usage=usage,
id=id,
)
elif event.content_block.type == "tool_use":
# 工具使用块开始,初始化缓冲区
@@ -210,8 +179,6 @@ class ProviderAnthropic(Provider):
role="assistant",
completion_text=event.delta.text,
is_chunk=True,
usage=usage,
id=id,
)
elif event.delta.type == "input_json_delta":
# 工具调用参数增量
@@ -248,8 +215,6 @@ class ProviderAnthropic(Provider):
tools_call_name=[tool_info["name"]],
tools_call_ids=[tool_info["id"]],
is_chunk=True,
usage=usage,
id=id,
)
except json.JSONDecodeError:
# JSON 解析失败,跳过这个工具调用
@@ -258,17 +223,11 @@ class ProviderAnthropic(Provider):
# 清理缓冲区
del tool_use_buffer[event.index]
elif event.type == "message_delta":
if event.usage:
self._update_usage(usage, event.usage)
# 返回最终的完整结果
final_response = LLMResponse(
role="assistant",
completion_text=final_text,
is_chunk=False,
usage=usage,
id=id,
)
if final_tool_calls:
+26 -63
View File
@@ -14,7 +14,7 @@ import astrbot.core.message.components as Comp
from astrbot import logger
from astrbot.api.provider import Provider
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.provider.entities import LLMResponse, TokenUsage
from astrbot.core.provider.entities import LLMResponse
from astrbot.core.provider.func_tool_manager import ToolSet
from astrbot.core.utils.io import download_image_by_url
@@ -138,7 +138,7 @@ class ProviderGoogleGenAI(Provider):
modalities = ["TEXT"]
tool_list: list[types.Tool] | None = []
model_name = payloads.get("model", self.get_model())
model_name = self.get_model()
native_coderunner = self.provider_config.get("gm_native_coderunner", False)
native_search = self.provider_config.get("gm_native_search", False)
url_context = self.provider_config.get("gm_url_context", False)
@@ -197,37 +197,6 @@ class ProviderGoogleGenAI(Provider):
types.Tool(function_declarations=func_desc["function_declarations"]),
]
# oper thinking config
thinking_config = None
if model_name.startswith("gemini-2.5"):
# The thinkingBudget parameter, introduced with the Gemini 2.5 series
thinking_budget = self.provider_config.get("gm_thinking_config", {}).get(
"budget", 0
)
if thinking_budget is not None:
thinking_config = types.ThinkingConfig(
thinking_budget=thinking_budget,
)
elif model_name.startswith("gemini-3"):
# The thinkingLevel parameter, recommended for Gemini 3 models and onwards
# Gemini 2.5 series models don't support thinkingLevel; use thinkingBudget instead.
thinking_level = self.provider_config.get("gm_thinking_config", {}).get(
"level", "HIGH"
)
if thinking_level and isinstance(thinking_level, str):
thinking_level = thinking_level.upper()
if thinking_level not in ["MINIMAL", "LOW", "MEDIUM", "HIGH"]:
logger.warning(
f"Invalid thinking level: {thinking_level}, using HIGH"
)
thinking_level = "HIGH"
level = types.ThinkingLevel(thinking_level)
thinking_config = types.ThinkingConfig()
if not hasattr(types.ThinkingConfig, "thinking_level"):
setattr(types.ThinkingConfig, "thinking_level", level)
else:
thinking_config.thinking_level = level
return types.GenerateContentConfig(
system_instruction=system_instruction,
temperature=temperature,
@@ -247,7 +216,22 @@ class ProviderGoogleGenAI(Provider):
response_modalities=modalities,
tools=cast(types.ToolListUnion | None, tool_list),
safety_settings=self.safety_settings if self.safety_settings else None,
thinking_config=thinking_config,
thinking_config=(
types.ThinkingConfig(
thinking_budget=min(
int(
self.provider_config.get("gm_thinking_config", {}).get(
"budget",
0,
),
),
24576,
),
)
if "gemini-2.5-flash" in self.get_model()
and hasattr(types.ThinkingConfig, "thinking_budget")
else None
),
automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True,
),
@@ -363,16 +347,6 @@ class ProviderGoogleGenAI(Provider):
]
return "".join(thought_buf).strip()
def _extract_usage(
self, usage_metadata: types.GenerateContentResponseUsageMetadata
) -> TokenUsage:
"""Extract usage from candidate"""
return TokenUsage(
input_other=usage_metadata.prompt_token_count or 0,
input_cached=usage_metadata.cached_content_token_count or 0,
output=usage_metadata.candidates_token_count or 0,
)
def _process_content_parts(
self,
candidate: types.Candidate,
@@ -457,8 +431,6 @@ class ProviderGoogleGenAI(Provider):
None,
)
model = payloads.get("model", self.get_model())
modalities = ["TEXT"]
if self.provider_config.get("gm_resp_image_modal", False):
modalities.append("IMAGE")
@@ -477,7 +449,7 @@ class ProviderGoogleGenAI(Provider):
temperature,
)
result = await self.client.models.generate_content(
model=model,
model=self.get_model(),
contents=cast(types.ContentListUnion, conversation),
config=config,
)
@@ -503,11 +475,11 @@ class ProviderGoogleGenAI(Provider):
e.message = ""
if "Developer instruction is not enabled" in e.message:
logger.warning(
f"{model} 不支持 system prompt,已自动去除(影响人格设置)",
f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)",
)
system_instruction = None
elif "Function calling is not enabled" in e.message:
logger.warning(f"{model} 不支持函数调用,已自动去除")
logger.warning(f"{self.get_model()} 不支持函数调用,已自动去除")
tools = None
elif (
"Multi-modal output is not supported" in e.message
@@ -516,7 +488,7 @@ class ProviderGoogleGenAI(Provider):
or "only supports text output" in e.message
):
logger.warning(
f"{model} 不支持多模态输出,降级为文本模态",
f"{self.get_model()} 不支持多模态输出,降级为文本模态",
)
modalities = ["TEXT"]
else:
@@ -529,9 +501,6 @@ class ProviderGoogleGenAI(Provider):
result.candidates[0],
llm_response,
)
llm_response.id = result.response_id
if result.usage_metadata:
llm_response.usage = self._extract_usage(result.usage_metadata)
return llm_response
async def _query_stream(
@@ -544,7 +513,7 @@ class ProviderGoogleGenAI(Provider):
(msg["content"] for msg in payloads["messages"] if msg["role"] == "system"),
None,
)
model = payloads.get("model", self.get_model())
conversation = self._prepare_conversation(payloads)
result = None
@@ -556,7 +525,7 @@ class ProviderGoogleGenAI(Provider):
system_instruction,
)
result = await self.client.models.generate_content_stream(
model=model,
model=self.get_model(),
contents=cast(types.ContentListUnion, conversation),
config=config,
)
@@ -566,11 +535,11 @@ class ProviderGoogleGenAI(Provider):
e.message = ""
if "Developer instruction is not enabled" in e.message:
logger.warning(
f"{model} 不支持 system prompt,已自动去除(影响人格设置)",
f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)",
)
system_instruction = None
elif "Function calling is not enabled" in e.message:
logger.warning(f"{model} 不支持函数调用,已自动去除")
logger.warning(f"{self.get_model()} 不支持函数调用,已自动去除")
tools = None
else:
raise
@@ -600,9 +569,6 @@ class ProviderGoogleGenAI(Provider):
chunk.candidates[0],
llm_response,
)
llm_response.id = chunk.response_id
if chunk.usage_metadata:
llm_response.usage = self._extract_usage(chunk.usage_metadata)
yield llm_response
return
@@ -630,9 +596,6 @@ class ProviderGoogleGenAI(Provider):
chunk.candidates[0],
final_response,
)
final_response.id = chunk.response_id
if chunk.usage_metadata:
final_response.usage = self._extract_usage(chunk.usage_metadata)
break
# Yield final complete response with accumulated text
+1 -18
View File
@@ -12,7 +12,6 @@ from openai._exceptions import NotFoundError
from openai.lib.streaming.chat._completions import ChatCompletionStreamState
from openai.types.chat.chat_completion import ChatCompletion
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from openai.types.completion_usage import CompletionUsage
import astrbot.core.message.components as Comp
from astrbot import logger
@@ -20,7 +19,7 @@ from astrbot.api.provider import Provider
from astrbot.core.agent.message import Message
from astrbot.core.agent.tool import ToolSet
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult
from astrbot.core.provider.entities import LLMResponse, ToolCallsResult
from astrbot.core.utils.io import download_image_by_url
from ..register import register_provider_adapter
@@ -209,7 +208,6 @@ class ProviderOpenAIOfficial(Provider):
# handle the content delta
reasoning = self._extract_reasoning_content(chunk)
_y = False
llm_response.id = chunk.id
if reasoning:
llm_response.reasoning_content = reasoning
_y = True
@@ -219,8 +217,6 @@ class ProviderOpenAIOfficial(Provider):
chain=[Comp.Plain(completion_text)],
)
_y = True
if chunk.usage:
llm_response.usage = self._extract_usage(chunk.usage)
if _y:
yield llm_response
@@ -249,15 +245,6 @@ class ProviderOpenAIOfficial(Provider):
reasoning_text = str(reasoning_attr)
return reasoning_text
def _extract_usage(self, usage: CompletionUsage) -> TokenUsage:
ptd = usage.prompt_tokens_details
cached = ptd.cached_tokens if ptd and ptd.cached_tokens else 0
return TokenUsage(
input_other=usage.prompt_tokens - cached,
input_cached=ptd.cached_tokens if ptd and ptd.cached_tokens else 0,
output=usage.completion_tokens,
)
async def _parse_openai_completion(
self, completion: ChatCompletion, tools: ToolSet | None
) -> LLMResponse:
@@ -334,10 +321,6 @@ class ProviderOpenAIOfficial(Provider):
raise Exception(f"API 返回的 completion 无法解析:{completion}")
llm_response.raw_completion = completion
llm_response.id = completion.id
if completion.usage:
llm_response.usage = self._extract_usage(completion.usage)
return llm_response
+1 -5
View File
@@ -2,19 +2,15 @@ from astrbot.core import html_renderer
from astrbot.core.provider import Provider
from astrbot.core.star.star_tools import StarTools
from astrbot.core.utils.command_parser import CommandParserMixin
from astrbot.core.utils.plugin_kv_store import PluginKVStoreMixin
from .context import Context
from .star import StarMetadata, star_map, star_registry
from .star_manager import PluginManager
class Star(CommandParserMixin, PluginKVStoreMixin):
class Star(CommandParserMixin):
"""所有插件(Star)的父类,所有插件都应该继承于这个类"""
author: str
name: str
def __init__(self, context: Context, config: dict | None = None):
StarTools.initialize(context)
self.context = context
-4
View File
@@ -296,10 +296,6 @@ class Context:
provider_type=ProviderType.CHAT_COMPLETION,
umo=umo,
)
if prov is None:
raise ProviderNotFoundError(
"provider not found, please choose provider first"
)
if not isinstance(prov, Provider):
raise ValueError("返回的 Provider 不是 Provider 类型")
return prov
-12
View File
@@ -468,18 +468,6 @@ class PluginManager:
metadata.star_cls = metadata.star_cls_type(
context=self.context,
)
p_name = (metadata.name or "unknown").lower().replace("/", "_")
p_author = (
(metadata.author or "unknown").lower().replace("/", "_")
)
setattr(metadata.star_cls, "name", p_name)
setattr(metadata.star_cls, "author", p_author)
setattr(
metadata.star_cls,
"plugin_id",
f"{p_author}/{p_name}",
)
else:
logger.info(f"插件 {metadata.name} 已被禁用。")
-28
View File
@@ -1,28 +0,0 @@
from typing import TypeVar
from astrbot.core import sp
SUPPORTED_VALUE_TYPES = int | float | str | bytes | bool | dict | list | None
_VT = TypeVar("_VT")
class PluginKVStoreMixin:
"""为插件提供键值存储功能的 Mixin 类"""
plugin_id: str
async def put_kv_data(
self,
key: str,
value: SUPPORTED_VALUE_TYPES,
) -> None:
"""为指定插件存储一个键值对"""
await sp.put_async("plugin", self.plugin_id, key, value)
async def get_kv_data(self, key: str, default: _VT) -> _VT | None:
"""获取指定插件存储的键值对"""
return await sp.get_async("plugin", self.plugin_id, key, default)
async def delete_kv_data(self, key: str) -> None:
"""删除指定插件存储的键值对"""
await sp.remove_async("plugin", self.plugin_id, key)
+13 -56
View File
@@ -227,19 +227,16 @@ class ChatRoute(Route):
text: str,
media_parts: list,
reasoning: str,
agent_stats: dict,
):
"""保存 bot 消息到历史记录,返回保存的记录"""
bot_message_parts = []
bot_message_parts.extend(media_parts)
if text:
bot_message_parts.append({"type": "plain", "text": text})
bot_message_parts.extend(media_parts)
new_his = {"type": "bot", "message": bot_message_parts}
if reasoning:
new_his["reasoning"] = reasoning
if agent_stats:
new_his["agent_stats"] = agent_stats
record = await self.platform_history_mgr.insert(
platform_id="webchat",
@@ -297,8 +294,7 @@ class ChatRoute(Route):
accumulated_parts = []
accumulated_text = ""
accumulated_reasoning = ""
tool_calls = {}
agent_stats = {}
try:
async with track_conversation(self.running_convs, webchat_conv_id):
while True:
@@ -318,16 +314,6 @@ class ChatRoute(Route):
result_text = result["data"]
msg_type = result.get("type")
streaming = result.get("streaming", False)
chain_type = result.get("chain_type")
if chain_type == "agent_stats":
stats_info = {
"type": "agent_stats",
"data": json.loads(result_text),
}
yield f"data: {json.dumps(stats_info, ensure_ascii=False)}\n\n"
agent_stats = stats_info["data"]
continue
# 发送 SSE 数据
try:
@@ -349,35 +335,11 @@ class ChatRoute(Route):
# 累积消息部分
if msg_type == "plain":
chain_type = result.get("chain_type")
if chain_type == "tool_call":
tool_call = json.loads(result_text)
tool_calls[tool_call.get("id")] = tool_call
if accumulated_text:
# 如果累积了文本,则先保存文本
accumulated_parts.append(
{"type": "plain", "text": accumulated_text}
)
accumulated_text = ""
elif chain_type == "tool_call_result":
tcr = json.loads(result_text)
tc_id = tcr.get("id")
if tc_id in tool_calls:
tool_calls[tc_id]["result"] = tcr.get("result")
tool_calls[tc_id]["finished_ts"] = tcr.get("ts")
accumulated_parts.append(
{
"type": "tool_call",
"tool_calls": [tool_calls[tc_id]],
}
)
tool_calls.pop(tc_id, None)
elif chain_type == "reasoning":
chain_type = result.get("chain_type", "normal")
if chain_type == "reasoning":
accumulated_reasoning += result_text
elif streaming:
accumulated_text += result_text
else:
accumulated_text = result_text
accumulated_text += result_text
elif msg_type == "image":
filename = result_text.replace("[IMAGE]", "")
part = await self._create_attachment_from_file(
@@ -405,20 +367,15 @@ class ChatRoute(Route):
if msg_type == "end":
break
elif (
(streaming and msg_type == "complete") or not streaming
# or msg_type == "break"
(streaming and msg_type == "complete")
or not streaming
or msg_type == "break"
):
if (
chain_type == "tool_call"
or chain_type == "tool_call_result"
):
continue
saved_record = await self._save_bot_message(
webchat_conv_id,
accumulated_text,
accumulated_parts,
accumulated_reasoning,
agent_stats,
)
# 发送保存的消息信息给前端
if saved_record and not client_disconnected:
@@ -433,11 +390,11 @@ class ChatRoute(Route):
yield f"data: {json.dumps(saved_info, ensure_ascii=False)}\n\n"
except Exception:
pass
accumulated_parts = []
accumulated_text = ""
accumulated_reasoning = ""
tool_calls = {}
agent_stats = {}
# 重置累积变量 (对于 break 后的下一段消息)
if msg_type == "break":
accumulated_parts = []
accumulated_text = ""
accumulated_reasoning = ""
except BaseException as e:
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
-134
View File
@@ -1,134 +0,0 @@
#!/usr/bin/env python3
"""
Use Nuitka to build the AstrBot project into standalone executables
"""
import os
import platform
import subprocess
import sys
from pathlib import Path
def get_platform_info():
"""fetch the current platform information"""
system = platform.system()
machine = platform.machine()
return system, machine
def build_with_nuitka():
"""use Nuitka to build the project"""
system, machine = get_platform_info()
print(f"🚀 Starting build for {system} ({machine}) platform...")
# Output directory
output_dir = Path("build/nuitka")
output_dir.mkdir(parents=True, exist_ok=True)
# Base Nuitka command
nuitka_cmd = [
sys.executable,
"-m",
"nuitka",
"--standalone", # Create standalone directory
"--onefile", # Single file mode
"--follow-imports", # Follow all imports
"--enable-plugin=multiprocessing", # Enable multiprocessing support
"--output-dir=build/nuitka", # Output directory
"--quiet", # Reduce output verbosity
"--assume-yes-for-downloads", # Automatically download dependencies
"--jobs=4", # Use multiple CPU cores
]
# include specific packages
include_packages = [
"astrbot",
]
for pkg in include_packages:
nuitka_cmd.extend([f"--include-package={pkg}"])
# include data directories
# data_includes = [
# "data/config",
# "data/plugins",
# "data/temp",
# ]
# for data_dir in data_includes:
# if os.path.exists(data_dir):
# nuitka_cmd.extend([f"--include-data-dir={data_dir}={data_dir}"])
# include packages directory (built-in plugins)
# if os.path.exists("packages"):
# nuitka_cmd.extend(["--include-data-dir=packages=packages"])
# Platform specific settings
if system == "Darwin": # macOS
nuitka_cmd.extend(
[
"--macos-create-app-bundle", # Create .app bundle
"--macos-app-name=AstrBot",
]
)
# macOS icon (if exists)
icon_path = "dashboard/src-tauri/icons/icon.icns"
if os.path.exists(icon_path):
nuitka_cmd.extend([f"--macos-app-icon={icon_path}"])
elif system == "Windows":
nuitka_cmd.extend(
[
"--windows-console-mode=disable", # 无控制台窗口
]
)
# Windows icon (if exists)
icon_path = "dashboard/src-tauri/icons/icon.ico"
if os.path.exists(icon_path):
nuitka_cmd.extend([f"--windows-icon-from-ico={icon_path}"])
# Main file to compile
nuitka_cmd.append("main.py")
print(f"📦 Executing command: {' '.join(nuitka_cmd)}")
try:
subprocess.run(nuitka_cmd, check=True)
print("✅ Nuitka build successful!")
# Find the generated executable
if system == "Darwin":
built_file = list(output_dir.glob("*.app"))
if built_file:
print(f"Generated macOS app: {built_file[0]}")
elif system == "Windows":
built_file = list(output_dir.glob("*.exe"))
if built_file:
print(f"Generated Windows executable: {built_file[0]}")
else: # Linux
built_file = list(output_dir.glob("main.bin"))
if built_file:
print(f"Generated Linux executable: {built_file[0]}")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Nuitka build failed: {e}")
return False
if __name__ == "__main__":
print("=" * 60)
print("AstrBot Nuitka Builder")
print("=" * 60)
# 构建
if build_with_nuitka():
print("\n" + "=" * 60)
print("🎉 Build Complete!")
print("=" * 60)
else:
print("\n" + "=" * 60)
print("❌ Build Failed")
print("=" * 60)
sys.exit(1)
-134
View File
@@ -1,134 +0,0 @@
#!/usr/bin/env python3
"""
Use PyInstaller to build the AstrBot project into standalone executables
"""
import platform
import subprocess
import sys
from pathlib import Path
def get_platform_info():
"""fetch the current platform information"""
system = platform.system()
machine = platform.machine()
return system, machine
def build_with_pyinstaller():
"""use PyInstaller to build the project"""
system, machine = get_platform_info()
print(f"🚀 Starting build for {system} ({machine}) platform...")
# Output directory
output_dir = Path("build/pyinstaller")
output_dir.mkdir(parents=True, exist_ok=True)
# Base PyInstaller command
pyinstaller_cmd = [
sys.executable,
"-m",
"PyInstaller",
"--clean", # Clean cache before build
"--noconfirm", # Replace output directory without asking
"--onefile", # Single file mode
"--distpath=build/pyinstaller/dist", # Distribution directory
"--workpath=build/pyinstaller/build", # Work directory
"--specpath=build/pyinstaller", # Spec file directory
"--name=AstrBot", # Output executable name
]
# Platform specific settings
# if system == "Darwin": # macOS
# # macOS icon (if exists)
# icon_path = "dashboard/src-tauri/icons/icon.icns"
# if os.path.exists(icon_path):
# pyinstaller_cmd.extend([f"--icon={icon_path}"])
# # Create .app bundle
# pyinstaller_cmd.extend(["--windowed"])
# elif system == "Windows":
# # Windows icon (if exists)
# icon_path = "dashboard/src-tauri/icons/icon.ico"
# if os.path.exists(icon_path):
# pyinstaller_cmd.extend([f"--icon={icon_path}"])
# # No console window
# pyinstaller_cmd.extend(["--windowed"])
# else: # Linux
# pyinstaller_cmd.extend(["--console"])
# Main file to compile
pyinstaller_cmd.append("main.py")
print(f"📦 Executing command: {' '.join(pyinstaller_cmd)}")
try:
subprocess.run(pyinstaller_cmd, check=True)
print("✅ PyInstaller build successful!")
# Find the generated executable
dist_dir = output_dir / "dist"
if system == "Darwin":
built_file = list(dist_dir.glob("AstrBot.app"))
if not built_file:
built_file = list(dist_dir.glob("AstrBot"))
if built_file:
print(f"📱 Generated macOS app: {built_file[0]}")
elif system == "Windows":
built_file = list(dist_dir.glob("AstrBot.exe"))
if built_file:
print(f"💻 Generated Windows executable: {built_file[0]}")
else: # Linux
built_file = list(dist_dir.glob("AstrBot"))
if built_file:
print(f"🐧 Generated Linux executable: {built_file[0]}")
print(f"\n📁 Output directory: {dist_dir.absolute()}")
return True
except subprocess.CalledProcessError as e:
print(f"❌ PyInstaller build failed: {e}")
return False
except Exception as e:
print(f"❌ Unexpected error: {e}")
return False
def install_pyinstaller():
"""Install PyInstaller if not already installed"""
try:
import PyInstaller
print(f"✅ PyInstaller already installed (version {PyInstaller.__version__})")
return True
except ImportError:
print("📥 PyInstaller not found, installing...")
try:
subprocess.run(
[sys.executable, "-m", "pip", "install", "pyinstaller"], check=True
)
print("✅ PyInstaller installed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install PyInstaller: {e}")
return False
if __name__ == "__main__":
print("=" * 60)
print("AstrBot PyInstaller Builder")
print("=" * 60)
# Check and install PyInstaller
if not install_pyinstaller():
sys.exit(1)
# Build
if build_with_pyinstaller():
print("\n" + "=" * 60)
print("🎉 Build Complete!")
print("=" * 60)
else:
print("\n" + "=" * 60)
print("❌ Build Failed")
print("=" * 60)
sys.exit(1)
-3
View File
@@ -1,3 +0,0 @@
## What's Changed
-
-17
View File
@@ -1,17 +0,0 @@
## What's Changed
### 修复
- 企业自部署飞书(自定义 domain)可以接收消息但无法发送消息的问题。
- 安装插件 Dialog 的深色样式问题。
### 优化
- 避免某些插件在流式响应结束后重d复发送消息的问题。
### 新增
- 支持在对话管理批量导出对话轨迹数据为 `jsonl` 格式文件。入口:WebUI -> 对话管理 -> 批量选中 -> 导出。
- 支持对 TTS(文本转语音)设置概率触发。
- (插件开发)支持在 schema 中对 float 和 int 类型设置 `slider` 滑块控件。例如 `slider: {min: 0, max: 1, step: 0.1}`
- (插件开发)支持 key-value 存储功能。例如使用 `await self.put_kv_data("key", value)`, `await self.get_kv_data("key", default_value)``await self.delete_kv_data("key")`
-225
View File
@@ -1,225 +0,0 @@
# AstrBot Dashboard - Tauri 桌面应用
本项目现已支持通过 Tauri 构建为桌面应用,同时保持与 Web 版本的兼容性。
## 环境要求
### 系统依赖
**macOS:**
```bash
# 安装 Xcode Command Line Tools
xcode-select --install
```
**Windows:**
- 安装 [Microsoft Visual Studio C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
- 安装 [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
**Linux (Ubuntu/Debian):**
```bash
sudo apt update
sudo apt install libwebkit2gtk-4.0-dev \
build-essential \
curl \
wget \
file \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev
```
### Rust 环境
```bash
# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 验证安装
rustc --version
cargo --version
```
## 安装依赖
```bash
cd dashboard
npm install
```
## 开发模式
### Web 端开发(不变)
```bash
npm run dev
```
访问 http://localhost:3000
### 桌面端开发
```bash
npm run tauri:dev
```
这会同时启动:
1. Vite 开发服务器(端口 3000)
2. Tauri 桌面应用窗口
热重载功能正常工作,修改代码后会自动刷新。
## 构建
### Web 端构建(不变)
```bash
npm run build
```
输出目录:`dist/`
### 桌面端构建
```bash
npm run tauri:build
```
构建产物位置:
- **macOS**: `src-tauri/target/release/bundle/dmg/`
- **Windows**: `src-tauri/target/release/bundle/msi/`
- **Linux**: `src-tauri/target/release/bundle/deb/``appimage/`
## 图标设置
### 自动生成图标
准备一个至少 512x512 像素的 PNG 图标,然后运行:
```bash
npm run tauri icon path/to/your/icon.png
```
### 手动设置图标
将以下图标放入 `src-tauri/icons/` 目录:
- `32x32.png`
- `128x128.png`
- `128x128@2x.png`
- `icon.icns` (macOS)
- `icon.ico` (Windows)
## 代码兼容性
项目已配置为同时支持 Web 和桌面端,使用相同的代码库。
### 环境检测工具
`src/utils/tauri.ts` 中提供了环境检测工具:
```typescript
import { isTauri, isWeb, PlatformAPI } from '@/utils/tauri';
// 检测运行环境
if (isTauri()) {
console.log('运行在桌面应用中');
} else {
console.log('运行在浏览器中');
}
// 获取正确的 API 端点
const baseURL = PlatformAPI.getBaseURL();
```
### API 调用注意事项
- **Web 端**: 使用 Vite 代理,API 路径为 `/api/*`
- **桌面端**: 直接连接到 `http://127.0.0.1:6185`
已在 `PlatformAPI.getBaseURL()` 中处理,使用 axios 时:
```typescript
import axios from 'axios';
import { PlatformAPI } from '@/utils/tauri';
const api = axios.create({
baseURL: PlatformAPI.getBaseURL()
});
```
## 配置说明
### tauri.conf.json
主要配置项:
- `build.devPath`: 开发服务器地址(http://localhost:3000
- `build.distDir`: 构建输出目录(../dist
- `tauri.allowlist`: API 权限配置
- `tauri.windows`: 窗口配置(大小、标题等)
### 安全性
默认配置已启用必要的权限:
- 文件系统访问(限定在 APPDATA 目录)
- HTTP 请求(限定到本地后端)
- 窗口控制
- 对话框(打开/保存文件)
可在 `tauri.conf.json``allowlist` 部分调整权限。
## 后端连接
桌面应用需要后端服务运行在 `http://127.0.0.1:6185`
### 启动流程
1. 启动 AstrBot 后端:
```bash
cd /path/to/AstrBot
uv run main.py
```
2. 启动桌面应用:
```bash
cd dashboard
npm run tauri:dev
```
或直接运行打包后的应用(后端需要已启动)。
## 常见问题
### Q: 桌面应用无法连接到后端?
确保:
1. AstrBot 后端正在运行(`uv run main.py`
2. 后端监听在 `127.0.0.1:6185`
3. 防火墙未阻止连接
### Q: 图标未显示?
检查 `src-tauri/icons/` 目录中是否有所需的图标文件,或使用 `npm run tauri icon` 命令生成。
### Q: 构建失败?
- 确保已安装 Rust 和系统依赖
- 运行 `cargo clean` 清理缓存后重试
- 检查 Rust 版本(需要 1.60+
### Q: Web 端功能是否受影响?
不受影响。`npm run dev``npm run build` 的行为完全不变。
## 开发建议
1. **优先使用 Web 端开发**: 更快的热重载,更好的调试体验
2. **定期测试桌面端**: 确保跨平台兼容性
3. **使用环境检测**: 针对不同平台提供最佳体验
4. **注意 API 差异**: Web 和桌面端的某些 API 可能有差异
## 更多资源
- [Tauri 官方文档](https://tauri.app/)
- [Tauri API 参考](https://tauri.app/v1/api/js/)
- [Tauri Discord 社区](https://discord.com/invite/tauri)
+1 -6
View File
@@ -10,14 +10,10 @@
"build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/",
"preview": "vite preview --port 5050",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@guolao/vue-monaco-editor": "^1.5.4",
"@tauri-apps/api": "^2.9.0",
"@tiptap/starter-kit": "2.1.7",
"@tiptap/vue-3": "2.1.7",
"apexcharts": "3.42.0",
@@ -47,7 +43,6 @@
"devDependencies": {
"@mdi/font": "7.2.96",
"@rushstack/eslint-patch": "1.3.3",
"@tauri-apps/cli": "^2.9.4",
"@types/chance": "1.1.3",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.5.7",
-4509
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
# Tauri specific
src-tauri/target/
src-tauri/WixTools/
-4692
View File
File diff suppressed because it is too large Load Diff
-27
View File
@@ -1,27 +0,0 @@
[package]
name = "astrbot-dashboard"
version = "4.5.6"
description = "AstrBot"
authors = ["AstrBot Team"]
license = "AGPL-3.0"
repository = "https://github.com/AstrBotDevs/AstrBot"
default-run = "astrbot-dashboard"
edition = "2021"
rust-version = "1.91.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.9.2", features = ["macos-private-api", "protocol-asset"] }
tauri-plugin-opener = "2"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
{}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

-104
View File
@@ -1,104 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Child, Command};
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Listener, Manager, State};
struct BackendProcess(Mutex<Option<Child>>);
fn start_backend_process(app_handle: &AppHandle) -> Option<Child> {
#[cfg(target_os = "macos")]
let backend_path = "astrbot-backend.app/Contents/MacOS/main";
#[cfg(target_os = "windows")]
let backend_path = "astrbot-backend.exe";
#[cfg(target_os = "linux")]
let backend_path = "astrbot-backend";
// 获取资源目录
let resource_dir = match app_handle
.path()
.resource_dir()
{
Ok(dir) => dir,
Err(e) => {
eprintln!("Failed to get resource directory: {}", e);
return None;
}
};
let full_backend_path = resource_dir.join(backend_path);
println!("Starting backend process at: {:?}", full_backend_path);
match Command::new(&full_backend_path).spawn() {
Ok(child) => {
println!(
"Backend process started successfully with PID: {}",
child.id()
);
Some(child)
}
Err(e) => {
eprintln!("Failed to start backend process: {}", e);
None
}
}
}
#[tauri::command]
fn restart_backend(
app_handle: AppHandle,
backend_state: State<BackendProcess>,
) -> Result<String, String> {
let mut backend = backend_state.0.lock().unwrap();
// 停止现有进程
if let Some(mut child) = backend.take() {
let _ = child.kill();
let _ = child.wait();
}
// 启动新进程
*backend = start_backend_process(&app_handle);
if backend.is_some() {
Ok("Backend restarted successfully".to_string())
} else {
Err("Failed to restart backend".to_string())
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// 启动后端进程
let backend_process = start_backend_process(app.handle());
app.manage(BackendProcess(Mutex::new(backend_process)));
Ok(())
})
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![restart_backend])
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { .. } = event {
// 关闭窗口时清理后端进程
if let Some(backend_state) = window.app_handle().try_state::<BackendProcess>() {
let mut backend = backend_state.0.lock().unwrap();
if let Some(mut child) = backend.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn main() {
run();
}
-53
View File
@@ -1,53 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "AstrBot",
"version": "4.5.6",
"identifier": "com.astrbot.app",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"macOSPrivateApi": true,
"windows": [
{
"title": "AstrBot",
"label": "main",
"url": "/",
"width": 1400,
"height": 900
}
],
"security": {
"csp": null,
"assetProtocol": {
"enable": true,
"scope": [
"$APPDATA/**"
]
}
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"resources/*"
]
},
"plugins": {
"fs": {
"requireLiteralLeadingDot": false
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

-4
View File
@@ -575,9 +575,5 @@ onBeforeUnmount(() => {
.chat-page-container {
padding: 0 !important;
}
.conversation-header {
padding: 2px;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More