Skip to content

Commit 24deea9

Browse files
committed
Squashed 'astrbot-sdk/' changes from 0a9c86345..8186cd3ee
8186cd3ee chore: refresh vendor snapshot [skip ci] 6e6dfd238 Merge pull request #107 from united-pooh/dev 86274a7bc fix: 修正文档注释格式,去除多余空格 1931f606f feat: 增强 StdioTransport 的错误处理,添加对格式错误的处理逻辑和相应的单元测试 566401bc5 feat: Implement mutual TLS support for websocket server and client transports cfb74da6e fix: 修正文档注释格式,清理多余的内容 de3677be1 refactor: 统一日志入口,引入 sdk_logger 集中管理 loguru patch 9dce3419f Merge pull request #106 from united-pooh:dev 22082ef8b feat: 增强能力描述符和插件导入机制,优化消息处理和错误管理 0789119d6 fix: 修复导入顺序,优化类型注解 REVERT: 0a9c86345 chore: refresh vendor snapshot [skip ci] git-subtree-dir: astrbot-sdk git-subtree-split: 8186cd3ee6c8afe876df62382cf3ddfff6063137
1 parent 34ad073 commit 24deea9

22 files changed

Lines changed: 1419 additions & 310 deletions

src/astrbot_sdk/_internal/decorator_lifecycle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from dataclasses import dataclass, field
77
from typing import Any
88

9-
from loguru import logger
109
from pydantic import ValidationError
1110

1211
from ..context import Context as RuntimeContext
@@ -23,6 +22,7 @@
2322
get_validate_config_meta,
2423
)
2524
from ..star import Star
25+
from .sdk_logger import logger
2626
from .star_runtime import bind_star_runtime
2727

2828
_RUNTIME_STATE_ATTR = "__astrbot_decorator_runtime_state__"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from __future__ import annotations
2+
3+
import os
4+
5+
from loguru import logger as _raw_loguru_logger
6+
7+
try:
8+
from astrbot.core.config.default import VERSION as _ASTRBOT_VERSION
9+
except Exception: # noqa: BLE001
10+
_ASTRBOT_VERSION = ""
11+
12+
_SHORT_LEVEL_NAMES = {
13+
"DEBUG": "DBUG",
14+
"INFO": "INFO",
15+
"WARNING": "WARN",
16+
"ERROR": "ERRO",
17+
"CRITICAL": "CRIT",
18+
}
19+
20+
21+
def _get_short_level_name(level_name: str) -> str:
22+
return _SHORT_LEVEL_NAMES.get(level_name.upper(), level_name[:4].upper())
23+
24+
25+
def _build_source_file(pathname: str | None) -> str:
26+
if not pathname:
27+
return "unknown"
28+
dirname = os.path.dirname(pathname)
29+
return (
30+
os.path.basename(dirname) + "." + os.path.basename(pathname).replace(".py", "")
31+
)
32+
33+
34+
def _patch_record(record: dict) -> None:
35+
extra = record["extra"]
36+
extra.setdefault("plugin_tag", "[Core]")
37+
extra.setdefault("short_levelname", _get_short_level_name(record["level"].name))
38+
level_no = record["level"].no
39+
version_tag = (
40+
f" [v{_ASTRBOT_VERSION}]" if _ASTRBOT_VERSION and level_no >= 30 else ""
41+
)
42+
extra.setdefault("astrbot_version_tag", version_tag)
43+
extra.setdefault("source_file", _build_source_file(record["file"].path))
44+
extra.setdefault("source_line", record["line"])
45+
extra.setdefault("is_trace", False)
46+
47+
48+
logger = _raw_loguru_logger.patch(_patch_record)
49+
50+
__all__ = ["logger"]

src/astrbot_sdk/_internal/testing_support.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ def __init__(self, router: MockCapabilityRouter) -> None:
389389
role="core",
390390
version="local",
391391
)
392-
self.remote_capabilities = list(router.descriptors())
392+
self.remote_capabilities = list(router.all_descriptors())
393393
self.remote_capability_map = {
394394
item.name: item for item in self.remote_capabilities
395395
}

src/astrbot_sdk/cli.py

Lines changed: 150 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
from typing import Any
3131

3232
import click
33-
from loguru import logger
3433

