mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-02 10:40:15 +08:00
Compare commits
1 Commits
feat/agent
...
fix/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d00309d70 |
20
.github/workflows/build-docs.yml
vendored
20
.github/workflows/build-docs.yml
vendored
@@ -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
|
||||
|
||||
16
.github/workflows/dashboard_ci.yml
vendored
16
.github/workflows/dashboard_ci.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/pr-title-check.yml
vendored
2
.github/workflows/pr-title-check.yml
vendored
@@ -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();
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
|
||||
49
.github/workflows/smoke_test.yml
vendored
49
.github/workflows/smoke_test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) | コミュニティ |
|
||||
|
||||
|
||||
|
||||
@@ -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) | Сообщество |
|
||||
|
||||
## Поддерживаемые сервисы моделей
|
||||
|
||||
@@ -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) | 社群維護 |
|
||||
|
||||
## 支援的模型服務
|
||||
|
||||
@@ -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) | 社区维护 |
|
||||
|
||||
## 支持的模型提供商
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.23.1"
|
||||
__version__ = "4.23.0"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 参数。
|
||||
|
||||
@@ -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 工具
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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">> 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>
|
||||
@@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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)},
|
||||
)
|
||||
|
||||
# 更新任务完成状态
|
||||
|
||||
@@ -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: "发送消息后",
|
||||
|
||||
@@ -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))
|
||||
@@ -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/",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 === '*';
|
||||
},
|
||||
|
||||
// 获取消息类型标签
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` 钩子。
|
||||
|
||||
@@ -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` 钩子。
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user