chore: 迁移服务器前同步本地修改

This commit is contained in:
2026-08-05 21:26:13 +08:00
parent aa47a19e98
commit 1a914a2517
73 changed files with 908 additions and 1961 deletions
+3 -3
View File
@@ -191,11 +191,11 @@ async def get_authority(user_id: str) -> "UserAuthority":
"""通过 PostgresDatabase Actor 查出指定用户的 ``UserAuthority``;用户不存在时抛 401。"""
from kilostar.utils.error import UserNotExistError
from kilostar.utils.i18n import t
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
postgres_database = ray_actor_hook("postgres_database").postgres_database
postgres_database = get_actor("postgres_database")
try:
user_authority = await postgres_database.get_user_authority.remote(
user_authority = await postgres_database.get_user_authority(
user_id=user_id
)
return user_authority
+180
View File
@@ -0,0 +1,180 @@
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""进程内 actor 运行时(位置透明的组件寻址层)。
设计目标:**一处编写,处处运行**。业务代码永远通过 ``get_actor(name)`` 拿到一个
``ActorHandle``,再 ``await handle.method(args)`` 调用,**不关心对端在哪**。
今天所有核心组件(PostgresDatabase / GlobalStateMachine / ...)都是本进程内的
普通异步对象,``get_actor`` 返回 ``LocalActorHandle``,直接转发到本地实例。
未来若要把某一层(如跑 vLLM 的重型 worker)拆到独立进程/机器上,只需新增一个
``RemoteActorHandle``(gRPC / 子进程 / 消息队列后端)并在注册时选择它——业务侧的
调用代码一行都不用改。这层接缝就是当初 Ray ``.remote()`` 提供的价值,这里用零依赖
的方式把它保留下来。
"""
from __future__ import annotations
import asyncio
from typing import Any, Awaitable, Dict, Optional, Set
_registry: Dict[str, Any] = {}
_handle_cache: Dict[str, "ActorHandle"] = {}
# 已 spawn 的后台任务:持强引用,防止事件循环只持弱引用导致任务被 GC 掉
_background_tasks: Set["asyncio.Task[Any]"] = set()
class _BoundMethod:
"""包装目标对象的一个方法,使调用统一返回 awaitable。
- 目标方法是 ``async def`` → 直接返回其协程
- 目标方法是普通函数 → 把返回值包成一个已完成的 awaitable
这样调用方永远可以 ``await handle.method(...)``,无需关心同步/异步、本地/远程。
"""
__slots__ = ("_fn",)
def __init__(self, fn: Any) -> None:
self._fn = fn
def __call__(self, *args: Any, **kwargs: Any) -> Any:
result = self._fn(*args, **kwargs)
if asyncio.iscoroutine(result):
return result
async def _wrap() -> Any:
return result
return _wrap()
class ActorHandle:
"""actor 句柄基类:定义位置透明的调用接口。
未来的 ``RemoteActorHandle`` 继承本类,改写方法解析为跨进程 RPC 即可。
"""
class LocalActorHandle(ActorHandle):
"""本进程句柄:方法调用直接转发到已注册的实例。"""
__slots__ = ("_impl",)
def __init__(self, impl: Any) -> None:
object.__setattr__(self, "_impl", impl)
def __getattr__(self, name: str) -> Any:
attr = getattr(object.__getattribute__(self, "_impl"), name)
if callable(attr):
return _BoundMethod(attr)
return attr
def register_actor(name: str, instance: Any) -> None:
"""注册一个 actor 实例。重复注册会覆盖(支持插件热重载)。"""
_registry[name] = instance
_handle_cache.pop(name, None)
def unregister_actor(name: str) -> None:
"""注销一个 actor(插件卸载用);不存在时静默。"""
_registry.pop(name, None)
_handle_cache.pop(name, None)
def get_actor(name: str) -> ActorHandle:
"""按名字取 actor 句柄;未注册时抛 KeyError。
动态名(如 ``f"org_{plugin}"``)也走这个统一入口。
"""
if name not in _registry:
raise KeyError(f"actor {name!r} 未注册")
handle = _handle_cache.get(name)
if handle is None:
handle = LocalActorHandle(_registry[name])
_handle_cache[name] = handle
return handle
def try_get_actor(name: str) -> Optional[ActorHandle]:
"""按名字取 actor 句柄;未注册时返回 None(可选依赖用)。"""
try:
return get_actor(name)
except KeyError:
return None
def clear_actors() -> None:
"""清空注册表(测试用)。"""
_registry.clear()
_handle_cache.clear()
_background_tasks.clear()
def spawn_background(coro: "Awaitable[Any]") -> "asyncio.Task[Any]":
"""在当前事件循环中后台调度一个协程,并维持强引用防止被 GC。
等价于旧 Ray `.remote()` 的 fire-and-forget 语义:
- 协程被调度执行;
- 异常不会静默消失,而是记录到日志;
- 调用方无需 await,也无需保存返回值。
"""
from kilostar.utils.logger import get_logger
task: asyncio.Task[Any] = asyncio.ensure_future(coro)
_background_tasks.add(task)
def _on_done(t: "asyncio.Task[Any]") -> None:
_background_tasks.discard(t)
if not t.cancelled() and t.exception() is not None:
get_logger("actor").exception(
f"后台任务 {t.get_name()!r} 异常退出", exc_info=t.exception()
)
task.add_done_callback(_on_done)
return task
# ─── 类型化 getter(核心单例,语义糖)──────────────────────────
def get_postgres() -> ActorHandle:
return get_actor("postgres_database")
def get_gsm() -> ActorHandle:
return get_actor("global_state_machine")
def get_gwm() -> ActorHandle:
return get_actor("global_workflow_manager")
def get_regulatory() -> ActorHandle:
return get_actor("regulatory_node")
def get_consciousness() -> ActorHandle:
return get_actor("consciousness_node")
def get_worker_cluster() -> ActorHandle:
return get_actor("worker_cluster")
def get_plugin_manager() -> ActorHandle:
return get_actor("global_plugin_manager")
-40
View File
@@ -1,40 +0,0 @@
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pydantic import BaseModel
class ResponseModel(BaseModel):
"""
ResponseModel类
继承自pydantic的BaseModel类,是一切回复模型的父类
"""
pass
class DepsModel(BaseModel):
"""
DepsModel类
继承自pydantic的BaseModel类,是agent运行时依赖模型的父类
"""
pass
class RequestModel(BaseModel):
"""
RequestModel类
继承自pydantic的BaseModel类,是一切请求模型的父类
"""
pass
-117
View File
@@ -1,117 +0,0 @@
import importlib.util
import json
import os
import sys
from typing import Callable, Dict, List, Optional
from kilostar.utils.logger import get_logger
from kilostar.utils.settings import get_toolset_dir
logger = get_logger("get_tool")
_tool_cache: Dict[str, Callable] = {}
_manifest_cache: Optional[Dict[str, Dict]] = None
def _load_manifests() -> Dict[str, Dict]:
"""扫描所有 toolset 的 manifest.json,建立 tool_name → {toolset_dir, file} 的映射。"""
global _manifest_cache
if _manifest_cache is not None:
return _manifest_cache
_manifest_cache = {}
toolset_root = get_toolset_dir()
if not toolset_root.exists():
return _manifest_cache
for item in toolset_root.iterdir():
if not item.is_dir() or item.name.startswith("__"):
continue
manifest_path = item / "manifest.json"
if not manifest_path.exists():
continue
try:
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = json.load(f)
for tool in manifest.get("tools", []):
tool_name = tool.get("name")
if tool_name:
_manifest_cache[tool_name] = {
"toolset_dir": str(item),
"toolset_name": item.name,
"file": tool.get("file", f"{tool_name}.py"),
}
except Exception as e:
logger.error(f"Failed to read manifest {manifest_path}: {e}")
return _manifest_cache
def _get_tool_func(tool_name: str) -> Callable | None:
"""按名字从 toolset 中加载工具函数。
根据 manifest 找到工具所在的 toolset 和文件,动态加载模块并取出同名函数。
"""
func = _tool_cache.get(tool_name)
if func:
return func
manifests = _load_manifests()
info = manifests.get(tool_name)
if not info:
logger.error(f"Tool '{tool_name}' not found in any toolset manifest")
return None
tool_file = os.path.join(info["toolset_dir"], info["file"])
if not os.path.exists(tool_file):
logger.error(f"Tool file not found: {tool_file}")
return None
try:
module_name = f"data.toolset.{info['toolset_name']}.{tool_name}"
spec = importlib.util.spec_from_file_location(module_name, tool_file)
if spec is None or spec.loader is None:
logger.error(f"Failed to create spec for {module_name}")
return None
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
func = getattr(module, tool_name, None)
if not callable(func):
logger.error(
f"Tool function '{tool_name}' not found or not callable in {module_name}"
)
return None
_tool_cache[tool_name] = func
return func
except Exception as e:
logger.error(f"Failed to load module {tool_name}: {e}")
return None
def del_tool_cache(tool_name: str) -> None:
"""从内存缓存中移除某个工具,下次调用 ``load_tools_from_list`` 会重新从磁盘加载。"""
if tool_name in _tool_cache:
del _tool_cache[tool_name]
def invalidate_manifest_cache() -> None:
"""清除 manifest 缓存,下次加载时重新扫描磁盘。"""
global _manifest_cache
_manifest_cache = None
def load_tools_from_list(tool_names: List[str] | None) -> List[Callable]:
"""批量加载工具:传入工具名列表,返回成功加载到的函数对象列表(失败项被跳过)。"""
if not tool_names:
return []
tool_list = []
for tool_name in tool_names:
tool_func = _get_tool_func(tool_name)
if tool_func:
tool_list.append(tool_func)
return tool_list
+5 -5
View File
@@ -17,7 +17,7 @@
from typing import Dict, List, Any, Optional, Sequence
from kilostar.utils.logger import get_logger
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
logger = get_logger("mcp_helper")
@@ -132,8 +132,8 @@ async def get_all_tools_and_toolsets_for_scope(
# 合入重型插件的 dispatch tools
try:
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
dispatch = await pm.get_dispatch_tools.remote()
pm = get_actor("global_plugin_manager")
dispatch = await pm.get_dispatch_tools()
if dispatch:
tools.extend(dispatch.values())
except Exception as e:
@@ -154,8 +154,8 @@ async def get_retrieval_toolsets_for_scope(scope: str) -> List[Any]:
"""仅返回 retrieval 工具集(system_node 专用)。不含 generation 和 MCP 工具。"""
toolsets: List[Any] = []
try:
gsm = ray_actor_hook("global_state_machine").global_state_machine
retrieval = await gsm.get_retrieval_toolsets_for_scope.remote(scope)
gsm = get_actor("global_state_machine")
retrieval = await gsm.get_retrieval_toolsets_for_scope(scope)
if retrieval:
toolsets.extend(retrieval)
except Exception as e:
-106
View File
@@ -1,106 +0,0 @@
"""KiloStar Ray 兼容层:单机/分布式模式无感切换 + 序列化工具。
单机模式下,所有 Actor 退化为普通 Python 异步单例,通过 StandaloneProxy
包装后暴露与 Ray Actor Handle 相同的 `.method.remote(args)` 调用接口,
使上层代码在两种模式间无感切换。
"""
from __future__ import annotations
import asyncio
import os
from typing import Any, Type, TypeVar
from pydantic import BaseModel
_STANDALONE = os.environ.get("KILOSTAR_MODE", "distributed") == "standalone"
T = TypeVar("T", bound=Type[BaseModel])
class _MethodProxy:
"""包装单个方法,使 .remote(*args, **kwargs) 返回一个可 await 的 Task。"""
__slots__ = ("_method",)
def __init__(self, method: Any):
self._method = method
def remote(self, *args: Any, **kwargs: Any) -> asyncio.Task:
async def _invoke():
result = self._method(*args, **kwargs)
if asyncio.iscoroutine(result):
return await result
return result
return asyncio.ensure_future(_invoke())
class StandaloneProxy:
"""包装一个普通 Python 实例,模拟 Ray Actor Handle 的属性访问接口。
用法:proxy.some_method.remote(x, y) → 等效于 await instance.some_method(x, y)
"""
__slots__ = ("_instance",)
def __init__(self, instance: Any):
object.__setattr__(self, "_instance", instance)
def __getattr__(self, name: str) -> _MethodProxy:
attr = getattr(object.__getattribute__(self, "_instance"), name)
if callable(attr):
return _MethodProxy(attr)
return attr
# ─── 条件装饰器 ───
def actor_class(cls):
"""条件装饰器:分布式模式 → @ray.remote,单机模式 → 原样返回类。"""
if _STANDALONE:
return cls
import ray
return ray.remote(cls)
def remote_task(func):
"""条件装饰器:分布式 → @ray.remote(func),单机 → .remote() 转为 asyncio task。
单机模式下返回一个 stub 对象,其 .remote() 方法把函数以协程方式调度到
当前事件循环(workflow task 需要用 await 版本的 _entry,由调用方处理)。
"""
if _STANDALONE:
class _TaskProxy:
@staticmethod
def remote(*args, **kwargs):
async def _run():
result = func(*args, **kwargs)
if asyncio.iscoroutine(result):
return await result
return result
return asyncio.ensure_future(_run())
return _TaskProxy()
import ray
return ray.remote(func)
# ─── Pickle (Ray 序列化优化) ───
def pickle(cls: T) -> T:
"""类装饰器:用 Pydantic 的高效 JSON 序列化替代 Python 原生 __reduce__,
使 Ray 跨进程通信时对 BaseModel 子类走 Rust 级序列化。
"""
def __reduce__(self):
data = self.model_dump_json()
return cls.model_validate_json, (data,)
cls.__reduce__ = __reduce__
return cls
-142
View File
@@ -1,142 +0,0 @@
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import time
from functools import lru_cache
from typing import Any, Dict
from kilostar.utils.ray_compat import _STANDALONE
if not _STANDALONE:
import ray
class ActorList:
"""属性式访问的简易容器,用 ``a.actor_name`` 取代 ``d["actor_name"]``。"""
def __init__(self):
super().__setattr__("dict", {})
def __setattr__(self, key, value):
self.dict[key] = value
def __getattr__(self, key):
if key in self.dict:
return self.dict[key]
raise AttributeError(f"ActorList 对象没有属性 '{key}'")
def __delattr__(self, key):
if key in self.dict:
del self.dict[key]
else:
raise AttributeError(f"ActorList对象没有属性 '{key}'")
# ─── Standalone Registry ───
_standalone_registry: Dict[str, Any] = {}
def register_standalone(name: str, instance: Any) -> None:
"""注册一个单机模式下的 Actor 单例(已包装为 StandaloneProxy)。"""
from kilostar.utils.ray_compat import StandaloneProxy
_standalone_registry[name] = StandaloneProxy(instance)
# ─── Distributed Mode Helpers ───
if not _STANDALONE:
@lru_cache(maxsize=128)
def _get_cached_actor_handle(actor_name: str):
"""缓存接口"""
return ray.get_actor(actor_name, namespace="kilostar")
def clear_actor_cache():
"""清理接口"""
_get_cached_actor_handle.cache_clear()
def wait_for_actor(
actor_name: str, *, timeout: float = 10.0, interval: float = 0.5
):
"""阻塞等待某个 actor 就绪,返回其句柄。"""
deadline = time.monotonic() + max(timeout, 0.0)
last_err: Exception | None = None
while True:
try:
return _get_cached_actor_handle(actor_name)
except Exception as e:
last_err = e
if time.monotonic() >= deadline:
raise TimeoutError(
f"等待 actor {actor_name!r} 就绪超时({timeout}s):{last_err}"
) from last_err
time.sleep(interval)
else:
def _get_cached_actor_handle(actor_name: str):
raise RuntimeError("单机模式下不应调用 _get_cached_actor_handle")
def clear_actor_cache():
pass
def wait_for_actor(actor_name: str, **kwargs):
raise RuntimeError("单机模式下不应调用 wait_for_actor")
# ─── 统一入口 ───
def ray_actor_hook(*actor_names: str, timeout: float = 0.0, interval: float = 0.5):
"""按名字批量取出 Actor 句柄,组装成一个 ActorList 返回。
单机模式从 _standalone_registry 取,分布式模式走 ray.get_actor。
"""
actor_list = ActorList()
if _STANDALONE:
for name in actor_names:
if name not in _standalone_registry:
raise ValueError(
f"Standalone registry: actor {name!r} not registered"
)
setattr(actor_list, name, _standalone_registry[name])
return actor_list
for actor_name in actor_names:
if timeout > 0:
handle = wait_for_actor(
actor_name, timeout=timeout, interval=interval
)
else:
handle = _get_cached_actor_handle(actor_name)
setattr(actor_list, actor_name, handle)
return actor_list
def get_worker_cluster(affinity: str = "cpu"):
"""按 node_affinity 标签取对应的 WorkerCluster actor 句柄。
单机模式统一返回唯一的 worker_cluster 实例。
分布式模式按 affinity 路由到 worker_cluster_cpu / _core / _gpu。
未知标签降级到 cpu。
"""
if _STANDALONE:
return _standalone_registry.get("worker_cluster")
_valid = {"cpu", "core", "gpu"}
node_type = affinity if affinity in _valid else "cpu"
return _get_cached_actor_handle(f"worker_cluster_{node_type}")
+4 -26
View File
@@ -27,8 +27,8 @@
1. ``contextvars`` 在 ``asyncio`` 协程间天然继承,不会跨协程串味;
2. ``loguru`` 的 ``patcher`` 钩子可以把它变成日志切面,业务代码不需要在每条
``logger.info`` 上手动 ``.bind(trace_id=...)``;
3. Ray 跨进程调用时 contextvars 不会自动传播 —— 这是有意为之,避免不同 actor
间的上下文意外串联。跨 actor 边界要走显式参数,由接收方再 ``bind_*`` 一次。
3. 跨 asyncio.create_task 边界时也会自动继承当前 context,符合单进程 actor 模型
的语义 —— 一次任务链路的 ID 与其入口请求一致,不必显式透传。
"""
from __future__ import annotations
@@ -36,7 +36,7 @@ from __future__ import annotations
import uuid
from contextlib import contextmanager
from contextvars import ContextVar, Token
from typing import Iterator, Optional
from typing import Iterator
_request_id_var: ContextVar[str] = ContextVar("kilostar_request_id", default="")
@@ -101,30 +101,8 @@ def new_request_id(prefix: str = "req") -> str:
def snapshot() -> dict[str, str]:
"""返回当前上下文 ID 的快照,便于跨 actor/task 边界显式透传。"""
"""返回当前上下文 ID 的快照,便于日志/追踪或结构化写库。"""
return {
"request_id": _request_id_var.get(),
"trace_id": _trace_id_var.get(),
}
@contextmanager
def apply_snapshot(snap: Optional[dict[str, str]]) -> Iterator[None]:
"""把外部传来的 snapshot 在当前 context 内生效一次(用于跨 Ray actor 调用时)。"""
if not snap:
yield
return
tokens: list[Token] = []
if snap.get("request_id"):
tokens.append(_request_id_var.set(snap["request_id"]))
if snap.get("trace_id"):
tokens.append(_trace_id_var.set(snap["trace_id"]))
try:
yield
finally:
for tok in reversed(tokens):
try:
tok.var.reset(tok)
except (ValueError, LookupError):
# token 可能因协程切换失效,宽容处理
pass
-1
View File
@@ -38,7 +38,6 @@ class OnebotSettings(BaseSettings):
class AppSettings(BaseSettings):
kilostar_mode: str = "distributed"
kilostar_lang: str = "zh"
kilostar_cors_origins: str = ""
kilostar_plugin_dir: str = ""