Compare commits

..

1 Commits

Author SHA1 Message Date
Soulter
4d00309d70 fix: remove python-ripgrep dependency, use system rg/grep instead
python-ripgrep 0.0.9 does not support Python 3.13 (requires <3.13,>=3.10).
This change replaces the python-ripgrep library with direct subprocess calls
to system ripgrep (rg) with fallback to grep.

Changes:
- Remove python-ripgrep import from local.py
- Rewrite search_files() to use shutil.which() to detect rg/grep availability
- Support ripgrep first, fallback to grep if not available
- Handle proper exit codes (0=success, 1=no matches for grep)
- Remove python-ripgrep from requirements.txt and pyproject.toml

Fixes #7496
2026-04-13 16:15:59 +08:00
43 changed files with 404 additions and 2234 deletions

View File

@@ -12,21 +12,15 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 10.28.2
- name: Setup Node.js
- name: nodejs installation
uses: actions/setup-node@v6
with:
node-version: "24.13.0"
cache: "pnpm"
cache-dependency-path: docs/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
working-directory: './docs'
- name: Build docs
run: pnpm run docs:build
node-version: "18"
- name: npm install
run: npm add -D vitepress
working-directory: './docs' # working-directory 指定 shell 命令运行目录
- name: npm run build
run: npm run docs:build
working-directory: './docs'
- name: scp
uses: appleboy/scp-action@v1.0.0

View File

@@ -14,22 +14,18 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 10.28.2
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.13.0'
cache: "pnpm"
cache-dependency-path: dashboard/pnpm-lock.yaml
- name: Install and Build
- name: npm install, build
run: |
pnpm --dir dashboard install --frozen-lockfile
pnpm --dir dashboard run build
cd dashboard
npm install pnpm -g
pnpm install
pnpm i --save-dev @types/markdown-it
pnpm run build
- name: Inject Commit SHA
id: get_sha

View File

@@ -98,7 +98,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Build and Push Nightly Image
uses: docker/build-push-action@v7.1.0
uses: docker/build-push-action@v7.0.0
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -183,7 +183,7 @@ jobs:
password: ${{ secrets.GHCR_GITHUB_TOKEN }}
- name: Build and Push Release Image
uses: docker/build-push-action@v7.1.0
uses: docker/build-push-action@v7.0.0
with:
context: .
platforms: linux/amd64,linux/arm64

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Validate PR title
uses: actions/github-script@v9
uses: actions/github-script@v8
with:
script: |
const title = (context.payload.pull_request.title || "").trim();

View File

@@ -51,7 +51,7 @@ jobs:
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.0
uses: pnpm/action-setup@v5.0.0
with:
version: 10.28.2

View File

