mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-07 05:10:16 +08:00
Compare commits
2 Commits
master
...
codex/umo-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31944e5424 | ||
|
|
31e7af82d7 |
@@ -1,10 +1,22 @@
|
||||
import os
|
||||
import uuid
|
||||
from contextvars import Token
|
||||
from typing import TypedDict, TypeVar
|
||||
|
||||
from astrbot.core import AstrBotConfig, logger
|
||||
from astrbot.core.config.astrbot_config import ASTRBOT_CONFIG_PATH
|
||||
from astrbot.core.config.default import DEFAULT_CONFIG
|
||||
from astrbot.core.config.overrides import (
|
||||
CORE_CONFIG_OVERRIDE_KEY,
|
||||
build_effective_core_config,
|
||||
build_effective_core_config_sync,
|
||||
get_current_effective_config,
|
||||
normalize_config_override_payload,
|
||||
remove_core_config_override_paths,
|
||||
reset_current_effective_config,
|
||||
set_current_effective_config,
|
||||
update_core_config_override_paths,
|
||||
)
|
||||
from astrbot.core.platform.message_session import MessageSession
|
||||
from astrbot.core.umop_config_router import UmopConfigRouter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_config_path
|
||||
@@ -31,6 +43,8 @@ DEFAULT_CONFIG_CONF_INFO = ConfInfo(
|
||||
class AstrBotConfigManager:
|
||||
"""A class to manage the system configuration of AstrBot, aka ACM"""
|
||||
|
||||
core_config_override_key = CORE_CONFIG_OVERRIDE_KEY
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default_config: AstrBotConfig,
|
||||
@@ -121,8 +135,15 @@ class AstrBotConfigManager:
|
||||
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
||||
self.abconf_data = abconf_data
|
||||
|
||||
def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig:
|
||||
"""获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。"""
|
||||
def get_base_conf(self, umo: str | MessageSession | None) -> AstrBotConfig:
|
||||
"""Get the base config profile for a UMO without applying overrides.
|
||||
|
||||
Args:
|
||||
umo: Unified message origin or message session.
|
||||
|
||||
Returns:
|
||||
Shared base config profile selected for the UMO.
|
||||
"""
|
||||
if not umo:
|
||||
return self.confs["default"]
|
||||
if isinstance(umo, MessageSession):
|
||||
@@ -136,6 +157,116 @@ class AstrBotConfigManager:
|
||||
|
||||
return conf
|
||||
|
||||
def get_conf(self, umo: str | MessageSession | None = None) -> AstrBotConfig:
|
||||
"""获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。"""
|
||||
umo_str = None
|
||||
if umo:
|
||||
umo_str = (
|
||||
f"{umo.platform_id}:{umo.message_type}:{umo.session_id}"
|
||||
if isinstance(umo, MessageSession)
|
||||
else str(umo)
|
||||
)
|
||||
|
||||
effective_config = get_current_effective_config(umo_str)
|
||||
if effective_config:
|
||||
return effective_config
|
||||
|
||||
if not umo:
|
||||
effective_config = get_current_effective_config()
|
||||
if effective_config:
|
||||
return effective_config
|
||||
return self.confs["default"]
|
||||
|
||||
base_config = self.get_base_conf(umo)
|
||||
return build_effective_core_config_sync(
|
||||
base_config, self.sp, umo_str or str(umo)
|
||||
)
|
||||
|
||||
def get_current_conf(self, default: AstrBotConfig | None = None) -> AstrBotConfig:
|
||||
"""Get the active task config or a caller-provided default config.
|
||||
|
||||
Args:
|
||||
default: Config returned when no event-scoped config is active.
|
||||
|
||||
Returns:
|
||||
Active effective config, default config, or the global default profile.
|
||||
"""
|
||||
return get_current_effective_config() or default or self.confs["default"]
|
||||
|
||||
async def build_effective_conf(
|
||||
self,
|
||||
umo: str,
|
||||
base_config: AstrBotConfig | None = None,
|
||||
) -> AstrBotConfig:
|
||||
"""Build an effective config for one UMO.
|
||||
|
||||
Args:
|
||||
umo: Unified message origin.
|
||||
base_config: Optional already-selected base config profile.
|
||||
|
||||
Returns:
|
||||
Config view with UMO overrides applied.
|
||||
"""
|
||||
return await build_effective_core_config(
|
||||
base_config or self.get_base_conf(umo),
|
||||
self.sp,
|
||||
umo,
|
||||
)
|
||||
|
||||
def activate_effective_conf(
|
||||
self,
|
||||
umo: str,
|
||||
config: AstrBotConfig,
|
||||
) -> Token:
|
||||
"""Bind an effective config to the current async task.
|
||||
|
||||
Args:
|
||||
umo: Unified message origin.
|
||||
config: Effective config for the current event.
|
||||
|
||||
Returns:
|
||||
Context variable token used to reset the binding.
|
||||
"""
|
||||
return set_current_effective_config(umo, config)
|
||||
|
||||
def reset_effective_conf(self, token: Token) -> None:
|
||||
"""Reset the current task's effective config binding.
|
||||
|
||||
Args:
|
||||
token: Token returned by activate_effective_conf.
|
||||
"""
|
||||
reset_current_effective_config(token)
|
||||
|
||||
async def update_conf_overrides(self, umo: str, paths: dict) -> None:
|
||||
"""Merge config override paths for one UMO.
|
||||
|
||||
Args:
|
||||
umo: Unified message origin.
|
||||
paths: Dot-separated config path to override value mapping.
|
||||
"""
|
||||
await update_core_config_override_paths(self.sp, umo, paths)
|
||||
|
||||
@staticmethod
|
||||
def normalize_conf_override_payload(payload: object) -> dict:
|
||||
"""Normalize a config override payload into path-value pairs.
|
||||
|
||||
Args:
|
||||
payload: Raw preference value for a config override.
|
||||
|
||||
Returns:
|
||||
Dot-separated config path to override value mapping.
|
||||
"""
|
||||
return normalize_config_override_payload(payload)
|
||||
|
||||
async def remove_conf_overrides(self, umo: str, paths: list[str]) -> None:
|
||||
"""Remove config override paths for one UMO.
|
||||
|
||||
Args:
|
||||
umo: Unified message origin.
|
||||
paths: Dot-separated config paths to remove.
|
||||
"""
|
||||
await remove_core_config_override_paths(self.sp, umo, paths)
|
||||
|
||||
@property
|
||||
def default_conf(self) -> AstrBotConfig:
|
||||
"""获取默认配置文件"""
|
||||
|
||||
@@ -66,6 +66,7 @@ DEFAULT_CONFIG = {
|
||||
"forward_threshold": 1500,
|
||||
"enable_id_white_list": True,
|
||||
"id_whitelist": [],
|
||||
"id_blacklist": [],
|
||||
"id_whitelist_log": True,
|
||||
"wl_ignore_admin_on_group": True,
|
||||
"wl_ignore_admin_on_friend": True,
|
||||
@@ -306,6 +307,7 @@ DEFAULT_CONFIG = {
|
||||
"callback_api_base": "",
|
||||
"default_kb_collection": "", # 默认知识库名称, 已经过时
|
||||
"plugin_set": ["*"], # "*" 表示使用所有可用的插件, 空列表表示不使用任何插件
|
||||
"plugin_disabled_set": [],
|
||||
"kb_names": [], # 默认知识库名称列表
|
||||
"kb_fusion_top_k": 20, # 知识库检索融合阶段返回结果数量
|
||||
"kb_final_top_k": 5, # 知识库检索最终返回结果数量
|
||||
@@ -1087,6 +1089,11 @@ CONFIG_METADATA_2 = {
|
||||
"items": {"type": "string"},
|
||||
"hint": "只处理填写的 ID 发来的消息事件,为空时不启用。可使用 /sid 指令获取在平台上的会话 ID(类似 abc:GroupMessage:123)。管理员可在 WebUI 的平台设置中管理白名单",
|
||||
},
|
||||
"id_blacklist": {
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"hint": "拒绝处理填写的 ID 发来的消息事件。命中黑名单时会优先于白名单被拒绝。",
|
||||
},
|
||||
"id_whitelist_log": {
|
||||
"type": "bool",
|
||||
"hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。",
|
||||
@@ -3924,6 +3931,12 @@ CONFIG_METADATA_3 = {
|
||||
"items": {"type": "string"},
|
||||
"hint": "使用 /sid 获取 ID。当白名单列表为空时,代表不启用白名单(即所有 ID 都在白名单内)。",
|
||||
},
|
||||
"platform_settings.id_blacklist": {
|
||||
"description": "黑名单 ID 列表",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"hint": "使用 /sid 获取 ID。命中黑名单的会话会被拒绝,黑名单优先于白名单。",
|
||||
},
|
||||
"platform_settings.id_whitelist_log": {
|
||||
"description": "输出日志",
|
||||
"type": "bool",
|
||||
@@ -4090,6 +4103,12 @@ CONFIG_METADATA_3 = {
|
||||
"hint": "默认启用全部未被禁用的插件。若插件在插件页面被禁用,则此处的选择不会生效。",
|
||||
"_special": "select_plugin_set",
|
||||
},
|
||||
"plugin_disabled_set": {
|
||||
"description": "禁用插件",
|
||||
"type": "bool",
|
||||
"hint": "明确禁用的插件列表。命中禁用列表的插件不会在会话中触发。",
|
||||
"_special": "select_plugin_set",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
316
astrbot/core/config/overrides.py
Normal file
316
astrbot/core/config/overrides.py
Normal file
@@ -0,0 +1,316 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from contextvars import ContextVar, Token
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.utils.shared_preferences import SharedPreferences
|
||||
|
||||
CORE_CONFIG_OVERRIDE_KEY = "config_override:core"
|
||||
PLUGIN_CONFIG_OVERRIDE_PREFIX = "config_override:plugin:"
|
||||
CONFIG_OVERRIDE_VERSION = 1
|
||||
|
||||
|
||||
class EffectiveConfigContext(NamedTuple):
|
||||
umo: str
|
||||
config: AstrBotConfig
|
||||
|
||||
|
||||
_current_effective_config: ContextVar[EffectiveConfigContext | None] = ContextVar(
|
||||
"astrbot_current_effective_config",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
class EffectiveAstrBotConfig(dict):
|
||||
"""Per-event AstrBot config view with no persistent file writes.
|
||||
|
||||
Args:
|
||||
data: Effective config data after applying override paths.
|
||||
base_config: Config instance used as the immutable base for this view.
|
||||
umo: Unified message origin this view belongs to.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
base_config: AstrBotConfig,
|
||||
umo: str,
|
||||
) -> None:
|
||||
super().__init__(data)
|
||||
object.__setattr__(self, "_base_config", base_config)
|
||||
object.__setattr__(self, "_umo", umo)
|
||||
object.__setattr__(self, "config_path", base_config.config_path)
|
||||
object.__setattr__(self, "default_config", base_config.default_config)
|
||||
object.__setattr__(self, "schema", base_config.schema)
|
||||
|
||||
def save_config(
|
||||
self,
|
||||
replace_config: dict | None = None,
|
||||
*,
|
||||
indent: int = 2,
|
||||
) -> None:
|
||||
"""Keep save-compatible call sites from writing an override view to disk.
|
||||
|
||||
Args:
|
||||
replace_config: Optional replacement data for the in-memory view.
|
||||
indent: Preserved for API compatibility with AstrBotConfig.
|
||||
"""
|
||||
_ = indent
|
||||
if replace_config:
|
||||
self.clear()
|
||||
self.update(copy.deepcopy(replace_config))
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
try:
|
||||
return self[item]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
self[key] = value
|
||||
|
||||
|
||||
def get_current_effective_config(umo: str | None = None) -> AstrBotConfig | None:
|
||||
"""Return the current task's effective config, if it matches the UMO.
|
||||
|
||||
Args:
|
||||
umo: Optional unified message origin to match.
|
||||
|
||||
Returns:
|
||||
The current effective config, or None when outside an event override scope.
|
||||
"""
|
||||
current = _current_effective_config.get()
|
||||
if current is None:
|
||||
return None
|
||||
if umo is not None and current.umo != umo:
|
||||
return None
|
||||
return current.config
|
||||
|
||||
|
||||
def set_current_effective_config(
|
||||
umo: str,
|
||||
config: AstrBotConfig,
|
||||
) -> Token[EffectiveConfigContext | None]:
|
||||
"""Bind an effective config to the current async task.
|
||||
|
||||
Args:
|
||||
umo: Unified message origin the config belongs to.
|
||||
config: Effective config for the current event.
|
||||
|
||||
Returns:
|
||||
Context variable token used to reset the binding.
|
||||
"""
|
||||
return _current_effective_config.set(EffectiveConfigContext(umo, config))
|
||||
|
||||
|
||||
def reset_current_effective_config(token: Token[EffectiveConfigContext | None]) -> None:
|
||||
"""Reset the current task's effective config binding.
|
||||
|
||||
Args:
|
||||
token: Token returned by set_current_effective_config.
|
||||
"""
|
||||
_current_effective_config.reset(token)
|
||||
|
||||
|
||||
def normalize_config_override_payload(payload: Any) -> dict[str, Any]:
|
||||
"""Normalize a stored override payload into path-value pairs.
|
||||
|
||||
Args:
|
||||
payload: Raw preference value for a config override.
|
||||
|
||||
Returns:
|
||||
A mapping from dot-separated config path to override value.
|
||||
"""
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
paths = payload.get("paths", payload)
|
||||
if not isinstance(paths, dict):
|
||||
return {}
|
||||
return {
|
||||
path: copy.deepcopy(value)
|
||||
for path, value in paths.items()
|
||||
if isinstance(path, str) and path.strip()
|
||||
}
|
||||
|
||||
|
||||
async def load_core_config_override_paths(
|
||||
preferences: SharedPreferences,
|
||||
umo: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Load core config override paths for a UMO.
|
||||
|
||||
Args:
|
||||
preferences: Shared preferences storage.
|
||||
umo: Unified message origin.
|
||||
|
||||
Returns:
|
||||
Path-value override mapping from the stored core override payload.
|
||||
"""
|
||||
override_payload = await preferences.session_get(
|
||||
umo,
|
||||
CORE_CONFIG_OVERRIDE_KEY,
|
||||
default={},
|
||||
)
|
||||
return normalize_config_override_payload(override_payload)
|
||||
|
||||
|
||||
def load_core_config_override_paths_sync(
|
||||
preferences: SharedPreferences,
|
||||
umo: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Synchronously load core config override paths for a UMO.
|
||||
|
||||
Args:
|
||||
preferences: Shared preferences storage.
|
||||
umo: Unified message origin.
|
||||
|
||||
Returns:
|
||||
Path-value override mapping from the stored core override payload.
|
||||
"""
|
||||
override_payload = preferences.get(
|
||||
CORE_CONFIG_OVERRIDE_KEY,
|
||||
default={},
|
||||
scope="umo",
|
||||
scope_id=umo,
|
||||
)
|
||||
return normalize_config_override_payload(override_payload)
|
||||
|
||||
|
||||
async def save_core_config_override_paths(
|
||||
preferences: SharedPreferences,
|
||||
umo: str,
|
||||
paths: dict[str, Any],
|
||||
) -> None:
|
||||
"""Persist core config override paths for a UMO.
|
||||
|
||||
Args:
|
||||
preferences: Shared preferences storage.
|
||||
umo: Unified message origin.
|
||||
paths: Dot-separated config path to override value mapping.
|
||||
"""
|
||||
await preferences.session_put(
|
||||
umo,
|
||||
CORE_CONFIG_OVERRIDE_KEY,
|
||||
{
|
||||
"version": CONFIG_OVERRIDE_VERSION,
|
||||
"paths": normalize_config_override_payload(paths),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def update_core_config_override_paths(
|
||||
preferences: SharedPreferences,
|
||||
umo: str,
|
||||
paths: dict[str, Any],
|
||||
) -> None:
|
||||
"""Merge core config override paths into the stored payload for a UMO.
|
||||
|
||||
Args:
|
||||
preferences: Shared preferences storage.
|
||||
umo: Unified message origin.
|
||||
paths: Dot-separated config path to override value mapping.
|
||||
"""
|
||||
override_payload = await preferences.session_get(
|
||||
umo,
|
||||
CORE_CONFIG_OVERRIDE_KEY,
|
||||
default={},
|
||||
)
|
||||
merged_paths = normalize_config_override_payload(override_payload)
|
||||
merged_paths.update(normalize_config_override_payload(paths))
|
||||
await save_core_config_override_paths(preferences, umo, merged_paths)
|
||||
|
||||
|
||||
async def remove_core_config_override_paths(
|
||||
preferences: SharedPreferences,
|
||||
umo: str,
|
||||
paths: list[str],
|
||||
) -> None:
|
||||
"""Remove core config override paths from the stored payload for a UMO.
|
||||
|
||||
Args:
|
||||
preferences: Shared preferences storage.
|
||||
umo: Unified message origin.
|
||||
paths: Dot-separated config paths to remove.
|
||||
"""
|
||||
override_payload = await preferences.session_get(
|
||||
umo,
|
||||
CORE_CONFIG_OVERRIDE_KEY,
|
||||
default={},
|
||||
)
|
||||
merged_paths = normalize_config_override_payload(override_payload)
|
||||
for path in paths:
|
||||
merged_paths.pop(path, None)
|
||||
await save_core_config_override_paths(preferences, umo, merged_paths)
|
||||
|
||||
|
||||
def apply_config_override_paths(
|
||||
config_data: dict[str, Any],
|
||||
paths: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Apply dot-separated override paths to a config dictionary.
|
||||
|
||||
Args:
|
||||
config_data: Config data to mutate.
|
||||
paths: Dot-separated config path to override value mapping.
|
||||
|
||||
Returns:
|
||||
The mutated config_data object.
|
||||
"""
|
||||
for path, value in paths.items():
|
||||
target = config_data
|
||||
parts = [part for part in path.split(".") if part]
|
||||
if not parts:
|
||||
continue
|
||||
for part in parts[:-1]:
|
||||
child = target.get(part)
|
||||
if not isinstance(child, dict):
|
||||
child = {}
|
||||
target[part] = child
|
||||
target = child
|
||||
target[parts[-1]] = copy.deepcopy(value)
|
||||
return config_data
|
||||
|
||||
|
||||
async def build_effective_core_config(
|
||||
base_config: AstrBotConfig,
|
||||
preferences: SharedPreferences,
|
||||
umo: str,
|
||||
) -> AstrBotConfig:
|
||||
"""Build a per-event effective config by applying UMO overrides.
|
||||
|
||||
Args:
|
||||
base_config: Shared config profile selected for the event.
|
||||
preferences: Shared preferences storage.
|
||||
umo: Unified message origin.
|
||||
|
||||
Returns:
|
||||
A no-write config view for this single event.
|
||||
"""
|
||||
config_data = copy.deepcopy(dict(base_config))
|
||||
override_paths = await load_core_config_override_paths(preferences, umo)
|
||||
apply_config_override_paths(config_data, override_paths)
|
||||
return EffectiveAstrBotConfig(config_data, base_config=base_config, umo=umo)
|
||||
|
||||
|
||||
def build_effective_core_config_sync(
|
||||
base_config: AstrBotConfig,
|
||||
preferences: SharedPreferences,
|
||||
umo: str,
|
||||
) -> AstrBotConfig:
|
||||
"""Synchronously build a per-UMO effective config.
|
||||
|
||||
Args:
|
||||
base_config: Shared config profile selected for the UMO.
|
||||
preferences: Shared preferences storage.
|
||||
umo: Unified message origin.
|
||||
|
||||
Returns:
|
||||
A no-write config view for this UMO.
|
||||
"""
|
||||
config_data = copy.deepcopy(dict(base_config))
|
||||
override_paths = load_core_config_override_paths_sync(preferences, umo)
|
||||
apply_config_override_paths(config_data, override_paths)
|
||||
return EffectiveAstrBotConfig(config_data, base_config=base_config, umo=umo)
|
||||
@@ -432,7 +432,12 @@ class AstrBotCoreLifecycle:
|
||||
mapping = {}
|
||||
for conf_id, ab_config in self.astrbot_config_mgr.confs.items():
|
||||
scheduler = PipelineScheduler(
|
||||
PipelineContext(ab_config, self.plugin_manager, conf_id),
|
||||
PipelineContext(
|
||||
ab_config,
|
||||
self.plugin_manager,
|
||||
conf_id,
|
||||
self.astrbot_config_mgr,
|
||||
),
|
||||
)
|
||||
await scheduler.initialize()
|
||||
mapping[conf_id] = scheduler
|
||||
@@ -449,7 +454,12 @@ class AstrBotCoreLifecycle:
|
||||
if not ab_config:
|
||||
raise ValueError(f"配置文件 {conf_id} 不存在")
|
||||
scheduler = PipelineScheduler(
|
||||
PipelineContext(ab_config, self.plugin_manager, conf_id),
|
||||
PipelineContext(
|
||||
ab_config,
|
||||
self.plugin_manager,
|
||||
conf_id,
|
||||
self.astrbot_config_mgr,
|
||||
),
|
||||
)
|
||||
await scheduler.initialize()
|
||||
self.pipeline_scheduler_mapping[conf_id] = scheduler
|
||||
|
||||
@@ -312,6 +312,10 @@ class SQLiteDatabase(BaseDatabase):
|
||||
base_query = base_query.where(
|
||||
col(ConversationV2.platform_id).in_(platform_ids),
|
||||
)
|
||||
if kwargs.get("user_id"):
|
||||
base_query = base_query.where(
|
||||
col(ConversationV2.user_id) == str(kwargs["user_id"]),
|
||||
)
|
||||
if search_query:
|
||||
search_query = search_query.encode("unicode_escape").decode("utf-8")
|
||||
base_query = base_query.where(
|
||||
|
||||
@@ -277,11 +277,15 @@ class KnowledgeBaseManager:
|
||||
kb_ids = []
|
||||
kb_id_helper_map = {}
|
||||
unavailable_kbs = []
|
||||
for kb_name in kb_names:
|
||||
if kb_helper := await self.get_kb_by_name(kb_name):
|
||||
for kb_ref in kb_names:
|
||||
kb_ref = str(kb_ref)
|
||||
kb_helper = await self.get_kb(kb_ref)
|
||||
if not kb_helper:
|
||||
kb_helper = await self.get_kb_by_name(kb_ref)
|
||||
if kb_helper:
|
||||
if kb_helper.init_error:
|
||||
unavailable_kbs.append((kb_name, kb_helper.init_error))
|
||||
logger.warning(f"知识库 {kb_name} 不可用: {kb_helper.init_error}")
|
||||
unavailable_kbs.append((kb_ref, kb_helper.init_error))
|
||||
logger.warning(f"知识库 {kb_ref} 不可用: {kb_helper.init_error}")
|
||||
continue
|
||||
kb_ids.append(kb_helper.kb.kb_id)
|
||||
kb_id_helper_map[kb_helper.kb.kb_id] = kb_helper
|
||||
|
||||
@@ -81,7 +81,6 @@ class RetrievalManager:
|
||||
query: 查询文本
|
||||
kb_ids: 知识库 ID 列表
|
||||
top_m_final: 最终返回数量
|
||||
enable_rerank: 是否启用 Rerank
|
||||
|
||||
Returns:
|
||||
List[RetrievalResult]: 检索结果列表
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from astrbot import logger
|
||||
from astrbot.api import sp
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.po import Persona, PersonaFolder, Personality
|
||||
@@ -86,28 +85,15 @@ class PersonaManager:
|
||||
tuple:
|
||||
- selected persona_id
|
||||
- selected persona object
|
||||
- force applied persona_id from session rule
|
||||
- force applied persona_id, retained for API compatibility
|
||||
- whether use webchat special default persona
|
||||
"""
|
||||
session_service_config = (
|
||||
await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=str(umo),
|
||||
key="session_service_config",
|
||||
default={},
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
force_applied_persona_id = session_service_config.get("persona_id")
|
||||
persona_id = force_applied_persona_id
|
||||
|
||||
if not persona_id:
|
||||
persona_id = conversation_persona_id
|
||||
if persona_id == "[%None]":
|
||||
pass
|
||||
elif persona_id is None:
|
||||
persona_id = (provider_settings or {}).get("default_personality")
|
||||
force_applied_persona_id = None
|
||||
persona_id = conversation_persona_id
|
||||
if persona_id == "[%None]":
|
||||
pass
|
||||
elif persona_id is None:
|
||||
persona_id = (provider_settings or {}).get("default_personality")
|
||||
|
||||
persona = next(
|
||||
(item for item in self.personas_v3 if item["name"] == persona_id),
|
||||
|
||||
@@ -24,7 +24,6 @@ if TYPE_CHECKING:
|
||||
from .rate_limit_check.stage import RateLimitStage
|
||||
from .respond.stage import RespondStage
|
||||
from .result_decorate.stage import ResultDecorateStage
|
||||
from .session_status_check.stage import SessionStatusCheckStage
|
||||
from .waking_check.stage import WakingCheckStage
|
||||
from .whitelist_check.stage import WhitelistCheckStage
|
||||
|
||||
@@ -53,10 +52,6 @@ _LAZY_EXPORTS = {
|
||||
"astrbot.core.pipeline.result_decorate.stage",
|
||||
"ResultDecorateStage",
|
||||
),
|
||||
"SessionStatusCheckStage": (
|
||||
"astrbot.core.pipeline.session_status_check.stage",
|
||||
"SessionStatusCheckStage",
|
||||
),
|
||||
"WakingCheckStage": (
|
||||
"astrbot.core.pipeline.waking_check.stage",
|
||||
"WakingCheckStage",
|
||||
@@ -75,7 +70,6 @@ if TYPE_CHECKING:
|
||||
from .rate_limit_check.stage import RateLimitStage
|
||||
from .respond.stage import RespondStage
|
||||
from .result_decorate.stage import ResultDecorateStage
|
||||
from .session_status_check.stage import SessionStatusCheckStage
|
||||
from .waking_check.stage import WakingCheckStage
|
||||
from .whitelist_check.stage import WhitelistCheckStage
|
||||
|
||||
@@ -88,7 +82,6 @@ __all__ = [
|
||||
"RateLimitStage",
|
||||
"RespondStage",
|
||||
"ResultDecorateStage",
|
||||
"SessionStatusCheckStage",
|
||||
"STAGES_ORDER",
|
||||
"WakingCheckStage",
|
||||
"WhitelistCheckStage",
|
||||
|
||||
@@ -7,7 +7,6 @@ from .stage import registered_stages
|
||||
_BUILTIN_STAGE_MODULES = (
|
||||
"astrbot.core.pipeline.waking_check.stage",
|
||||
"astrbot.core.pipeline.whitelist_check.stage",
|
||||
"astrbot.core.pipeline.session_status_check.stage",
|
||||
"astrbot.core.pipeline.rate_limit_check.stage",
|
||||
"astrbot.core.pipeline.content_safety_check.stage",
|
||||
"astrbot.core.pipeline.preprocess_stage.stage",
|
||||
@@ -19,7 +18,6 @@ _BUILTIN_STAGE_MODULES = (
|
||||
_EXPECTED_STAGE_NAMES = {
|
||||
"WakingCheckStage",
|
||||
"WhitelistCheckStage",
|
||||
"SessionStatusCheckStage",
|
||||
"RateLimitStage",
|
||||
"ContentSafetyCheckStage",
|
||||
"PreProcessStage",
|
||||
|
||||
@@ -17,8 +17,7 @@ class ContentSafetyCheckStage(Stage):
|
||||
"""
|
||||
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
config = ctx.astrbot_config["content_safety"]
|
||||
self.strategy_selector = StrategySelector(config)
|
||||
self.ctx = ctx
|
||||
|
||||
async def process(
|
||||
self,
|
||||
@@ -27,7 +26,8 @@ class ContentSafetyCheckStage(Stage):
|
||||
) -> AsyncGenerator[None, None]:
|
||||
"""检查内容安全"""
|
||||
text = check_text if check_text else event.get_message_str()
|
||||
ok, info = self.strategy_selector.check(text)
|
||||
strategy_selector = StrategySelector(self.ctx.astrbot_config["content_safety"])
|
||||
ok, info = strategy_selector.check(text)
|
||||
if not ok:
|
||||
if event.is_at_or_wake_command:
|
||||
event.set_result(
|
||||
|
||||
@@ -8,15 +8,27 @@ from astrbot.core.config import AstrBotConfig
|
||||
from .context_utils import call_event_hook, call_handler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.star import PluginManager
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineContext:
|
||||
"""上下文对象,包含管道执行所需的上下文信息"""
|
||||
"""Context object with the state needed to execute a pipeline."""
|
||||
|
||||
astrbot_config: AstrBotConfig # AstrBot 配置对象
|
||||
plugin_manager: PluginManager # 插件管理器对象
|
||||
base_astrbot_config: AstrBotConfig
|
||||
plugin_manager: PluginManager
|
||||
astrbot_config_id: str
|
||||
astrbot_config_mgr: AstrBotConfigManager
|
||||
call_handler = call_handler
|
||||
call_event_hook = call_event_hook
|
||||
|
||||
@property
|
||||
def astrbot_config(self) -> AstrBotConfig:
|
||||
"""Return the current event config view, or the shared base config.
|
||||
|
||||
Returns:
|
||||
Effective config bound to the current task when a pipeline event is
|
||||
running; otherwise the shared config profile.
|
||||
"""
|
||||
return self.astrbot_config_mgr.get_current_conf(self.base_astrbot_config)
|
||||
|
||||
@@ -24,12 +24,8 @@ from ..stage import Stage, register_stage
|
||||
class PreProcessStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.config = ctx.astrbot_config
|
||||
self.plugin_manager = ctx.plugin_manager
|
||||
|
||||
self.stt_settings: dict = self.config.get("provider_stt_settings", {})
|
||||
self.platform_settings: dict = self.config.get("platform_settings", {})
|
||||
|
||||
@staticmethod
|
||||
def _track_temp_media(event: AstrMessageEvent, media_path: str) -> None:
|
||||
"""Track a media file owned by the current event.
|
||||
@@ -52,11 +48,15 @@ class PreProcessStage(Stage):
|
||||
event: AstrMessageEvent,
|
||||
) -> None | AsyncGenerator[None, None]:
|
||||
"""在处理事件之前的预处理"""
|
||||
config = self.ctx.astrbot_config
|
||||
stt_settings: dict = config.get("provider_stt_settings", {})
|
||||
platform_settings: dict = config.get("platform_settings", {})
|
||||
|
||||
# 平台特异配置:platform_specific.<platform>.pre_ack_emoji
|
||||
supported = {"telegram", "lark", "discord"}
|
||||
platform = event.get_platform_name()
|
||||
cfg = (
|
||||
self.config.get("platform_specific", {})
|
||||
config.get("platform_specific", {})
|
||||
.get(platform, {})
|
||||
.get("pre_ack_emoji", {})
|
||||
) or {}
|
||||
@@ -73,7 +73,7 @@ class PreProcessStage(Stage):
|
||||
logger.warning(f"{platform} 预回应表情发送失败: {e}")
|
||||
|
||||
# 路径映射
|
||||
if mappings := self.platform_settings.get("path_mapping", []):
|
||||
if mappings := platform_settings.get("path_mapping", []):
|
||||
# 支持 Record,Image 消息段的路径映射。
|
||||
message_chain = event.get_messages()
|
||||
|
||||
@@ -164,7 +164,7 @@ class PreProcessStage(Stage):
|
||||
)
|
||||
|
||||
# STT
|
||||
if self.stt_settings.get("enable", False):
|
||||
if stt_settings.get("enable", False):
|
||||
# TODO: 独立
|
||||
ctx = self.plugin_manager.context
|
||||
stt_provider = ctx.get_using_stt_provider(event.unified_msg_origin)
|
||||
|
||||
@@ -2,7 +2,6 @@ from collections.abc import AsyncGenerator
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.star.session_llm_manager import SessionServiceManager
|
||||
|
||||
from ...context import PipelineContext
|
||||
from ..stage import Stage
|
||||
@@ -13,36 +12,33 @@ from .agent_sub_stages.third_party import ThirdPartyAgentSubStage
|
||||
class AgentRequestSubStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.config = ctx.astrbot_config
|
||||
|
||||
self.bot_wake_prefixs: list[str] = self.config["wake_prefix"]
|
||||
self.prov_wake_prefix: str = self.config["provider_settings"]["wake_prefix"]
|
||||
for bwp in self.bot_wake_prefixs:
|
||||
if self.prov_wake_prefix.startswith(bwp):
|
||||
logger.info(
|
||||
f"识别 LLM 聊天额外唤醒前缀 {self.prov_wake_prefix} 以机器人唤醒前缀 {bwp} 开头,已自动去除。",
|
||||
)
|
||||
self.prov_wake_prefix = self.prov_wake_prefix[len(bwp) :]
|
||||
|
||||
agent_runner_type = self.config["provider_settings"]["agent_runner_type"]
|
||||
if agent_runner_type == "local":
|
||||
self.agent_sub_stage = InternalAgentSubStage()
|
||||
else:
|
||||
self.agent_sub_stage = ThirdPartyAgentSubStage()
|
||||
await self.agent_sub_stage.initialize(ctx)
|
||||
self.internal_agent_sub_stage = InternalAgentSubStage()
|
||||
self.third_party_agent_sub_stage = ThirdPartyAgentSubStage()
|
||||
await self.internal_agent_sub_stage.initialize(ctx)
|
||||
await self.third_party_agent_sub_stage.initialize(ctx)
|
||||
|
||||
async def process(self, event: AstrMessageEvent) -> AsyncGenerator[None, None]:
|
||||
if not self.ctx.astrbot_config["provider_settings"]["enable"]:
|
||||
config = self.ctx.astrbot_config
|
||||
provider_settings = config["provider_settings"]
|
||||
if not provider_settings["enable"]:
|
||||
logger.debug(
|
||||
"This pipeline does not enable AI capability, skip processing."
|
||||
)
|
||||
return
|
||||
|
||||
if not await SessionServiceManager.should_process_llm_request(event):
|
||||
logger.debug(
|
||||
f"The session {event.unified_msg_origin} has disabled AI capability, skipping processing."
|
||||
)
|
||||
return
|
||||
bot_wake_prefixes: list[str] = config["wake_prefix"]
|
||||
provider_wake_prefix: str = provider_settings["wake_prefix"]
|
||||
for bot_wake_prefix in bot_wake_prefixes:
|
||||
if provider_wake_prefix.startswith(bot_wake_prefix):
|
||||
logger.info(
|
||||
f"识别 LLM 聊天额外唤醒前缀 {provider_wake_prefix} 以机器人唤醒前缀 {bot_wake_prefix} 开头,已自动去除。",
|
||||
)
|
||||
provider_wake_prefix = provider_wake_prefix[len(bot_wake_prefix) :]
|
||||
|
||||
async for resp in self.agent_sub_stage.process(event, self.prov_wake_prefix):
|
||||
agent_sub_stage = (
|
||||
self.internal_agent_sub_stage
|
||||
if provider_settings["agent_runner_type"] == "local"
|
||||
else self.third_party_agent_sub_stage
|
||||
)
|
||||
async for resp in agent_sub_stage.process(event, provider_wake_prefix):
|
||||
yield resp
|
||||
|
||||
@@ -53,107 +53,8 @@ from ...follow_up import (
|
||||
class InternalAgentSubStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
conf = ctx.astrbot_config
|
||||
settings = conf["provider_settings"]
|
||||
self.streaming_response: bool = settings["streaming_response"]
|
||||
self.unsupported_streaming_strategy: str = settings[
|
||||
"unsupported_streaming_strategy"
|
||||
]
|
||||
self.max_step: int = settings.get("max_agent_step", 30)
|
||||
self.tool_call_timeout: int = settings.get("tool_call_timeout", 60)
|
||||
self.tool_schema_mode: str = settings.get("tool_schema_mode", "full")
|
||||
if self.tool_schema_mode not in ("skills_like", "full"):
|
||||
logger.warning(
|
||||
"Unsupported tool_schema_mode: %s, fallback to skills_like",
|
||||
self.tool_schema_mode,
|
||||
)
|
||||
self.tool_schema_mode = "full"
|
||||
if isinstance(self.max_step, bool): # workaround: #2622
|
||||
self.max_step = 30
|
||||
self.show_tool_use: bool = settings.get("show_tool_use_status", True)
|
||||
self.show_tool_call_result: bool = settings.get("show_tool_call_result", False)
|
||||
self.buffer_intermediate_messages: bool = settings.get(
|
||||
"buffer_intermediate_messages",
|
||||
False,
|
||||
)
|
||||
self.show_reasoning = settings.get("display_reasoning_text", False)
|
||||
self.sanitize_context_by_modalities: bool = settings.get(
|
||||
"sanitize_context_by_modalities",
|
||||
False,
|
||||
)
|
||||
self.kb_agentic_mode: bool = conf.get("kb_agentic_mode", False)
|
||||
|
||||
file_extract_conf: dict = settings.get("file_extract", {})
|
||||
self.file_extract_enabled: bool = file_extract_conf.get("enable", False)
|
||||
self.file_extract_prov: str = file_extract_conf.get("provider", "moonshotai")
|
||||
self.file_extract_msh_api_key: str = file_extract_conf.get(
|
||||
"moonshotai_api_key", ""
|
||||
)
|
||||
|
||||
# 上下文管理相关
|
||||
self.context_limit_reached_strategy: str = settings.get(
|
||||
"context_limit_reached_strategy", "truncate_by_turns"
|
||||
)
|
||||
self.llm_compress_instruction: str = settings.get(
|
||||
"llm_compress_instruction", ""
|
||||
)
|
||||
self.llm_compress_keep_recent_ratio: float = settings.get(
|
||||
"llm_compress_keep_recent_ratio", 0.15
|
||||
)
|
||||
self.llm_compress_provider_id: str = settings.get(
|
||||
"llm_compress_provider_id", ""
|
||||
)
|
||||
self.max_context_length = settings["max_context_length"] # int
|
||||
self.dequeue_context_length: int = min(
|
||||
max(1, settings["dequeue_context_length"]),
|
||||
self.max_context_length - 1,
|
||||
)
|
||||
if self.dequeue_context_length <= 0:
|
||||
self.dequeue_context_length = 1
|
||||
self.fallback_max_context_tokens: int = settings.get(
|
||||
"fallback_max_context_tokens", 128000
|
||||
)
|
||||
|
||||
self.llm_safety_mode = settings.get("llm_safety_mode", True)
|
||||
self.safety_mode_strategy = settings.get(
|
||||
"safety_mode_strategy", "system_prompt"
|
||||
)
|
||||
|
||||
self.computer_use_runtime = settings.get("computer_use_runtime")
|
||||
self.sandbox_cfg = settings.get("sandbox", {})
|
||||
|
||||
# Proactive capability configuration
|
||||
proactive_cfg = settings.get("proactive_capability", {})
|
||||
self.add_cron_tools = proactive_cfg.get("add_cron_tools", True)
|
||||
|
||||
self.conv_manager = ctx.plugin_manager.context.conversation_manager
|
||||
|
||||
self.main_agent_cfg = MainAgentBuildConfig(
|
||||
tool_call_timeout=self.tool_call_timeout,
|
||||
tool_schema_mode=self.tool_schema_mode,
|
||||
sanitize_context_by_modalities=self.sanitize_context_by_modalities,
|
||||
kb_agentic_mode=self.kb_agentic_mode,
|
||||
file_extract_enabled=self.file_extract_enabled,
|
||||
file_extract_prov=self.file_extract_prov,
|
||||
file_extract_msh_api_key=self.file_extract_msh_api_key,
|
||||
context_limit_reached_strategy=self.context_limit_reached_strategy,
|
||||
llm_compress_instruction=self.llm_compress_instruction,
|
||||
llm_compress_keep_recent_ratio=self.llm_compress_keep_recent_ratio,
|
||||
llm_compress_provider_id=self.llm_compress_provider_id,
|
||||
max_context_length=self.max_context_length,
|
||||
dequeue_context_length=self.dequeue_context_length,
|
||||
fallback_max_context_tokens=self.fallback_max_context_tokens,
|
||||
llm_safety_mode=self.llm_safety_mode,
|
||||
safety_mode_strategy=self.safety_mode_strategy,
|
||||
computer_use_runtime=self.computer_use_runtime,
|
||||
sandbox_cfg=self.sandbox_cfg,
|
||||
add_cron_tools=self.add_cron_tools,
|
||||
provider_settings=settings,
|
||||
subagent_orchestrator=conf.get("subagent_orchestrator", {}),
|
||||
timezone=self.ctx.plugin_manager.context.get_config().get("timezone"),
|
||||
max_quoted_fallback_images=settings.get("max_quoted_fallback_images", 20),
|
||||
)
|
||||
|
||||
async def _send_llm_error_message(
|
||||
self, event: AstrMessageEvent, message: object
|
||||
) -> None:
|
||||
@@ -162,12 +63,96 @@ class InternalAgentSubStage(Stage):
|
||||
async def process(
|
||||
self, event: AstrMessageEvent, provider_wake_prefix: str
|
||||
) -> AsyncGenerator[None, None]:
|
||||
conf = self.ctx.astrbot_config
|
||||
settings = conf["provider_settings"]
|
||||
streaming_response: bool = settings["streaming_response"]
|
||||
unsupported_streaming_strategy: str = settings["unsupported_streaming_strategy"]
|
||||
max_step: int = settings.get("max_agent_step", 30)
|
||||
tool_call_timeout: int = settings.get("tool_call_timeout", 60)
|
||||
tool_schema_mode: str = settings.get("tool_schema_mode", "full")
|
||||
if tool_schema_mode not in ("skills_like", "full"):
|
||||
logger.warning(
|
||||
"Unsupported tool_schema_mode: %s, fallback to full",
|
||||
tool_schema_mode,
|
||||
)
|
||||
tool_schema_mode = "full"
|
||||
if isinstance(max_step, bool): # workaround: #2622
|
||||
max_step = 30
|
||||
show_tool_use: bool = settings.get("show_tool_use_status", True)
|
||||
show_tool_call_result: bool = settings.get("show_tool_call_result", False)
|
||||
buffer_intermediate_messages: bool = settings.get(
|
||||
"buffer_intermediate_messages",
|
||||
False,
|
||||
)
|
||||
show_reasoning = settings.get("display_reasoning_text", False)
|
||||
sanitize_context_by_modalities: bool = settings.get(
|
||||
"sanitize_context_by_modalities",
|
||||
False,
|
||||
)
|
||||
kb_agentic_mode: bool = conf.get("kb_agentic_mode", False)
|
||||
|
||||
file_extract_conf: dict = settings.get("file_extract", {})
|
||||
file_extract_enabled: bool = file_extract_conf.get("enable", False)
|
||||
file_extract_prov: str = file_extract_conf.get("provider", "moonshotai")
|
||||
file_extract_msh_api_key: str = file_extract_conf.get("moonshotai_api_key", "")
|
||||
|
||||
context_limit_reached_strategy: str = settings.get(
|
||||
"context_limit_reached_strategy", "truncate_by_turns"
|
||||
)
|
||||
llm_compress_instruction: str = settings.get("llm_compress_instruction", "")
|
||||
llm_compress_keep_recent_ratio: float = settings.get(
|
||||
"llm_compress_keep_recent_ratio", 0.15
|
||||
)
|
||||
llm_compress_provider_id: str = settings.get("llm_compress_provider_id", "")
|
||||
max_context_length = settings["max_context_length"]
|
||||
dequeue_context_length: int = min(
|
||||
max(1, settings["dequeue_context_length"]),
|
||||
max_context_length - 1,
|
||||
)
|
||||
if dequeue_context_length <= 0:
|
||||
dequeue_context_length = 1
|
||||
fallback_max_context_tokens: int = settings.get(
|
||||
"fallback_max_context_tokens", 128000
|
||||
)
|
||||
|
||||
llm_safety_mode = settings.get("llm_safety_mode", True)
|
||||
safety_mode_strategy = settings.get("safety_mode_strategy", "system_prompt")
|
||||
computer_use_runtime = settings.get("computer_use_runtime")
|
||||
sandbox_cfg = settings.get("sandbox", {})
|
||||
proactive_cfg = settings.get("proactive_capability", {})
|
||||
add_cron_tools = proactive_cfg.get("add_cron_tools", True)
|
||||
|
||||
main_agent_cfg = MainAgentBuildConfig(
|
||||
tool_call_timeout=tool_call_timeout,
|
||||
tool_schema_mode=tool_schema_mode,
|
||||
sanitize_context_by_modalities=sanitize_context_by_modalities,
|
||||
kb_agentic_mode=kb_agentic_mode,
|
||||
file_extract_enabled=file_extract_enabled,
|
||||
file_extract_prov=file_extract_prov,
|
||||
file_extract_msh_api_key=file_extract_msh_api_key,
|
||||
context_limit_reached_strategy=context_limit_reached_strategy,
|
||||
llm_compress_instruction=llm_compress_instruction,
|
||||
llm_compress_keep_recent_ratio=llm_compress_keep_recent_ratio,
|
||||
llm_compress_provider_id=llm_compress_provider_id,
|
||||
max_context_length=max_context_length,
|
||||
dequeue_context_length=dequeue_context_length,
|
||||
fallback_max_context_tokens=fallback_max_context_tokens,
|
||||
llm_safety_mode=llm_safety_mode,
|
||||
safety_mode_strategy=safety_mode_strategy,
|
||||
computer_use_runtime=computer_use_runtime,
|
||||
sandbox_cfg=sandbox_cfg,
|
||||
add_cron_tools=add_cron_tools,
|
||||
provider_settings=settings,
|
||||
subagent_orchestrator=conf.get("subagent_orchestrator", {}),
|
||||
timezone=self.ctx.plugin_manager.context.get_config().get("timezone"),
|
||||
max_quoted_fallback_images=settings.get("max_quoted_fallback_images", 20),
|
||||
)
|
||||
|
||||
follow_up_capture: FollowUpCapture | None = None
|
||||
follow_up_consumed_marked = False
|
||||
follow_up_activated = False
|
||||
typing_requested = False
|
||||
try:
|
||||
streaming_response = self.streaming_response
|
||||
if (enable_streaming := event.get_extra("enable_streaming")) is not None:
|
||||
streaming_response = bool(enable_streaming)
|
||||
|
||||
@@ -219,7 +204,7 @@ class InternalAgentSubStage(Stage):
|
||||
runner_registered = False
|
||||
try:
|
||||
build_cfg = replace(
|
||||
self.main_agent_cfg,
|
||||
main_agent_cfg,
|
||||
provider_wake_prefix=provider_wake_prefix,
|
||||
streaming_response=streaming_response,
|
||||
)
|
||||
@@ -258,7 +243,7 @@ class InternalAgentSubStage(Stage):
|
||||
return
|
||||
|
||||
stream_to_general = (
|
||||
self.unsupported_streaming_strategy == "turn_off"
|
||||
unsupported_streaming_strategy == "turn_off"
|
||||
and not event.platform_meta.support_streaming_message
|
||||
)
|
||||
|
||||
@@ -311,11 +296,11 @@ class InternalAgentSubStage(Stage):
|
||||
run_live_agent(
|
||||
agent_runner,
|
||||
tts_provider,
|
||||
self.max_step,
|
||||
self.show_tool_use,
|
||||
self.show_tool_call_result,
|
||||
show_reasoning=self.show_reasoning,
|
||||
buffer_intermediate_messages=self.buffer_intermediate_messages,
|
||||
max_step,
|
||||
show_tool_use,
|
||||
show_tool_call_result,
|
||||
show_reasoning=show_reasoning,
|
||||
buffer_intermediate_messages=buffer_intermediate_messages,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -342,11 +327,11 @@ class InternalAgentSubStage(Stage):
|
||||
.set_async_stream(
|
||||
run_agent(
|
||||
agent_runner,
|
||||
self.max_step,
|
||||
self.show_tool_use,
|
||||
self.show_tool_call_result,
|
||||
show_reasoning=self.show_reasoning,
|
||||
buffer_intermediate_messages=self.buffer_intermediate_messages,
|
||||
max_step,
|
||||
show_tool_use,
|
||||
show_tool_call_result,
|
||||
show_reasoning=show_reasoning,
|
||||
buffer_intermediate_messages=buffer_intermediate_messages,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -372,12 +357,12 @@ class InternalAgentSubStage(Stage):
|
||||
else:
|
||||
async for _ in run_agent(
|
||||
agent_runner,
|
||||
self.max_step,
|
||||
self.show_tool_use,
|
||||
self.show_tool_call_result,
|
||||
max_step,
|
||||
show_tool_use,
|
||||
show_tool_call_result,
|
||||
stream_to_general,
|
||||
show_reasoning=self.show_reasoning,
|
||||
buffer_intermediate_messages=self.buffer_intermediate_messages,
|
||||
show_reasoning=show_reasoning,
|
||||
buffer_intermediate_messages=buffer_intermediate_messages,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@@ -164,27 +164,6 @@ async def _close_runner_if_supported(runner: "BaseAgentRunner") -> None:
|
||||
class ThirdPartyAgentSubStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.conf = ctx.astrbot_config
|
||||
self.runner_type = self.conf["provider_settings"]["agent_runner_type"]
|
||||
self.prov_id = self.conf["provider_settings"].get(
|
||||
AGENT_RUNNER_TYPE_KEY.get(self.runner_type, ""),
|
||||
"",
|
||||
)
|
||||
settings = ctx.astrbot_config["provider_settings"]
|
||||
self.streaming_response: bool = settings["streaming_response"]
|
||||
self.unsupported_streaming_strategy: str = settings[
|
||||
"unsupported_streaming_strategy"
|
||||
]
|
||||
self.stream_consumption_close_timeout_sec: int = coerce_int_config(
|
||||
settings.get(
|
||||
"third_party_stream_consumption_close_timeout_sec",
|
||||
STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC,
|
||||
),
|
||||
default=STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC,
|
||||
min_value=1,
|
||||
field_name="third_party_stream_consumption_close_timeout_sec",
|
||||
source="Third-party runner config",
|
||||
)
|
||||
|
||||
async def _resolve_persona_custom_error_message(
|
||||
self, event: AstrMessageEvent
|
||||
@@ -197,7 +176,7 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
return await resolve_persona_custom_error_message(
|
||||
event=event,
|
||||
persona_manager=self.ctx.plugin_manager.context.persona_manager,
|
||||
provider_settings=self.conf["provider_settings"],
|
||||
provider_settings=self.ctx.astrbot_config["provider_settings"],
|
||||
conversation_persona_id=conversation_persona_id,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -290,22 +269,45 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
self, event: AstrMessageEvent, provider_wake_prefix: str
|
||||
) -> AsyncGenerator[None, None]:
|
||||
req: ProviderRequest | None = None
|
||||
conf = self.ctx.astrbot_config
|
||||
settings = conf["provider_settings"]
|
||||
runner_type = settings["agent_runner_type"]
|
||||
provider_id = settings.get(
|
||||
AGENT_RUNNER_TYPE_KEY.get(runner_type, ""),
|
||||
"",
|
||||
)
|
||||
streaming_response: bool = settings["streaming_response"]
|
||||
unsupported_streaming_strategy: str = settings["unsupported_streaming_strategy"]
|
||||
stream_consumption_close_timeout_sec: int = coerce_int_config(
|
||||
settings.get(
|
||||
"third_party_stream_consumption_close_timeout_sec",
|
||||
STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC,
|
||||
),
|
||||
default=STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC,
|
||||
min_value=1,
|
||||
field_name="third_party_stream_consumption_close_timeout_sec",
|
||||
source="Third-party runner config",
|
||||
)
|
||||
|
||||
if provider_wake_prefix and not event.message_str.startswith(
|
||||
provider_wake_prefix
|
||||
):
|
||||
return
|
||||
|
||||
self.prov_cfg: dict = next(
|
||||
(p for p in astrbot_config["provider"] if p["id"] == self.prov_id),
|
||||
provider_config: dict = next(
|
||||
(
|
||||
p
|
||||
for p in conf.get("provider", astrbot_config["provider"])
|
||||
if p["id"] == provider_id
|
||||
),
|
||||
{},
|
||||
)
|
||||
if not self.prov_id:
|
||||
if not provider_id:
|
||||
logger.error("没有填写 Agent Runner 提供商 ID,请前往配置页面配置。")
|
||||
return
|
||||
if not self.prov_cfg:
|
||||
if not provider_config:
|
||||
logger.error(
|
||||
f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。"
|
||||
f"Agent Runner 提供商 {provider_id} 配置不存在,请前往配置页面修改配置。"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -331,17 +333,17 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
|
||||
return
|
||||
|
||||
if self.runner_type == "dify":
|
||||
if runner_type == "dify":
|
||||
runner = DifyAgentRunner[AstrAgentContext]()
|
||||
elif self.runner_type == "coze":
|
||||
elif runner_type == "coze":
|
||||
runner = CozeAgentRunner[AstrAgentContext]()
|
||||
elif self.runner_type == "dashscope":
|
||||
elif runner_type == "dashscope":
|
||||
runner = DashscopeAgentRunner[AstrAgentContext]()
|
||||
elif self.runner_type == DEERFLOW_PROVIDER_TYPE:
|
||||
elif runner_type == DEERFLOW_PROVIDER_TYPE:
|
||||
runner = DeerFlowAgentRunner[AstrAgentContext]()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported third party agent runner type: {self.runner_type}",
|
||||
f"Unsupported third party agent runner type: {runner_type}",
|
||||
)
|
||||
|
||||
astr_agent_ctx = AstrAgentContext(
|
||||
@@ -349,12 +351,11 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
event=event,
|
||||
)
|
||||
|
||||
streaming_response = self.streaming_response
|
||||
if (enable_streaming := event.get_extra("enable_streaming")) is not None:
|
||||
streaming_response = bool(enable_streaming)
|
||||
|
||||
stream_to_general = (
|
||||
self.unsupported_streaming_strategy == "turn_off"
|
||||
unsupported_streaming_strategy == "turn_off"
|
||||
and not event.platform_meta.support_streaming_message
|
||||
)
|
||||
streaming_used = streaming_response and not stream_to_general
|
||||
@@ -384,13 +385,13 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
tool_call_timeout=120,
|
||||
),
|
||||
agent_hooks=MAIN_AGENT_HOOKS,
|
||||
provider_config=self.prov_cfg,
|
||||
provider_config=provider_config,
|
||||
streaming=streaming_response,
|
||||
)
|
||||
|
||||
if streaming_used:
|
||||
stream_watchdog_task = _start_stream_watchdog(
|
||||
timeout_sec=self.stream_consumption_close_timeout_sec,
|
||||
timeout_sec=stream_consumption_close_timeout_sec,
|
||||
is_stream_consumed=lambda: stream_consumed,
|
||||
close_runner_once=close_runner_once,
|
||||
)
|
||||
@@ -423,7 +424,7 @@ class ThirdPartyAgentSubStage(Stage):
|
||||
asyncio.create_task(
|
||||
Metric.upload(
|
||||
llm_tick=1,
|
||||
model_name=self.runner_type,
|
||||
provider_type=self.runner_type,
|
||||
model_name=runner_type,
|
||||
provider_type=runner_type,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -16,8 +16,6 @@ from ..stage import Stage
|
||||
|
||||
class StarRequestSubStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.prompt_prefix = ctx.astrbot_config["provider_settings"]["prompt_prefix"]
|
||||
self.identifier = ctx.astrbot_config["provider_settings"]["identifier"]
|
||||
self.ctx = ctx
|
||||
|
||||
async def process(
|
||||
|
||||
@@ -14,7 +14,6 @@ from .method.star_request import StarRequestSubStage
|
||||
class ProcessStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.config = ctx.astrbot_config
|
||||
self.plugin_manager = ctx.plugin_manager
|
||||
|
||||
# initialize agent sub stage
|
||||
|
||||
@@ -24,21 +24,14 @@ class RateLimitStage(Stage):
|
||||
self.event_timestamps: defaultdict[str, deque[datetime]] = defaultdict(deque)
|
||||
# 为每个会话设置一个锁,避免并发冲突
|
||||
self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
||||
# 限流参数
|
||||
self.rate_limit_count: int = 0
|
||||
self.rate_limit_time: timedelta = timedelta(0)
|
||||
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
"""初始化限流器,根据配置设置限流参数。"""
|
||||
self.rate_limit_count = ctx.astrbot_config["platform_settings"]["rate_limit"][
|
||||
"count"
|
||||
]
|
||||
self.rate_limit_time = timedelta(
|
||||
seconds=ctx.astrbot_config["platform_settings"]["rate_limit"]["time"],
|
||||
)
|
||||
self.rl_strategy = ctx.astrbot_config["platform_settings"]["rate_limit"][
|
||||
"strategy"
|
||||
] # stall or discard
|
||||
"""Initialize the rate limiter state.
|
||||
|
||||
Args:
|
||||
ctx: Pipeline context used to read config during event processing.
|
||||
"""
|
||||
self.ctx = ctx
|
||||
|
||||
async def process(
|
||||
self,
|
||||
@@ -56,22 +49,26 @@ class RateLimitStage(Stage):
|
||||
"""
|
||||
session_id = event.session_id
|
||||
now = datetime.now()
|
||||
rate_limit = self.ctx.astrbot_config["platform_settings"]["rate_limit"]
|
||||
rate_limit_count = rate_limit["count"]
|
||||
rate_limit_time = timedelta(seconds=rate_limit["time"])
|
||||
rl_strategy = rate_limit["strategy"]
|
||||
|
||||
async with self.locks[session_id]: # 确保同一会话不会并发修改队列
|
||||
# 检查并处理限流,可能需要多次检查直到满足条件
|
||||
while True:
|
||||
timestamps = self.event_timestamps[session_id]
|
||||
self._remove_expired_timestamps(timestamps, now)
|
||||
self._remove_expired_timestamps(timestamps, now, rate_limit_time)
|
||||
|
||||
if self.rate_limit_count <= 0:
|
||||
if rate_limit_count <= 0:
|
||||
break
|
||||
if len(timestamps) < self.rate_limit_count:
|
||||
if len(timestamps) < rate_limit_count:
|
||||
timestamps.append(now)
|
||||
break
|
||||
next_window_time = timestamps[0] + self.rate_limit_time
|
||||
next_window_time = timestamps[0] + rate_limit_time
|
||||
stall_duration = (next_window_time - now).total_seconds() + 0.3
|
||||
|
||||
match self.rl_strategy:
|
||||
match rl_strategy:
|
||||
case RateLimitStrategy.STALL.value:
|
||||
logger.info(
|
||||
f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。",
|
||||
@@ -88,14 +85,16 @@ class RateLimitStage(Stage):
|
||||
self,
|
||||
timestamps: deque[datetime],
|
||||
now: datetime,
|
||||
rate_limit_time: timedelta,
|
||||
) -> None:
|
||||
"""移除时间窗口外的时间戳。
|
||||
|
||||
Args:
|
||||
timestamps (Deque[datetime]): 当前会话的时间戳队列。
|
||||
now (datetime): 当前时间,用于计算过期时间。
|
||||
rate_limit_time: Current rate-limit window duration.
|
||||
|
||||
"""
|
||||
expiry_threshold: datetime = now - self.rate_limit_time
|
||||
expiry_threshold: datetime = now - rate_limit_time
|
||||
while timestamps and timestamps[0] < expiry_threshold:
|
||||
timestamps.popleft()
|
||||
|
||||
@@ -51,41 +51,6 @@ class RespondStage(Stage):
|
||||
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.config = ctx.astrbot_config
|
||||
self.platform_settings: dict = self.config.get("platform_settings", {})
|
||||
|
||||
self.reply_with_mention = ctx.astrbot_config["platform_settings"][
|
||||
"reply_with_mention"
|
||||
]
|
||||
self.reply_with_quote = ctx.astrbot_config["platform_settings"][
|
||||
"reply_with_quote"
|
||||
]
|
||||
|
||||
# 分段回复
|
||||
self.enable_seg: bool = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
]["enable"]
|
||||
self.only_llm_result = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
]["only_llm_result"]
|
||||
|
||||
self.interval_method = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
]["interval_method"]
|
||||
self.log_base = float(
|
||||
ctx.astrbot_config["platform_settings"]["segmented_reply"]["log_base"],
|
||||
)
|
||||
self.interval = [1.5, 3.5]
|
||||
if self.enable_seg:
|
||||
interval_str: str = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
]["interval"]
|
||||
interval_str_ls = interval_str.replace(" ", "").split(",")
|
||||
try:
|
||||
self.interval = [float(t) for t in interval_str_ls]
|
||||
except BaseException as e:
|
||||
logger.error(f"解析分段回复的间隔时间失败。{e}")
|
||||
logger.info(f"分段回复间隔时间:{self.interval}")
|
||||
|
||||
async def _word_cnt(self, text: str) -> int:
|
||||
"""分段回复 统计字数"""
|
||||
@@ -95,16 +60,22 @@ class RespondStage(Stage):
|
||||
word_count = len([c for c in text if c.isalnum()])
|
||||
return word_count
|
||||
|
||||
async def _calc_comp_interval(self, comp: BaseMessageComponent) -> float:
|
||||
async def _calc_comp_interval(
|
||||
self,
|
||||
comp: BaseMessageComponent,
|
||||
interval_method: str,
|
||||
log_base: float,
|
||||
interval: list[float],
|
||||
) -> float:
|
||||
"""分段回复 计算间隔时间"""
|
||||
if self.interval_method == "log":
|
||||
if interval_method == "log":
|
||||
if isinstance(comp, Comp.Plain):
|
||||
wc = await self._word_cnt(comp.text)
|
||||
i = math.log(wc + 1, self.log_base)
|
||||
i = math.log(wc + 1, log_base)
|
||||
return random.uniform(i, i + 0.5)
|
||||
return random.uniform(1, 1.75)
|
||||
# random
|
||||
return random.uniform(self.interval[0], self.interval[1])
|
||||
return random.uniform(interval[0], interval[1])
|
||||
|
||||
async def _is_empty_message_chain(self, chain: list[BaseMessageComponent]) -> bool:
|
||||
"""检查消息链是否为空
|
||||
@@ -127,14 +98,19 @@ class RespondStage(Stage):
|
||||
# 如果所有组件都为空
|
||||
return True
|
||||
|
||||
def is_seg_reply_required(self, event: AstrMessageEvent) -> bool:
|
||||
def is_seg_reply_required(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
enable_seg: bool,
|
||||
only_llm_result: bool,
|
||||
) -> bool:
|
||||
"""检查是否需要分段回复"""
|
||||
if not self.enable_seg:
|
||||
if not enable_seg:
|
||||
return False
|
||||
|
||||
if (result := event.get_result()) is None:
|
||||
return False
|
||||
if self.only_llm_result and not result.is_model_result():
|
||||
if only_llm_result and not result.is_model_result():
|
||||
return False
|
||||
|
||||
if event.get_platform_name() in [
|
||||
@@ -170,6 +146,23 @@ class RespondStage(Stage):
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
) -> None | AsyncGenerator[None, None]:
|
||||
config = self.ctx.astrbot_config
|
||||
platform_settings: dict = config.get("platform_settings", {})
|
||||
segmented_reply = platform_settings.get("segmented_reply", {})
|
||||
enable_seg: bool = segmented_reply.get("enable", False)
|
||||
only_llm_result = segmented_reply.get("only_llm_result", True)
|
||||
interval_method = segmented_reply.get("interval_method", "random")
|
||||
log_base = float(segmented_reply.get("log_base", 2.6))
|
||||
interval = [1.5, 3.5]
|
||||
if enable_seg:
|
||||
interval_str: str = segmented_reply.get("interval", "1.5,3.5")
|
||||
interval_str_ls = interval_str.replace(" ", "").split(",")
|
||||
try:
|
||||
interval = [float(t) for t in interval_str_ls]
|
||||
except BaseException as e:
|
||||
logger.error(f"解析分段回复的间隔时间失败。{e}")
|
||||
logger.debug(f"分段回复间隔时间:{interval}")
|
||||
|
||||
result = event.get_result()
|
||||
if result is None:
|
||||
return
|
||||
@@ -213,7 +206,7 @@ class RespondStage(Stage):
|
||||
return
|
||||
# 流式结果直接交付平台适配器处理
|
||||
realtime_segmenting = (
|
||||
self.config.get("provider_settings", {}).get(
|
||||
config.get("provider_settings", {}).get(
|
||||
"unsupported_streaming_strategy",
|
||||
"realtime_segmenting",
|
||||
)
|
||||
@@ -224,7 +217,7 @@ class RespondStage(Stage):
|
||||
return
|
||||
if len(result.chain) > 0:
|
||||
# 检查路径映射
|
||||
if mappings := self.platform_settings.get("path_mapping", []):
|
||||
if mappings := platform_settings.get("path_mapping", []):
|
||||
for idx, component in enumerate(result.chain):
|
||||
if isinstance(component, Comp.File) and component.file:
|
||||
# 支持 File 消息段的路径映射。
|
||||
@@ -252,7 +245,7 @@ class RespondStage(Stage):
|
||||
# 发送消息链
|
||||
# Record 需要强制单独发送
|
||||
need_separately = {ComponentType.Record}
|
||||
if self.is_seg_reply_required(event):
|
||||
if self.is_seg_reply_required(event, enable_seg, only_llm_result):
|
||||
header_comps = self._extract_comp(
|
||||
result.chain,
|
||||
{ComponentType.Reply, ComponentType.At},
|
||||
@@ -265,7 +258,12 @@ class RespondStage(Stage):
|
||||
)
|
||||
return
|
||||
for comp in result.chain:
|
||||
i = await self._calc_comp_interval(comp)
|
||||
i = await self._calc_comp_interval(
|
||||
comp,
|
||||
interval_method,
|
||||
log_base,
|
||||
interval,
|
||||
)
|
||||
await asyncio.sleep(i)
|
||||
try:
|
||||
if comp.type in need_separately:
|
||||
|
||||
@@ -10,7 +10,6 @@ from astrbot.core.message.message_event_result import ResultContentType
|
||||
from astrbot.core.pipeline.content_safety_check.stage import ContentSafetyCheckStage
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core.star.session_llm_manager import SessionServiceManager
|
||||
from astrbot.core.star.star import star_map
|
||||
from astrbot.core.star.star_handler import EventType, star_handlers_registry
|
||||
|
||||
@@ -22,98 +21,30 @@ from ..stage import Stage, register_stage, registered_stages
|
||||
class ResultDecorateStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.reply_prefix = ctx.astrbot_config["platform_settings"]["reply_prefix"]
|
||||
self.reply_with_mention = ctx.astrbot_config["platform_settings"][
|
||||
"reply_with_mention"
|
||||
]
|
||||
self.reply_with_quote = ctx.astrbot_config["platform_settings"][
|
||||
"reply_with_quote"
|
||||
]
|
||||
self.t2i_word_threshold = ctx.astrbot_config["t2i_word_threshold"]
|
||||
try:
|
||||
self.t2i_word_threshold = int(self.t2i_word_threshold)
|
||||
self.t2i_word_threshold = max(self.t2i_word_threshold, 50)
|
||||
except BaseException:
|
||||
self.t2i_word_threshold = 150
|
||||
self.t2i_strategy = ctx.astrbot_config["t2i_strategy"]
|
||||
self.t2i_use_network = self.t2i_strategy == "remote"
|
||||
self.t2i_active_template = ctx.astrbot_config["t2i_active_template"]
|
||||
|
||||
self.forward_threshold = ctx.astrbot_config["platform_settings"][
|
||||
"forward_threshold"
|
||||
]
|
||||
|
||||
trigger_probability = ctx.astrbot_config["provider_tts_settings"].get(
|
||||
"trigger_probability",
|
||||
1,
|
||||
)
|
||||
try:
|
||||
self.tts_trigger_probability = max(
|
||||
0.0,
|
||||
min(float(trigger_probability), 1.0),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self.tts_trigger_probability = 1.0
|
||||
|
||||
# 分段回复
|
||||
self.words_count_threshold = int(
|
||||
ctx.astrbot_config["platform_settings"]["segmented_reply"][
|
||||
"words_count_threshold"
|
||||
],
|
||||
)
|
||||
self.enable_segmented_reply = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
]["enable"]
|
||||
self.only_llm_result = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
]["only_llm_result"]
|
||||
self.split_mode = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
].get("split_mode", "regex")
|
||||
self.regex = ctx.astrbot_config["platform_settings"]["segmented_reply"]["regex"]
|
||||
self.split_words = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
].get("split_words", ["。", "?", "!", "~", "…"])
|
||||
if self.split_words:
|
||||
escaped_words = sorted(
|
||||
[re.escape(word) for word in self.split_words], key=len, reverse=True
|
||||
)
|
||||
self.split_words_pattern = re.compile(
|
||||
f"(.*?({'|'.join(escaped_words)})|.+$)", re.DOTALL
|
||||
)
|
||||
else:
|
||||
self.split_words_pattern = None
|
||||
self.content_cleanup_rule = ctx.astrbot_config["platform_settings"][
|
||||
"segmented_reply"
|
||||
]["content_cleanup_rule"]
|
||||
|
||||
# exception
|
||||
self.content_safe_check_reply = ctx.astrbot_config["content_safety"][
|
||||
"also_use_in_response"
|
||||
]
|
||||
self.content_safe_check_stage = None
|
||||
if self.content_safe_check_reply:
|
||||
for stage_cls in registered_stages:
|
||||
if stage_cls.__name__ == "ContentSafetyCheckStage":
|
||||
self.content_safe_check_stage = stage_cls()
|
||||
await self.content_safe_check_stage.initialize(ctx)
|
||||
for stage_cls in registered_stages:
|
||||
if stage_cls.__name__ == "ContentSafetyCheckStage":
|
||||
self.content_safe_check_stage = stage_cls()
|
||||
await self.content_safe_check_stage.initialize(ctx)
|
||||
|
||||
provider_cfg = ctx.astrbot_config.get("provider_settings", {})
|
||||
self.show_reasoning = provider_cfg.get("display_reasoning_text", False)
|
||||
|
||||
def _split_text_by_words(self, text: str) -> list[str]:
|
||||
def _split_text_by_words(
|
||||
self,
|
||||
text: str,
|
||||
split_words_pattern: re.Pattern[str] | None,
|
||||
split_words: list[str],
|
||||
) -> list[str]:
|
||||
"""使用分段词列表分段文本"""
|
||||
if not self.split_words_pattern:
|
||||
if not split_words_pattern:
|
||||
return [text]
|
||||
|
||||
segments = self.split_words_pattern.findall(text)
|
||||
segments = split_words_pattern.findall(text)
|
||||
result = []
|
||||
for seg in segments:
|
||||
if isinstance(seg, tuple):
|
||||
content = seg[0]
|
||||
if not isinstance(content, str):
|
||||
continue
|
||||
for word in self.split_words:
|
||||
for word in split_words:
|
||||
if content.endswith(word):
|
||||
content = content[: -len(word)]
|
||||
break
|
||||
@@ -135,10 +66,64 @@ class ResultDecorateStage(Stage):
|
||||
return
|
||||
|
||||
is_stream = result.result_content_type == ResultContentType.STREAMING_FINISH
|
||||
config = self.ctx.astrbot_config
|
||||
platform_settings = config.get("platform_settings", {})
|
||||
segmented_reply = platform_settings.get("segmented_reply", {})
|
||||
reply_prefix = platform_settings.get("reply_prefix", "")
|
||||
reply_with_mention = platform_settings.get("reply_with_mention", False)
|
||||
reply_with_quote = platform_settings.get("reply_with_quote", False)
|
||||
forward_threshold = platform_settings.get("forward_threshold", 1500)
|
||||
t2i_word_threshold = config.get("t2i_word_threshold", 150)
|
||||
try:
|
||||
t2i_word_threshold = max(int(t2i_word_threshold), 50)
|
||||
except BaseException:
|
||||
t2i_word_threshold = 150
|
||||
t2i_strategy = config.get("t2i_strategy", "local")
|
||||
t2i_use_network = t2i_strategy == "remote"
|
||||
t2i_active_template = config.get("t2i_active_template", "")
|
||||
|
||||
tts_settings = config.get("provider_tts_settings", {})
|
||||
trigger_probability = tts_settings.get("trigger_probability", 1)
|
||||
try:
|
||||
tts_trigger_probability = max(
|
||||
0.0,
|
||||
min(float(trigger_probability), 1.0),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
tts_trigger_probability = 1.0
|
||||
|
||||
words_count_threshold = int(segmented_reply.get("words_count_threshold", 150))
|
||||
enable_segmented_reply = segmented_reply.get("enable", False)
|
||||
only_llm_result = segmented_reply.get("only_llm_result", True)
|
||||
split_mode = segmented_reply.get("split_mode", "regex")
|
||||
split_regex = segmented_reply.get("regex", r".*?[。?!~…]+|.+$")
|
||||
split_words = segmented_reply.get(
|
||||
"split_words",
|
||||
["。", "?", "!", "~", "…"],
|
||||
)
|
||||
if split_words:
|
||||
escaped_words = sorted(
|
||||
[re.escape(word) for word in split_words],
|
||||
key=len,
|
||||
reverse=True,
|
||||
)
|
||||
split_words_pattern = re.compile(
|
||||
f"(.*?({'|'.join(escaped_words)})|.+$)",
|
||||
re.DOTALL,
|
||||
)
|
||||
else:
|
||||
split_words_pattern = None
|
||||
content_cleanup_rule = segmented_reply.get("content_cleanup_rule", "")
|
||||
content_safe_check_reply = config.get("content_safety", {}).get(
|
||||
"also_use_in_response",
|
||||
False,
|
||||
)
|
||||
provider_cfg = config.get("provider_settings", {})
|
||||
show_reasoning = provider_cfg.get("display_reasoning_text", False)
|
||||
|
||||
# 回复时检查内容安全
|
||||
if (
|
||||
self.content_safe_check_reply
|
||||
content_safe_check_reply
|
||||
and self.content_safe_check_stage
|
||||
and result.is_llm_result()
|
||||
and not is_stream # 流式输出不检查内容安全
|
||||
@@ -195,36 +180,40 @@ class ResultDecorateStage(Stage):
|
||||
|
||||
if len(result.chain) > 0:
|
||||
# 回复前缀
|
||||
if self.reply_prefix:
|
||||
if reply_prefix:
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Plain):
|
||||
comp.text = self.reply_prefix + comp.text
|
||||
comp.text = reply_prefix + comp.text
|
||||
break
|
||||
|
||||
# 分段回复
|
||||
if self.enable_segmented_reply and event.get_platform_name() not in [
|
||||
if enable_segmented_reply and event.get_platform_name() not in [
|
||||
"qq_official_webhook",
|
||||
"weixin_official_account",
|
||||
"dingtalk",
|
||||
]:
|
||||
if (
|
||||
self.only_llm_result and result.is_model_result()
|
||||
) or not self.only_llm_result:
|
||||
only_llm_result and result.is_model_result()
|
||||
) or not only_llm_result:
|
||||
new_chain = []
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Plain):
|
||||
if len(comp.text) > self.words_count_threshold:
|
||||
if len(comp.text) > words_count_threshold:
|
||||
# 不分段回复
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
|
||||
# 根据 split_mode 选择分段方式
|
||||
if self.split_mode == "words":
|
||||
split_response = self._split_text_by_words(comp.text)
|
||||
if split_mode == "words":
|
||||
split_response = self._split_text_by_words(
|
||||
comp.text,
|
||||
split_words_pattern,
|
||||
split_words,
|
||||
)
|
||||
else: # regex 模式
|
||||
try:
|
||||
split_response = re.findall(
|
||||
self.regex,
|
||||
split_regex,
|
||||
comp.text,
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
@@ -242,8 +231,8 @@ class ResultDecorateStage(Stage):
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
for seg in split_response:
|
||||
if self.content_cleanup_rule:
|
||||
seg = re.sub(self.content_cleanup_rule, "", seg)
|
||||
if content_cleanup_rule:
|
||||
seg = re.sub(content_cleanup_rule, "", seg)
|
||||
seg = seg.strip()
|
||||
if seg:
|
||||
new_chain.append(Plain(seg))
|
||||
@@ -258,10 +247,9 @@ class ResultDecorateStage(Stage):
|
||||
)
|
||||
|
||||
should_tts = (
|
||||
bool(self.ctx.astrbot_config["provider_tts_settings"]["enable"])
|
||||
bool(tts_settings.get("enable", False))
|
||||
and result.is_llm_result()
|
||||
and await SessionServiceManager.should_process_tts_request(event)
|
||||
and random.random() <= self.tts_trigger_probability
|
||||
and random.random() <= tts_trigger_probability
|
||||
and tts_provider
|
||||
)
|
||||
if should_tts and not tts_provider:
|
||||
@@ -271,7 +259,7 @@ class ResultDecorateStage(Stage):
|
||||
|
||||
if (
|
||||
not should_tts
|
||||
and self.show_reasoning
|
||||
and show_reasoning
|
||||
and event.get_extra("_llm_reasoning_content")
|
||||
):
|
||||
# inject reasoning content to chain
|
||||
@@ -310,15 +298,12 @@ class ResultDecorateStage(Stage):
|
||||
|
||||
event.track_temporary_local_file(audio_path)
|
||||
|
||||
use_file_service = self.ctx.astrbot_config[
|
||||
"provider_tts_settings"
|
||||
]["use_file_service"]
|
||||
callback_api_base = self.ctx.astrbot_config[
|
||||
"callback_api_base"
|
||||
]
|
||||
dual_output = self.ctx.astrbot_config[
|
||||
"provider_tts_settings"
|
||||
]["dual_output"]
|
||||
use_file_service = tts_settings.get(
|
||||
"use_file_service",
|
||||
False,
|
||||
)
|
||||
callback_api_base = config.get("callback_api_base", "")
|
||||
dual_output = tts_settings.get("dual_output", False)
|
||||
|
||||
url = None
|
||||
if use_file_service and callback_api_base:
|
||||
@@ -347,7 +332,7 @@ class ResultDecorateStage(Stage):
|
||||
|
||||
# 文本转图片
|
||||
elif (
|
||||
result.use_t2i_ is None and self.ctx.astrbot_config["t2i"]
|
||||
result.use_t2i_ is None and config.get("t2i", False)
|
||||
) or result.use_t2i_:
|
||||
parts = []
|
||||
for comp in result.chain:
|
||||
@@ -356,14 +341,14 @@ class ResultDecorateStage(Stage):
|
||||
else:
|
||||
break
|
||||
plain_str = "".join(parts)
|
||||
if plain_str and len(plain_str) > self.t2i_word_threshold:
|
||||
if plain_str and len(plain_str) > t2i_word_threshold:
|
||||
render_start = time.time()
|
||||
try:
|
||||
url = await html_renderer.render_t2i(
|
||||
plain_str,
|
||||
return_url=True,
|
||||
use_network=self.t2i_use_network,
|
||||
template_name=self.t2i_active_template,
|
||||
use_network=t2i_use_network,
|
||||
template_name=t2i_active_template,
|
||||
)
|
||||
except BaseException:
|
||||
logger.error("文本转图片失败,使用文本发送。")
|
||||
@@ -375,12 +360,11 @@ class ResultDecorateStage(Stage):
|
||||
if url:
|
||||
if url.startswith("http"):
|
||||
result.chain = [Image.fromURL(url)]
|
||||
elif (
|
||||
self.ctx.astrbot_config["t2i_use_file_service"]
|
||||
and self.ctx.astrbot_config["callback_api_base"]
|
||||
elif config.get("t2i_use_file_service", False) and config.get(
|
||||
"callback_api_base", ""
|
||||
):
|
||||
token = await file_token_service.register_file(url)
|
||||
url = f"{self.ctx.astrbot_config['callback_api_base']}/api/file/{token}"
|
||||
url = f"{config['callback_api_base']}/api/file/{token}"
|
||||
logger.debug(f"已注册:{url}")
|
||||
result.chain = [Image.fromURL(url)]
|
||||
else:
|
||||
@@ -392,7 +376,7 @@ class ResultDecorateStage(Stage):
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Plain):
|
||||
word_cnt += len(comp.text)
|
||||
if word_cnt > self.forward_threshold:
|
||||
if word_cnt > forward_threshold:
|
||||
node = Node(
|
||||
uin=event.get_self_id(),
|
||||
name="AstrBot",
|
||||
@@ -407,7 +391,7 @@ class ResultDecorateStage(Stage):
|
||||
if can_decorate:
|
||||
# at 回复
|
||||
if (
|
||||
self.reply_with_mention
|
||||
reply_with_mention
|
||||
and event.get_message_type() != MessageType.FRIEND_MESSAGE
|
||||
):
|
||||
result.chain.insert(
|
||||
@@ -418,5 +402,5 @@ class ResultDecorateStage(Stage):
|
||||
result.chain[1].text = "\n" + result.chain[1].text
|
||||
|
||||
# 引用回复
|
||||
if self.reply_with_quote:
|
||||
if reply_with_quote:
|
||||
result.chain.insert(0, Reply(id=event.message_obj.message_id))
|
||||
|
||||
@@ -83,6 +83,14 @@ class PipelineScheduler:
|
||||
|
||||
"""
|
||||
active_event_registry.register(event)
|
||||
effective_config = await self.ctx.astrbot_config_mgr.build_effective_conf(
|
||||
event.unified_msg_origin,
|
||||
self.ctx.base_astrbot_config,
|
||||
)
|
||||
config_token = self.ctx.astrbot_config_mgr.activate_effective_conf(
|
||||
event.unified_msg_origin,
|
||||
effective_config,
|
||||
)
|
||||
try:
|
||||
await self._process_stages(event)
|
||||
|
||||
@@ -92,5 +100,6 @@ class PipelineScheduler:
|
||||
|
||||
logger.debug("pipeline execution completed.")
|
||||
finally:
|
||||
self.ctx.astrbot_config_mgr.reset_effective_conf(config_token)
|
||||
event.cleanup_temporary_local_files()
|
||||
active_event_registry.unregister(event)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.star.session_llm_manager import SessionServiceManager
|
||||
|
||||
from ..context import PipelineContext
|
||||
from ..stage import Stage, register_stage
|
||||
|
||||
|
||||
@register_stage
|
||||
class SessionStatusCheckStage(Stage):
|
||||
"""检查会话是否整体启用"""
|
||||
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.conv_mgr = ctx.plugin_manager.context.conversation_manager
|
||||
|
||||
async def process(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
) -> None | AsyncGenerator[None, None]:
|
||||
# 检查会话是否整体启用
|
||||
if not await SessionServiceManager.is_session_enabled(event.unified_msg_origin):
|
||||
logger.debug(f"会话 {event.unified_msg_origin} 已被关闭,已终止事件传播。")
|
||||
|
||||
# workaround for #2309
|
||||
conv_id = await self.conv_mgr.get_curr_conversation_id(
|
||||
event.unified_msg_origin,
|
||||
)
|
||||
if not conv_id:
|
||||
await self.conv_mgr.new_conversation(
|
||||
event.unified_msg_origin,
|
||||
platform_id=event.get_platform_id(),
|
||||
)
|
||||
|
||||
event.stop_event()
|
||||
@@ -3,7 +3,6 @@
|
||||
STAGES_ORDER = [
|
||||
"WakingCheckStage", # 检查是否需要唤醒
|
||||
"WhitelistCheckStage", # 检查是否在群聊/私聊白名单
|
||||
"SessionStatusCheckStage", # 检查会话是否整体启用
|
||||
"RateLimitStage", # 检查会话是否超过频率限制
|
||||
"ContentSafetyCheckStage", # 检查内容安全
|
||||
"PreProcessStage", # 预处理
|
||||
|
||||
@@ -7,7 +7,6 @@ from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
||||
from astrbot.core.star.filter.permission import PermissionTypeFilter
|
||||
from astrbot.core.star.session_plugin_manager import SessionPluginManager
|
||||
from astrbot.core.star.star import star_map
|
||||
from astrbot.core.star.star_handler import EventType, star_handlers_registry
|
||||
|
||||
@@ -51,56 +50,46 @@ class WakingCheckStage(Stage):
|
||||
|
||||
"""
|
||||
self.ctx = ctx
|
||||
self.no_permission_reply = self.ctx.astrbot_config["platform_settings"].get(
|
||||
"no_permission_reply",
|
||||
True,
|
||||
)
|
||||
# 私聊是否需要 wake_prefix 才能唤醒机器人
|
||||
self.friend_message_needs_wake_prefix = self.ctx.astrbot_config[
|
||||
"platform_settings"
|
||||
].get("friend_message_needs_wake_prefix", False)
|
||||
# 是否忽略机器人自己发送的消息
|
||||
self.ignore_bot_self_message = self.ctx.astrbot_config["platform_settings"].get(
|
||||
"ignore_bot_self_message",
|
||||
False,
|
||||
)
|
||||
self.ignore_at_all = self.ctx.astrbot_config["platform_settings"].get(
|
||||
"ignore_at_all",
|
||||
False,
|
||||
)
|
||||
self.disable_builtin_commands = self.ctx.astrbot_config.get(
|
||||
"disable_builtin_commands", False
|
||||
)
|
||||
platform_settings = self.ctx.astrbot_config.get("platform_settings", {})
|
||||
self.unique_session = platform_settings.get("unique_session", False)
|
||||
|
||||
async def process(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
) -> None | AsyncGenerator[None, None]:
|
||||
config = self.ctx.astrbot_config
|
||||
platform_settings = config.get("platform_settings", {})
|
||||
no_permission_reply = platform_settings.get("no_permission_reply", True)
|
||||
friend_message_needs_wake_prefix = platform_settings.get(
|
||||
"friend_message_needs_wake_prefix",
|
||||
False,
|
||||
)
|
||||
ignore_bot_self_message = platform_settings.get(
|
||||
"ignore_bot_self_message",
|
||||
False,
|
||||
)
|
||||
ignore_at_all = platform_settings.get("ignore_at_all", False)
|
||||
disable_builtin_commands = config.get("disable_builtin_commands", False)
|
||||
unique_session = platform_settings.get("unique_session", False)
|
||||
|
||||
# apply unique session
|
||||
if self.unique_session and event.message_obj.type == MessageType.GROUP_MESSAGE:
|
||||
if unique_session and event.message_obj.type == MessageType.GROUP_MESSAGE:
|
||||
sid = build_unique_session_id(event)
|
||||
if sid:
|
||||
event.session_id = sid
|
||||
|
||||
# ignore bot self message
|
||||
if (
|
||||
self.ignore_bot_self_message
|
||||
and event.get_self_id() == event.get_sender_id()
|
||||
):
|
||||
if ignore_bot_self_message and event.get_self_id() == event.get_sender_id():
|
||||
event.stop_event()
|
||||
return
|
||||
|
||||
# 设置 sender 身份
|
||||
event.message_str = event.message_str.strip()
|
||||
for admin_id in self.ctx.astrbot_config["admins_id"]:
|
||||
for admin_id in config["admins_id"]:
|
||||
if str(event.get_sender_id()) == admin_id:
|
||||
event.role = "admin"
|
||||
break
|
||||
|
||||
# 检查 wake
|
||||
wake_prefixes = self.ctx.astrbot_config["wake_prefix"]
|
||||
wake_prefixes = config["wake_prefix"]
|
||||
messages = event.get_messages()
|
||||
is_wake = False
|
||||
for wake_prefix in wake_prefixes:
|
||||
@@ -126,7 +115,7 @@ class WakingCheckStage(Stage):
|
||||
isinstance(message, At)
|
||||
and (str(message.qq) == str(event.get_self_id()))
|
||||
)
|
||||
or (isinstance(message, AtAll) and not self.ignore_at_all)
|
||||
or (isinstance(message, AtAll) and not ignore_at_all)
|
||||
or (
|
||||
isinstance(message, Reply)
|
||||
and str(message.sender_id) == str(event.get_self_id())
|
||||
@@ -138,7 +127,7 @@ class WakingCheckStage(Stage):
|
||||
event.is_at_or_wake_command = True
|
||||
break
|
||||
# 检查是否是私聊
|
||||
if event.is_private_chat() and not self.friend_message_needs_wake_prefix:
|
||||
if event.is_private_chat() and not friend_message_needs_wake_prefix:
|
||||
is_wake = True
|
||||
event.is_wake = True
|
||||
event.is_at_or_wake_command = True
|
||||
@@ -149,7 +138,8 @@ class WakingCheckStage(Stage):
|
||||
handlers_parsed_params = {} # 注册了指令的 handler
|
||||
|
||||
# 将 plugins_name 设置到 event 中
|
||||
enabled_plugins_name = self.ctx.astrbot_config.get("plugin_set", ["*"])
|
||||
enabled_plugins_name = config.get("plugin_set", ["*"])
|
||||
disabled_plugins_name = set(config.get("plugin_disabled_set", []))
|
||||
if enabled_plugins_name == ["*"]:
|
||||
# 如果是 *,则表示所有插件都启用
|
||||
event.plugins_name = None
|
||||
@@ -162,11 +152,25 @@ class WakingCheckStage(Stage):
|
||||
plugins_name=event.plugins_name,
|
||||
):
|
||||
if (
|
||||
self.disable_builtin_commands
|
||||
disable_builtin_commands
|
||||
and handler.handler_module_path
|
||||
== "astrbot.builtin_stars.builtin_commands.main"
|
||||
):
|
||||
continue
|
||||
plugin = star_map.get(handler.handler_module_path)
|
||||
if (
|
||||
plugin
|
||||
and not plugin.reserved
|
||||
and plugin.name
|
||||
and plugin.name in disabled_plugins_name
|
||||
):
|
||||
logger.debug(
|
||||
"Plugin %s is disabled by config for session %s; skipping handler %s",
|
||||
plugin.name,
|
||||
event.unified_msg_origin,
|
||||
handler.handler_name,
|
||||
)
|
||||
continue
|
||||
|
||||
# filter 需满足 AND 逻辑关系
|
||||
passed = True
|
||||
@@ -178,10 +182,10 @@ class WakingCheckStage(Stage):
|
||||
for filter in handler.event_filters:
|
||||
try:
|
||||
if isinstance(filter, PermissionTypeFilter):
|
||||
if not filter.filter(event, self.ctx.astrbot_config):
|
||||
if not filter.filter(event, config):
|
||||
permission_not_pass = True
|
||||
permission_filter_raise_error = filter.raise_error
|
||||
elif not filter.filter(event, self.ctx.astrbot_config):
|
||||
elif not filter.filter(event, config):
|
||||
passed = False
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -198,7 +202,7 @@ class WakingCheckStage(Stage):
|
||||
if not permission_filter_raise_error:
|
||||
# 跳过
|
||||
continue
|
||||
if self.no_permission_reply:
|
||||
if no_permission_reply:
|
||||
await event.send(
|
||||
MessageChain().message(
|
||||
f"您(ID: {event.get_sender_id()})的权限不足以使用此指令。通过 /sid 获取 ID 并请管理员添加。",
|
||||
@@ -225,12 +229,6 @@ class WakingCheckStage(Stage):
|
||||
|
||||
event._extras.pop("parsed_params", None)
|
||||
|
||||
# 根据会话配置过滤插件处理器
|
||||
activated_handlers = await SessionPluginManager.filter_handlers_by_session(
|
||||
event,
|
||||
activated_handlers,
|
||||
)
|
||||
|
||||
event.set_extra("activated_handlers", activated_handlers)
|
||||
event.set_extra("handlers_parsed_params", handlers_parsed_params)
|
||||
|
||||
|
||||
@@ -13,30 +13,38 @@ class WhitelistCheckStage(Stage):
|
||||
"""检查是否在群聊/私聊白名单"""
|
||||
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.enable_whitelist_check = ctx.astrbot_config["platform_settings"][
|
||||
"enable_id_white_list"
|
||||
]
|
||||
self.whitelist = ctx.astrbot_config["platform_settings"]["id_whitelist"]
|
||||
self.whitelist = [
|
||||
str(i).strip() for i in self.whitelist if str(i).strip() != ""
|
||||
]
|
||||
self.wl_ignore_admin_on_group = ctx.astrbot_config["platform_settings"][
|
||||
"wl_ignore_admin_on_group"
|
||||
]
|
||||
self.wl_ignore_admin_on_friend = ctx.astrbot_config["platform_settings"][
|
||||
"wl_ignore_admin_on_friend"
|
||||
]
|
||||
self.wl_log = ctx.astrbot_config["platform_settings"]["id_whitelist_log"]
|
||||
self.ctx = ctx
|
||||
|
||||
async def process(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
) -> None | AsyncGenerator[None, None]:
|
||||
if not self.enable_whitelist_check:
|
||||
platform_settings = self.ctx.astrbot_config["platform_settings"]
|
||||
blacklist = [
|
||||
str(i).strip()
|
||||
for i in platform_settings.get("id_blacklist", [])
|
||||
if str(i).strip()
|
||||
]
|
||||
event_group_id = str(event.get_group_id()).strip()
|
||||
if event.unified_msg_origin in blacklist or event_group_id in blacklist:
|
||||
logger.info(
|
||||
"Session %s is in the session blacklist; stopping event propagation.",
|
||||
event.unified_msg_origin,
|
||||
)
|
||||
event.stop_event()
|
||||
return
|
||||
|
||||
enable_whitelist_check = platform_settings["enable_id_white_list"]
|
||||
if not enable_whitelist_check:
|
||||
# 白名单检查未启用
|
||||
return
|
||||
|
||||
if len(self.whitelist) == 0:
|
||||
whitelist = [
|
||||
str(i).strip()
|
||||
for i in platform_settings["id_whitelist"]
|
||||
if str(i).strip() != ""
|
||||
]
|
||||
if len(whitelist) == 0:
|
||||
# 白名单为空,不检查
|
||||
return
|
||||
|
||||
@@ -45,23 +53,23 @@ class WhitelistCheckStage(Stage):
|
||||
return
|
||||
|
||||
# 检查是否在白名单
|
||||
if self.wl_ignore_admin_on_group:
|
||||
if platform_settings["wl_ignore_admin_on_group"]:
|
||||
if (
|
||||
event.role == "admin"
|
||||
and event.get_message_type() == MessageType.GROUP_MESSAGE
|
||||
):
|
||||
return
|
||||
if self.wl_ignore_admin_on_friend:
|
||||
if platform_settings["wl_ignore_admin_on_friend"]:
|
||||
if (
|
||||
event.role == "admin"
|
||||
and event.get_message_type() == MessageType.FRIEND_MESSAGE
|
||||
):
|
||||
return
|
||||
if (
|
||||
event.unified_msg_origin not in self.whitelist
|
||||
and str(event.get_group_id()).strip() not in self.whitelist
|
||||
event.unified_msg_origin not in whitelist
|
||||
and event_group_id not in whitelist
|
||||
):
|
||||
if self.wl_log:
|
||||
if platform_settings["id_whitelist_log"]:
|
||||
logger.info(
|
||||
f"会话 ID {event.unified_msg_origin} 不在会话白名单中,已终止事件传播。请在配置文件中添加该会话 ID 到白名单。",
|
||||
)
|
||||
|
||||
@@ -159,10 +159,14 @@ class ProviderManager:
|
||||
if provider_id not in self.inst_map:
|
||||
raise ValueError(f"提供商 {provider_id} 不存在,无法设置。")
|
||||
if umo:
|
||||
await sp.session_put(
|
||||
provider_path_map = {
|
||||
ProviderType.CHAT_COMPLETION: "provider_settings.default_provider_id",
|
||||
ProviderType.SPEECH_TO_TEXT: "provider_stt_settings.provider_id",
|
||||
ProviderType.TEXT_TO_SPEECH: "provider_tts_settings.provider_id",
|
||||
}
|
||||
await self.acm.update_conf_overrides(
|
||||
umo,
|
||||
f"provider_perf_{provider_type.value}",
|
||||
provider_id,
|
||||
{provider_path_map[provider_type]: provider_id},
|
||||
)
|
||||
self._notify_provider_changed(provider_id, provider_type, umo)
|
||||
return
|
||||
@@ -225,18 +229,9 @@ class ProviderManager:
|
||||
"""
|
||||
provider = None
|
||||
provider_id = None
|
||||
if umo:
|
||||
provider_id = sp.get(
|
||||
f"provider_perf_{provider_type.value}",
|
||||
None,
|
||||
scope="umo",
|
||||
scope_id=umo,
|
||||
)
|
||||
if provider_id:
|
||||
provider = self.inst_map.get(provider_id)
|
||||
config = self.acm.get_conf(umo)
|
||||
if not provider:
|
||||
# default setting
|
||||
config = self.acm.get_conf(umo)
|
||||
if provider_type == ProviderType.CHAT_COMPLETION:
|
||||
provider_id = config["provider_settings"].get("default_provider_id")
|
||||
provider = self.inst_map.get(provider_id)
|
||||
|
||||
@@ -498,9 +498,6 @@ class Context:
|
||||
Note:
|
||||
如果不提供 umo 参数,将返回默认配置。
|
||||
"""
|
||||
if not umo:
|
||||
# 使用默认配置
|
||||
return self._config
|
||||
return self.astrbot_config_mgr.get_conf(umo)
|
||||
|
||||
async def send_message(
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
"""会话服务管理器 - 负责管理每个会话的LLM、TTS等服务的启停状态"""
|
||||
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
|
||||
|
||||
class SessionServiceManager:
|
||||
"""管理会话级别的服务启停状态,包括LLM和TTS"""
|
||||
|
||||
# =============================================================================
|
||||
# LLM 相关方法
|
||||
# =============================================================================
|
||||
|
||||
@staticmethod
|
||||
async def is_llm_enabled_for_session(session_id: str) -> bool:
|
||||
"""检查LLM是否在指定会话中启用
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
bool: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
# 获取会话服务配置
|
||||
session_services = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_service_config",
|
||||
default={},
|
||||
)
|
||||
|
||||
# 如果配置了该会话的LLM状态,返回该状态
|
||||
llm_enabled = session_services.get("llm_enabled")
|
||||
if llm_enabled is not None:
|
||||
return llm_enabled
|
||||
|
||||
# 如果没有配置,默认为启用(兼容性考虑)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def set_llm_status_for_session(session_id: str, enabled: bool) -> None:
|
||||
"""设置LLM在指定会话中的启停状态
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
enabled: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
session_config = (
|
||||
await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_service_config",
|
||||
default={},
|
||||
)
|
||||
or {}
|
||||
)
|
||||
session_config["llm_enabled"] = enabled
|
||||
await sp.put_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_service_config",
|
||||
value=session_config,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def should_process_llm_request(event: AstrMessageEvent) -> bool:
|
||||
"""检查是否应该处理LLM请求
|
||||
|
||||
Args:
|
||||
event: 消息事件
|
||||
|
||||
Returns:
|
||||
bool: True表示应该处理,False表示跳过
|
||||
|
||||
"""
|
||||
session_id = event.unified_msg_origin
|
||||
return await SessionServiceManager.is_llm_enabled_for_session(session_id)
|
||||
|
||||
# =============================================================================
|
||||
# TTS 相关方法
|
||||
# =============================================================================
|
||||
|
||||
@staticmethod
|
||||
async def is_tts_enabled_for_session(session_id: str) -> bool:
|
||||
"""检查TTS是否在指定会话中启用
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
bool: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
# 获取会话服务配置
|
||||
session_services = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_service_config",
|
||||
default={},
|
||||
)
|
||||
|
||||
# 如果配置了该会话的TTS状态,返回该状态
|
||||
tts_enabled = session_services.get("tts_enabled")
|
||||
if tts_enabled is not None:
|
||||
return tts_enabled
|
||||
|
||||
# 如果没有配置,默认为启用(兼容性考虑)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def set_tts_status_for_session(session_id: str, enabled: bool) -> None:
|
||||
"""设置TTS在指定会话中的启停状态
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
enabled: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
session_config = (
|
||||
await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_service_config",
|
||||
default={},
|
||||
)
|
||||
or {}
|
||||
)
|
||||
session_config["tts_enabled"] = enabled
|
||||
await sp.put_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_service_config",
|
||||
value=session_config,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"会话 {session_id} 的TTS状态已更新为: {'启用' if enabled else '禁用'}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def should_process_tts_request(event: AstrMessageEvent) -> bool:
|
||||
"""检查是否应该处理TTS请求
|
||||
|
||||
Args:
|
||||
event: 消息事件
|
||||
|
||||
Returns:
|
||||
bool: True表示应该处理,False表示跳过
|
||||
|
||||
"""
|
||||
session_id = event.unified_msg_origin
|
||||
return await SessionServiceManager.is_tts_enabled_for_session(session_id)
|
||||
|
||||
# =============================================================================
|
||||
# 会话整体启停相关方法
|
||||
# =============================================================================
|
||||
|
||||
@staticmethod
|
||||
async def is_session_enabled(session_id: str) -> bool:
|
||||
"""检查会话是否整体启用
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
bool: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
# 获取会话服务配置
|
||||
session_services = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_service_config",
|
||||
default={},
|
||||
)
|
||||
|
||||
# 如果配置了该会话的整体状态,返回该状态
|
||||
session_enabled = session_services.get("session_enabled")
|
||||
if session_enabled is not None:
|
||||
return session_enabled
|
||||
|
||||
# 如果没有配置,默认为启用(兼容性考虑)
|
||||
return True
|
||||
@@ -1,101 +0,0 @@
|
||||
"""会话插件管理器 - 负责管理每个会话的插件启停状态"""
|
||||
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
|
||||
|
||||
class SessionPluginManager:
|
||||
"""管理会话级别的插件启停状态"""
|
||||
|
||||
@staticmethod
|
||||
async def is_plugin_enabled_for_session(
|
||||
session_id: str,
|
||||
plugin_name: str,
|
||||
) -> bool:
|
||||
"""检查插件是否在指定会话中启用
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
plugin_name: 插件名称
|
||||
|
||||
Returns:
|
||||
bool: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
# 获取会话插件配置
|
||||
session_plugin_config = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_plugin_config",
|
||||
default={},
|
||||
)
|
||||
session_config = session_plugin_config.get(session_id, {})
|
||||
|
||||
enabled_plugins = session_config.get("enabled_plugins", [])
|
||||
disabled_plugins = session_config.get("disabled_plugins", [])
|
||||
|
||||
# 如果插件在禁用列表中,返回False
|
||||
if plugin_name in disabled_plugins:
|
||||
return False
|
||||
|
||||
# 如果插件在启用列表中,返回True
|
||||
if plugin_name in enabled_plugins:
|
||||
return True
|
||||
|
||||
# 如果都没有配置,默认为启用(兼容性考虑)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def filter_handlers_by_session(
|
||||
event: AstrMessageEvent,
|
||||
handlers: list,
|
||||
) -> list:
|
||||
"""根据会话配置过滤处理器列表
|
||||
|
||||
Args:
|
||||
event: 消息事件
|
||||
handlers: 原始处理器列表
|
||||
|
||||
Returns:
|
||||
List: 过滤后的处理器列表
|
||||
|
||||
"""
|
||||
from astrbot.core.star.star import star_map
|
||||
|
||||
session_id = event.unified_msg_origin
|
||||
filtered_handlers = []
|
||||
|
||||
session_plugin_config = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_plugin_config",
|
||||
default={},
|
||||
)
|
||||
session_config = session_plugin_config.get(session_id, {})
|
||||
disabled_plugins = session_config.get("disabled_plugins", [])
|
||||
|
||||
for handler in handlers:
|
||||
# 获取处理器对应的插件
|
||||
plugin = star_map.get(handler.handler_module_path)
|
||||
if not plugin:
|
||||
# 如果找不到插件元数据,允许执行(可能是系统插件)
|
||||
filtered_handlers.append(handler)
|
||||
continue
|
||||
|
||||
# 跳过保留插件(系统插件)
|
||||
if plugin.reserved:
|
||||
filtered_handlers.append(handler)
|
||||
continue
|
||||
|
||||
if plugin.name is None:
|
||||
continue
|
||||
|
||||
# 检查插件是否在当前会话中启用
|
||||
if plugin.name in disabled_plugins:
|
||||
logger.debug(
|
||||
f"插件 {plugin.name} 在会话 {session_id} 中被禁用,跳过处理器 {handler.handler_name}",
|
||||
)
|
||||
else:
|
||||
filtered_handlers.append(handler)
|
||||
|
||||
return filtered_handlers
|
||||
@@ -1,7 +1,7 @@
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from astrbot.api import logger, sp
|
||||
from astrbot.api import logger
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
@@ -30,41 +30,21 @@ async def retrieve_knowledge_base(
|
||||
kb_mgr = context.kb_manager
|
||||
config = context.get_config(umo=umo)
|
||||
|
||||
session_config = await sp.session_get(umo, "kb_config", default={})
|
||||
if session_config and "kb_ids" in session_config:
|
||||
kb_ids = session_config.get("kb_ids", [])
|
||||
if not kb_ids:
|
||||
logger.info(f"[知识库] 会话 {umo} 已被配置为不使用知识库")
|
||||
return None
|
||||
|
||||
top_k = session_config.get("top_k", 5)
|
||||
kb_names = []
|
||||
invalid_kb_ids = []
|
||||
for kb_id in kb_ids:
|
||||
kb_helper = await kb_mgr.get_kb(kb_id)
|
||||
if kb_helper:
|
||||
kb_names.append(kb_helper.kb.kb_name)
|
||||
else:
|
||||
logger.warning(f"[知识库] 知识库不存在或未加载: {kb_id}")
|
||||
invalid_kb_ids.append(kb_id)
|
||||
|
||||
if invalid_kb_ids:
|
||||
logger.warning(
|
||||
f"[知识库] 会话 {umo} 配置的以下知识库无效: {invalid_kb_ids}",
|
||||
)
|
||||
if not kb_names:
|
||||
return None
|
||||
logger.debug(f"[知识库] 使用会话级配置,知识库数量: {len(kb_names)}")
|
||||
else:
|
||||
kb_names = config.get("kb_names", [])
|
||||
top_k = config.get("kb_final_top_k", 5)
|
||||
logger.debug(f"[知识库] 使用全局配置,知识库数量: {len(kb_names)}")
|
||||
kb_names = config.get("kb_names", [])
|
||||
top_k = config.get("kb_final_top_k", 5)
|
||||
logger.debug(f"[知识库] 使用配置文件,知识库数量: {len(kb_names)}")
|
||||
|
||||
top_k_fusion = config.get("kb_fusion_top_k", 20)
|
||||
if not kb_names:
|
||||
return None
|
||||
|
||||
all_kbs = [await kb_mgr.get_kb_by_name(kb) for kb in kb_names]
|
||||
all_kbs = []
|
||||
for kb_ref in kb_names:
|
||||
kb_ref = str(kb_ref)
|
||||
kb_helper = await kb_mgr.get_kb(kb_ref)
|
||||
if not kb_helper:
|
||||
kb_helper = await kb_mgr.get_kb_by_name(kb_ref)
|
||||
all_kbs.append(kb_helper)
|
||||
if check_all_kb(all_kbs):
|
||||
logger.debug("所配置的所有知识库全为空,跳过检索过程")
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import traceback
|
||||
|
||||
from astrbot.core import astrbot_config, logger
|
||||
from astrbot.core import astrbot_config, logger, sp
|
||||
from astrbot.core.agent.runners.deerflow.constants import (
|
||||
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
|
||||
DEERFLOW_PROVIDER_TYPE,
|
||||
@@ -9,6 +9,9 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfig, AstrBotConfigManager
|
||||
from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46
|
||||
from astrbot.core.db.migration.migra_token_usage import migrate_token_usage
|
||||
from astrbot.core.db.migration.migra_webchat_session import migrate_webchat_session
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
|
||||
CONFIG_OVERRIDE_MIGRATION_DONE_KEY = "migration_config_overrides_v1_done"
|
||||
|
||||
|
||||
def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None:
|
||||
@@ -128,6 +131,98 @@ def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None:
|
||||
logger.info("Provider-source structure migration completed")
|
||||
|
||||
|
||||
async def _migrate_session_rules_to_config_overrides(
|
||||
db,
|
||||
acm: AstrBotConfigManager,
|
||||
) -> None:
|
||||
"""Migrate legacy UMO session rules into config override preferences.
|
||||
|
||||
Args:
|
||||
db: Database helper used to migrate custom UMO names into aliases.
|
||||
acm: Config manager used to write override paths.
|
||||
"""
|
||||
migration_done = await sp.global_get(CONFIG_OVERRIDE_MIGRATION_DONE_KEY, False)
|
||||
if migration_done:
|
||||
return
|
||||
|
||||
provider_path_map = {
|
||||
f"provider_perf_{ProviderType.CHAT_COMPLETION.value}": (
|
||||
"provider_settings.default_provider_id"
|
||||
),
|
||||
f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}": (
|
||||
"provider_stt_settings.provider_id"
|
||||
),
|
||||
f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}": (
|
||||
"provider_tts_settings.provider_id"
|
||||
),
|
||||
}
|
||||
legacy_keys = {
|
||||
"session_service_config",
|
||||
"session_plugin_config",
|
||||
"kb_config",
|
||||
*provider_path_map.keys(),
|
||||
}
|
||||
prefs = await sp.session_get(None, None)
|
||||
migrated_count = 0
|
||||
for pref in prefs:
|
||||
if pref.key not in legacy_keys:
|
||||
continue
|
||||
umo = pref.scope_id
|
||||
value = pref.value.get("val") if isinstance(pref.value, dict) else None
|
||||
override_paths = {}
|
||||
|
||||
if pref.key == "session_service_config" and isinstance(value, dict):
|
||||
if "llm_enabled" in value:
|
||||
override_paths["provider_settings.enable"] = bool(value["llm_enabled"])
|
||||
if "tts_enabled" in value:
|
||||
override_paths["provider_tts_settings.enable"] = bool(
|
||||
value["tts_enabled"]
|
||||
)
|
||||
if "persona_id" in value:
|
||||
override_paths["provider_settings.default_personality"] = (
|
||||
value.get("persona_id") or ""
|
||||
)
|
||||
if value.get("session_enabled") is False:
|
||||
override_paths["platform_settings.id_blacklist"] = [umo]
|
||||
custom_name = str(value.get("custom_name") or "").strip()
|
||||
if custom_name:
|
||||
alias = await db.get_umo_alias(umo)
|
||||
if not alias or not alias.user_alias:
|
||||
await db.upsert_umo_alias(
|
||||
umo,
|
||||
alias.creator_sender_id if alias else "",
|
||||
alias.auto_name if alias else None,
|
||||
custom_name,
|
||||
)
|
||||
elif pref.key == "session_plugin_config" and isinstance(value, dict):
|
||||
session_config = value.get(umo, value)
|
||||
if isinstance(session_config, dict):
|
||||
disabled_plugins = session_config.get("disabled_plugins")
|
||||
if isinstance(disabled_plugins, list):
|
||||
override_paths["plugin_disabled_set"] = disabled_plugins
|
||||
elif pref.key == "kb_config" and isinstance(value, dict):
|
||||
if "top_k" in value:
|
||||
override_paths["kb_final_top_k"] = value["top_k"]
|
||||
kb_ids = value.get("kb_ids")
|
||||
if isinstance(kb_ids, list):
|
||||
override_paths["kb_names"] = [
|
||||
str(kb_id) for kb_id in kb_ids if str(kb_id).strip()
|
||||
]
|
||||
elif pref.key in provider_path_map and value:
|
||||
override_paths[provider_path_map[pref.key]] = value
|
||||
|
||||
if override_paths:
|
||||
await acm.update_conf_overrides(umo, override_paths)
|
||||
migrated_count += 1
|
||||
|
||||
await sp.global_put(CONFIG_OVERRIDE_MIGRATION_DONE_KEY, True)
|
||||
if migrated_count:
|
||||
logger.info(
|
||||
"Migrated %s legacy session rule preferences to config overrides.",
|
||||
migrated_count,
|
||||
)
|
||||
|
||||
|
||||
async def migra(
|
||||
db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager
|
||||
) -> None:
|
||||
@@ -156,6 +251,12 @@ async def migra(
|
||||
logger.error(f"Migration for token_usage column failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
try:
|
||||
await _migrate_session_rules_to_config_overrides(db, acm)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration for session rule config overrides failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# migra third party agent runner configs
|
||||
_c = False
|
||||
providers = astrbot_config["provider"]
|
||||
|
||||
@@ -93,6 +93,7 @@ async def _list_conversations(
|
||||
platforms: str,
|
||||
message_types: str,
|
||||
search: str,
|
||||
user_id: str,
|
||||
exclude_ids: str,
|
||||
exclude_platforms: str,
|
||||
):
|
||||
@@ -103,6 +104,7 @@ async def _list_conversations(
|
||||
platforms=platforms,
|
||||
message_types=message_types,
|
||||
search_query=search,
|
||||
user_id=user_id,
|
||||
exclude_ids=exclude_ids,
|
||||
exclude_platforms=exclude_platforms,
|
||||
)
|
||||
@@ -116,6 +118,7 @@ async def list_conversations(
|
||||
platforms: str = Query(default=""),
|
||||
message_types: str = Query(default=""),
|
||||
search: str = Query(default=""),
|
||||
user_id: str = Query(default=""),
|
||||
exclude_ids: str = Query(default=""),
|
||||
exclude_platforms: str = Query(default=""),
|
||||
_auth: AuthContext = Depends(require_data_scope),
|
||||
@@ -128,6 +131,7 @@ async def list_conversations(
|
||||
platforms=platforms,
|
||||
message_types=message_types,
|
||||
search=search,
|
||||
user_id=user_id,
|
||||
exclude_ids=exclude_ids,
|
||||
exclude_platforms=exclude_platforms,
|
||||
)
|
||||
@@ -222,6 +226,7 @@ async def list_dashboard_conversations(
|
||||
platforms: str = Query(default=""),
|
||||
message_types: str = Query(default=""),
|
||||
search: str = Query(default=""),
|
||||
user_id: str = Query(default=""),
|
||||
exclude_ids: str = Query(default=""),
|
||||
exclude_platforms: str = Query(default=""),
|
||||
_username: str = Depends(require_dashboard_user),
|
||||
@@ -234,6 +239,7 @@ async def list_dashboard_conversations(
|
||||
platforms=platforms,
|
||||
message_types=message_types,
|
||||
search=search,
|
||||
user_id=user_id,
|
||||
exclude_ids=exclude_ids,
|
||||
exclude_platforms=exclude_platforms,
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ from .subagents import router as subagents_router
|
||||
from .t2i import router as t2i_router
|
||||
from .tools import router as tools_router
|
||||
from .updates import router as updates_router
|
||||
from .workspaces import router as workspaces_router
|
||||
|
||||
API_V1_PREFIX = "/api/v1"
|
||||
|
||||
@@ -58,6 +59,7 @@ def build_api_router() -> APIRouter:
|
||||
router.include_router(t2i_router)
|
||||
router.include_router(personas_router)
|
||||
router.include_router(updates_router)
|
||||
router.include_router(workspaces_router)
|
||||
router.include_router(open_api_router)
|
||||
router.include_router(live_chat_router)
|
||||
return router
|
||||
|
||||
@@ -8,6 +8,9 @@ from astrbot.dashboard.responses import error, ok
|
||||
from astrbot.dashboard.schemas import (
|
||||
BatchSessionProviderRequest,
|
||||
BatchSessionServiceRequest,
|
||||
SessionAliasRequest,
|
||||
SessionConfigOverrideDeleteRequest,
|
||||
SessionConfigOverrideRequest,
|
||||
SessionGroupRequest,
|
||||
SessionRuleRequest,
|
||||
UmoListRequest,
|
||||
@@ -165,6 +168,82 @@ async def delete_session_rule(
|
||||
return _unexpected_error("删除会话规则失败", exc)
|
||||
|
||||
|
||||
@router.get("/sessions/config-overrides")
|
||||
async def list_session_config_overrides(
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(10),
|
||||
search: str = Query(""),
|
||||
umo: str = Query(""),
|
||||
_auth: AuthContext = Depends(require_data_scope),
|
||||
service: SessionManagementService = Depends(get_service),
|
||||
):
|
||||
try:
|
||||
return ok(
|
||||
await service.list_session_config_overrides(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search.strip(),
|
||||
umo=umo.strip(),
|
||||
)
|
||||
)
|
||||
except SessionManagementServiceError as exc:
|
||||
return _service_error(exc)
|
||||
except Exception as exc:
|
||||
return _unexpected_error("获取会话配置覆盖项列表失败", exc)
|
||||
|
||||
|
||||
@router.post("/sessions/config-overrides")
|
||||
async def upsert_session_config_override(
|
||||
payload: SessionConfigOverrideRequest,
|
||||
_auth: AuthContext = Depends(require_data_scope),
|
||||
service: SessionManagementService = Depends(get_service),
|
||||
):
|
||||
try:
|
||||
return ok(
|
||||
await service.update_session_config_override(
|
||||
payload.model_dump(exclude_unset=True)
|
||||
)
|
||||
)
|
||||
except SessionManagementServiceError as exc:
|
||||
return _service_error(exc)
|
||||
except Exception as exc:
|
||||
return _unexpected_error("更新会话配置覆盖项失败", exc)
|
||||
|
||||
|
||||
@router.post("/sessions/config-overrides/delete")
|
||||
async def delete_session_config_override(
|
||||
payload: SessionConfigOverrideDeleteRequest,
|
||||
_auth: AuthContext = Depends(require_data_scope),
|
||||
service: SessionManagementService = Depends(get_service),
|
||||
):
|
||||
try:
|
||||
return ok(
|
||||
await service.delete_session_config_override(
|
||||
payload.model_dump(exclude_none=True)
|
||||
)
|
||||
)
|
||||
except SessionManagementServiceError as exc:
|
||||
return _service_error(exc)
|
||||
except Exception as exc:
|
||||
return _unexpected_error("删除会话配置覆盖项失败", exc)
|
||||
|
||||
|
||||
@router.post("/sessions/aliases")
|
||||
async def upsert_session_alias(
|
||||
payload: SessionAliasRequest,
|
||||
_auth: AuthContext = Depends(require_data_scope),
|
||||
service: SessionManagementService = Depends(get_service),
|
||||
):
|
||||
try:
|
||||
return ok(
|
||||
await service.update_session_alias(payload.model_dump(exclude_unset=True))
|
||||
)
|
||||
except SessionManagementServiceError as exc:
|
||||
return _service_error(exc)
|
||||
except Exception as exc:
|
||||
return _unexpected_error("更新会话备注失败", exc)
|
||||
|
||||
|
||||
@router.patch("/sessions/provider")
|
||||
async def update_session_provider(
|
||||
payload: BatchSessionProviderRequest,
|
||||
|
||||
131
astrbot/dashboard/api/workspaces.py
Normal file
131
astrbot/dashboard/api/workspaces.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
from astrbot.core.utils.datetime_utils import to_utc_isoformat
|
||||
from astrbot.core.workspace import resolve_workspace_root_for_umo
|
||||
from astrbot.dashboard.responses import ApiError, ok
|
||||
|
||||
from .auth import AuthContext, require_scope
|
||||
|
||||
router = APIRouter(tags=["Workspaces"])
|
||||
|
||||
|
||||
async def require_data_scope(request: Request) -> AuthContext:
|
||||
return await require_scope(request, "data")
|
||||
|
||||
|
||||
def _format_mtime(path: Path) -> str | None:
|
||||
"""Format a filesystem modified timestamp for API responses.
|
||||
|
||||
Args:
|
||||
path: File or directory path.
|
||||
|
||||
Returns:
|
||||
UTC ISO timestamp, or None when the timestamp cannot be read.
|
||||
"""
|
||||
try:
|
||||
return to_utc_isoformat(
|
||||
datetime.fromtimestamp(path.stat().st_mtime, timezone.utc)
|
||||
)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/workspaces/by-umo")
|
||||
async def list_umo_workspace_files(
|
||||
request: Request,
|
||||
umo: str = Query(..., min_length=1),
|
||||
path: str | None = Query(default=None),
|
||||
_auth: AuthContext = Depends(require_data_scope),
|
||||
):
|
||||
"""List files in the workspace resolved for one UMO.
|
||||
|
||||
Args:
|
||||
request: FastAPI request with database state.
|
||||
umo: Unified message origin.
|
||||
path: Relative directory path under the workspace root.
|
||||
_auth: Auth context.
|
||||
|
||||
Returns:
|
||||
Workspace metadata and one directory level of file entries.
|
||||
|
||||
Raises:
|
||||
ApiError: If the requested path escapes the workspace root.
|
||||
"""
|
||||
root = await resolve_workspace_root_for_umo(umo, request.app.state.db)
|
||||
root = root.expanduser().resolve(strict=False)
|
||||
raw_path = str(path or "").strip()
|
||||
requested = root if raw_path in {"", "."} else root / raw_path
|
||||
current = requested.resolve(strict=False)
|
||||
|
||||
if current != root and not current.is_relative_to(root):
|
||||
raise ApiError("Path is outside the workspace", status_code=400)
|
||||
|
||||
if not root.exists() or not root.is_dir():
|
||||
return ok(
|
||||
{
|
||||
"umo": umo,
|
||||
"absolute_path": str(root),
|
||||
"relative_path": "",
|
||||
"exists": False,
|
||||
"is_directory": False,
|
||||
"items": [],
|
||||
}
|
||||
)
|
||||
if not current.exists() or not current.is_dir():
|
||||
raise ApiError("Workspace folder not found", status_code=404)
|
||||
|
||||
items = []
|
||||
try:
|
||||
children = sorted(
|
||||
current.iterdir(),
|
||||
key=lambda item: (
|
||||
not (item.is_dir() and not item.is_symlink()),
|
||||
item.name.lower(),
|
||||
),
|
||||
)
|
||||
except OSError as exc:
|
||||
raise ApiError(f"Failed to list workspace files: {exc!s}") from exc
|
||||
|
||||
for child in children:
|
||||
try:
|
||||
is_symlink = child.is_symlink()
|
||||
stat_result = child.lstat() if is_symlink else child.stat()
|
||||
if is_symlink:
|
||||
item_type = "symlink"
|
||||
elif child.is_dir():
|
||||
item_type = "directory"
|
||||
elif child.is_file():
|
||||
item_type = "file"
|
||||
else:
|
||||
item_type = "other"
|
||||
items.append(
|
||||
{
|
||||
"name": child.name,
|
||||
"type": item_type,
|
||||
"relative_path": child.relative_to(root).as_posix(),
|
||||
"size_bytes": 0
|
||||
if item_type == "directory"
|
||||
else stat_result.st_size,
|
||||
"modified_at": _format_mtime(child),
|
||||
}
|
||||
)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
return ok(
|
||||
{
|
||||
"umo": umo,
|
||||
"absolute_path": str(root),
|
||||
"relative_path": ""
|
||||
if current == root
|
||||
else current.relative_to(root).as_posix(),
|
||||
"exists": True,
|
||||
"is_directory": True,
|
||||
"items": items,
|
||||
}
|
||||
)
|
||||
@@ -613,6 +613,29 @@ class SessionRuleRequest(OpenModel):
|
||||
rule_value: Any = None
|
||||
|
||||
|
||||
class SessionConfigOverrideRequest(OpenModel):
|
||||
umo: str | None = None
|
||||
path: str | None = None
|
||||
value: Any = None
|
||||
|
||||
|
||||
class SessionConfigOverrideDeleteRequest(OpenModel):
|
||||
umo: str | None = None
|
||||
umos: list[str] | None = None
|
||||
scope: Literal["all", "group", "private", "custom_group"] | None = None
|
||||
group_id: str | None = None
|
||||
path: str | None = None
|
||||
paths: list[str] | None = None
|
||||
|
||||
|
||||
class SessionAliasRequest(OpenModel):
|
||||
umo: str | None = None
|
||||
umos: list[str] | None = None
|
||||
scope: Literal["all", "group", "private", "custom_group"] | None = None
|
||||
group_id: str | None = None
|
||||
custom_name: str | None = None
|
||||
|
||||
|
||||
class UmoListRequest(OpenModel):
|
||||
umo: str | None = None
|
||||
umos: list[str] | None = None
|
||||
|
||||
@@ -40,6 +40,7 @@ class ConversationService:
|
||||
platforms: str,
|
||||
message_types: str,
|
||||
search_query: str,
|
||||
user_id: str,
|
||||
exclude_ids: str,
|
||||
exclude_platforms: str,
|
||||
) -> dict:
|
||||
@@ -62,6 +63,7 @@ class ConversationService:
|
||||
platforms=platform_list,
|
||||
message_types=message_type_list,
|
||||
search_query=search_query,
|
||||
user_id=user_id,
|
||||
exclude_ids=exclude_id_list,
|
||||
exclude_platforms=exclude_platform_list,
|
||||
)
|
||||
|
||||
@@ -110,27 +110,87 @@ class SessionManagementService:
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
search: str = "",
|
||||
umo: str = "",
|
||||
) -> tuple[dict, int]:
|
||||
umo_rules = {}
|
||||
config_mgr = self.core_lifecycle.astrbot_config_mgr
|
||||
async with self.db_helper.get_db() as session:
|
||||
session: AsyncSession
|
||||
result = await session.execute(
|
||||
select(Preference).where(
|
||||
col(Preference.scope) == "umo",
|
||||
col(Preference.key).in_(AVAILABLE_SESSION_RULE_KEYS),
|
||||
col(Preference.key) == config_mgr.core_config_override_key,
|
||||
)
|
||||
)
|
||||
prefs = result.scalars().all()
|
||||
for pref in prefs:
|
||||
umo_id = pref.scope_id
|
||||
if umo_id not in umo_rules:
|
||||
umo_rules[umo_id] = {}
|
||||
if pref.key == "session_plugin_config" and umo_id in pref.value["val"]:
|
||||
umo_rules[umo_id][pref.key] = pref.value["val"][umo_id]
|
||||
else:
|
||||
umo_rules[umo_id][pref.key] = pref.value["val"]
|
||||
paths = config_mgr.normalize_conf_override_payload(
|
||||
pref.value.get("val") if isinstance(pref.value, dict) else None
|
||||
)
|
||||
rules = {}
|
||||
service_config = {}
|
||||
if "provider_settings.enable" in paths:
|
||||
service_config["llm_enabled"] = bool(
|
||||
paths["provider_settings.enable"]
|
||||
)
|
||||
if "provider_tts_settings.enable" in paths:
|
||||
service_config["tts_enabled"] = bool(
|
||||
paths["provider_tts_settings.enable"]
|
||||
)
|
||||
if "provider_settings.default_personality" in paths:
|
||||
service_config["persona_id"] = paths[
|
||||
"provider_settings.default_personality"
|
||||
]
|
||||
if "platform_settings.id_blacklist" in paths:
|
||||
blacklist = [
|
||||
str(item).strip()
|
||||
for item in paths["platform_settings.id_blacklist"]
|
||||
if str(item).strip()
|
||||
]
|
||||
service_config["session_enabled"] = umo_id not in blacklist
|
||||
if service_config:
|
||||
rules["session_service_config"] = service_config
|
||||
|
||||
alias_map = await self.get_umo_alias_map(list(umo_rules.keys()))
|
||||
if isinstance(paths.get("plugin_disabled_set"), list):
|
||||
rules["session_plugin_config"] = {
|
||||
"disabled_plugins": paths["plugin_disabled_set"]
|
||||
}
|
||||
|
||||
kb_config = {}
|
||||
if "kb_names" in paths:
|
||||
kb_config["kb_names"] = paths["kb_names"]
|
||||
if "kb_final_top_k" in paths:
|
||||
kb_config["top_k"] = paths["kb_final_top_k"]
|
||||
if kb_config:
|
||||
rules["kb_config"] = kb_config
|
||||
|
||||
provider_rule_map = {
|
||||
"provider_settings.default_provider_id": (
|
||||
f"provider_perf_{ProviderType.CHAT_COMPLETION.value}"
|
||||
),
|
||||
"provider_stt_settings.provider_id": (
|
||||
f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}"
|
||||
),
|
||||
"provider_tts_settings.provider_id": (
|
||||
f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}"
|
||||
),
|
||||
}
|
||||
for config_path, rule_key in provider_rule_map.items():
|
||||
if config_path in paths:
|
||||
rules[rule_key] = paths[config_path]
|
||||
|
||||
if rules:
|
||||
umo_rules[umo_id] = rules
|
||||
|
||||
aliases = await self.db_helper.get_umo_aliases()
|
||||
alias_map = build_umo_alias_map(aliases)
|
||||
for alias in aliases:
|
||||
alias_info = serialize_umo_alias(alias, alias.umo)
|
||||
if alias_info.get("user_alias"):
|
||||
umo_rules.setdefault(alias.umo, {}).setdefault(
|
||||
"session_service_config", {}
|
||||
)["custom_name"] = alias_info["user_alias"]
|
||||
|
||||
if search:
|
||||
search_lower = search.lower()
|
||||
@@ -155,6 +215,11 @@ class SessionManagementService:
|
||||
filtered_rules[umo_id] = rules
|
||||
umo_rules = filtered_rules
|
||||
|
||||
if umo:
|
||||
umo_rules = {
|
||||
umo_id: rules for umo_id, rules in umo_rules.items() if umo_id == umo
|
||||
}
|
||||
|
||||
total = len(umo_rules)
|
||||
all_umo_ids = list(umo_rules.keys())
|
||||
start_idx = (page - 1) * page_size
|
||||
@@ -169,12 +234,15 @@ class SessionManagementService:
|
||||
page: int,
|
||||
page_size: int,
|
||||
search: str,
|
||||
include_available_options: bool = True,
|
||||
umo: str = "",
|
||||
) -> dict:
|
||||
page, page_size = self._normalize_page(page, page_size, default_page_size=10)
|
||||
umo_rules, total = await self.get_umo_rules(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search,
|
||||
umo=umo,
|
||||
)
|
||||
|
||||
alias_map = await self.get_umo_alias_map(list(umo_rules.keys()))
|
||||
@@ -186,6 +254,15 @@ class SessionManagementService:
|
||||
for umo, rules in umo_rules.items()
|
||||
]
|
||||
|
||||
result = {
|
||||
"rules": rules_list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
if not include_available_options:
|
||||
return result
|
||||
|
||||
provider_manager = self.core_lifecycle.provider_manager
|
||||
persona_mgr = getattr(self.core_lifecycle, "persona_mgr", None)
|
||||
plugin_manager = getattr(self.core_lifecycle, "plugin_manager", None)
|
||||
@@ -222,24 +299,229 @@ class SessionManagementService:
|
||||
except Exception as exc:
|
||||
logger.warning(f"获取知识库列表失败: {exc!s}")
|
||||
|
||||
kb_name_to_id = {
|
||||
kb["kb_name"]: kb["kb_id"]
|
||||
for kb in available_kbs
|
||||
if kb.get("kb_name") and kb.get("kb_id")
|
||||
}
|
||||
kb_id_set = {kb["kb_id"] for kb in available_kbs if kb.get("kb_id")}
|
||||
for item in rules_list:
|
||||
kb_config = item.get("rules", {}).get("kb_config")
|
||||
if isinstance(kb_config, dict) and "kb_names" in kb_config:
|
||||
kb_config["kb_ids"] = [
|
||||
str(kb_ref)
|
||||
if str(kb_ref) in kb_id_set
|
||||
else kb_name_to_id[str(kb_ref)]
|
||||
for kb_ref in kb_config["kb_names"]
|
||||
if str(kb_ref) in kb_id_set or str(kb_ref) in kb_name_to_id
|
||||
]
|
||||
|
||||
result.update(
|
||||
{
|
||||
"available_personas": available_personas,
|
||||
"available_chat_providers": self._serialize_provider_insts(
|
||||
getattr(provider_manager, "provider_insts", [])
|
||||
),
|
||||
"available_stt_providers": self._serialize_provider_insts(
|
||||
getattr(provider_manager, "stt_provider_insts", [])
|
||||
),
|
||||
"available_tts_providers": self._serialize_provider_insts(
|
||||
getattr(provider_manager, "tts_provider_insts", [])
|
||||
),
|
||||
"available_plugins": available_plugins,
|
||||
"available_kbs": available_kbs,
|
||||
"available_rule_keys": AVAILABLE_SESSION_RULE_KEYS,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
async def list_session_config_overrides(
|
||||
self,
|
||||
*,
|
||||
page: int,
|
||||
page_size: int,
|
||||
search: str,
|
||||
umo: str = "",
|
||||
) -> dict:
|
||||
"""List UMO config overrides without editor option payloads.
|
||||
|
||||
Args:
|
||||
page: Page number.
|
||||
page_size: Number of items per page.
|
||||
search: Search keyword.
|
||||
|
||||
Returns:
|
||||
Paginated config override rows.
|
||||
"""
|
||||
return await self.list_session_rules(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search,
|
||||
umo=umo,
|
||||
include_available_options=False,
|
||||
)
|
||||
|
||||
async def update_session_config_override(self, data: object) -> dict:
|
||||
"""Update one core config override path for a UMO.
|
||||
|
||||
Args:
|
||||
data: Request payload containing umo, path, and value.
|
||||
|
||||
Returns:
|
||||
Operation result with the updated UMO and config path.
|
||||
|
||||
Raises:
|
||||
SessionManagementServiceError: If required payload fields are missing.
|
||||
"""
|
||||
payload = self._payload(data)
|
||||
umo = str(payload.get("umo") or "").strip()
|
||||
path = str(payload.get("path") or "").strip()
|
||||
|
||||
if not umo:
|
||||
raise SessionManagementServiceError("缺少必要参数: umo")
|
||||
if not path:
|
||||
raise SessionManagementServiceError("缺少必要参数: path")
|
||||
if "value" not in payload:
|
||||
raise SessionManagementServiceError("缺少必要参数: value")
|
||||
|
||||
await self.core_lifecycle.astrbot_config_mgr.update_conf_overrides(
|
||||
umo,
|
||||
{path: payload.get("value")},
|
||||
)
|
||||
return {"message": f"配置覆盖项 {path} 已更新", "umo": umo, "path": path}
|
||||
|
||||
async def delete_session_config_override(self, data: object) -> dict:
|
||||
"""Delete one or more core config override paths for a UMO.
|
||||
|
||||
Args:
|
||||
data: Request payload containing UMO selection and path or paths.
|
||||
|
||||
Returns:
|
||||
Operation result with updated UMOs and removed paths.
|
||||
|
||||
Raises:
|
||||
SessionManagementServiceError: If required payload fields are missing.
|
||||
"""
|
||||
payload = self._payload(data)
|
||||
umos = [
|
||||
str(umo).strip()
|
||||
for umo in (
|
||||
payload.get("umos") if isinstance(payload.get("umos"), list) else []
|
||||
)
|
||||
if str(umo).strip()
|
||||
]
|
||||
umo = str(payload.get("umo") or "").strip()
|
||||
if umo:
|
||||
umos.append(umo)
|
||||
|
||||
scope = str(payload.get("scope") or "").strip()
|
||||
group_id = str(payload.get("group_id") or "").strip()
|
||||
if scope and not umos:
|
||||
umos = await self.get_umos_by_scope(scope, group_id)
|
||||
umos = list(dict.fromkeys(umos))
|
||||
|
||||
raw_paths = payload.get("paths")
|
||||
paths = [
|
||||
str(path).strip()
|
||||
for path in (raw_paths if isinstance(raw_paths, list) else [])
|
||||
if str(path).strip()
|
||||
]
|
||||
path = str(payload.get("path") or "").strip()
|
||||
if path:
|
||||
paths.append(path)
|
||||
paths = list(dict.fromkeys(paths))
|
||||
|
||||
if not umos:
|
||||
raise SessionManagementServiceError(
|
||||
"缺少必要参数: umo、umos 或有效的 scope"
|
||||
)
|
||||
if not paths:
|
||||
raise SessionManagementServiceError("缺少必要参数: path 或 paths")
|
||||
|
||||
success_count = 0
|
||||
failed_umos = []
|
||||
for target_umo in umos:
|
||||
try:
|
||||
await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides(
|
||||
target_umo,
|
||||
paths,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"Failed to delete config override for {target_umo}: {exc!s}"
|
||||
)
|
||||
failed_umos.append(target_umo)
|
||||
|
||||
return {
|
||||
"rules": rules_list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"available_personas": available_personas,
|
||||
"available_chat_providers": self._serialize_provider_insts(
|
||||
getattr(provider_manager, "provider_insts", [])
|
||||
),
|
||||
"available_stt_providers": self._serialize_provider_insts(
|
||||
getattr(provider_manager, "stt_provider_insts", [])
|
||||
),
|
||||
"available_tts_providers": self._serialize_provider_insts(
|
||||
getattr(provider_manager, "tts_provider_insts", [])
|
||||
),
|
||||
"available_plugins": available_plugins,
|
||||
"available_kbs": available_kbs,
|
||||
"available_rule_keys": AVAILABLE_SESSION_RULE_KEYS,
|
||||
"message": f"已删除 {success_count} 个会话的配置覆盖项",
|
||||
"success_count": success_count,
|
||||
"failed_count": len(failed_umos),
|
||||
"failed_umos": failed_umos,
|
||||
"paths": paths,
|
||||
}
|
||||
|
||||
async def update_session_alias(self, data: object) -> dict:
|
||||
"""Update UMO user aliases used as session remarks.
|
||||
|
||||
Args:
|
||||
data: Request payload containing UMO selection and custom_name.
|
||||
|
||||
Returns:
|
||||
Operation result with updated UMO count.
|
||||
|
||||
Raises:
|
||||
SessionManagementServiceError: If required payload fields are missing.
|
||||
"""
|
||||
payload = self._payload(data)
|
||||
if "custom_name" not in payload:
|
||||
raise SessionManagementServiceError("缺少必要参数: custom_name")
|
||||
|
||||
umos = [
|
||||
str(umo).strip()
|
||||
for umo in (
|
||||
payload.get("umos") if isinstance(payload.get("umos"), list) else []
|
||||
)
|
||||
if str(umo).strip()
|
||||
]
|
||||
umo = str(payload.get("umo") or "").strip()
|
||||
if umo:
|
||||
umos.append(umo)
|
||||
|
||||
scope = str(payload.get("scope") or "").strip()
|
||||
group_id = str(payload.get("group_id") or "").strip()
|
||||
if scope and not umos:
|
||||
umos = await self.get_umos_by_scope(scope, group_id)
|
||||
umos = list(dict.fromkeys(umos))
|
||||
|
||||
if not umos:
|
||||
raise SessionManagementServiceError(
|
||||
"缺少必要参数: umo、umos 或有效的 scope"
|
||||
)
|
||||
|
||||
custom_name = str(payload.get("custom_name") or "").strip()
|
||||
success_count = 0
|
||||
failed_umos = []
|
||||
for target_umo in umos:
|
||||
try:
|
||||
alias = await self.db_helper.get_umo_alias(target_umo)
|
||||
if alias or custom_name:
|
||||
await self.db_helper.upsert_umo_alias(
|
||||
target_umo,
|
||||
alias.creator_sender_id if alias else "",
|
||||
alias.auto_name if alias else None,
|
||||
custom_name or None,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as exc:
|
||||
logger.error(f"Failed to update alias for {target_umo}: {exc!s}")
|
||||
failed_umos.append(target_umo)
|
||||
|
||||
return {
|
||||
"message": f"已更新 {success_count} 个会话备注",
|
||||
"success_count": success_count,
|
||||
"failed_count": len(failed_umos),
|
||||
"failed_umos": failed_umos,
|
||||
}
|
||||
|
||||
async def update_session_rule(self, data: object) -> dict:
|
||||
@@ -255,10 +537,72 @@ class SessionManagementService:
|
||||
if rule_key not in AVAILABLE_SESSION_RULE_KEYS:
|
||||
raise SessionManagementServiceError(f"不支持的规则键: {rule_key}")
|
||||
|
||||
if rule_key == "session_plugin_config":
|
||||
rule_value = {umo: rule_value}
|
||||
override_paths = {}
|
||||
remove_override_paths = []
|
||||
if rule_key == "session_service_config" and isinstance(rule_value, dict):
|
||||
if "llm_enabled" in rule_value:
|
||||
override_paths["provider_settings.enable"] = bool(
|
||||
rule_value["llm_enabled"]
|
||||
)
|
||||
if "tts_enabled" in rule_value:
|
||||
override_paths["provider_tts_settings.enable"] = bool(
|
||||
rule_value["tts_enabled"]
|
||||
)
|
||||
if "persona_id" in rule_value:
|
||||
override_paths["provider_settings.default_personality"] = (
|
||||
rule_value.get("persona_id") or ""
|
||||
)
|
||||
if "session_enabled" in rule_value:
|
||||
if rule_value["session_enabled"] is False:
|
||||
override_paths["platform_settings.id_blacklist"] = [umo]
|
||||
else:
|
||||
remove_override_paths.append("platform_settings.id_blacklist")
|
||||
if "custom_name" in rule_value:
|
||||
custom_name = str(rule_value.get("custom_name") or "").strip()
|
||||
alias = await self.db_helper.get_umo_alias(umo)
|
||||
await self.db_helper.upsert_umo_alias(
|
||||
umo,
|
||||
alias.creator_sender_id if alias else "",
|
||||
alias.auto_name if alias else None,
|
||||
custom_name or None,
|
||||
)
|
||||
elif rule_key == "session_plugin_config" and isinstance(rule_value, dict):
|
||||
disabled_plugins = rule_value.get("disabled_plugins")
|
||||
if isinstance(disabled_plugins, list):
|
||||
override_paths["plugin_disabled_set"] = disabled_plugins
|
||||
elif rule_key == "kb_config" and isinstance(rule_value, dict):
|
||||
if "top_k" in rule_value:
|
||||
override_paths["kb_final_top_k"] = rule_value["top_k"]
|
||||
kb_ids = rule_value.get("kb_ids")
|
||||
if isinstance(kb_ids, list):
|
||||
override_paths["kb_names"] = [
|
||||
str(kb_id) for kb_id in kb_ids if str(kb_id).strip()
|
||||
]
|
||||
else:
|
||||
provider_path_map = {
|
||||
f"provider_perf_{ProviderType.CHAT_COMPLETION.value}": (
|
||||
"provider_settings.default_provider_id"
|
||||
),
|
||||
f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}": (
|
||||
"provider_stt_settings.provider_id"
|
||||
),
|
||||
f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}": (
|
||||
"provider_tts_settings.provider_id"
|
||||
),
|
||||
}
|
||||
if rule_key in provider_path_map and rule_value:
|
||||
override_paths[provider_path_map[rule_key]] = rule_value
|
||||
|
||||
await sp.session_put(umo, rule_key, rule_value)
|
||||
if override_paths:
|
||||
await self.core_lifecycle.astrbot_config_mgr.update_conf_overrides(
|
||||
umo,
|
||||
override_paths,
|
||||
)
|
||||
if remove_override_paths:
|
||||
await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides(
|
||||
umo,
|
||||
remove_override_paths,
|
||||
)
|
||||
return {"message": f"规则 {rule_key} 已更新", "umo": umo}
|
||||
|
||||
async def delete_session_rule(self, data: object) -> dict:
|
||||
@@ -272,10 +616,62 @@ class SessionManagementService:
|
||||
if rule_key:
|
||||
if rule_key not in AVAILABLE_SESSION_RULE_KEYS:
|
||||
raise SessionManagementServiceError(f"不支持的规则键: {rule_key}")
|
||||
await sp.session_remove(umo, rule_key)
|
||||
if rule_key == "session_service_config":
|
||||
await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides(
|
||||
umo,
|
||||
[
|
||||
"provider_settings.enable",
|
||||
"provider_tts_settings.enable",
|
||||
"provider_settings.default_personality",
|
||||
"platform_settings.id_blacklist",
|
||||
],
|
||||
)
|
||||
alias = await self.db_helper.get_umo_alias(umo)
|
||||
if alias:
|
||||
await self.db_helper.upsert_umo_alias(
|
||||
umo,
|
||||
alias.creator_sender_id,
|
||||
alias.auto_name,
|
||||
None,
|
||||
)
|
||||
elif rule_key == "session_plugin_config":
|
||||
await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides(
|
||||
umo,
|
||||
["plugin_disabled_set"],
|
||||
)
|
||||
elif rule_key == "kb_config":
|
||||
await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides(
|
||||
umo,
|
||||
["kb_names", "kb_final_top_k"],
|
||||
)
|
||||
else:
|
||||
provider_path_map = {
|
||||
f"provider_perf_{ProviderType.CHAT_COMPLETION.value}": (
|
||||
"provider_settings.default_provider_id"
|
||||
),
|
||||
f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}": (
|
||||
"provider_stt_settings.provider_id"
|
||||
),
|
||||
f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}": (
|
||||
"provider_tts_settings.provider_id"
|
||||
),
|
||||
}
|
||||
if rule_key in provider_path_map:
|
||||
await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides(
|
||||
umo,
|
||||
[provider_path_map[rule_key]],
|
||||
)
|
||||
return {"message": f"规则 {rule_key} 已删除", "umo": umo}
|
||||
|
||||
await sp.clear_async("umo", umo)
|
||||
alias = await self.db_helper.get_umo_alias(umo)
|
||||
if alias:
|
||||
await self.db_helper.upsert_umo_alias(
|
||||
umo,
|
||||
alias.creator_sender_id,
|
||||
alias.auto_name,
|
||||
None,
|
||||
)
|
||||
return {"message": "所有规则已删除", "umo": umo}
|
||||
|
||||
async def delete_session_rules(self, data: object) -> dict:
|
||||
@@ -306,9 +702,9 @@ class SessionManagementService:
|
||||
for umo in umos:
|
||||
try:
|
||||
if rule_key:
|
||||
await sp.session_remove(umo, rule_key)
|
||||
await self.delete_session_rule({"umo": umo, "rule_key": rule_key})
|
||||
else:
|
||||
await sp.clear_async("umo", umo)
|
||||
await self.delete_session_rule({"umo": umo})
|
||||
success_count += 1
|
||||
except Exception as exc:
|
||||
logger.error(f"删除 umo {umo} 的规则失败: {exc!s}")
|
||||
@@ -458,24 +854,24 @@ class SessionManagementService:
|
||||
|
||||
for umo in umos:
|
||||
try:
|
||||
session_config = (
|
||||
sp.get("session_service_config", {}, scope="umo", scope_id=umo)
|
||||
or {}
|
||||
)
|
||||
|
||||
override_paths = {}
|
||||
if llm_enabled is not None:
|
||||
session_config["llm_enabled"] = llm_enabled
|
||||
override_paths["provider_settings.enable"] = bool(llm_enabled)
|
||||
if tts_enabled is not None:
|
||||
session_config["tts_enabled"] = tts_enabled
|
||||
override_paths["provider_tts_settings.enable"] = bool(tts_enabled)
|
||||
if session_enabled is not None:
|
||||
session_config["session_enabled"] = session_enabled
|
||||
|
||||
sp.put(
|
||||
"session_service_config",
|
||||
session_config,
|
||||
scope="umo",
|
||||
scope_id=umo,
|
||||
)
|
||||
if session_enabled is False:
|
||||
override_paths["platform_settings.id_blacklist"] = [umo]
|
||||
else:
|
||||
await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides(
|
||||
umo,
|
||||
["platform_settings.id_blacklist"],
|
||||
)
|
||||
if override_paths:
|
||||
await self.core_lifecycle.astrbot_config_mgr.update_conf_overrides(
|
||||
umo,
|
||||
override_paths,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as exc:
|
||||
logger.error(f"更新 {umo} 服务状态失败: {exc!s}")
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -550,6 +550,34 @@ export type ReorderRequest = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SessionAliasRequest = {
|
||||
umo?: string;
|
||||
umos?: Array<(string)>;
|
||||
scope?: 'all' | 'group' | 'private' | 'custom_group';
|
||||
group_id?: string;
|
||||
custom_name: string;
|
||||
[key: string]: unknown | string;
|
||||
};
|
||||
|
||||
export type scope = 'all' | 'group' | 'private' | 'custom_group';
|
||||
|
||||
export type SessionConfigOverrideDeleteRequest = {
|
||||
umo?: string;
|
||||
umos?: Array<(string)>;
|
||||
scope?: 'all' | 'group' | 'private' | 'custom_group';
|
||||
group_id?: string;
|
||||
path?: string;
|
||||
paths?: Array<(string)>;
|
||||
[key: string]: unknown | string;
|
||||
};
|
||||
|
||||
export type SessionConfigOverrideRequest = {
|
||||
umo: string;
|
||||
path: string;
|
||||
value: unknown;
|
||||
[key: string]: unknown | string;
|
||||
};
|
||||
|
||||
export type SessionGroupRequest = {
|
||||
name?: string;
|
||||
umos?: Array<(string)>;
|
||||
@@ -623,8 +651,6 @@ export type UmoListRequest = {
|
||||
rule_key?: string;
|
||||
};
|
||||
|
||||
export type scope = 'all' | 'group' | 'private' | 'custom_group';
|
||||
|
||||
export type UpdateAccountRequest = {
|
||||
password: string;
|
||||
new_password?: string;
|
||||
@@ -2952,6 +2978,7 @@ export type ListSessionRulesData = {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
search?: string;
|
||||
umo?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2975,6 +3002,43 @@ export type DeleteSessionRulesResponse = (SuccessEnvelope);
|
||||
|
||||
export type DeleteSessionRulesError = unknown;
|
||||
|
||||
export type ListSessionConfigOverridesData = {
|
||||
query?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
search?: string;
|
||||
umo?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ListSessionConfigOverridesResponse = (SuccessEnvelope);
|
||||
|
||||
export type ListSessionConfigOverridesError = unknown;
|
||||
|
||||
export type UpsertSessionConfigOverrideData = {
|
||||
body: SessionConfigOverrideRequest;
|
||||
};
|
||||
|
||||
export type UpsertSessionConfigOverrideResponse = (SuccessEnvelope);
|
||||
|
||||
export type UpsertSessionConfigOverrideError = unknown;
|
||||
|
||||
export type DeleteSessionConfigOverrideData = {
|
||||
body: SessionConfigOverrideDeleteRequest;
|
||||
};
|
||||
|
||||
export type DeleteSessionConfigOverrideResponse = (SuccessEnvelope);
|
||||
|
||||
export type DeleteSessionConfigOverrideError = unknown;
|
||||
|
||||
export type UpsertSessionAliasData = {
|
||||
body: SessionAliasRequest;
|
||||
};
|
||||
|
||||
export type UpsertSessionAliasResponse = (SuccessEnvelope);
|
||||
|
||||
export type UpsertSessionAliasError = unknown;
|
||||
|
||||
export type BatchUpdateSessionProviderData = {
|
||||
body: BatchSessionProviderRequest;
|
||||
};
|
||||
@@ -3124,6 +3188,17 @@ export type ExportConversationsResponse = (unknown);
|
||||
|
||||
export type ExportConversationsError = unknown;
|
||||
|
||||
export type ListUmoWorkspaceFilesData = {
|
||||
query: {
|
||||
path?: string;
|
||||
umo: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ListUmoWorkspaceFilesResponse = (SuccessEnvelope);
|
||||
|
||||
export type ListUmoWorkspaceFilesError = unknown;
|
||||
|
||||
export type GetStatsData = {
|
||||
query?: {
|
||||
offset_sec?: number;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type GhproxyTestRequest,
|
||||
type KnowledgeBaseCreateRequest,
|
||||
type KnowledgeBaseRequest,
|
||||
type ListUmoWorkspaceFilesData,
|
||||
type LoginRequest,
|
||||
type ListConversationsData,
|
||||
type McpServerConfig,
|
||||
@@ -46,6 +47,9 @@ import {
|
||||
type BatchSessionProviderRequest,
|
||||
type BatchSessionServiceRequest,
|
||||
type SetupAuthRequest,
|
||||
type SessionAliasRequest,
|
||||
type SessionConfigOverrideDeleteRequest,
|
||||
type SessionConfigOverrideRequest,
|
||||
type SessionGroupRequest,
|
||||
type SessionRuleRequest,
|
||||
type UmoListRequest,
|
||||
@@ -163,6 +167,7 @@ export interface SessionRuleListParams {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
search?: string;
|
||||
umo?: string;
|
||||
}
|
||||
|
||||
export interface ChatSessionListParams {
|
||||
@@ -942,12 +947,30 @@ export const sessionApi = {
|
||||
openApiV1.listSessionRules({ query: generatedQuery(params) }),
|
||||
);
|
||||
},
|
||||
listConfigOverrides(params?: SessionRuleListParams) {
|
||||
return typed<any>(
|
||||
openApiV1.listSessionConfigOverrides({ query: generatedQuery(params) }),
|
||||
);
|
||||
},
|
||||
upsertRule(payload: SessionRuleRequest) {
|
||||
return typed<any>(openApiV1.upsertSessionRule({ body: payload }));
|
||||
},
|
||||
deleteRules(payload: UmoListRequest) {
|
||||
return typed<any>(openApiV1.deleteSessionRules({ body: payload }));
|
||||
},
|
||||
upsertConfigOverride(payload: SessionConfigOverrideRequest) {
|
||||
return typed<any>(
|
||||
openApiV1.upsertSessionConfigOverride({ body: payload }),
|
||||
);
|
||||
},
|
||||
deleteConfigOverride(payload: SessionConfigOverrideDeleteRequest) {
|
||||
return typed<any>(
|
||||
openApiV1.deleteSessionConfigOverride({ body: payload }),
|
||||
);
|
||||
},
|
||||
upsertAlias(payload: SessionAliasRequest) {
|
||||
return typed<any>(openApiV1.upsertSessionAlias({ body: payload }));
|
||||
},
|
||||
batchUpdateProvider(payload: BatchSessionProviderRequest) {
|
||||
return typed<any>(
|
||||
openApiV1.batchUpdateSessionProvider({ body: payload }),
|
||||
@@ -1683,6 +1706,16 @@ export const conversationApi = {
|
||||
},
|
||||
};
|
||||
|
||||
export const workspaceApi = {
|
||||
byUmo(params: ListUmoWorkspaceFilesData['query']) {
|
||||
return typed<any>(
|
||||
openApiV1.listUmoWorkspaceFiles({
|
||||
query: params,
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const statsApi = {
|
||||
get(offsetSec?: number) {
|
||||
return typed<any>(
|
||||
|
||||
@@ -323,7 +323,13 @@ function getSpecialSubtype(value) {
|
||||
<div v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0"
|
||||
class="selected-plugins-full-width">
|
||||
<div class="plugins-header">
|
||||
<small class="text-grey">{{ t('core.shared.pluginSetSelector.selectedPluginsLabel') }}</small>
|
||||
<small class="text-grey">
|
||||
{{
|
||||
itemKey.split('.').pop() === 'plugin_disabled_set'
|
||||
? t('core.shared.pluginSetSelector.disabledPluginsLabel')
|
||||
: t('core.shared.pluginSetSelector.selectedPluginsLabel')
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap ga-2 mt-2">
|
||||
<v-chip v-for="plugin in (createSelectorModel(itemKey).value || [])" :key="plugin" size="small" label
|
||||
@@ -410,7 +416,13 @@ function getSpecialSubtype(value) {
|
||||
<div v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0"
|
||||
class="selected-plugins-full-width">
|
||||
<div class="plugins-header">
|
||||
<small class="text-grey">{{ t('core.shared.pluginSetSelector.selectedPluginsLabel') }}</small>
|
||||
<small class="text-grey">
|
||||
{{
|
||||
itemKey.split('.').pop() === 'plugin_disabled_set'
|
||||
? t('core.shared.pluginSetSelector.disabledPluginsLabel')
|
||||
: t('core.shared.pluginSetSelector.selectedPluginsLabel')
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap ga-2 mt-2">
|
||||
<v-chip v-for="plugin in (createSelectorModel(itemKey).value || [])" :key="plugin" size="small" label
|
||||
|
||||
@@ -40,7 +40,15 @@
|
||||
<KnowledgeBaseSelector :model-value="modelValue" @update:model-value="emitUpdate" />
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'select_plugin_set'">
|
||||
<PluginSetSelector :model-value="modelValue" @update:model-value="emitUpdate" />
|
||||
<PluginSetSelector
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
:show-all-option="!isDisabledPluginSet"
|
||||
:not-selected-label="isDisabledPluginSet ? t('core.shared.pluginSetSelector.noDisabledPlugins') : ''"
|
||||
:none-label="isDisabledPluginSet ? t('core.shared.pluginSetSelector.noDisabledPlugins') : ''"
|
||||
:custom-label="isDisabledPluginSet ? t('core.shared.pluginSetSelector.selectDisabledPlugins') : ''"
|
||||
:button-text="isDisabledPluginSet ? t('core.shared.pluginSetSelector.selectDisabledPlugins') : ''"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 't2i_template'">
|
||||
<T2ITemplateEditor />
|
||||
@@ -307,6 +315,8 @@ const listSelectItems = computed(() =>
|
||||
: []
|
||||
)
|
||||
|
||||
const isDisabledPluginSet = computed(() => props.configKey?.split('.').pop() === 'plugin_disabled_set')
|
||||
|
||||
function toNumber(val) {
|
||||
const n = parseFloat(val)
|
||||
return isNaN(n) ? 0 : n
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
{{ tm('pluginSetSelector.notSelected') }}
|
||||
{{ notSelectedLabel || tm('pluginSetSelector.notSelected') }}
|
||||
</span>
|
||||
<span v-else-if="isAllPlugins" style="color: rgb(var(--v-theme-primaryText));">
|
||||
{{ tm('pluginSetSelector.allPlugins') }}
|
||||
@@ -32,19 +32,20 @@
|
||||
<div v-if="!loading">
|
||||
<!-- 预设选项 -->
|
||||
<v-radio-group v-model="selectionMode" class="mb-4" hide-details>
|
||||
<v-radio
|
||||
<v-radio
|
||||
v-if="showAllOption"
|
||||
value="all"
|
||||
:label="tm('pluginSetSelector.enableAll')"
|
||||
:label="allLabel || tm('pluginSetSelector.enableAll')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
<v-radio
|
||||
value="none"
|
||||
:label="tm('pluginSetSelector.enableNone')"
|
||||
:label="noneLabel || tm('pluginSetSelector.enableNone')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
<v-radio
|
||||
value="custom"
|
||||
:label="tm('pluginSetSelector.customSelect')"
|
||||
:label="customLabel || tm('pluginSetSelector.customSelect')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
</v-radio-group>
|
||||
@@ -120,6 +121,26 @@ const props = defineProps({
|
||||
maxDisplayItems: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
showAllOption: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
allLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
noneLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
notSelectedLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -138,7 +159,7 @@ const pluginDescription = (plugin) => pluginDesc(plugin)
|
||||
|
||||
// 判断是否为"所有插件"模式
|
||||
const isAllPlugins = computed(() => {
|
||||
return props.modelValue && props.modelValue.length === 1 && props.modelValue[0] === '*'
|
||||
return props.showAllOption && props.modelValue && props.modelValue.length === 1 && props.modelValue[0] === '*'
|
||||
})
|
||||
|
||||
// 移除插件
|
||||
@@ -154,7 +175,7 @@ watch(() => props.modelValue, (newValue) => {
|
||||
if (!newValue || newValue.length === 0) {
|
||||
selectionMode.value = 'none'
|
||||
selectedPlugins.value = []
|
||||
} else if (newValue.length === 1 && newValue[0] === '*') {
|
||||
} else if (props.showAllOption && newValue.length === 1 && newValue[0] === '*') {
|
||||
selectionMode.value = 'all'
|
||||
selectedPlugins.value = []
|
||||
} else {
|
||||
@@ -195,7 +216,7 @@ function confirmSelection() {
|
||||
|
||||
switch (selectionMode.value) {
|
||||
case 'all':
|
||||
newValue = ['*']
|
||||
newValue = props.showAllOption ? ['*'] : []
|
||||
break
|
||||
case 'none':
|
||||
newValue = []
|
||||
@@ -215,7 +236,7 @@ function cancelSelection() {
|
||||
if (currentValue.length === 0) {
|
||||
selectionMode.value = 'none'
|
||||
selectedPlugins.value = []
|
||||
} else if (currentValue.length === 1 && currentValue[0] === '*') {
|
||||
} else if (props.showAllOption && currentValue.length === 1 && currentValue[0] === '*') {
|
||||
selectionMode.value = 'all'
|
||||
selectedPlugins.value = []
|
||||
} else {
|
||||
|
||||
@@ -59,6 +59,7 @@ export class I18nLoader {
|
||||
{ name: 'features/alkaid/memory', path: 'features/alkaid/memory.json' },
|
||||
{ name: 'features/persona', path: 'features/persona.json' },
|
||||
{ name: 'features/welcome', path: 'features/welcome.json' },
|
||||
{ name: 'features/observer', path: 'features/observer.json' },
|
||||
|
||||
// 消息模块
|
||||
{ name: 'messages/errors', path: 'messages/errors.json' },
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"components": "Handlers"
|
||||
},
|
||||
"conversation": "Conversations",
|
||||
"observer": "Observer",
|
||||
"sessionManagement": "Custom Rules",
|
||||
"console": "Console",
|
||||
"trace": "Trace",
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"notActivated": "Not activated",
|
||||
"note": "*System plugins and disabled plugins are not shown.",
|
||||
"selectedPluginsLabel": "Selected Plugins:",
|
||||
"disabledPluginsLabel": "Disabled Plugins:",
|
||||
"noDisabledPlugins": "No disabled plugins",
|
||||
"selectDisabledPlugins": "Select disabled plugins",
|
||||
"allPluginsLabel": "All Plugins"
|
||||
},
|
||||
"providerSelector": {
|
||||
|
||||
@@ -807,6 +807,10 @@
|
||||
"description": "Whitelist ID List",
|
||||
"hint": "Use /sid to get IDs. If the list is empty, it means whitelist is disabled (all IDs are in the whitelist)."
|
||||
},
|
||||
"id_blacklist": {
|
||||
"description": "Blacklist ID List",
|
||||
"hint": "Use /sid to get IDs. Sessions in the blacklist are rejected, and blacklist rules take priority over the whitelist."
|
||||
},
|
||||
"id_whitelist_log": {
|
||||
"description": "Output Logs",
|
||||
"hint": "When enabled, INFO level logs will be output when a message doesn't pass the whitelist."
|
||||
@@ -933,6 +937,10 @@
|
||||
"plugin_set": {
|
||||
"description": "Available Plugins",
|
||||
"hint": "All non-disabled plugins are enabled by default. If a plugin is disabled on the plugins page, selections here will not take effect."
|
||||
},
|
||||
"plugin_disabled_set": {
|
||||
"description": "Disabled Plugins",
|
||||
"hint": "Plugins explicitly listed here will not trigger in sessions."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
"onceAt": "One-off · {time}",
|
||||
"runAt": "Run at: {time}",
|
||||
"nextRun": "Next run: {time}",
|
||||
"dailyAt": "Daily at {time}",
|
||||
"weeklyAt": "Every {day} at {time}",
|
||||
"dailyAt": "Daily · {time}",
|
||||
"weeklyAt": "{day} · {time}",
|
||||
"weekdaySeparator": ", ",
|
||||
"monthlyAt": "Monthly on day {day} at {time}",
|
||||
"everyMinutes": "Every {count} minutes",
|
||||
"everyHours": "Every {count} hours",
|
||||
|
||||
76
dashboard/src/i18n/locales/en-US/features/observer.json
Normal file
76
dashboard/src/i18n/locales/en-US/features/observer.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"title": "Observer",
|
||||
"searchPlaceholder": "Search UMO, alias, or session ID",
|
||||
"emptyTitle": "What do you want to inspect?",
|
||||
"emptySubtitle": "Search a UMO to inspect its conversations, custom rules, workspace, and trace.",
|
||||
"knownSources": "Message Sources",
|
||||
"home": {
|
||||
"recent": "Recent Sources",
|
||||
"resources": "More Resources",
|
||||
"noSources": "No message sources yet",
|
||||
"noMatches": "No matching UMO",
|
||||
"resourceCards": {
|
||||
"rules": "Custom Rules",
|
||||
"rulesHint": "Manage config overrides",
|
||||
"conversation": "Conversation Viewer",
|
||||
"conversationHint": "Browse conversation data",
|
||||
"trace": "Trace",
|
||||
"traceHint": "Inspect execution trace",
|
||||
"stats": "Data Dashboard",
|
||||
"statsHint": "View runtime metrics"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
"title": "UMO Detail",
|
||||
"config": "Config Profile",
|
||||
"route": "Route",
|
||||
"defaultConfig": "Default Config",
|
||||
"noRoute": "No dedicated route",
|
||||
"copy": "Copy UMO",
|
||||
"openChat": "Open ChatUI",
|
||||
"openConfig": "View config profile",
|
||||
"configDrawerTitle": "Config Profile",
|
||||
"configDrawerIdLabel": "Config ID"
|
||||
},
|
||||
"resources": {
|
||||
"conversation": "Conversation",
|
||||
"overrides": "Custom Rules",
|
||||
"workspace": "Workspace",
|
||||
"trace": "Trace"
|
||||
},
|
||||
"conversation": {
|
||||
"title": "Conversation Preview",
|
||||
"count": "{count} items",
|
||||
"empty": "No conversation data",
|
||||
"select": "Select a conversation to preview",
|
||||
"latest": "Latest Conversation"
|
||||
},
|
||||
"tokens": {
|
||||
"title": "Token Usage",
|
||||
"window": "24H",
|
||||
"unit": "Last 24 hours"
|
||||
},
|
||||
"overrides": {
|
||||
"title": "Config Overrides",
|
||||
"count": "{count} overrides",
|
||||
"empty": "No config overrides for this UMO",
|
||||
"open": "Manage Custom Rules"
|
||||
},
|
||||
"workspace": {
|
||||
"title": "Workspace",
|
||||
"empty": "Workspace is empty",
|
||||
"missing": "Workspace is unavailable",
|
||||
"copyPath": "Copy workspace path",
|
||||
"parent": "Parent folder",
|
||||
"root": "Root"
|
||||
},
|
||||
"trace": {
|
||||
"title": "Trace",
|
||||
"hint": "For now this shows the full trace stream. UMO filtering can be added later."
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
"open": "Open",
|
||||
"back": "Back"
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,25 @@
|
||||
"ruleEditor": {
|
||||
"title": "Edit Custom Rules",
|
||||
"description": "Configure custom rules for this session. These rules take priority over global settings.",
|
||||
"overrideCount": "{count} overrides",
|
||||
"groups": {
|
||||
"ai": "AI Settings",
|
||||
"platform": "Platform Settings"
|
||||
},
|
||||
"note": {
|
||||
"title": "General",
|
||||
"source": "UMO note"
|
||||
},
|
||||
"addOverride": {
|
||||
"select": "Select a setting to override",
|
||||
"button": "Add Override",
|
||||
"empty": "No configuration overrides added",
|
||||
"remove": "Remove override"
|
||||
},
|
||||
"serviceConfig": {
|
||||
"title": "Service Configuration",
|
||||
"sessionEnabled": "Enable Session",
|
||||
"sessionEnabled": "Allow Interaction",
|
||||
"sessionEnabledHint": "When disabled, this source cannot communicate with the system.",
|
||||
"llmEnabled": "Enable LLM",
|
||||
"ttsEnabled": "Enable TTS",
|
||||
"customName": "Custom Name"
|
||||
@@ -77,13 +93,14 @@
|
||||
"pluginConfig": {
|
||||
"title": "Plugin Configuration",
|
||||
"disabledPlugins": "Disabled Plugins",
|
||||
"noneDisabled": "No disabled plugins",
|
||||
"selectDisabled": "Select disabled plugins",
|
||||
"hint": "Select plugins to disable for this session. Unselected plugins will remain enabled."
|
||||
},
|
||||
"kbConfig": {
|
||||
"title": "Knowledge Base Configuration",
|
||||
"selectKbs": "Select Knowledge Bases",
|
||||
"topK": "Top K Results",
|
||||
"enableRerank": "Enable Reranking"
|
||||
"topK": "Top K Results"
|
||||
}
|
||||
},
|
||||
"deleteConfirm": {
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
"config": "Конфигурация",
|
||||
"chat": "Чат",
|
||||
"cron": "Запланированные задачи",
|
||||
"conversation": "Данные диалогов",
|
||||
"sessionManagement": "Пользовательские правила",
|
||||
"conversation": "Данные диалогов",
|
||||
"observer": "Observer",
|
||||
"sessionManagement": "Пользовательские правила",
|
||||
"console": "Логи платформы",
|
||||
"trace": "Трассировка",
|
||||
"alkaid": "Alkaid Lab",
|
||||
@@ -47,4 +48,4 @@
|
||||
"system": "Системная конфигурация"
|
||||
},
|
||||
"pluginWebui": "Страницы плагинов"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"notActivated": "Не активирован",
|
||||
"note": "*Системные и уже выключенные в настройках плагины не отображаются.",
|
||||
"selectedPluginsLabel": "Выбранные плагины:",
|
||||
"disabledPluginsLabel": "Отключенные плагины:",
|
||||
"noDisabledPlugins": "Не отключать плагины",
|
||||
"selectDisabledPlugins": "Выбрать отключенные плагины",
|
||||
"allPluginsLabel": "Все плагины"
|
||||
},
|
||||
"providerSelector": {
|
||||
|
||||
@@ -808,6 +808,10 @@
|
||||
"description": "Список разрешенных ID",
|
||||
"hint": "Используйте /sid для получения ID."
|
||||
},
|
||||
"id_blacklist": {
|
||||
"description": "Список запрещенных ID",
|
||||
"hint": "Используйте /sid для получения ID. Сессии из этого списка отклоняются до проверки белого списка."
|
||||
},
|
||||
"id_whitelist_log": {
|
||||
"description": "Выводить логи",
|
||||
"hint": "Логировать попытки доступа от пользователей не из белого списка."
|
||||
@@ -934,6 +938,10 @@
|
||||
"plugin_set": {
|
||||
"description": "Доступные плагины",
|
||||
"hint": "Все невыключенные плагины включены по умолчанию. Если плагин отключен на странице плагинов, выбор здесь не будет иметь силы."
|
||||
},
|
||||
"plugin_disabled_set": {
|
||||
"description": "Отключенные плагины",
|
||||
"hint": "Плагины из этого списка не будут срабатывать в сессиях."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
"onceAt": "Один раз · {time}",
|
||||
"runAt": "Время запуска: {time}",
|
||||
"nextRun": "Следующий запуск: {time}",
|
||||
"dailyAt": "Каждый день в {time}",
|
||||
"weeklyAt": "Каждый {day} в {time}",
|
||||
"dailyAt": "Каждый день · {time}",
|
||||
"weeklyAt": "{day} · {time}",
|
||||
"weekdaySeparator": ", ",
|
||||
"monthlyAt": "Каждый месяц, день {day}, {time}",
|
||||
"everyMinutes": "Каждые {count} мин.",
|
||||
"everyHours": "Каждые {count} ч.",
|
||||
|
||||
76
dashboard/src/i18n/locales/ru-RU/features/observer.json
Normal file
76
dashboard/src/i18n/locales/ru-RU/features/observer.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"title": "Observer",
|
||||
"searchPlaceholder": "Поиск UMO, заметки или session ID",
|
||||
"emptyTitle": "Что проверить?",
|
||||
"emptySubtitle": "Найдите UMO, чтобы посмотреть диалоги, правила, workspace и trace.",
|
||||
"knownSources": "Источники",
|
||||
"home": {
|
||||
"recent": "Недавние источники",
|
||||
"resources": "Еще ресурсы",
|
||||
"noSources": "Источников пока нет",
|
||||
"noMatches": "UMO не найден",
|
||||
"resourceCards": {
|
||||
"rules": "Правила",
|
||||
"rulesHint": "Настроить overrides",
|
||||
"conversation": "Диалоги",
|
||||
"conversationHint": "Смотреть историю",
|
||||
"trace": "Trace",
|
||||
"traceHint": "Смотреть выполнение",
|
||||
"stats": "Дашборд",
|
||||
"statsHint": "Смотреть метрики"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
"title": "UMO Detail",
|
||||
"config": "Профиль конфигурации",
|
||||
"route": "Маршрут",
|
||||
"defaultConfig": "Конфигурация по умолчанию",
|
||||
"noRoute": "Нет отдельного маршрута",
|
||||
"copy": "Копировать UMO",
|
||||
"openChat": "Открыть ChatUI",
|
||||
"openConfig": "Открыть профиль",
|
||||
"configDrawerTitle": "Профиль конфигурации",
|
||||
"configDrawerIdLabel": "ID конфигурации"
|
||||
},
|
||||
"resources": {
|
||||
"conversation": "Диалоги",
|
||||
"overrides": "Правила",
|
||||
"workspace": "Workspace",
|
||||
"trace": "Trace"
|
||||
},
|
||||
"conversation": {
|
||||
"title": "Предпросмотр диалога",
|
||||
"count": "{count} шт.",
|
||||
"empty": "Нет данных диалога",
|
||||
"select": "Выберите диалог для просмотра",
|
||||
"latest": "Последний диалог"
|
||||
},
|
||||
"tokens": {
|
||||
"title": "Расход токенов",
|
||||
"window": "24H",
|
||||
"unit": "За последние 24 часа"
|
||||
},
|
||||
"overrides": {
|
||||
"title": "Overrides конфигурации",
|
||||
"count": "{count} overrides",
|
||||
"empty": "Для этого UMO нет overrides",
|
||||
"open": "Управлять правилами"
|
||||
},
|
||||
"workspace": {
|
||||
"title": "Workspace",
|
||||
"empty": "Workspace пуст",
|
||||
"missing": "Workspace недоступен",
|
||||
"copyPath": "Скопировать путь workspace",
|
||||
"parent": "Вверх",
|
||||
"root": "Корень"
|
||||
},
|
||||
"trace": {
|
||||
"title": "Trace",
|
||||
"hint": "Сейчас отображается полный trace stream. Фильтр UMO можно добавить позже."
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Обновить",
|
||||
"open": "Открыть",
|
||||
"back": "Назад"
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,25 @@
|
||||
"ruleEditor": {
|
||||
"title": "Редактор правил",
|
||||
"description": "Настройте поведение для этой сессии. Настройки ниже перекроют глобальный конфиг.",
|
||||
"overrideCount": "Переопределений: {count}",
|
||||
"groups": {
|
||||
"ai": "AI-настройки",
|
||||
"platform": "Платформа"
|
||||
},
|
||||
"note": {
|
||||
"title": "Общие",
|
||||
"source": "UMO-заметка"
|
||||
},
|
||||
"addOverride": {
|
||||
"select": "Выберите настройку для переопределения",
|
||||
"button": "Добавить",
|
||||
"empty": "Переопределения не добавлены",
|
||||
"remove": "Удалить переопределение"
|
||||
},
|
||||
"serviceConfig": {
|
||||
"title": "Сервисные настройки",
|
||||
"sessionEnabled": "Обрабатывать сообщения",
|
||||
"sessionEnabled": "Разрешить взаимодействие",
|
||||
"sessionEnabledHint": "Если отключено, этот источник не сможет общаться с системой.",
|
||||
"llmEnabled": "Использовать LLM",
|
||||
"ttsEnabled": "Использовать TTS",
|
||||
"customName": "Заметка для сессии"
|
||||
@@ -77,13 +93,14 @@
|
||||
"pluginConfig": {
|
||||
"title": "Плагины",
|
||||
"disabledPlugins": "Отключенные плагины",
|
||||
"noneDisabled": "Не отключать плагины",
|
||||
"selectDisabled": "Выбрать отключенные плагины",
|
||||
"hint": "Выберите плагины, которые нужно ОТКЛЮЧИТЬ в этой сессии. Остальные останутся активными."
|
||||
},
|
||||
"kbConfig": {
|
||||
"title": "База знаний",
|
||||
"selectKbs": "Выбор баз знаний",
|
||||
"topK": "Количество результатов (Top K)",
|
||||
"enableRerank": "Использовать Rerank"
|
||||
"topK": "Количество результатов (Top K)"
|
||||
}
|
||||
},
|
||||
"deleteConfirm": {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"chat": "聊天",
|
||||
"cron": "未来任务",
|
||||
"conversation": "对话数据",
|
||||
"observer": "Observer",
|
||||
"sessionManagement": "自定义规则",
|
||||
"console": "平台日志",
|
||||
"trace": "追踪",
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"notActivated": "未激活",
|
||||
"note": "*不显示系统插件和已经在插件页禁用的插件。",
|
||||
"selectedPluginsLabel": "已选择的插件:",
|
||||
"disabledPluginsLabel": "已禁用的插件:",
|
||||
"noDisabledPlugins": "不禁用插件",
|
||||
"selectDisabledPlugins": "选择禁用插件",
|
||||
"allPluginsLabel": "所有插件"
|
||||
},
|
||||
"providerSelector": {
|
||||
|
||||
@@ -809,6 +809,10 @@
|
||||
"description": "白名单 ID 列表",
|
||||
"hint": "使用 /sid 获取 ID。列表为空时表示该白名单不启用(即所有 ID 都在白名单内)。"
|
||||
},
|
||||
"id_blacklist": {
|
||||
"description": "黑名单 ID 列表",
|
||||
"hint": "使用 /sid 获取 ID。命中黑名单的会话会被拒绝,黑名单优先于白名单。"
|
||||
},
|
||||
"id_whitelist_log": {
|
||||
"description": "输出日志",
|
||||
"hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。"
|
||||
@@ -935,6 +939,10 @@
|
||||
"plugin_set": {
|
||||
"description": "可用插件",
|
||||
"hint": "默认启用全部未被禁用的插件。若插件在插件页面被禁用,则此处的选择不会生效。"
|
||||
},
|
||||
"plugin_disabled_set": {
|
||||
"description": "禁用插件",
|
||||
"hint": "明确禁用的插件列表。命中禁用列表的插件不会在会话中触发。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
"onceAt": "一次性 · {time}",
|
||||
"runAt": "执行时间:{time}",
|
||||
"nextRun": "下次执行:{time}",
|
||||
"dailyAt": "每天 {time}",
|
||||
"weeklyAt": "每周{day} {time}",
|
||||
"dailyAt": "每天 · {time}",
|
||||
"weeklyAt": "{day} · {time}",
|
||||
"weekdaySeparator": "、",
|
||||
"monthlyAt": "每月 {day} 日 {time}",
|
||||
"everyMinutes": "每隔 {count} 分钟",
|
||||
"everyHours": "每隔 {count} 小时",
|
||||
|
||||
76
dashboard/src/i18n/locales/zh-CN/features/observer.json
Normal file
76
dashboard/src/i18n/locales/zh-CN/features/observer.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"title": "Observer",
|
||||
"searchPlaceholder": "搜索 UMO、备注或会话 ID",
|
||||
"emptyTitle": "想查什么?",
|
||||
"emptySubtitle": "搜索一个 UMO,查看它关联的对话、自定义规则、工作区与追踪。",
|
||||
"knownSources": "消息来源",
|
||||
"home": {
|
||||
"recent": "最近来源",
|
||||
"resources": "更多资源",
|
||||
"noSources": "暂无可用消息来源",
|
||||
"noMatches": "没有匹配的 UMO",
|
||||
"resourceCards": {
|
||||
"rules": "自定义规则",
|
||||
"rulesHint": "管理配置覆盖",
|
||||
"conversation": "对话数据查看器",
|
||||
"conversationHint": "查看历史对话",
|
||||
"trace": "追踪",
|
||||
"traceHint": "查看执行链路",
|
||||
"stats": "数据大盘",
|
||||
"statsHint": "查看运行指标"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
"title": "UMO Detail",
|
||||
"config": "配置文件",
|
||||
"route": "路由",
|
||||
"defaultConfig": "默认配置",
|
||||
"noRoute": "未设置专属路由",
|
||||
"copy": "复制 UMO",
|
||||
"openChat": "打开 ChatUI",
|
||||
"openConfig": "查看配置文件",
|
||||
"configDrawerTitle": "配置文件",
|
||||
"configDrawerIdLabel": "配置 ID"
|
||||
},
|
||||
"resources": {
|
||||
"conversation": "对话",
|
||||
"overrides": "自定义规则",
|
||||
"workspace": "工作区",
|
||||
"trace": "追踪"
|
||||
},
|
||||
"conversation": {
|
||||
"title": "对话预览",
|
||||
"count": "{count} 条",
|
||||
"empty": "暂无对话数据",
|
||||
"select": "选择对话以预览",
|
||||
"latest": "最近对话"
|
||||
},
|
||||
"tokens": {
|
||||
"title": "消耗 Token",
|
||||
"window": "24H",
|
||||
"unit": "近 24 小时"
|
||||
},
|
||||
"overrides": {
|
||||
"title": "配置覆盖",
|
||||
"count": "{count} 项覆盖项",
|
||||
"empty": "该 UMO 暂无配置覆盖项",
|
||||
"open": "管理自定义规则"
|
||||
},
|
||||
"workspace": {
|
||||
"title": "工作区",
|
||||
"empty": "工作区为空",
|
||||
"missing": "工作区暂不可用",
|
||||
"copyPath": "复制工作区路径",
|
||||
"parent": "上级目录",
|
||||
"root": "根目录"
|
||||
},
|
||||
"trace": {
|
||||
"title": "追踪",
|
||||
"hint": "当前先展示全量追踪流,UMO 过滤后续再接入。"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "刷新",
|
||||
"open": "打开",
|
||||
"back": "返回"
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,25 @@
|
||||
"ruleEditor": {
|
||||
"title": "编辑自定义规则",
|
||||
"description": "为此会话配置自定义规则,这些规则将优先于全局配置生效。",
|
||||
"overrideCount": "{count} 项覆盖",
|
||||
"groups": {
|
||||
"ai": "AI 配置",
|
||||
"platform": "平台配置"
|
||||
},
|
||||
"note": {
|
||||
"title": "通用",
|
||||
"source": "UMO 备注"
|
||||
},
|
||||
"addOverride": {
|
||||
"select": "选择要覆盖的配置项",
|
||||
"button": "添加覆盖",
|
||||
"empty": "尚未添加配置覆盖项",
|
||||
"remove": "移除覆盖"
|
||||
},
|
||||
"serviceConfig": {
|
||||
"title": "服务配置",
|
||||
"sessionEnabled": "启用该消息会话来源的消息处理",
|
||||
"sessionEnabled": "允许交互",
|
||||
"sessionEnabledHint": "禁用后将禁止该来源与 AstrBot 交互",
|
||||
"llmEnabled": "启用 LLM",
|
||||
"ttsEnabled": "启用 TTS",
|
||||
"customName": "消息会话来源备注名称"
|
||||
@@ -77,13 +93,14 @@
|
||||
"pluginConfig": {
|
||||
"title": "插件配置",
|
||||
"disabledPlugins": "禁用的插件",
|
||||
"noneDisabled": "不禁用插件",
|
||||
"selectDisabled": "选择禁用插件",
|
||||
"hint": "选择要在此会话中禁用的插件。未选择的插件将保持启用状态。"
|
||||
},
|
||||
"kbConfig": {
|
||||
"title": "知识库配置",
|
||||
"selectKbs": "选择知识库",
|
||||
"topK": "返回结果数量 (Top K)",
|
||||
"enableRerank": "启用重排序"
|
||||
"topK": "返回结果数量 (Top K)"
|
||||
}
|
||||
},
|
||||
"deleteConfirm": {
|
||||
|
||||
@@ -37,6 +37,7 @@ import zhCNPersona from './locales/zh-CN/features/persona.json';
|
||||
import zhCNCommand from './locales/zh-CN/features/command.json';
|
||||
import zhCNSubagent from './locales/zh-CN/features/subagent.json';
|
||||
import zhCNWelcome from './locales/zh-CN/features/welcome.json';
|
||||
import zhCNObserver from './locales/zh-CN/features/observer.json';
|
||||
|
||||
import zhCNErrors from './locales/zh-CN/messages/errors.json';
|
||||
import zhCNSuccess from './locales/zh-CN/messages/success.json';
|
||||
@@ -78,6 +79,7 @@ import enUSPersona from './locales/en-US/features/persona.json';
|
||||
import enUSCommand from './locales/en-US/features/command.json';
|
||||
import enUSSubagent from './locales/en-US/features/subagent.json';
|
||||
import enUSWelcome from './locales/en-US/features/welcome.json';
|
||||
import enUSObserver from './locales/en-US/features/observer.json';
|
||||
|
||||
import enUSErrors from './locales/en-US/messages/errors.json';
|
||||
import enUSSuccess from './locales/en-US/messages/success.json';
|
||||
@@ -119,6 +121,7 @@ import ruRUPersona from './locales/ru-RU/features/persona.json';
|
||||
import ruRUCommand from './locales/ru-RU/features/command.json';
|
||||
import ruRUSubagent from './locales/ru-RU/features/subagent.json';
|
||||
import ruRUWelcome from './locales/ru-RU/features/welcome.json';
|
||||
import ruRUObserver from './locales/ru-RU/features/observer.json';
|
||||
|
||||
import ruRUErrors from './locales/ru-RU/messages/errors.json';
|
||||
import ruRUSuccess from './locales/ru-RU/messages/success.json';
|
||||
@@ -167,7 +170,8 @@ export const translations = {
|
||||
persona: zhCNPersona,
|
||||
command: zhCNCommand,
|
||||
subagent: zhCNSubagent,
|
||||
welcome: zhCNWelcome
|
||||
welcome: zhCNWelcome,
|
||||
observer: zhCNObserver
|
||||
},
|
||||
messages: {
|
||||
errors: zhCNErrors,
|
||||
@@ -216,7 +220,8 @@ export const translations = {
|
||||
persona: enUSPersona,
|
||||
command: enUSCommand,
|
||||
subagent: enUSSubagent,
|
||||
welcome: enUSWelcome
|
||||
welcome: enUSWelcome,
|
||||
observer: enUSObserver
|
||||
},
|
||||
messages: {
|
||||
errors: enUSErrors,
|
||||
@@ -265,7 +270,8 @@ export const translations = {
|
||||
persona: ruRUPersona,
|
||||
command: ruRUCommand,
|
||||
subagent: ruRUSubagent,
|
||||
welcome: ruRUWelcome
|
||||
welcome: ruRUWelcome,
|
||||
observer: ruRUObserver
|
||||
},
|
||||
messages: {
|
||||
errors: ruRUErrors,
|
||||
|
||||
@@ -87,6 +87,11 @@ const sidebarItem: menu[] = [
|
||||
title: 'core.navigation.groups.more',
|
||||
icon: 'mdi-dots-horizontal',
|
||||
children: [
|
||||
{
|
||||
title: 'core.navigation.observer',
|
||||
icon: 'mdi-eye-outline',
|
||||
to: '/observer'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.conversation',
|
||||
icon: 'mdi-database',
|
||||
|
||||
@@ -79,6 +79,11 @@ const MainRoutes = {
|
||||
path: '/session-management',
|
||||
component: () => import('@/views/SessionManagementPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'Observer',
|
||||
path: '/observer',
|
||||
component: () => import('@/views/ObserverPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'Persona',
|
||||
path: '/persona',
|
||||
|
||||
@@ -587,6 +587,23 @@ const weekdayOptions = computed(() => [
|
||||
{ label: tm("form.weekdays.saturday"), value: 6 },
|
||||
]);
|
||||
|
||||
const CRON_WEEKDAY_ALIASES: Record<string, number> = {
|
||||
sun: 0,
|
||||
sunday: 0,
|
||||
mon: 1,
|
||||
monday: 1,
|
||||
tue: 2,
|
||||
tuesday: 2,
|
||||
wed: 3,
|
||||
wednesday: 3,
|
||||
thu: 4,
|
||||
thursday: 4,
|
||||
fri: 5,
|
||||
friday: 5,
|
||||
sat: 6,
|
||||
saturday: 6,
|
||||
};
|
||||
|
||||
function toast(
|
||||
message: string,
|
||||
color: "success" | "error" | "warning" = "success",
|
||||
@@ -692,23 +709,17 @@ function scheduleProductLabel(item: any): string {
|
||||
const minuteNumber = Number(minute);
|
||||
const hourNumber = Number(hour);
|
||||
const dayOfMonthNumber = Number(dayOfMonth);
|
||||
const dayOfWeekNumber = Number(dayOfWeek);
|
||||
if (!isCronTime(minuteNumber, hourNumber)) {
|
||||
return tm("card.customCron", { cron });
|
||||
}
|
||||
const time = `${padTimePart(hourNumber)}:${padTimePart(minuteNumber)}`;
|
||||
const time = formatScheduleTime(hourNumber, minuteNumber);
|
||||
if (dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
||||
return tm("card.dailyAt", { time });
|
||||
}
|
||||
if (
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
Number.isInteger(dayOfWeekNumber) &&
|
||||
dayOfWeekNumber >= 0 &&
|
||||
dayOfWeekNumber <= 6
|
||||
) {
|
||||
const weekDays = parseCronWeekdayField(dayOfWeek);
|
||||
if (dayOfMonth === "*" && month === "*" && weekDays?.length) {
|
||||
return tm("card.weeklyAt", {
|
||||
day: weekdayText(dayOfWeekNumber),
|
||||
day: weekdayListText(weekDays),
|
||||
time,
|
||||
});
|
||||
}
|
||||
@@ -1005,6 +1016,83 @@ function isCronTime(minute: number, hour: number): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function formatScheduleTime(hour: number, minute: number): string {
|
||||
return `${hour}:${padTimePart(minute)}`;
|
||||
}
|
||||
|
||||
function parseCronWeekdayValue(value: string): number | null {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized in CRON_WEEKDAY_ALIASES) {
|
||||
return CRON_WEEKDAY_ALIASES[normalized];
|
||||
}
|
||||
if (!/^\d+$/.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
const day = Number(normalized);
|
||||
if (!Number.isInteger(day) || day < 0 || day > 7) {
|
||||
return null;
|
||||
}
|
||||
return day === 7 ? 0 : day;
|
||||
}
|
||||
|
||||
function parseCronWeekdayField(field: string): number[] | null {
|
||||
const normalized = field.trim().toLowerCase();
|
||||
if (!normalized || normalized === "*" || normalized === "?") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const days: number[] = [];
|
||||
for (const token of normalized.split(",")) {
|
||||
const item = token.trim();
|
||||
if (!item || item.includes("/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rangeParts = item.split("-");
|
||||
if (rangeParts.length > 2) {
|
||||
return null;
|
||||
}
|
||||
if (rangeParts.length === 2) {
|
||||
const start = parseCronWeekdayValue(rangeParts[0]);
|
||||
const end = parseCronWeekdayValue(rangeParts[1]);
|
||||
if (start === null || end === null) {
|
||||
return null;
|
||||
}
|
||||
if (start <= end) {
|
||||
for (let day = start; day <= end; day += 1) {
|
||||
days.push(day);
|
||||
}
|
||||
} else {
|
||||
for (let day = start; day <= 6; day += 1) {
|
||||
days.push(day);
|
||||
}
|
||||
for (let day = 0; day <= end; day += 1) {
|
||||
days.push(day);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const day = parseCronWeekdayValue(item);
|
||||
if (day === null) {
|
||||
return null;
|
||||
}
|
||||
days.push(day);
|
||||
}
|
||||
|
||||
const uniqueDays: number[] = [];
|
||||
for (const day of days) {
|
||||
if (!uniqueDays.includes(day)) {
|
||||
uniqueDays.push(day);
|
||||
}
|
||||
}
|
||||
return uniqueDays.length ? uniqueDays : null;
|
||||
}
|
||||
|
||||
function weekdayListText(days: number[]): string {
|
||||
return days.map((day) => weekdayText(day)).join(tm("card.weekdaySeparator"));
|
||||
}
|
||||
|
||||
function buildCronExpression(): string {
|
||||
const mode = newJob.value.schedule_mode;
|
||||
if (mode === "interval") {
|
||||
@@ -1066,7 +1154,6 @@ function readScheduleFromJob(job: any) {
|
||||
const minuteNumber = Number(minute);
|
||||
const hourNumber = Number(hour);
|
||||
const dayOfMonthNumber = Number(dayOfMonth);
|
||||
const dayOfWeekNumber = Number(dayOfWeek);
|
||||
const hasCronTime = isCronTime(minuteNumber, hourNumber);
|
||||
const time = hasCronTime
|
||||
? `${padTimePart(hourNumber)}:${padTimePart(minuteNumber)}`
|
||||
@@ -1128,18 +1215,17 @@ function readScheduleFromJob(job: any) {
|
||||
};
|
||||
}
|
||||
|
||||
const weekDays = parseCronWeekdayField(dayOfWeek);
|
||||
if (
|
||||
hasCronTime &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
Number.isInteger(dayOfWeekNumber) &&
|
||||
dayOfWeekNumber >= 0 &&
|
||||
dayOfWeekNumber <= 6
|
||||
weekDays?.length === 1
|
||||
) {
|
||||
return {
|
||||
...fallback,
|
||||
schedule_mode: "weekly" as ScheduleMode,
|
||||
weekly_day: dayOfWeekNumber,
|
||||
weekly_day: weekDays[0],
|
||||
weekly_time: time,
|
||||
};
|
||||
}
|
||||
|
||||
1617
dashboard/src/views/ObserverPage.vue
Normal file
1617
dashboard/src/views/ObserverPage.vue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3862,6 +3862,10 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: umo
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Ok"
|
||||
@@ -3896,6 +3900,73 @@ paths:
|
||||
"200":
|
||||
$ref: "#/components/responses/Ok"
|
||||
|
||||
/api/v1/sessions/config-overrides:
|
||||
get:
|
||||
tags: [Sessions]
|
||||
summary: List UMO config overrides
|
||||
operationId: listSessionConfigOverrides
|
||||
x-astrbot-scope: data
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PageSize"
|
||||
- name: search
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: umo
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Ok"
|
||||
post:
|
||||
tags: [Sessions]
|
||||
summary: Update or create a UMO config override
|
||||
operationId: upsertSessionConfigOverride
|
||||
x-astrbot-scope: data
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SessionConfigOverrideRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Ok"
|
||||
|
||||
/api/v1/sessions/config-overrides/delete:
|
||||
post:
|
||||
tags: [Sessions]
|
||||
summary: Delete one or more UMO config overrides
|
||||
operationId: deleteSessionConfigOverride
|
||||
x-astrbot-scope: data
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SessionConfigOverrideDeleteRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Ok"
|
||||
|
||||
/api/v1/sessions/aliases:
|
||||
post:
|
||||
tags: [Sessions]
|
||||
summary: Update UMO aliases used as session remarks
|
||||
operationId: upsertSessionAlias
|
||||
x-astrbot-scope: data
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SessionAliasRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Ok"
|
||||
|
||||
/api/v1/sessions/provider:
|
||||
patch:
|
||||
tags: [Sessions]
|
||||
@@ -4133,6 +4204,27 @@ paths:
|
||||
"200":
|
||||
description: Exported conversation data
|
||||
|
||||
/api/v1/workspaces/by-umo:
|
||||
get:
|
||||
tags: [Workspaces]
|
||||
summary: List workspace files for one UMO
|
||||
operationId: listUmoWorkspaceFiles
|
||||
x-astrbot-scope: data
|
||||
parameters:
|
||||
- name: umo
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 1
|
||||
- name: path
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Ok"
|
||||
|
||||
/api/v1/stats:
|
||||
get:
|
||||
tags: [Stats]
|
||||
@@ -5863,6 +5955,58 @@ components:
|
||||
$ref: "#/components/schemas/DynamicConfig"
|
||||
additionalProperties: true
|
||||
|
||||
SessionConfigOverrideRequest:
|
||||
type: object
|
||||
required: [umo, path, value]
|
||||
properties:
|
||||
umo:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
value: {}
|
||||
additionalProperties: true
|
||||
|
||||
SessionConfigOverrideDeleteRequest:
|
||||
type: object
|
||||
properties:
|
||||
umo:
|
||||
type: string
|
||||
umos:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
scope:
|
||||
type: string
|
||||
enum: [all, group, private, custom_group]
|
||||
group_id:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
additionalProperties: true
|
||||
|
||||
SessionAliasRequest:
|
||||
type: object
|
||||
required: [custom_name]
|
||||
properties:
|
||||
umo:
|
||||
type: string
|
||||
umos:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
scope:
|
||||
type: string
|
||||
enum: [all, group, private, custom_group]
|
||||
group_id:
|
||||
type: string
|
||||
custom_name:
|
||||
type: string
|
||||
additionalProperties: true
|
||||
|
||||
UmoListRequest:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -28,6 +28,12 @@ class FakeEvent:
|
||||
self.temporary_local_files.append(path)
|
||||
|
||||
|
||||
def _make_preprocess_stage(config: dict) -> PreProcessStage:
|
||||
stage = PreProcessStage()
|
||||
stage.ctx = SimpleNamespace(astrbot_config=config)
|
||||
return stage
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preprocess_preserves_image_formats_and_tracks_temp_files(
|
||||
tmp_path, monkeypatch
|
||||
@@ -77,10 +83,7 @@ async def test_preprocess_preserves_image_formats_and_tracks_temp_files(
|
||||
),
|
||||
]
|
||||
)
|
||||
stage = PreProcessStage()
|
||||
stage.config = {}
|
||||
stage.platform_settings = {}
|
||||
stage.stt_settings = {"enable": False}
|
||||
stage = _make_preprocess_stage({"provider_stt_settings": {"enable": False}})
|
||||
|
||||
await stage.process(event)
|
||||
|
||||
@@ -114,10 +117,12 @@ async def test_preprocess_path_mapping_accepts_file_uri(tmp_path):
|
||||
target_image = target_root / "photo.jpg"
|
||||
PILImage.new("RGB", (2, 2), (255, 0, 0)).save(target_image)
|
||||
event = FakeEvent([Image(file="", url=source_image.as_uri())])
|
||||
stage = PreProcessStage()
|
||||
stage.config = {}
|
||||
stage.platform_settings = {"path_mapping": [f"{source_root}:{target_root}"]}
|
||||
stage.stt_settings = {"enable": False}
|
||||
stage = _make_preprocess_stage(
|
||||
{
|
||||
"platform_settings": {"path_mapping": [f"{source_root}:{target_root}"]},
|
||||
"provider_stt_settings": {"enable": False},
|
||||
}
|
||||
)
|
||||
|
||||
await stage.process(event)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import re
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock
|
||||
@@ -287,8 +286,6 @@ async def test_webhook_group_send_by_session_without_cached_msg_id_omits_msg_id(
|
||||
|
||||
def test_qqofficial_ws_is_not_excluded_from_segmented_reply():
|
||||
stage = RespondStage()
|
||||
stage.enable_seg = True
|
||||
stage.only_llm_result = False
|
||||
result = MessageEventResult(chain=[Plain("hello")])
|
||||
|
||||
event = SimpleNamespace(
|
||||
@@ -296,13 +293,11 @@ def test_qqofficial_ws_is_not_excluded_from_segmented_reply():
|
||||
get_platform_name=lambda: "qq_official",
|
||||
)
|
||||
|
||||
assert stage.is_seg_reply_required(cast(Any, event)) is True
|
||||
assert stage.is_seg_reply_required(cast(Any, event), True, False) is True
|
||||
|
||||
|
||||
def test_qqofficial_webhook_remains_excluded_from_segmented_reply():
|
||||
stage = RespondStage()
|
||||
stage.enable_seg = True
|
||||
stage.only_llm_result = False
|
||||
result = MessageEventResult(chain=[Plain("hello")])
|
||||
|
||||
event = SimpleNamespace(
|
||||
@@ -310,26 +305,12 @@ def test_qqofficial_webhook_remains_excluded_from_segmented_reply():
|
||||
get_platform_name=lambda: "qq_official_webhook",
|
||||
)
|
||||
|
||||
assert stage.is_seg_reply_required(cast(Any, event)) is False
|
||||
assert stage.is_seg_reply_required(cast(Any, event), True, False) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_result_decorate_segments_qqofficial_ws_plain_result():
|
||||
stage = ResultDecorateStage()
|
||||
stage.reply_prefix = ""
|
||||
stage.content_safe_check_reply = False
|
||||
stage.enable_segmented_reply = True
|
||||
stage.only_llm_result = False
|
||||
stage.words_count_threshold = 100
|
||||
stage.split_mode = "words"
|
||||
stage.split_words = ["。"]
|
||||
stage.split_words_pattern = re.compile(r"(.*?(。)|.+$)", re.DOTALL)
|
||||
stage.content_cleanup_rule = ""
|
||||
stage.show_reasoning = False
|
||||
stage.tts_trigger_probability = 0
|
||||
stage.reply_with_mention = False
|
||||
stage.reply_with_quote = False
|
||||
stage.forward_threshold = 1000
|
||||
setattr(
|
||||
stage,
|
||||
"ctx",
|
||||
@@ -338,13 +319,33 @@ async def test_result_decorate_segments_qqofficial_ws_plain_result():
|
||||
context=SimpleNamespace(get_using_tts_provider=lambda _umo: None)
|
||||
),
|
||||
astrbot_config={
|
||||
"platform_settings": {
|
||||
"reply_prefix": "",
|
||||
"reply_with_mention": False,
|
||||
"reply_with_quote": False,
|
||||
"forward_threshold": 1000,
|
||||
"segmented_reply": {
|
||||
"enable": True,
|
||||
"only_llm_result": False,
|
||||
"words_count_threshold": 100,
|
||||
"split_mode": "words",
|
||||
"split_words": ["。"],
|
||||
"content_cleanup_rule": "",
|
||||
},
|
||||
},
|
||||
"provider_tts_settings": {
|
||||
"enable": False,
|
||||
"use_file_service": False,
|
||||
"dual_output": False,
|
||||
"trigger_probability": 0,
|
||||
},
|
||||
"provider_settings": {"display_reasoning_text": False},
|
||||
"content_safety": {"also_use_in_response": False},
|
||||
"callback_api_base": "",
|
||||
"t2i": False,
|
||||
"t2i_word_threshold": 150,
|
||||
"t2i_strategy": "local",
|
||||
"t2i_active_template": "",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -77,7 +77,6 @@ def test_builtin_stage_bootstrap_is_idempotent() -> None:
|
||||
expected_stage_names = {
|
||||
"WakingCheckStage",
|
||||
"WhitelistCheckStage",
|
||||
"SessionStatusCheckStage",
|
||||
"RateLimitStage",
|
||||
"ContentSafetyCheckStage",
|
||||
"PreProcessStage",
|
||||
|
||||
@@ -264,6 +264,37 @@ async def test_ensure_vec_db_clears_stale_init_error(
|
||||
assert helper.vec_db is mock_vec_db
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieve_accepts_kb_id_in_kb_names(
|
||||
stub_provider_manager_module,
|
||||
mock_provider_manager,
|
||||
mock_knowledge_base,
|
||||
):
|
||||
"""Test that retrieve accepts KB IDs through the legacy kb_names field."""
|
||||
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
||||
|
||||
kb_helper = MagicMock()
|
||||
kb_helper.kb = mock_knowledge_base
|
||||
kb_helper.init_error = None
|
||||
|
||||
kb_mgr = KnowledgeBaseManager.__new__(KnowledgeBaseManager)
|
||||
kb_mgr.provider_manager = mock_provider_manager
|
||||
kb_mgr.kb_insts = {mock_knowledge_base.kb_id: kb_helper}
|
||||
kb_mgr.retrieval_manager = MagicMock()
|
||||
kb_mgr.retrieval_manager.retrieve = AsyncMock(return_value=[])
|
||||
|
||||
result = await kb_mgr.retrieve(
|
||||
query="hello",
|
||||
kb_names=[mock_knowledge_base.kb_id],
|
||||
)
|
||||
|
||||
assert result is None
|
||||
kb_mgr.retrieval_manager.retrieve.assert_awaited_once()
|
||||
assert kb_mgr.retrieval_manager.retrieve.await_args.kwargs["kb_ids"] == [
|
||||
mock_knowledge_base.kb_id
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_vec_db_sets_init_error_on_failure(
|
||||
stub_provider_manager_module,
|
||||
|
||||
@@ -96,9 +96,15 @@ class _DummyRespondEvent:
|
||||
def _make_respond_stage() -> RespondStage:
|
||||
"""Build a minimally initialized RespondStage for unit tests."""
|
||||
stage = RespondStage()
|
||||
stage.config = {"provider_settings": {}}
|
||||
stage.platform_settings = {"path_mapping": []}
|
||||
stage.enable_seg = False
|
||||
stage.ctx = SimpleNamespace(
|
||||
astrbot_config={
|
||||
"provider_settings": {},
|
||||
"platform_settings": {
|
||||
"path_mapping": [],
|
||||
"segmented_reply": {"enable": False},
|
||||
},
|
||||
}
|
||||
)
|
||||
return stage
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user