34+
from ._internal.sdk_logger import logger
3535
from .errors import AstrBotError
3636
from .runtime.bootstrap import run_plugin_worker, run_supervisor, run_websocket_server
3737
from .runtime.loader import load_plugin, load_plugin_spec, validate_plugin_spec
@@ -1144,6 +1144,39 @@ def _build_plugin(plugin_dir: Path, output_dir: Path | None) -> None:
11441144
click.echo(f"artifact: {archive_path}")
11451145

11461146

1147+
def _run_websocket_worker_entrypoint(
1148+
*,
1149+
worker_id: str | None,
1150+
plugin_dirs: tuple[Path, ...],
1151+
host: str,
1152+
port: int,
1153+
path: str,
1154+
tls_ca_file: Path,
1155+
tls_cert_file: Path,
1156+
tls_key_file: Path,
1157+
) -> None:
1158+
resolved_plugin_dirs = list(plugin_dirs) if plugin_dirs else [Path.cwd()]
1159+
_run_async_entrypoint(
1160+
run_websocket_server(
1161+
worker_id=worker_id,
1162+
plugin_dirs=resolved_plugin_dirs,
1163+
host=host,
1164+
port=port,
1165+
path=path,
1166+
tls_ca_file=tls_ca_file,
1167+
tls_cert_file=tls_cert_file,
1168+
tls_key_file=tls_key_file,
1169+
),
1170+
log_message=f"启动 WebSocket Worker,端口:{port}",
1171+
context={
1172+
"worker_id": worker_id,
1173+
"plugin_dirs": resolved_plugin_dirs,
1174+
"port": port,
1175+
"path": path,
1176+
},
1177+
)
1178+
1179+
11471180
@click.group()
11481181
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output")
11491182
@click.pass_context
@@ -1161,20 +1194,37 @@ def cli(ctx, verbose: bool) -> None:
11611194
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
11621195
help="Directory containing plugin folders",
11631196
)
1197+
@click.option(
1198+
"--workers-manifest",
1199+
default=None,
1200+
type=click.Path(file_okay=True, dir_okay=False, path_type=Path),
1201+
help="Supervisor manifest describing remote websocket workers",
1202+
)
11641203
@click.option(
11651204
"--protocol-stdout",
11661205
default=None,
11671206
type=str,
11681207
help="Redirect runtime protocol stdout to console, silent, or a file path",
11691208
)
1170-
def run(plugins_dir: Path, protocol_stdout: str | None) -> None:
1209+
def run(
1210+
plugins_dir: Path,
1211+
workers_manifest: Path | None,
1212+
protocol_stdout: str | None,
1213+
) -> None:
11711214
"""Start the plugin supervisor over stdio."""
11721215
transport_stdout, opened_stdout = _resolve_protocol_stdout(protocol_stdout)
11731216
try:
11741217
_run_async_entrypoint(
1175-
run_supervisor(plugins_dir=plugins_dir, stdout=transport_stdout),
1218+
run_supervisor(
1219+
plugins_dir=plugins_dir,
1220+
stdout=transport_stdout,
1221+
workers_manifest=workers_manifest,
1222+
),
11761223
log_message=f"启动插件主管进程,插件目录:{plugins_dir}",
1177-
context={"plugins_dir": plugins_dir},
1224+
context={
1225+
"plugins_dir": plugins_dir,
1226+
"workers_manifest": workers_manifest,
1227+
},
11781228
)
11791229
finally:
11801230
if opened_stdout is not None:
@@ -1362,12 +1412,101 @@ def worker(
13621412
opened_stdout.close()
13631413

13641414

1415+
@cli.command("serve-worker")
1416+
@click.option("--worker-id", default=None, type=str, help="Stable websocket worker id")
1417+
@click.option(
1418+
"--plugin-dir",
1419+
"plugin_dirs",
1420+
multiple=True,
1421+
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
1422+
help="Plugin directory to serve; repeat to host multiple plugins in one worker",
1423+
)
1424+
@click.option("--host", default="127.0.0.1", show_default=True)
1425+
@click.option("--port", default=8765, type=int, show_default=True)
1426+
@click.option("--path", default="/", show_default=True)
1427+
@click.option(
1428+
"--tls-ca-file",
1429+
required=True,
1430+
type=click.Path(file_okay=True, dir_okay=False, exists=True, path_type=Path),
1431+
)
1432+
@click.option(
1433+
"--tls-cert-file",
1434+
required=True,
1435+
type=click.Path(file_okay=True, dir_okay=False, exists=True, path_type=Path),
1436+
)
1437+
@click.option(
1438+
"--tls-key-file",
1439+
required=True,
1440+
type=click.Path(file_okay=True, dir_okay=False, exists=True, path_type=Path),
1441+
)
1442+
def serve_worker(
1443+
worker_id: str | None,
1444+
plugin_dirs: tuple[Path, ...],
1445+
host: str,
1446+
port: int,
1447+
path: str,
1448+
tls_ca_file: Path,
1449+
tls_cert_file: Path,
1450+
tls_key_file: Path,
1451+
) -> None:
1452+
"""Serve one or more plugins as a standalone websocket worker."""
1453+
_run_websocket_worker_entrypoint(
1454+
worker_id=worker_id,
1455+
plugin_dirs=plugin_dirs,
1456+
host=host,
1457+
port=port,
1458+
path=path,
1459+
tls_ca_file=tls_ca_file,
1460+
tls_cert_file=tls_cert_file,
1461+
tls_key_file=tls_key_file,
1462+
)
1463+
1464+
13651465
@cli.command(hidden=True)
1366-
@click.option("--port", default=8765, type=int, help="WebSocket server port")
1367-
def websocket(port: int) -> None:
1368-
"""WebSocket runtime entrypoint kept for standalone bridge scenarios."""
1369-
_run_async_entrypoint(
1370-
run_websocket_server(port=port),
1371-
log_message=f"启动 WebSocket 服务器,端口:{port}",
1372-
context={"port": port},
1466+
@click.option("--worker-id", default=None, type=str)
1467+
@click.option(
1468+
"--plugin-dir",
1469+
"plugin_dirs",
1470+
multiple=True,
1471+
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
1472+
)
1473+
@click.option("--host", default="127.0.0.1", show_default=True)
1474+
@click.option("--port", default=8765, type=int, show_default=True)
1475+
@click.option("--path", default="/", show_default=True)
1476+
@click.option(
1477+
"--tls-ca-file",
1478+
required=True,
1479+
type=click.Path(file_okay=True, dir_okay=False, exists=True, path_type=Path),
1480+
)
1481+
@click.option(
1482+
"--tls-cert-file",
1483+
required=True,
1484+
type=click.Path(file_okay=True, dir_okay=False, exists=True, path_type=Path),
1485+
)
1486+
@click.option(
1487+
"--tls-key-file",
1488+
required=True,
1489+
type=click.Path(file_okay=True, dir_okay=False, exists=True, path_type=Path),
1490+
)
1491+
def websocket(
1492+
worker_id: str | None,
1493+
plugin_dirs: tuple[Path, ...],
1494+
host: str,
1495+
port: int,
1496+
path: str,
1497+
tls_ca_file: Path,
1498+
tls_cert_file: Path,
1499+
tls_key_file: Path,
1500+
) -> None:
1501+
"""Deprecated websocket runtime wrapper for standalone worker scenarios."""
1502+
logger.warning("'astr websocket' is deprecated; use 'astr serve-worker' instead")
1503+
_run_websocket_worker_entrypoint(
1504+
worker_id=worker_id,
1505+
plugin_dirs=plugin_dirs,
1506+
host=host,
1507+
port=port,
1508+
path=path,
1509+
tls_ca_file=tls_ca_file,
1510+
tls_cert_file=tls_cert_file,
1511+
tls_key_file=tls_key_file,
13731512
)

src/astrbot_sdk/context.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@
3737
from pathlib import Path
3838
from typing import Any
3939

40-
from loguru import logger as base_logger
41-
4240
from ._internal.plugin_logger import PluginLogger
41+
from ._internal.sdk_logger import logger as base_logger
4342
from ._internal.star_runtime import current_star_instance
4443
from ._message_types import normalize_message_type
4544
from .clients import (

src/astrbot_sdk/conversation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ class ConversationSession:
4141
_owner_task: asyncio.Task[Any] | None = None
4242

4343
def __post_init__(self) -> None:
44-
if self.state != ConversationState.ACTIVE:
44+
if self.state is None:
4545
self.state = ConversationState.ACTIVE
46+
return
47+
if not isinstance(self.state, ConversationState):
48+
self.state = ConversationState(str(self.state))
4649

4750
def bind_owner_task(self, task: asyncio.Task[Any]) -> None:
4851
self._owner_task = task

0 commit comments

Comments
 (0)