@@ -13,23 +13,10 @@ on:
jobs:
smoke-test:
name: Smoke test (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
name: Run smoke tests
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
python-version:
- '3.10'
- '3.11'
- '3.12'
- '3.13'
- '3.14'
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -39,21 +26,33 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: requirements.txt
- name: Install uv
python-version: '3.12'
- name: Install UV package manager
run: |
python -m pip install --upgrade pip
python -m pip install uv
pip install uv
- name: Install dependencies
run: |
uv pip install --system -r requirements.txt
uv sync
timeout-minutes: 15
- name: Run smoke tests
run: |
python scripts/smoke_startup_check.py
uv run main.py &
APP_PID=$!
echo "Waiting for application to start..."
for i in {1..60}; do
if curl -f http://localhost:6185 > /dev/null 2>&1; then
echo "Application started successfully!"
kill $APP_PID
exit 0
fi
sleep 1
done
echo "Application failed to start within 30 seconds"
kill $APP_PID 2>/dev/null || true
exit 1
timeout-minutes: 2

View File

@@ -157,12 +157,11 @@ Connect AstrBot to your favorite chat platform.
| Discord | Official |
| LINE | Official |
| Satori | Official |
| KOOK | Official |
| Misskey | Official |
| Mattermost | Official |
| WhatsApp (Coming Soon) | Official |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Community |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | Community |
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Community |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Community |
## Supported Model Services

View File

@@ -156,12 +156,10 @@ Connectez AstrBot à vos plateformes de chat préférées.
| Discord | Officielle |
| LINE | Officielle |
| Satori | Officielle |
| KOOK | Officielle |
| Misskey | Officielle |
| Mattermost | Officielle |
| WhatsApp (Bientôt disponible) | Officielle |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Communauté |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | Communauté |
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Communauté |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Communauté |
## Services de modèles pris en charge

View File

@@ -156,12 +156,10 @@ AstrBot をよく使うチャットプラットフォームに接続できます
| Discord | 公式 |
| LINE | 公式 |
| Satori | 公式 |
| KOOK | 公式 |
| Misskey | 公式 |
| Mattermost | 公式 |
| WhatsApp (近日対応予定) | 公式 |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | コミュニティ |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | コミュニティ |
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | コミュニティ |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | コミュニティ |

View File

@@ -156,12 +156,10 @@ yay -S astrbot-git
| Discord | Официальная |
| LINE | Официальная |
| Satori | Официальная |
| KOOK | Официальная |
| Misskey | Официальная |
| Mattermost | Официальная |
| WhatsApp (Скоро) | Официальная |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Сообщество |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | Сообщество |
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Сообщество |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Сообщество |
## Поддерживаемые сервисы моделей

View File

@@ -156,12 +156,10 @@ yay -S astrbot-git
| Discord | 官方維護 |
| LINE | 官方維護 |
| Satori | 官方維護 |
| KOOK | 官方維護 |
| Misskey | 官方維護 |
| Mattermost | 官方維護 |
| Whatsapp即將支援 | 官方維護 |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | 社群維護 |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | 社群維護 |
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | 社群維護 |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | 社群維護 |
## 支援的模型服務

View File

@@ -156,12 +156,10 @@ yay -S astrbot-git
| **Discord** | 官方维护 |
| **LINE** | 官方维护 |
| **Satori** | 官方维护 |
| **KOOK** | 官方维护 |
| **Misskey** | 官方维护 |
| **Mattermost** | 官方维护 |
| **Whatsapp (将支持)** | 官方维护 |
| [**Matrix**](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | 社区维护 |
| [**Rocket.Chat**](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | 社区维护 |
| [**KOOK**](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | 社区维护 |
| [**VoceChat**](https://github.com/HikariFroya/astrbot_plugin_vocechat) | 社区维护 |
## 支持的模型提供商

View File

@@ -14,8 +14,6 @@ from astrbot.core.star.register import register_command_group as command_group
from astrbot.core.star.register import register_custom_filter as custom_filter
from astrbot.core.star.register import register_event_message_type as event_message_type
from astrbot.core.star.register import register_llm_tool as llm_tool
from astrbot.core.star.register import register_on_agent_begin as on_agent_begin
from astrbot.core.star.register import register_on_agent_done as on_agent_done
from astrbot.core.star.register import register_on_astrbot_loaded as on_astrbot_loaded
from astrbot.core.star.register import (
register_on_decorating_result as on_decorating_result,
@@ -53,8 +51,6 @@ __all__ = [
"custom_filter",
"event_message_type",
"llm_tool",
"on_agent_begin",
"on_agent_done",
"on_astrbot_loaded",
"on_decorating_result",
"on_llm_request",

View File

@@ -1 +1 @@
__version__ = "4.23.1"
__version__ = "4.23.0"

View File

@@ -12,15 +12,6 @@ from astrbot.core.star.star_handler import EventType
class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
async def on_agent_begin(
self, run_context: ContextWrapper[AstrAgentContext]
) -> None:
await call_event_hook(
run_context.context.event,
EventType.OnAgentBeginEvent,
run_context,
)
async def on_agent_done(self, run_context, llm_response) -> None:
# 执行事件钩子
if llm_response and llm_response.reasoning_content:
@@ -34,12 +25,6 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
EventType.OnLLMResponseEvent,
llm_response,
)
await call_event_hook(
run_context.context.event,
EventType.OnAgentDoneEvent,
run_context,
llm_response,
)
async def on_tool_start(
self,

View File

@@ -9,8 +9,6 @@ import sys
from dataclasses import dataclass
from typing import Any
from python_ripgrep import search
from astrbot.api import logger
from astrbot.core.computer.file_read_utils import (
detect_text_encoding,
@@ -221,15 +219,57 @@ class LocalFileSystemComponent(FileSystemComponent):
before_context: int | None = None,
) -> dict[str, Any]:
def _run() -> dict[str, Any]:
results = search(
patterns=[pattern],
paths=[path] if path else None,
globs=[glob] if glob else None,
after_context=after_context,
before_context=before_context,
line_number=True,
)
return {"success": True, "content": _truncate_long_lines("".join(results))}
search_path = path if path else "."
# Try ripgrep first, fallback to grep
if shutil.which("rg"):
cmd = ["rg", "--line-number", "--color=never"]
if glob:
cmd.extend(["--glob", glob])
if after_context:
cmd.extend(["--after-context", str(after_context)])
if before_context:
cmd.extend(["--before-context", str(before_context)])
cmd.extend([pattern, search_path])
elif shutil.which("grep"):
cmd = ["grep", "-rn", "--color=never"]
if after_context:
cmd.extend(["-A", str(after_context)])
if before_context:
cmd.extend(["-B", str(before_context)])
# grep doesn't support glob directly, use include if available
if glob and shutil.which("grep"):
# Try to use --include if grep supports it (GNU grep)
cmd.extend(["--include", glob])
cmd.extend([pattern, search_path])
else:
return {
"success": False,
"error": "Neither ripgrep (rg) nor grep is available on the system",
}
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30,
)
# grep returns exit code 1 when no matches found, which is not an error
if result.returncode not in (0, 1):
return {
"success": False,
"error": f"Search command failed with exit code {result.returncode}: {result.stderr}",
}
output = result.stdout if result.stdout else ""
return {"success": True, "content": _truncate_long_lines(output)}
except subprocess.TimeoutExpired:
return {
"success": False,
"error": "Search command timed out after 30 seconds",
}
except Exception as e:
return {"success": False, "error": f"Search failed: {str(e)}"}
return await asyncio.to_thread(_run)

View File

@@ -5,7 +5,7 @@ from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.23.1"
VERSION = "4.23.0"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
PERSONAL_WECHAT_CONFIG_METADATA = {
"weixin_oc_base_url": {

View File

@@ -4,7 +4,6 @@ import uuid
import numpy as np
from astrbot import logger
from astrbot.core.exceptions import KnowledgeBaseUploadError
from astrbot.core.provider.provider import EmbeddingProvider, RerankProvider
from ..base import BaseVecDB, Result
@@ -81,32 +80,6 @@ class FaissVecDB(BaseVecDB):
)
return []
content_count = len(contents)
if len(metadatas) != content_count:
raise KnowledgeBaseUploadError(
stage="storage",
user_message=(
f"存储失败:文本分块数量与元数据数量不一致(期望 {content_count}"
f"实际 {len(metadatas)})。"
),
details={
"expected_contents": content_count,
"actual_metadatas": len(metadatas),
},
)
if len(ids) != content_count:
raise KnowledgeBaseUploadError(
stage="storage",
user_message=(
f"存储失败:文本分块数量与文档 ID 数量不一致(期望 {content_count}"
f"实际 {len(ids)})。"
),
details={
"expected_contents": content_count,
"actual_ids": len(ids),
},
)
start = time.time()
logger.debug(f"Generating embeddings for {len(contents)} contents...")
vectors = await self.embedding_provider.get_embeddings_batch(
@@ -120,20 +93,6 @@ class FaissVecDB(BaseVecDB):
logger.debug(
f"Generated embeddings for {len(contents)} contents in {end - start:.2f} seconds.",
)
if len(vectors) != content_count:
raise KnowledgeBaseUploadError(
stage="embedding",
user_message=(
"向量化失败:嵌入模型返回的向量数量与文本分块数量不一致"
f"(期望 {content_count},实际 {len(vectors)})。"
"这通常说明当前 Embedding 接口未完整返回批量结果,"
"或该服务不兼容当前批量请求格式。"
),
details={
"expected_contents": content_count,
"actual_vectors": len(vectors),
},
)
# 使用 DocumentStorage 的批量插入方法
int_ids = await self.document_storage.insert_documents_batch(
@@ -141,52 +100,9 @@ class FaissVecDB(BaseVecDB):
contents,
metadatas,
)
if len(int_ids) != content_count:
raise KnowledgeBaseUploadError(
stage="storage",
user_message=(
f"存储失败:写入文档索引后返回的内部 ID 数量与文本分块数量不一致"
f"(期望 {content_count},实际 {len(int_ids)})。"
),
details={
"expected_contents": content_count,
"actual_int_ids": len(int_ids),
},
)
# 批量插入向量到 FAISS
try:
vectors_array = np.asarray(vectors, dtype=np.float32)
except (TypeError, ValueError) as exc:
raise KnowledgeBaseUploadError(
stage="embedding",
user_message=(
"向量化失败:嵌入模型返回的向量格式不正确,"
"无法转换为统一的浮点向量矩阵。"
),
details={"vector_count": len(vectors)},
) from exc
if vectors_array.ndim != 2:
raise KnowledgeBaseUploadError(
stage="embedding",
user_message=(
"向量化失败:嵌入模型返回的向量格式不正确,无法构造成二维向量矩阵。"
),
details={"actual_ndim": int(vectors_array.ndim)},
)
if vectors_array.shape[1] != self.embedding_storage.dimension:
raise KnowledgeBaseUploadError(
stage="embedding",
user_message=(
"向量化失败:返回向量维度与当前知识库索引维度不一致"
f"(期望 {self.embedding_storage.dimension}"
f"实际 {vectors_array.shape[1]})。"
),
details={
"expected_dimension": self.embedding_storage.dimension,
"actual_dimension": int(vectors_array.shape[1]),
},
)
vectors_array = np.array(vectors).astype("float32")
await self.embedding_storage.insert_batch(vectors_array, int_ids)
return int_ids

View File

@@ -11,22 +11,3 @@ class ProviderNotFoundError(AstrBotError):
class EmptyModelOutputError(AstrBotError):
"""Raised when the model response contains no usable assistant output."""
class KnowledgeBaseUploadError(AstrBotError):
"""Raised when knowledge base upload fails with a user-facing message."""
def __init__(
self,
*,
stage: str,
user_message: str,
details: dict | None = None,
) -> None:
super().__init__(user_message)
self.stage = stage
self.user_message = user_message
self.details = details or {}
def __str__(self) -> str:
return self.user_message

View File

@@ -10,7 +10,6 @@ import aiofiles
from astrbot.core import logger
from astrbot.core.db.vec_db.base import BaseVecDB
from astrbot.core.exceptions import KnowledgeBaseUploadError
from astrbot.core.provider.manager import ProviderManager
from astrbot.core.provider.provider import (
EmbeddingProvider,
@@ -265,31 +264,10 @@ class KBHelper:
if progress_callback:
await progress_callback("parsing", 0, 100)
try:
parser = await select_parser(f".{file_type}")
parse_result = await parser.parse(file_content, file_name)
except KnowledgeBaseUploadError:
raise
except Exception as exc:
raise KnowledgeBaseUploadError(
stage="parsing",
user_message=(
"文档解析失败:无法读取或解析上传文件。"
"请确认文件格式受支持且文件内容未损坏。"
),
details={"file_name": file_name},
) from exc
parser = await select_parser(f".{file_type}")
parse_result = await parser.parse(file_content, file_name)
text_content = parse_result.text
media_items = parse_result.media
if not text_content or not text_content.strip():
raise KnowledgeBaseUploadError(
stage="parsing",
user_message=(
"文档解析失败:未能从文件中提取可索引文本。"
"该文件可能是扫描件、纯图片 PDF或格式暂不受支持。"
),
details={"file_name": file_name},
)
if progress_callback:
await progress_callback("parsing", 100, 100)
@@ -310,40 +288,11 @@ class KBHelper:
if progress_callback:
await progress_callback("chunking", 0, 100)
try:
chunks_text = await self.chunker.chunk(
text_content,
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
)
except KnowledgeBaseUploadError:
raise
except Exception as exc:
raise KnowledgeBaseUploadError(
stage="chunking",
user_message=(
"分块失败:文档内容在切分文本块时发生错误。"
"请稍后重试,或调整分块参数后再次上传。"
),
details={"file_name": file_name},
) from exc
if not chunks_text or not any(chunk.strip() for chunk in chunks_text):
if pre_chunked_text is not None:
raise KnowledgeBaseUploadError(
stage="validation",
user_message=("预分块文本为空,未提供任何可索引文本块。"),
details={"file_name": file_name},
)
else:
raise KnowledgeBaseUploadError(
stage="chunking",
user_message=(
"分块失败:文档内容为空,未生成任何可索引文本块。"
),
details={"file_name": file_name},
)
chunks_text = await self.chunker.chunk(
text_content,
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
)
contents = []
metadatas = []
for idx, chunk_text in enumerate(chunks_text):
@@ -364,23 +313,14 @@ class KBHelper:
if progress_callback:
await progress_callback("embedding", current, total)
try:
await self.vec_db.insert_batch(
contents=contents,
metadatas=metadatas,
batch_size=batch_size,
tasks_limit=tasks_limit,
max_retries=max_retries,
progress_callback=embedding_progress_callback,
)
except KnowledgeBaseUploadError:
raise
except Exception as exc:
raise KnowledgeBaseUploadError(
stage="storage",
user_message=("存储失败:文本块已生成,但写入知识库索引时出错。"),
details={"file_name": file_name},
) from exc
await self.vec_db.insert_batch(
contents=contents,
metadatas=metadatas,
batch_size=batch_size,
tasks_limit=tasks_limit,
max_retries=max_retries,
progress_callback=embedding_progress_callback,
)
# 保存文档的元数据
doc = KBDocument(
@@ -394,47 +334,22 @@ class KBHelper:
chunk_count=len(chunks_text),
media_count=0,
)
try:
async with self.kb_db.get_db() as session:
async with session.begin():
session.add(doc)
for media in saved_media:
session.add(media)
await session.commit()
async with self.kb_db.get_db() as session:
async with session.begin():
session.add(doc)
for media in saved_media:
session.add(media)
await session.commit()
await session.refresh(doc)
except KnowledgeBaseUploadError:
raise
except Exception as exc:
raise KnowledgeBaseUploadError(
stage="metadata",
user_message=(
"元数据保存失败:文本块已写入知识库,但文档记录保存失败。"
),
details={"file_name": file_name, "doc_id": doc_id},
) from exc
await session.refresh(doc)
vec_db: FaissVecDB = self.vec_db # type: ignore
try:
await self.kb_db.update_kb_stats(kb_id=self.kb.kb_id, vec_db=vec_db)
await self.refresh_kb()
await self.refresh_document(doc_id)
except KnowledgeBaseUploadError:
raise
except Exception as exc:
raise KnowledgeBaseUploadError(
stage="metadata",
user_message=(
"元数据更新失败:文档已上传,但知识库统计信息刷新失败。"
),
details={"file_name": file_name, "doc_id": doc_id},
) from exc
await self.kb_db.update_kb_stats(kb_id=self.kb.kb_id, vec_db=vec_db)
await self.refresh_kb()
await self.refresh_document(doc_id)
return doc
except Exception as e:
if isinstance(e, KnowledgeBaseUploadError):
logger.warning(f"上传文档失败: {e}", extra={"details": e.details})
else:
logger.error(f"上传文档失败: {e}", exc_info=True)
logger.error(f"上传文档失败: {e}")
# if file_path.exists():
# file_path.unlink()
@@ -445,7 +360,7 @@ class KBHelper:
except Exception as me:
logger.warning(f"清理多媒体文件失败 {media_path}: {me}")
raise
raise e
async def list_documents(
self,

View File

@@ -7,8 +7,6 @@ from .star_handler import (
register_custom_filter,
register_event_message_type,
register_llm_tool,
register_on_agent_begin,
register_on_agent_done,
register_on_astrbot_loaded,
register_on_decorating_result,
register_on_llm_request,
@@ -33,8 +31,6 @@ __all__ = [
"register_custom_filter",
"register_event_message_type",
"register_llm_tool",
"register_on_agent_begin",
"register_on_agent_done",
"register_on_astrbot_loaded",
"register_on_decorating_result",
"register_on_llm_request",

View File

@@ -460,64 +460,6 @@ def register_on_llm_response(**kwargs):
return decorator
def register_on_agent_begin(**kwargs):
"""当 Agent 开始运行时的事件
Examples:
```py
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.astr_agent_context import AstrAgentContext
@on_agent_begin()
async def test(
self,
event: AstrMessageEvent,
run_context: ContextWrapper[AstrAgentContext],
) -> None:
...
```
请务必接收两个参数event, run_context
"""
def decorator(awaitable):
_ = get_handler_or_create(awaitable, EventType.OnAgentBeginEvent, **kwargs)
return awaitable
return decorator
def register_on_agent_done(**kwargs):
"""当 Agent 运行完成后的事件
Examples:
```py
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.astr_agent_context import AstrAgentContext
from astrbot.api.provider import LLMResponse
@on_agent_done()
async def test(
self,
event: AstrMessageEvent,
run_context: ContextWrapper[AstrAgentContext],
response: LLMResponse,
) -> None:
...
```
请务必接收三个参数event, run_context, response
"""
def decorator(awaitable):
_ = get_handler_or_create(awaitable, EventType.OnAgentDoneEvent, **kwargs)
return awaitable
return decorator
def register_on_using_llm_tool(**kwargs):
"""当调用函数工具前的事件。
会传入 tool 和 tool_args 参数。

View File

@@ -71,22 +71,6 @@ class StarHandlerRegistry(Generic[T]):
plugins_name: list[str] | None = None,
) -> list[StarHandlerMetadata[Callable[..., Awaitable[Any]]]]: ...
@overload
def get_handlers_by_event_type(
self,
event_type: Literal[EventType.OnAgentBeginEvent],
only_activated=True,
plugins_name: list[str] | None = None,
) -> list[StarHandlerMetadata[Callable[..., Awaitable[Any]]]]: ...
@overload
def get_handlers_by_event_type(
self,
event_type: Literal[EventType.OnAgentDoneEvent],
only_activated=True,
plugins_name: list[str] | None = None,
) -> list[StarHandlerMetadata[Callable[..., Awaitable[Any]]]]: ...
@overload
def get_handlers_by_event_type(
self,
@@ -229,8 +213,6 @@ class EventType(enum.Enum):
OnWaitingLLMRequestEvent = enum.auto() # 等待调用 LLM在获取锁之前仅通知
OnLLMRequestEvent = enum.auto() # 收到 LLM 请求(可以是用户也可以是插件)
OnLLMResponseEvent = enum.auto() # LLM 响应后
OnAgentBeginEvent = enum.auto() # Agent 开始运行
OnAgentDoneEvent = enum.auto() # Agent 运行完成
OnDecoratingResultEvent = enum.auto() # 发送消息前
OnCallingFuncToolEvent = enum.auto() # 调用函数工具
OnUsingLLMToolEvent = enum.auto() # 使用 LLM 工具

View File

@@ -1,9 +1,6 @@
import asyncio
import base64
import logging
import random
from functools import lru_cache
from pathlib import Path
import aiohttp
@@ -19,31 +16,6 @@ ASTRBOT_T2I_DEFAULT_ENDPOINT = "https://t2i.soulter.top/text2img"
logger = logging.getLogger("astrbot")
@lru_cache(maxsize=1)
def get_shiki_runtime() -> str:
runtime_path = (
Path(__file__).resolve().parent / "template" / "shiki_runtime.iife.js"
)
if not runtime_path.exists():
logger.error(
"T2I Shiki runtime not found at %s. Run `cd dashboard && pnpm run build:t2i-shiki-runtime` to regenerate it. Continuing without code highlighting.",
runtime_path,
)
return ""
try:
runtime = runtime_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as err:
logger.warning(
"Failed to load T2I Shiki runtime from %s: %s. Continuing without code highlighting.",
runtime_path,
err,
)
return ""
return runtime.replace("</script", "<\\/script")
class NetworkRenderStrategy(RenderStrategy):
def __init__(self, base_url: str | None = None) -> None:
super().__init__()
@@ -105,7 +77,6 @@ class NetworkRenderStrategy(RenderStrategy):
if options:
default_options |= options
tmpl_data = {"shiki_runtime": get_shiki_runtime()} | tmpl_data
post_data = {
"tmpl": tmpl_str,
"json": return_url,
@@ -158,9 +129,9 @@ class NetworkRenderStrategy(RenderStrategy):
if not template_name:
template_name = "base"
tmpl_str = await self.get_template(name=template_name)
text_base64 = base64.b64encode(text.encode("utf-8")).decode("ascii")
text = text.replace("`", "\\`")
return await self.render_custom_template(
tmpl_str,
{"text_base64": text_base64, "version": f"v{VERSION}"},
{"text": text, "version": f"v{VERSION}"},
return_url,
)

View File

@@ -2,15 +2,20 @@
<html>
<head>
<meta charset="utf-8"/>
<title>Astrbot PowerShell {{ version }}</title>
<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;
--font-family: 'Consolas', 'Microsoft YaHei Mono', 'Dengxian Mono', 'Courier New', monospace;
--glow-color: rgba(200, 220, 255, 0.7);
}
@@ -31,6 +36,7 @@
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;
@@ -57,9 +63,9 @@
color: var(--title-text-color);
font-size: 16px;
border-bottom: 1px solid #30363d;
text-shadow: none;
text-shadow: none; /* No glow for title bar */
}
.header .title {
font-weight: bold;
font-size: 28px;
@@ -72,10 +78,13 @@
main {
padding: 1rem 1.5rem;
position: relative;
z-index: 1;
}
#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;
@@ -135,16 +144,7 @@
font-size: 100%;
background-color: transparent;
border-radius: 0;
text-shadow: none;
}
pre.shiki {
padding: 1rem;
}
pre.shiki > code,
pre.shiki span {
text-shadow: none;
text-shadow: none; /* Disable glow inside code blocks for clarity */
}
a {
@@ -165,8 +165,9 @@
</style>
</head>
<body>
<div class="header">
<span class="title">&gt; Astrbot PowerShell</span>
<span class="title">> Astrbot PowerShell</span>
<span class="version">{{ version }}</span>
</div>
@@ -174,45 +175,10 @@
<div id="content"></div>
</main>
<script>{{ shiki_runtime | safe }}</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"></script>
<script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
contentElement.innerHTML = marked.parse(source);
if (window.AstrBotT2IShiki) {
window.AstrBotT2IShiki.highlightAllCodeBlocks(contentElement, "github-dark");
}
if (window.renderMathInElement) {
window.renderMathInElement(contentElement, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false }
]
});
}
function decodeBase64Utf8(base64Text) {
const binary = window.atob(base64Text || "");
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (window.TextDecoder) {
return new TextDecoder().decode(bytes);
}
let fallback = "";
bytes.forEach((byte) => {
fallback += String.fromCharCode(byte);
});
return decodeURIComponent(escape(fallback));
}
})();
document.getElementById('content').innerHTML = marked.parse(`{{ text | safe }}`);
</script>
</body>
</html>
</html>

View File

@@ -1,552 +0,0 @@
<!doctype html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="utf-8"/>
<title>AstrBot Docs {{ 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">
<style>
:root {
--vp-c-bg: #1b1b1f;
--vp-c-bg-soft: #202127;
--vp-c-bg-alt: #161618;
--vp-c-bg-elv: #202127;
--vp-c-border: #3c3f44;
--vp-c-divider: #2e2e32;
--vp-c-gutter: #000000;
--vp-c-text-1: #dfdfd6;
--vp-c-text-2: #98989f;
--vp-c-text-3: #6a6a71;
--vp-c-brand-1: #a8b1ff;
--vp-c-brand-2: #5c73e7;
--vp-c-brand-3: #3e63dd;
--vp-c-brand-soft: rgba(100, 108, 255, 0.16);
--vp-c-default-soft: rgba(101, 117, 133, 0.16);
--vp-code-bg: var(--vp-c-default-soft);
--vp-code-block-bg: var(--vp-c-bg-alt);
--vp-code-line-height: 1.7;
--vp-code-font-size: 0.875em;
--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, 0.07), 0 1px 4px rgba(0, 0, 0, 0.07);
--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
--vp-layout-max-width: 1440px;
--vp-nav-height: 64px;
--vp-radius: 12px;
--vp-font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--vp-font-family-cjk: "Inter4CJK", -apple-system, BlinkMacSystemFont, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--vp-code-font-family: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;
}
* {
box-sizing: border-box;
}
html {
background: var(--vp-c-bg);
}
body {
margin: 0;
min-height: 100vh;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family);
background: linear-gradient(180deg, rgba(27, 27, 31, 0.96), rgba(27, 27, 31, 1));
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body:lang(zh),
body:lang(ja) {
font-family: var(--vp-font-family-cjk);
}
a {
color: var(--vp-c-brand-1);
text-decoration: underline;
text-underline-offset: 2px;
transition: color 0.25s, opacity 0.25s;
}
a:hover {
color: var(--vp-c-brand-2);
}
#app {
max-width: var(--vp-layout-max-width);
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.vp-nav {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
height: var(--vp-nav-height);
padding: 0 32px;
backdrop-filter: blur(18px);
background: rgba(27, 27, 31, 0.9);
border-bottom: 1px solid var(--vp-c-gutter);
}
.vp-brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
font-size: 20px;
letter-spacing: -0.01em;
}
.vp-brand-logo {
width: 28px;
height: 28px;
object-fit: contain;
filter: drop-shadow(0 6px 16px rgba(62, 99, 221, 0.24));
}
.vp-brand-name {
color: #ffffff;
}
.vp-nav-actions {
display: flex;
align-items: center;
gap: 12px;
color: var(--vp-c-text-2);
font-size: 14px;
}
.vp-search {
display: none;
}
.vp-search kbd {
display: none;
}
.vp-shell {
padding: 42px 32px 56px;
flex: 1 0 auto;
}
.vp-main {
min-width: 0;
}
.vp-content-frame {
max-width: 980px;
margin: 0 auto;
}
.vp-hero {
display: block;
margin-bottom: 36px;
}
.vp-hero.is-hidden {
display: none;
}
.vp-hero-copy h1 {
margin: 0;
font-size: 48px;
line-height: 1.05;
letter-spacing: -0.04em;
color: #ffffff;
}
.vp-hero-copy p {
max-width: 720px;
margin: 16px 0 0;
font-size: 18px;
line-height: 1.78;
color: var(--vp-c-text-2);
}
.vp-doc {
color: var(--vp-c-text-1);
font-size: 16px;
line-height: 28px;
}
.vp-doc > *:first-child {
margin-top: 0;
}
.vp-doc h1,
.vp-doc h2,
.vp-doc h3,
.vp-doc h4 {
position: relative;
scroll-margin-top: 100px;
color: #ffffff;
font-weight: 600;
letter-spacing: -0.02em;
}
.vp-doc h1 {
margin: 0 0 20px;
font-size: 32px;
line-height: 40px;
}
.vp-doc h2 {
margin: 48px 0 16px;
padding-top: 24px;
font-size: 24px;
line-height: 32px;
border-top: 1px solid var(--vp-c-divider);
}
.vp-doc h3 {
margin: 32px 0 0;
font-size: 20px;
line-height: 28px;
}
.vp-doc p,
.vp-doc ul,
.vp-doc ol,
.vp-doc table,
.vp-doc blockquote,
.vp-doc [class*="language-"],
.vp-doc .math {
margin: 16px 0;
}
.vp-doc strong {
color: #ffffff;
}
.vp-doc code {
font-family: var(--vp-code-font-family);
padding: 3px 6px;
border-radius: 4px;
color: var(--vp-c-brand-1);
background: var(--vp-code-bg);
}
.vp-doc pre code {
padding: 0;
background: transparent;
color: inherit;
}
.vp-doc [class*="language-"],
.vp-block {
position: relative;
overflow: hidden;
background-color: var(--vp-code-block-bg);
transition: background-color 0.5s;
border-radius: 8px;
}
.vp-doc [class*="language-"] > span.lang,
.vp-block > span.lang {
position: absolute;
top: 2px;
right: 8px;
z-index: 2;
font-size: 12px;
font-weight: 500;
user-select: none;
color: var(--vp-c-text-2);
background: transparent;
transition: color 0.4s, opacity 0.4s;
}
.vp-doc [class*="language-"] pre.shiki,
.vp-doc [class*="language-"] pre,
.vp-block pre.shiki,
.vp-block pre {
margin: 0;
padding: 20px 0;
border: 0;
border-radius: 8px;
overflow-x: auto;
background: var(--vp-code-block-bg) !important;
line-height: var(--vp-code-line-height);
font-size: var(--vp-code-font-size);
}
.vp-doc [class*="language-"] code,
.vp-block code {
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
}
.vp-doc [class*="language-"] pre.shiki code,
.vp-doc [class*="language-"] pre code,
.vp-block pre.shiki code,
.vp-block pre code {
font-family: var(--vp-code-font-family);
display: block;
width: fit-content;
min-width: 100%;
padding: 0 24px;
font-size: 14px;
color: inherit;
}
.vp-doc [class*="language-"] pre.shiki,
.vp-block pre.shiki {
color: var(--shiki-dark, #e1e4e8) !important;
background-color: var(--shiki-dark-bg, var(--vp-code-block-bg)) !important;
}
.vp-doc [class*="language-"] pre.shiki > code,
.vp-doc [class*="language-"] pre.shiki span,
.vp-block pre.shiki > code,
.vp-block pre.shiki span {
text-shadow: none;
}
.vp-doc blockquote {
margin: 16px 0;
padding-left: 16px;
border-left: 2px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.vp-doc blockquote p {
margin: 0;
font-size: 16px;
}
.vp-doc hr {
height: 1px;
border: 0;
margin: 38px 0;
background: var(--vp-c-divider);
}
.vp-doc img {
max-width: 100%;
display: block;
margin: 22px auto;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
box-shadow: var(--vp-shadow-3);
}
.vp-doc ul,
.vp-doc ol {
padding-left: 1.35rem;
color: var(--vp-c-text-1);
}
.vp-doc li {
margin: 8px 0;
}
.vp-doc table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border-radius: 8px;
border: 1px solid var(--vp-c-border);
background: rgba(255, 255, 255, 0.02);
}
.vp-doc thead {
background: rgba(255, 255, 255, 0.04);
}
.vp-doc th,
.vp-doc td {
padding: 8px 16px;
border-bottom: 1px solid var(--vp-c-divider);
text-align: left;
}
.vp-doc tr:last-child td {
border-bottom: 0;
}
.vp-footer {
height: 72px;
padding: 0 32px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 72px;
color: var(--vp-c-text-3);
font-size: 14px;
border-top: 1px solid var(--vp-c-gutter);
}
@media (max-width: 1180px) {
.vp-shell {
padding-inline: 28px;
}
}
</style>
</head>
<body>
<div id="app">
<header class="vp-nav">
<div class="vp-brand">
<img class="vp-brand-logo" src="https://cf.s3.soulter.top/astrbot-logo.svg" alt="AstrBot logo" />
<div class="vp-brand-name">AstrBot</div>
</div>
<div class="vp-nav-actions">
<span>{{ version }}</span>
</div>
</header>
<div class="vp-shell">
<main class="vp-main">
<div class="vp-content-frame">
<div class="vp-hero is-hidden" id="heroBlock">
<div class="vp-hero-copy">
<h1 id="heroTitle">AstrBot Docs</h1>
<p id="heroLead">将长文本内容整理为单页文档,参考 VitePress 默认主题的深色配色、正文排版与代码块节奏,适合技术说明与发布页。</p>
</div>
</div>
<article class="vp-doc" id="content"></article>
</div>
</main>
</div>
<footer class="vp-footer">Rendered by AstrBot {{ version }}</footer>
</div>
<script>{{ shiki_runtime | safe }}</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"></script>
<script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
marked.setOptions({
gfm: true,
breaks: false,
});
contentElement.innerHTML = marked.parse(source);
assignHeadingIds(contentElement);
enhanceCodeBlocks(contentElement);
if (window.renderMathInElement) {
window.renderMathInElement(contentElement, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false }
]
});
}
const headings = collectHeadings(contentElement);
populateHero(contentElement, headings);
function decodeBase64Utf8(base64Text) {
const binary = window.atob(base64Text || "");
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (window.TextDecoder) {
return new TextDecoder().decode(bytes);
}
let fallback = "";
bytes.forEach((byte) => {
fallback += String.fromCharCode(byte);
});
return decodeURIComponent(escape(fallback));
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function assignHeadingIds(root) {
Array.from(root.querySelectorAll("h1, h2, h3")).forEach((heading, index) => {
heading.id = `section-${index + 1}`;
});
}
function collectHeadings(root) {
return Array.from(root.querySelectorAll("h1, h2, h3")).map((heading) => ({
element: heading,
id: heading.id,
level: Number(heading.tagName.slice(1)),
text: heading.textContent.trim(),
}));
}
function populateHero(root, headings) {
const heroBlock = document.getElementById("heroBlock");
const heroTitle = document.getElementById("heroTitle");
const heroLead = document.getElementById("heroLead");
const firstHeading = headings.find((heading) => heading.level === 1) || headings[0];
if (!firstHeading) {
return;
}
heroBlock.classList.remove("is-hidden");
const leadParagraph = firstHeading.element.nextElementSibling;
const title = firstHeading.text;
heroTitle.textContent = title;
if (leadParagraph && leadParagraph.tagName === "P") {
heroLead.textContent = leadParagraph.textContent.trim();
leadParagraph.remove();
}
if (firstHeading.element.parentElement === root) {
firstHeading.element.remove();
}
}
function extractLanguage(codeElement) {
const className = codeElement.className || "";
const match = className.match(/language-([\\w+#.-]+)/i);
return match ? match[1] : "";
}
function enhanceCodeBlocks(root) {
const blocks = Array.from(root.querySelectorAll("pre > code")).map((codeElement) => ({
rawLanguage: extractLanguage(codeElement),
displayLanguage: extractLanguage(codeElement).trim().split(/\\s+/, 1)[0].toLowerCase(),
}));
if (window.AstrBotT2IShiki) {
window.AstrBotT2IShiki.highlightAllCodeBlocks(root, "github-dark");
}
Array.from(root.querySelectorAll("pre")).forEach((preElement, index) => {
if (preElement.parentElement && preElement.parentElement.classList.contains("vp-code-block")) {
return;
}
const block = blocks[index] || { displayLanguage: "" };
const wrapper = document.createElement("div");
wrapper.className = `language-${block.displayLanguage || "text"}`;
if (block.displayLanguage) {
wrapper.innerHTML = `<span class="lang">${escapeHtml(block.displayLanguage)}</span>`;
}
preElement.replaceWith(wrapper);
wrapper.appendChild(preElement);
});
}
})();
</script>
</body>
</html>

View File

@@ -2,285 +2,246 @@
<html>
<head>
<meta charset="utf-8"/>
<title>AstrBot {{ 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">
<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;
}
pre.shiki {
padding: 15px 12px;
}
pre.shiki > code {
padding: 0;
background: transparent !important;
color: inherit;
font-size: 12px;
}
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: 0 -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>
<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;">
<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>{{ shiki_runtime | safe }}</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"></script>
<script>
(function () {
const contentElement = document.getElementById("content");
const source = decodeBase64Utf8("{{ text_base64 }}");
contentElement.innerHTML = marked.parse(source);
if (window.AstrBotT2IShiki) {
window.AstrBotT2IShiki.highlightAllCodeBlocks(contentElement, "github-light");
}
if (window.renderMathInElement) {
window.renderMathInElement(contentElement, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false }
]
});
}
function decodeBase64Utf8(base64Text) {
const binary = window.atob(base64Text || "");
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (window.TextDecoder) {
return new TextDecoder().decode(bytes);
}
let fallback = "";
bytes.forEach((byte) => {
fallback += String.fromCharCode(byte);
});
return decodeURIComponent(escape(fallback));
}
})();
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>

File diff suppressed because one or more lines are too long

View File

@@ -12,11 +12,7 @@ class TemplateManager:
所有创建、更新、删除操作仅影响用户目录,以确保更新框架时用户数据安全。
"""
CORE_TEMPLATES = [
"base.html",
"astrbot_powershell.html",
"astrbot_vitepress.html",
]
CORE_TEMPLATES = ["base.html", "astrbot_powershell.html"]
def __init__(self) -> None:
self.builtin_template_dir = os.path.join(

View File

@@ -128,13 +128,6 @@ class KnowledgeBaseRoute(Route):
return _callback
@staticmethod
def _format_failed_doc_error(file_name: str, error: Exception) -> str:
message = str(error).strip() or "上传失败:发生未知错误。"
if message.startswith(file_name):
return message
return f"{file_name}: {message}"
async def _background_upload_task(
self,
task_id: str,
@@ -196,12 +189,7 @@ class KnowledgeBaseRoute(Route):
except Exception as e:
logger.error(f"上传文档 {file_info['file_name']} 失败: {e}")
failed_docs.append(
{
"file_name": file_info["file_name"],
"error": self._format_failed_doc_error(
file_info["file_name"], e
),
},
{"file_name": file_info["file_name"], "error": str(e)},
)
# 更新任务完成状态
@@ -288,10 +276,7 @@ class KnowledgeBaseRoute(Route):
except Exception as e:
logger.error(f"导入文档 {file_name} 失败: {e}")
failed_docs.append(
{
"file_name": file_name,
"error": self._format_failed_doc_error(file_name, e),
},
{"file_name": file_name, "error": str(e)},
)
# 更新任务完成状态

View File

@@ -79,8 +79,6 @@ class PluginRoute(Route):
EventType.AdapterMessageEvent: "平台消息下发时",
EventType.OnLLMRequestEvent: "LLM 请求时",
EventType.OnLLMResponseEvent: "LLM 响应后",
EventType.OnAgentBeginEvent: "Agent 开始运行时",
EventType.OnAgentDoneEvent: "Agent 运行完成后",
EventType.OnDecoratingResultEvent: "回复消息前",
EventType.OnCallingFuncToolEvent: "函数工具",
EventType.OnAfterMessageSentEvent: "发送消息后",

View File

@@ -1,29 +0,0 @@
- [更新日志(简体中文)](#chinese)
- [Changelog(English)](#english)
<a id="chinese"></a>
## What's Changed
*hotfix of v4.23.0*
-`python-ripgrep` 依赖降级到 `0.0.8`,修复 Python 3.13, 3.14 版本无法正常启动的问题。([#7514](https://github.com/AstrBotDevs/AstrBot/pull/7514)
- 修复会话 ID 包含冒号 `:` 时,平台路由在 Dashboard 中无法显示的问题。([#7517](https://github.com/AstrBotDevs/AstrBot/pull/7517)
- 适配 DeerFlow 2.0,更新 DeerFlow Runner、API Client、会话命令、Provider 管理逻辑、配置文档与相关测试。([#7500](https://github.com/AstrBotDevs/AstrBot/pull/7500)
- 移除 Dashboard `v-main` 不必要的 margin额外的外边距会把背景色显露出来因此视觉上看起来像右侧多了一条边框。[#7481](https://github.com/AstrBotDevs/AstrBot/pull/7481)
- 修复插件安装状态检查时格式不一致导致的判断问题。([#7493](https://github.com/AstrBotDevs/AstrBot/pull/7493)
- Dashboard 代码块高亮切换到 Shiki并同步暗色/亮色主题渲染,优化 README、更新日志与聊天消息中的代码块显示效果。[#7497](https://github.com/AstrBotDevs/AstrBot/pull/7497)
<a id="english"></a>
## What's Changed (EN)
*hotfix of v4.23.0*
- Fixed inconsistent format handling when checking whether a plugin is installed. ([#7493](https://github.com/AstrBotDevs/AstrBot/pull/7493))
- Downgraded `python-ripgrep` to `0.0.8` to fix dependency compatibility issues. ([#7514](https://github.com/AstrBotDevs/AstrBot/pull/7514))
- Fixed platform routes not being displayed in Dashboard when the session ID contains a colon `:`. ([#7517](https://github.com/AstrBotDevs/AstrBot/pull/7517))
- Switched Dashboard code-block highlighting to Shiki, synchronized dark/light theme rendering, and improved code-block display in README, changelog, and chat messages. ([#7497](https://github.com/AstrBotDevs/AstrBot/pull/7497))
- Aligned the DeerFlow runner with DeerFlow 2.0, including updates to the runner, API client, conversation commands, provider management, documentation, and tests. ([#7500](https://github.com/AstrBotDevs/AstrBot/pull/7500))
- Removed unnecessary `v-main` margins in Dashboard for more consistent layout across viewports. ([#7481](https://github.com/AstrBotDevs/AstrBot/pull/7481))
- Updated the smoke test workflow to cover multiple operating systems and Python versions, and added a startup check script. ([#7514](https://github.com/AstrBotDevs/AstrBot/pull/7514))

View File

@@ -6,7 +6,6 @@
"scripts": {
"dev": "vite --host",
"subset-icons": "node scripts/subset-mdi-font.mjs",
"build:t2i-shiki-runtime": "node scripts/build-t2i-shiki-runtime.mjs",
"build": "vue-tsc --noEmit && vite build",
"build-stage": "vue-tsc --noEmit && vite build --base=/vue/free/stage/",
"build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/",

View File

@@ -1,232 +0,0 @@
import { createRequire } from "node:module";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { build } from "vite";
const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dashboardRoot = path.resolve(__dirname, "..");
const runtimeOutputFile = path.resolve(
dashboardRoot,
"..",
"astrbot",
"core",
"utils",
"t2i",
"template",
"shiki_runtime.iife.js",
);
const shikiRequire = createRequire(require.resolve("shiki/package.json"));
const languageSpecs = [
["bash", "bash"],
["css", "css"],
["html", "html"],
["javascript", "javascript"],
["json", "json"],
["jsx", "jsx"],
["markdown", "markdown"],
["powershell", "powershell"],
["python", "python"],
["sql", "sql"],
["tsx", "tsx"],
["typescript", "typescript"],
["xml", "xml"],
["yaml", "yaml"],
];
const themeSpecs = [
["github-light", "github-light"],
["github-dark", "github-dark"],
];
// Shiki exposes plain text as a built-in special language, so we keep it
// in the supported language list without importing a package for it.
const builtInLanguageSpecs = ["text"];
const languageAliases = {
bat: "powershell",
cjs: "javascript",
console: "bash",
cts: "typescript",
dockerfile: "bash",
env: "bash",
htm: "html",
js: "javascript",
md: "markdown",
mjs: "javascript",
mts: "typescript",
plain: "text",
plaintext: "text",
ps1: "powershell",
pwsh: "powershell",
py: "python",
shell: "bash",
shellscript: "bash",
sh: "bash",
svg: "xml",
text: "text",
ts: "typescript",
txt: "text",
vue: "html",
xhtml: "html",
xml: "xml",
yml: "yaml",
zsh: "bash",
};
function resolveShikiModule(specifier) {
return pathToFileURL(shikiRequire.resolve(specifier)).href;
}
function buildVirtualSource() {
const shikiImport = JSON.stringify(
pathToFileURL(require.resolve("shiki")).href,
);
const languageImports = languageSpecs
.map(
([, packageName], index) =>
`import lang${index} from ${JSON.stringify(resolveShikiModule(`@shikijs/langs/${packageName}`))};`,
)
.join("\n");
const themeImports = themeSpecs
.map(
([, packageName], index) =>
`import theme${index} from ${JSON.stringify(resolveShikiModule(`@shikijs/themes/${packageName}`))};`,
)
.join("\n");
const supportedLanguages = [
...builtInLanguageSpecs,
...languageSpecs.map(([runtimeName]) => runtimeName),
];
return `import { createHighlighterCoreSync, createJavaScriptRegexEngine } from ${shikiImport};
${languageImports}
${themeImports}
const highlighter = createHighlighterCoreSync({
engine: createJavaScriptRegexEngine(),
langs: [${languageSpecs.map((_, index) => `...lang${index}`).join(", ")}],
themes: [${themeSpecs.map((_, index) => `theme${index}`).join(", ")}],
});
const supportedLanguages = new Set(${JSON.stringify(supportedLanguages)});
const languageAliases = ${JSON.stringify(languageAliases)};
const supportedThemes = new Set(${JSON.stringify(themeSpecs.map(([theme]) => theme))});
function normalizeLanguage(language) {
const normalized = String(language || "").trim().toLowerCase();
if (!normalized) {
return "text";
}
if (normalized in languageAliases) {
return languageAliases[normalized];
}
return supportedLanguages.has(normalized) ? normalized : "text";
}
function normalizeTheme(theme) {
const normalized = String(theme || "").trim();
return supportedThemes.has(normalized) ? normalized : "github-light";
}
function extractLanguage(codeElement) {
const className = codeElement.className || "";
const match = className.match(/language-([\\w+#.-]+)/i);
return match ? match[1] : "";
}
function renderCodeToHtml(code, language, theme) {
const normalizedTheme = normalizeTheme(theme);
try {
return highlighter.codeToHtml(String(code || ""), {
lang: normalizeLanguage(language),
theme: normalizedTheme,
});
} catch (error) {
console.warn("Failed to render T2I code block with Shiki.", error);
return highlighter.codeToHtml(String(code || ""), {
lang: "text",
theme: normalizedTheme,
});
}
}
function highlightAllCodeBlocks(root, theme) {
if (!root) {
return;
}
root.querySelectorAll("pre > code").forEach((codeElement) => {
const preElement = codeElement.parentElement;
if (!preElement || preElement.classList.contains("shiki")) {
return;
}
preElement.outerHTML = renderCodeToHtml(
codeElement.textContent || "",
extractLanguage(codeElement),
theme,
);
});
}
window.AstrBotT2IShiki = Object.freeze({
highlightAllCodeBlocks,
normalizeLanguage,
renderCodeToHtml,
});
`;
}
async function main() {
const tempDir = mkdtempSync(path.join(tmpdir(), "astrbot-t2i-shiki-runtime-"));
const entryPath = path.join(tempDir, "entry.mjs");
writeFileSync(entryPath, buildVirtualSource(), "utf-8");
try {
await build({
configFile: false,
logLevel: "info",
publicDir: false,
build: {
chunkSizeWarningLimit: 1500,
cssCodeSplit: false,
emptyOutDir: false,
lib: {
entry: entryPath,
fileName: () => path.basename(runtimeOutputFile),
formats: ["iife"],
name: "AstrBotT2IShikiRuntime",
},
minify: "esbuild",
outDir: path.dirname(runtimeOutputFile),
rollupOptions: {
output: {
inlineDynamicImports: true,
},
},
sourcemap: false,
target: "es2018",
},
});
} finally {
rmSync(tempDir, { force: true, recursive: true });
}
console.log(`Built ${runtimeOutputFile}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -860,15 +860,17 @@ export default {
// 过滤出属于该平台的路由,并保持顺序
const routes = [];
for (const [umop, confId] of Object.entries(routingTable)) {
const parsedUmop = this.parseUmop(umop);
if (this.isParsedUmopMatchPlatform(parsedUmop, platformId)) {
routes.push({
umop: umop,
originalUmop: umop, // 保存原始 UMOP 用于更新时查找
messageType: parsedUmop.messageType || '*',
sessionId: parsedUmop.sessionId || '*',
configId: confId
});
if (this.isUmopMatchPlatform(umop, platformId)) {
const parts = umop.split(':');
if (parts.length === 3) {
routes.push({
umop: umop,
originalUmop: umop, // 保存原始 UMOP 用于更新时查找
messageType: parts[1] === '' || parts[1] === '*' ? '*' : parts[1],
sessionId: parts[2] === '' || parts[2] === '*' ? '*' : parts[2],
configId: confId
});
}
}
}
@@ -990,29 +992,11 @@ export default {
},
isUmopMatchPlatform(umop, platformId) {
const parsedUmop = this.parseUmop(umop);
return this.isParsedUmopMatchPlatform(parsedUmop, platformId);
},
isParsedUmopMatchPlatform(parsedUmop, platformId) {
if (!parsedUmop) return false;
return parsedUmop.platform === platformId || parsedUmop.platform === '' || parsedUmop.platform === '*';
},
parseUmop(umop) {
if (!umop) return null;
const firstSeparatorIndex = umop.indexOf(':');
if (firstSeparatorIndex === -1) return null;
const secondSeparatorIndex = umop.indexOf(':', firstSeparatorIndex + 1);
if (secondSeparatorIndex === -1) return null;
return {
platform: umop.slice(0, firstSeparatorIndex),
messageType: umop.slice(firstSeparatorIndex + 1, secondSeparatorIndex),
sessionId: umop.slice(secondSeparatorIndex + 1)
};
if (!umop) return false;
const parts = umop.split(':');
if (parts.length !== 3) return false;
const platform = parts[0];
return platform === platformId || platform === '' || platform === '*';
},
// 获取消息类型标签

View File

@@ -289,94 +289,6 @@ async def on_llm_resp(self, event: AstrMessageEvent, resp: LLMResponse): # Note
> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly.
#### On Agent Begin
> Requires AstrBot version > v4.23.1
When the Agent starts running, the `on_agent_begin` hook is triggered.
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.astr_agent_context import AstrAgentContext
@filter.on_agent_begin()
async def on_agent_begin(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext]): # Note there are three parameters
print("Agent started")
```
> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly.
#### Before LLM Tool Call
> Requires AstrBot version > v4.23.1
When the Agent is about to call an LLM tool, the `on_using_llm_tool` hook is triggered.
You can obtain the `FunctionTool` object and tool call arguments.
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.tool import FunctionTool
@filter.on_using_llm_tool()
async def on_using_llm_tool(
self,
event: AstrMessageEvent,
tool: FunctionTool,
tool_args: dict | None,
):
print(tool.name, tool_args)
```
> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly.
#### After LLM Tool Call
> Requires AstrBot version > v4.23.1
After the LLM tool call completes, the `on_llm_tool_respond` hook is triggered.
You can obtain the `FunctionTool` object, tool call arguments, and tool call result.
```python
from mcp.types import CallToolResult
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.tool import FunctionTool
@filter.on_llm_tool_respond()
async def on_llm_tool_respond(
self,
event: AstrMessageEvent,
tool: FunctionTool,
tool_args: dict | None,
tool_result: CallToolResult | None,
):
print(tool.name, tool_args, tool_result)
```
> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly.
#### On Agent Done
> Requires AstrBot version > v4.23.1
After the Agent finishes running, the `on_agent_done` hook is triggered. This hook is triggered after `on_llm_response`.
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.provider import LLMResponse
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.astr_agent_context import AstrAgentContext
@filter.on_agent_done()
async def on_agent_done(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext], resp: LLMResponse): # Note there are four parameters
print(resp)
```
> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly.
#### Before Sending Message
Before sending a message, the `on_decorating_result` hook is triggered.

View File

@@ -305,94 +305,6 @@ async def on_llm_resp(self, event: AstrMessageEvent, resp: LLMResponse): # 请
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
#### Agent 开始运行时
> 适用于 AstrBot 版本 > v4.23.1
在 Agent 开始运行时,会触发 `on_agent_begin` 钩子。
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.astr_agent_context import AstrAgentContext
@filter.on_agent_begin()
async def on_agent_begin(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext]):
print("Agent 开始运行")
```
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
#### LLM 工具调用前
> 适用于 AstrBot 版本 > v4.23.1
在 Agent 准备调用 LLM 工具时,会触发 `on_using_llm_tool` 钩子。
可以获取到 `FunctionTool` 对象和工具调用参数。
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.tool import FunctionTool
@filter.on_using_llm_tool()
async def on_using_llm_tool(
self,
event: AstrMessageEvent,
tool: FunctionTool,
tool_args: dict | None,
):
print(tool.name, tool_args)
```
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
#### LLM 工具调用后
> 适用于 AstrBot 版本 > v4.23.1
在 LLM 工具调用完成后,会触发 `on_llm_tool_respond` 钩子。
可以获取到 `FunctionTool` 对象、工具调用参数和工具调用结果。
```python
from mcp.types import CallToolResult
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.tool import FunctionTool
@filter.on_llm_tool_respond()
async def on_llm_tool_respond(
self,
event: AstrMessageEvent,
tool: FunctionTool,
tool_args: dict | None,
tool_result: CallToolResult | None,
):
print(tool.name, tool_args, tool_result)
```
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
#### Agent 运行完成时
> 适用于 AstrBot 版本 > v4.23.1
在 Agent 运行完成后,会触发 `on_agent_done` 钩子。这个钩子会在 `on_llm_response` 之后触发。本质上和 `on_llm_response` 一样。
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.provider import LLMResponse
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.astr_agent_context import AstrAgentContext
@filter.on_agent_done()
async def on_agent_done(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext], resp: LLMResponse):
print(resp)
```
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
#### 发送消息前
在发送消息前,会触发 `on_decorating_result` 钩子。

View File

@@ -507,96 +507,6 @@ async def on_llm_resp(self, event: AstrMessageEvent, resp: LLMResponse): # 请
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
##### Agent 开始运行时
> 适用于 AstrBot 版本 > v4.23.1
在 Agent 开始运行时,会触发 `on_agent_begin` 钩子。
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.astr_agent_context import AstrAgentContext
@filter.on_agent_begin()
async def on_agent_begin(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext]): # 请注意有三个参数
print("Agent 开始运行")
```
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
##### LLM 工具调用前
> 适用于 AstrBot 版本 > v4.23.1
在 Agent 准备调用 LLM 工具时,会触发 `on_using_llm_tool` 钩子。
可以获取到 `FunctionTool` 对象和工具调用参数。
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.tool import FunctionTool
@filter.on_using_llm_tool()
async def on_using_llm_tool(
self,
event: AstrMessageEvent,
tool: FunctionTool,
tool_args: dict | None,
):
print(tool.name, tool_args)
```
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
##### LLM 工具调用后
> 适用于 AstrBot 版本 > v4.23.1
在 LLM 工具调用完成后,会触发 `on_llm_tool_respond` 钩子。
可以获取到 `FunctionTool` 对象、工具调用参数和工具调用结果。
```python
from mcp.types import CallToolResult
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.core.agent.tool import FunctionTool
@filter.on_llm_tool_respond()
async def on_llm_tool_respond(
self,
event: AstrMessageEvent,
tool: FunctionTool,
tool_args: dict | None,
tool_result: CallToolResult | None,
):
print(tool.name, tool_args, tool_result)
```
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
##### Agent 运行完成时
> 适用于 AstrBot 版本 > v4.23.1
在 Agent 运行完成后,会触发 `on_agent_done` 钩子。这个钩子会在 `on_llm_response` 之后触发。
可以获取到 `LLMResponse` 对象,可以对其进行修改。
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.provider import LLMResponse
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.astr_agent_context import AstrAgentContext
@filter.on_agent_done()
async def on_agent_done(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext], resp: LLMResponse): # 请注意有四个参数
print(resp)
```
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
##### 发送消息前
在发送消息前,会触发 `on_decorating_result` 钩子。

View File

@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.23.1"
version = "4.23.0"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
license = { text = "AGPL-3.0-or-later" }
@@ -64,7 +64,6 @@ dependencies = [
"python-socks>=2.8.0",
"pysocks>=1.7.1",
"packaging>=24.2",
"python-ripgrep==0.0.8",
]
[dependency-groups]

View File

@@ -52,5 +52,4 @@ tenacity>=9.1.2
shipyard-python-sdk>=0.2.4
shipyard-neo-sdk>=0.2.0
packaging>=24.2
qrcode>=8.2
python-ripgrep==0.0.8
qrcode>=8.2

View File

@@ -1,116 +0,0 @@
"""Cross-platform startup smoke check for AstrBot."""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
HEALTH_URL = "http://127.0.0.1:6185"
STARTUP_TIMEOUT_SECONDS = 60
REQUEST_TIMEOUT_SECONDS = 2
def _tail(path: Path, lines: int = 80) -> str:
try:
content = path.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError as exc:
return f"Unable to read smoke log: {exc}"
return "\n".join(content[-lines:])
def _is_ready() -> bool:
try:
with urllib.request.urlopen( # noqa: S310
HEALTH_URL,
timeout=REQUEST_TIMEOUT_SECONDS,
) as response:
return response.status < 400
except (OSError, urllib.error.URLError):
return False
def _stop_process(proc: subprocess.Popen[bytes]) -> None:
if proc.poll() is not None:
return
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=10)
def main() -> int:
env = os.environ.copy()
env.setdefault("PYTHONUTF8", "1")
env.setdefault("TESTING", "true")
smoke_root = Path(tempfile.mkdtemp(prefix="astrbot-smoke-root-"))
env["ASTRBOT_ROOT"] = str(smoke_root)
log_path = smoke_root / "smoke.log"
webui_dir = smoke_root / "webui"
webui_dir.mkdir()
(webui_dir / "index.html").write_text(
"<!doctype html><title>AstrBot</title>",
encoding="utf-8",
)
with log_path.open("wb") as log_file:
proc = subprocess.Popen(
[
sys.executable,
str(REPO_ROOT / "main.py"),
"--webui-dir",
str(webui_dir),
],
cwd=REPO_ROOT,
stdout=log_file,
stderr=subprocess.STDOUT,
env=env,
)
print(f"Starting smoke test on {HEALTH_URL}")
deadline = time.monotonic() + STARTUP_TIMEOUT_SECONDS
try:
while time.monotonic() < deadline:
if _is_ready():
print("Smoke test passed")
return 0
return_code = proc.poll()
if return_code is not None:
print(
f"AstrBot exited before becoming healthy. Exit code: {return_code}",
file=sys.stderr,
)
print(_tail(log_path), file=sys.stderr)
return 1
time.sleep(1)
print(
"Smoke test failed: health endpoint did not become ready in time.",
file=sys.stderr,
)
print(_tail(log_path), file=sys.stderr)
return 1
finally:
_stop_process(proc)
try:
log_path.unlink()
except OSError:
pass
shutil.rmtree(smoke_root, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -8,10 +8,8 @@ from quart import Quart
from astrbot.core import LogBroker
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
from astrbot.core.db.sqlite import SQLiteDatabase
from astrbot.core.exceptions import KnowledgeBaseUploadError
from astrbot.core.knowledge_base.kb_helper import KBHelper
from astrbot.core.knowledge_base.models import KBDocument
from astrbot.dashboard.routes.knowledge_base import KnowledgeBaseRoute
from astrbot.dashboard.server import AstrBotDashboard
@@ -89,9 +87,6 @@ async def test_import_documents(
):
"""Tests the import documents functionality."""
test_client = app.test_client()
kb_helper = await core_lifecycle_td.kb_manager.get_kb("test_kb_id")
kb_helper.upload_document.reset_mock()
kb_helper.upload_document.side_effect = None
# Test data
import_data = {
@@ -134,6 +129,7 @@ async def test_import_documents(
assert result["failed_count"] == 0
# Verify kb_helper.upload_document was called correctly
kb_helper = await core_lifecycle_td.kb_manager.get_kb("test_kb_id")
assert kb_helper.upload_document.call_count == 2
# Check first call arguments
@@ -150,48 +146,6 @@ async def test_import_documents(
assert kwargs2["pre_chunked_text"] == ["chunk3", "chunk4", "chunk5"]
@pytest.mark.asyncio
async def test_import_documents_returns_friendly_failure_message(
core_lifecycle_td: AstrBotCoreLifecycle,
):
kb_helper = await core_lifecycle_td.kb_manager.get_kb("test_kb_id")
kb_helper.upload_document.reset_mock()
kb_helper.upload_document.side_effect = KnowledgeBaseUploadError(
stage="embedding",
user_message=(
"向量化失败:嵌入模型返回的向量数量与文本分块数量不一致(期望 2实际 1"
),
details={"expected_contents": 2, "actual_vectors": 1},
)
route = KnowledgeBaseRoute.__new__(KnowledgeBaseRoute)
route.upload_progress = {}
route.upload_tasks = {}
await KnowledgeBaseRoute._background_import_task(
route,
task_id="task-1",
kb_helper=kb_helper,
documents=[{"file_name": "broken.txt", "chunks": ["chunk1", "chunk2"]}],
batch_size=32,
tasks_limit=3,
max_retries=3,
)
assert route.upload_tasks["task-1"]["status"] == "completed"
result = route.upload_tasks["task-1"]["result"]
assert result["success_count"] == 0
assert result["failed_count"] == 1
assert result["failed"][0]["file_name"] == "broken.txt"
assert result["failed"][0]["error"].startswith("broken.txt:")
assert "向量化失败" in result["failed"][0]["error"]
assert "期望 2实际 1" in result["failed"][0]["error"]
assert "not same nb of vectors as ids" not in result["failed"][0]["error"]
assert kb_helper.upload_document.await_count == 1
kb_helper.upload_document.side_effect = None
@pytest.mark.asyncio
async def test_import_documents_invalid_input(app: Quart, authenticated_header: dict):
"""Tests import documents with invalid input."""

View File

@@ -3,7 +3,6 @@ from unittest.mock import AsyncMock
import pytest
from astrbot.core.db.vec_db.faiss_impl.vec_db import FaissVecDB
from astrbot.core.exceptions import KnowledgeBaseUploadError
@pytest.mark.asyncio
@@ -19,28 +18,3 @@ async def test_insert_batch_skips_empty_contents() -> None:
vec_db.embedding_provider.get_embeddings_batch.assert_not_awaited()
vec_db.document_storage.insert_documents_batch.assert_not_awaited()
vec_db.embedding_storage.insert_batch.assert_not_awaited()
@pytest.mark.asyncio
async def test_insert_batch_raises_friendly_error_for_embedding_count_mismatch() -> (
None
):
vec_db = FaissVecDB.__new__(FaissVecDB)
vec_db.embedding_provider = AsyncMock()
vec_db.embedding_provider.get_embeddings_batch.return_value = [[0.1, 0.2]]
vec_db.document_storage = AsyncMock()
vec_db.embedding_storage = AsyncMock()
vec_db.embedding_storage.dimension = 2
with pytest.raises(KnowledgeBaseUploadError) as exc_info:
await FaissVecDB.insert_batch(
vec_db,
contents=["chunk-1", "chunk-2"],
metadatas=[{}, {}],
ids=["doc-1", "doc-2"],
)
assert "向量化失败" in str(exc_info.value)
assert "期望 2实际 1" in str(exc_info.value)
vec_db.document_storage.insert_documents_batch.assert_not_awaited()
vec_db.embedding_storage.insert_batch.assert_not_awaited()