my-pi-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -0
- package/package.json +45 -0
- package/pyproject.toml +50 -0
- package/src/my_agent_core/__init__.py +123 -0
- package/src/my_agent_core/agent.py +441 -0
- package/src/my_agent_core/background.py +121 -0
- package/src/my_agent_core/context.py +505 -0
- package/src/my_agent_core/events.py +153 -0
- package/src/my_agent_core/extensions/__init__.py +9 -0
- package/src/my_agent_core/extensions/core.py +197 -0
- package/src/my_agent_core/hooks.py +130 -0
- package/src/my_agent_core/loop.py +709 -0
- package/src/my_agent_core/main.py +134 -0
- package/src/my_agent_core/memory.py +241 -0
- package/src/my_agent_core/message_queue.py +110 -0
- package/src/my_agent_core/plugins.py +212 -0
- package/src/my_agent_core/registry.py +186 -0
- package/src/my_agent_core/session/__init__.py +79 -0
- package/src/my_agent_core/session/entries.py +197 -0
- package/src/my_agent_core/session/jsonl.py +60 -0
- package/src/my_agent_core/session/memory.py +137 -0
- package/src/my_agent_core/session/session.py +400 -0
- package/src/my_agent_core/session/storage.py +245 -0
- package/src/my_agent_core/session/store.py +131 -0
- package/src/my_agent_core/session/tree.py +86 -0
- package/src/my_agent_core/skills.py +149 -0
- package/src/my_agent_core/subagent_tasks.py +170 -0
- package/src/my_agent_core/subagents.py +148 -0
- package/src/my_agent_core/task_store.py +248 -0
- package/src/my_agent_core/tool_history.py +189 -0
- package/src/my_agent_core/tools/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/task.py +30 -0
- package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
- package/src/my_agent_core/tools/core.py +239 -0
- package/src/my_agent_llm/__init__.py +45 -0
- package/src/my_agent_llm/auth/__init__.py +46 -0
- package/src/my_agent_llm/auth/antigravity.py +209 -0
- package/src/my_agent_llm/auth/manager.py +259 -0
- package/src/my_agent_llm/auth/quota.py +56 -0
- package/src/my_agent_llm/auth/schema.py +94 -0
- package/src/my_agent_llm/client.py +116 -0
- package/src/my_agent_llm/config.py +17 -0
- package/src/my_agent_llm/events.py +84 -0
- package/src/my_agent_llm/models.py +195 -0
- package/src/my_agent_llm/providers/__init__.py +4 -0
- package/src/my_agent_llm/providers/_base.py +94 -0
- package/src/my_agent_llm/providers/anthropic.py +298 -0
- package/src/my_agent_llm/providers/antigravity.py +480 -0
- package/src/my_agent_llm/providers/deepseek.py +196 -0
- package/src/my_agent_llm/providers/openai.py +364 -0
- package/src/my_agent_llm/providers/registry.py +16 -0
- package/src/my_agent_llm/stream.py +218 -0
- package/src/my_coding_agent/__init__.py +66 -0
- package/src/my_coding_agent/agent.py +208 -0
- package/src/my_coding_agent/cli.py +78 -0
- package/src/my_coding_agent/file_reference.py +80 -0
- package/src/my_coding_agent/macro.py +408 -0
- package/src/my_coding_agent/mcp.py +243 -0
- package/src/my_coding_agent/mutation_queue.py +37 -0
- package/src/my_coding_agent/paths.py +119 -0
- package/src/my_coding_agent/permissions.py +84 -0
- package/src/my_coding_agent/prompt.py +54 -0
- package/src/my_coding_agent/rpc_server.py +2817 -0
- package/src/my_coding_agent/settings.py +126 -0
- package/src/my_coding_agent/tools/__init__.py +55 -0
- package/src/my_coding_agent/tools/base.py +58 -0
- package/src/my_coding_agent/tools/bash.py +206 -0
- package/src/my_coding_agent/tools/edit.py +226 -0
- package/src/my_coding_agent/tools/find.py +118 -0
- package/src/my_coding_agent/tools/grep.py +177 -0
- package/src/my_coding_agent/tools/ls.py +112 -0
- package/src/my_coding_agent/tools/read.py +113 -0
- package/src/my_coding_agent/tools/write.py +72 -0
- package/tui/README.md +27 -0
- package/tui/bin/my-agent.js +98 -0
- package/tui/dist/app.d.ts +41 -0
- package/tui/dist/app.js +110 -0
- package/tui/dist/bridge/event-translator.d.ts +92 -0
- package/tui/dist/bridge/event-translator.js +216 -0
- package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
- package/tui/dist/bridge/kernel-bridge.js +132 -0
- package/tui/dist/client.d.ts +63 -0
- package/tui/dist/client.js +239 -0
- package/tui/dist/components/assistant-message.d.ts +19 -0
- package/tui/dist/components/assistant-message.js +90 -0
- package/tui/dist/components/compaction-summary-message.d.ts +19 -0
- package/tui/dist/components/compaction-summary-message.js +46 -0
- package/tui/dist/components/custom-editor.d.ts +18 -0
- package/tui/dist/components/custom-editor.js +56 -0
- package/tui/dist/components/dynamic-border.d.ts +9 -0
- package/tui/dist/components/dynamic-border.js +14 -0
- package/tui/dist/components/footer.d.ts +39 -0
- package/tui/dist/components/footer.js +199 -0
- package/tui/dist/components/header.d.ts +4 -0
- package/tui/dist/components/header.js +21 -0
- package/tui/dist/components/keys.d.ts +5 -0
- package/tui/dist/components/keys.js +12 -0
- package/tui/dist/components/login-selector.d.ts +26 -0
- package/tui/dist/components/login-selector.js +181 -0
- package/tui/dist/components/logout-selector.d.ts +19 -0
- package/tui/dist/components/logout-selector.js +88 -0
- package/tui/dist/components/model-selector.d.ts +40 -0
- package/tui/dist/components/model-selector.js +268 -0
- package/tui/dist/components/session-selector.d.ts +54 -0
- package/tui/dist/components/session-selector.js +393 -0
- package/tui/dist/components/settings-selector.d.ts +24 -0
- package/tui/dist/components/settings-selector.js +146 -0
- package/tui/dist/components/status-indicator.d.ts +25 -0
- package/tui/dist/components/status-indicator.js +60 -0
- package/tui/dist/components/theme-selector.d.ts +14 -0
- package/tui/dist/components/theme-selector.js +77 -0
- package/tui/dist/components/thinking-selector.d.ts +21 -0
- package/tui/dist/components/thinking-selector.js +128 -0
- package/tui/dist/components/tool-execution.d.ts +31 -0
- package/tui/dist/components/tool-execution.js +206 -0
- package/tui/dist/components/tree-selector.d.ts +40 -0
- package/tui/dist/components/tree-selector.js +173 -0
- package/tui/dist/components/user-message-selector.d.ts +21 -0
- package/tui/dist/components/user-message-selector.js +103 -0
- package/tui/dist/components/user-message.d.ts +5 -0
- package/tui/dist/components/user-message.js +15 -0
- package/tui/dist/index.d.ts +11 -0
- package/tui/dist/index.js +11 -0
- package/tui/dist/interactive/chat-viewport.d.ts +19 -0
- package/tui/dist/interactive/chat-viewport.js +41 -0
- package/tui/dist/interactive/components.d.ts +1 -0
- package/tui/dist/interactive/components.js +1 -0
- package/tui/dist/interactive/interactive-mode.d.ts +89 -0
- package/tui/dist/interactive/interactive-mode.js +1625 -0
- package/tui/dist/interactive/theme.d.ts +1 -0
- package/tui/dist/interactive/theme.js +1 -0
- package/tui/dist/interactive/tui-renderer.d.ts +8 -0
- package/tui/dist/interactive/tui-renderer.js +10 -0
- package/tui/dist/protocol.d.ts +78 -0
- package/tui/dist/protocol.js +1 -0
- package/tui/dist/theme/dark.json +54 -0
- package/tui/dist/theme/light.json +71 -0
- package/tui/dist/theme/theme.d.ts +20 -0
- package/tui/dist/theme/theme.js +86 -0
- package/tui/package.json +25 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""extension 机制 —— 外部扩展加载(事件订阅 + 工具注册 + 命令)。
|
|
2
|
+
|
|
3
|
+
ExtensionAPI 是传给 extension 的能力面(on / tool / command 三件套);
|
|
4
|
+
ExtensionManager 是 Repository(发现 / 加载 / 命令调度),对标 SkillManager /
|
|
5
|
+
SubagentManager。加载协议:扩展文件定义 `def extension(api)`(或 default /
|
|
6
|
+
第一个形参名含 api 的函数),由 importlib 动态加载。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
import inspect
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING, Any, overload
|
|
16
|
+
|
|
17
|
+
from my_agent_core.events import Event
|
|
18
|
+
from my_agent_core.tools import Tool
|
|
19
|
+
from my_agent_core.tools import tool as _tool
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from my_agent_core.agent import Agent
|
|
23
|
+
|
|
24
|
+
CommandHandler = Callable[..., Any]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ExtensionAPI:
|
|
28
|
+
"""暴露给 extension 的能力面:事件订阅 + 工具注册 + 命令。"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, agent: Agent) -> None:
|
|
31
|
+
self.agent = agent
|
|
32
|
+
self._commands: dict[str, CommandHandler] = {}
|
|
33
|
+
self._descriptions: dict[str, str] = {}
|
|
34
|
+
|
|
35
|
+
# ── 事件订阅 / Hook 拦截注册(支持 @api.on 作装饰器)────────
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def on(
|
|
39
|
+
self, target: type, handler: None = None
|
|
40
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def on(self, target: type, handler: Callable[..., Any]) -> None: ...
|
|
44
|
+
|
|
45
|
+
def on(self, target: type, handler: Callable[..., Any] | None = None) -> Any:
|
|
46
|
+
"""注册只读事件订阅(Event 监听)或双向 Hook 拦截点回调(Hook 拦截干预)。
|
|
47
|
+
|
|
48
|
+
handler 签名统一为 (payload, api)。
|
|
49
|
+
- 若 target 是 Event 子类:注册至 agent.subscribe() 作为只读通知,忽略返回值;
|
|
50
|
+
- 否则:注册至 agent.hooks.register(),可返回 HookResult 进行干预。
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def _register(h: Callable[..., Any]) -> Callable[..., Any]:
|
|
54
|
+
if isinstance(target, type) and issubclass(target, Event):
|
|
55
|
+
|
|
56
|
+
def event_listener(event: Event) -> Any:
|
|
57
|
+
if isinstance(event, target):
|
|
58
|
+
return h(event, self)
|
|
59
|
+
|
|
60
|
+
self.agent.subscribe(event_listener)
|
|
61
|
+
else:
|
|
62
|
+
if inspect.iscoroutinefunction(h):
|
|
63
|
+
|
|
64
|
+
async def wrapped_async(hook_payload: Any) -> Any:
|
|
65
|
+
return await h(hook_payload, self)
|
|
66
|
+
|
|
67
|
+
self.agent.hooks.register(target, wrapped_async)
|
|
68
|
+
else:
|
|
69
|
+
|
|
70
|
+
def wrapped_sync(hook_payload: Any) -> Any:
|
|
71
|
+
return h(hook_payload, self)
|
|
72
|
+
|
|
73
|
+
self.agent.hooks.register(target, wrapped_sync)
|
|
74
|
+
return h
|
|
75
|
+
|
|
76
|
+
if handler is not None:
|
|
77
|
+
_register(handler)
|
|
78
|
+
return None
|
|
79
|
+
return _register
|
|
80
|
+
|
|
81
|
+
# ── 工具注册(复用 @tool)────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
def register_tool(self, tool: Tool) -> None:
|
|
84
|
+
"""注册工具 → agent.registry(撞名静默覆盖,registry 语义)。"""
|
|
85
|
+
self.agent.registry.register(tool)
|
|
86
|
+
|
|
87
|
+
def tool(self, **kwargs: Any):
|
|
88
|
+
"""@api.tool(description=...) 装饰器:@tool 包装 + register_tool。"""
|
|
89
|
+
|
|
90
|
+
def decorator(func) -> Tool:
|
|
91
|
+
t = _tool(**kwargs)(func)
|
|
92
|
+
self.register_tool(t)
|
|
93
|
+
return t
|
|
94
|
+
|
|
95
|
+
return decorator
|
|
96
|
+
|
|
97
|
+
# ── 命令(注册 + 存表,调度在 ExtensionManager)────────────────────
|
|
98
|
+
|
|
99
|
+
def register_command(
|
|
100
|
+
self, name: str, handler: CommandHandler, description: str = ""
|
|
101
|
+
) -> None:
|
|
102
|
+
"""注册命令(name 不含 /)。"""
|
|
103
|
+
self._commands[name] = handler
|
|
104
|
+
self._descriptions[name] = description
|
|
105
|
+
|
|
106
|
+
def command(self, name: str, description: str = ""):
|
|
107
|
+
"""@api.command("now") 装饰器。"""
|
|
108
|
+
|
|
109
|
+
def decorator(func: CommandHandler) -> CommandHandler:
|
|
110
|
+
self.register_command(name, func, description)
|
|
111
|
+
return func
|
|
112
|
+
|
|
113
|
+
return decorator
|
|
114
|
+
|
|
115
|
+
def get_commands(self) -> dict[str, CommandHandler]:
|
|
116
|
+
"""已注册命令的拷贝(name → handler)。"""
|
|
117
|
+
return self._commands.copy()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ExtensionManager:
|
|
121
|
+
"""扩展管理器(Repository):发现 / 加载 / 命令调度。"""
|
|
122
|
+
|
|
123
|
+
DEFAULT_DIR_NAME = "extensions" # <cwd>/.agents/extensions
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self, agent: Agent, extension_dirs: Sequence[str | Path] | None = None
|
|
127
|
+
):
|
|
128
|
+
"""解析目录(三态同 skill_dirs):None → 探测 <cwd>/.agents/extensions;
|
|
129
|
+
[] → 禁用;非空 → 只扫这些目录。load() 显式加载(有副作用)。"""
|
|
130
|
+
self.agent = agent
|
|
131
|
+
self.api = ExtensionAPI(agent)
|
|
132
|
+
self.extensions: dict[str, Any] = {}
|
|
133
|
+
if extension_dirs is None:
|
|
134
|
+
dirs = [Path.cwd() / ".agents" / self.DEFAULT_DIR_NAME]
|
|
135
|
+
else:
|
|
136
|
+
dirs = list(extension_dirs)
|
|
137
|
+
self._dirs: list[Path] = [Path(d) for d in dirs]
|
|
138
|
+
|
|
139
|
+
def discover(self, directory: Path | str) -> list[Path]:
|
|
140
|
+
"""扫目录下 **/*.py(递归),跳过 _ 开头私有文件;目录不存在 → []。"""
|
|
141
|
+
directory = Path(directory)
|
|
142
|
+
if not directory.exists():
|
|
143
|
+
return []
|
|
144
|
+
return [p for p in directory.glob("**/*.py") if not p.name.startswith("_")]
|
|
145
|
+
|
|
146
|
+
async def load_extension(self, path: Path | str) -> None:
|
|
147
|
+
"""importlib 动态加载 .py;找 extension → default → 第一个形参名含 api 的函数;
|
|
148
|
+
找不到 → ValueError;找到则 extension_func(self.api) 执行并登记。支持 async/sync 扩展。"""
|
|
149
|
+
path = Path(path)
|
|
150
|
+
if not path.exists():
|
|
151
|
+
raise FileNotFoundError(f"Extension not found: {path}")
|
|
152
|
+
spec = importlib.util.spec_from_file_location(path.stem, path)
|
|
153
|
+
if spec is None or spec.loader is None:
|
|
154
|
+
raise ImportError(f"Cannot load extension: {path}")
|
|
155
|
+
module = importlib.util.module_from_spec(spec)
|
|
156
|
+
spec.loader.exec_module(module)
|
|
157
|
+
|
|
158
|
+
extension_func = None
|
|
159
|
+
if hasattr(module, "extension"):
|
|
160
|
+
extension_func = module.extension
|
|
161
|
+
elif hasattr(module, "default"):
|
|
162
|
+
extension_func = module.default
|
|
163
|
+
else:
|
|
164
|
+
for _name, obj in inspect.getmembers(module, inspect.isfunction):
|
|
165
|
+
sig = inspect.signature(obj)
|
|
166
|
+
params = list(sig.parameters.values())
|
|
167
|
+
if params and "api" in params[0].name.lower():
|
|
168
|
+
extension_func = obj
|
|
169
|
+
break
|
|
170
|
+
if extension_func is None:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Extension {path} must define an 'extension' function that takes ExtensionAPI"
|
|
173
|
+
)
|
|
174
|
+
if inspect.iscoroutinefunction(extension_func):
|
|
175
|
+
await extension_func(self.api)
|
|
176
|
+
else:
|
|
177
|
+
extension_func(self.api)
|
|
178
|
+
self.extensions[path.name] = module
|
|
179
|
+
|
|
180
|
+
async def load(self) -> None:
|
|
181
|
+
"""遍历 self._dirs 逐个 load_extension;单个失败只 print 不抛(隔离坏扩展)。"""
|
|
182
|
+
for directory in self._dirs:
|
|
183
|
+
for path in self.discover(directory):
|
|
184
|
+
try:
|
|
185
|
+
await self.load_extension(path)
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
print(f"Failed to load extension {path}: {exc}")
|
|
188
|
+
|
|
189
|
+
def handle_command(self, command: str, args: str | None = None) -> Any:
|
|
190
|
+
"""查表调用命令(未知命令 ValueError;0 参直接调,>0 参传 args)。"""
|
|
191
|
+
commands = self.api.get_commands()
|
|
192
|
+
if command not in commands:
|
|
193
|
+
raise ValueError(f"Unknown command: /{command}")
|
|
194
|
+
handler = commands[command]
|
|
195
|
+
if len(inspect.signature(handler).parameters) > 0:
|
|
196
|
+
return handler(args)
|
|
197
|
+
return handler()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""专职 Hook 拦截点契约与 HookRegistry —— Agent 关键节点双向拦截干预与控制流门禁。
|
|
2
|
+
|
|
3
|
+
架构设计(对齐 Pi 架构):
|
|
4
|
+
1. 专职 Hook 拦截点契约(*Hook):独立于 Event,专职控制流拦截、入参安全审批与出参改写。
|
|
5
|
+
2. 统一干预结果模型(HookResult):携带 block、reason、updated_* 属性,指示核心状态机如何处置。
|
|
6
|
+
3. HookRegistry:负责拦截钩子的注册、注销、async/sync 混合调用及严格的 Never-Throw 异常隔离。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
import inspect
|
|
13
|
+
import logging
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from my_agent_llm import Message # pyright: ignore[reportMissingImports]
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── 五大专职 Hook 拦截点契约(独立门禁系统,非 Event)
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class UserInputHook:
|
|
26
|
+
"""Hook 1 (input): 拦截或改写用户原始输入文本。"""
|
|
27
|
+
|
|
28
|
+
input_text: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class AgentStartHook:
|
|
33
|
+
"""Hook 2 (before_agent_start): 拦截启动或动态重写 system_prompt。"""
|
|
34
|
+
|
|
35
|
+
system_prompt: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class BeforeModelCallHook:
|
|
40
|
+
"""Hook 3 (context): 调模型前 1ms 审查或临时改写发送视图。"""
|
|
41
|
+
|
|
42
|
+
messages: list[Message]
|
|
43
|
+
iteration: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class ToolCallHook:
|
|
48
|
+
"""Hook 4 (tool_call): 工具执行前安全审批、阻断高危命令或改写入参。"""
|
|
49
|
+
|
|
50
|
+
tool_call_id: str
|
|
51
|
+
tool_name: str
|
|
52
|
+
args: dict[str, Any]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class ToolResultHook:
|
|
57
|
+
"""Hook 5 (tool_result): 工具执行后改写返回内容或篡改报错状态。"""
|
|
58
|
+
|
|
59
|
+
tool_call_id: str
|
|
60
|
+
tool_name: str
|
|
61
|
+
result: str
|
|
62
|
+
is_error: bool
|
|
63
|
+
terminate: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class HookResult:
|
|
68
|
+
"""Hook 拦截点的统一干预结果。返回 None = 纯观察,返回 HookResult = 干预。
|
|
69
|
+
|
|
70
|
+
- UserInputHook 用 block / reason / updated_input(拦截 / 改写用户输入)
|
|
71
|
+
- AgentStartHook 用 block / reason / updated_system_prompt(拦截 / 改写 system prompt)
|
|
72
|
+
- BeforeModelCallHook 用 block / reason / updated_messages(拦截 / 临时改写送给 LLM 的 messages 视图)
|
|
73
|
+
- ToolCallHook 用 block / reason / updated_args(拦截 / 改参数)
|
|
74
|
+
- ToolResultHook 用 updated_result(改结果)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
block: bool = False
|
|
78
|
+
reason: str | None = None
|
|
79
|
+
updated_input: str | None = None
|
|
80
|
+
updated_system_prompt: str | None = None
|
|
81
|
+
updated_messages: list[Message] | None = None
|
|
82
|
+
updated_args: dict[str, Any] | None = None
|
|
83
|
+
updated_result: str | None = None
|
|
84
|
+
terminate: bool | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class HookRegistry:
|
|
88
|
+
"""Hook 注册表:Hook 类型 → callback 列表。
|
|
89
|
+
|
|
90
|
+
支持 async / sync 钩子混合执行与 Never-Throw 异常捕获隔离。
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self._handlers: dict[type, list[Callable[..., Any]]] = {}
|
|
95
|
+
self._hooks = self._handlers
|
|
96
|
+
|
|
97
|
+
def register(self, hook_cls: type, callback: Callable[..., Any]) -> None:
|
|
98
|
+
"""挂一个 hook 回调到 hook 类型。同一 hook 可挂多个,按注册顺序触发。"""
|
|
99
|
+
self._handlers.setdefault(hook_cls, []).append(callback)
|
|
100
|
+
|
|
101
|
+
def unregister(self, hook_cls: type, callback: Callable[..., Any]) -> None:
|
|
102
|
+
"""移除 hook 回调。"""
|
|
103
|
+
with contextlib.suppress(ValueError):
|
|
104
|
+
self._handlers.get(hook_cls, []).remove(callback)
|
|
105
|
+
|
|
106
|
+
async def emit(self, hook_payload: Any) -> HookResult | None:
|
|
107
|
+
"""异步触发 hook 的所有回调,支持协程与普通函数。
|
|
108
|
+
|
|
109
|
+
- 返回第一个非 None 结果(短路)。
|
|
110
|
+
- 坚守 Never-Throw 保证:若回调执行抛出异常,捕获并记录日志,绝不向外抛出异常,继续执行后续回调。
|
|
111
|
+
"""
|
|
112
|
+
for cb in list(self._handlers.get(type(hook_payload), [])):
|
|
113
|
+
try:
|
|
114
|
+
if inspect.iscoroutinefunction(cb):
|
|
115
|
+
result = await cb(hook_payload)
|
|
116
|
+
else:
|
|
117
|
+
result = cb(hook_payload)
|
|
118
|
+
if inspect.isawaitable(result):
|
|
119
|
+
result = await result
|
|
120
|
+
if result is not None:
|
|
121
|
+
return result
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(
|
|
124
|
+
"Hook callback %r failed on %r: %s",
|
|
125
|
+
cb,
|
|
126
|
+
hook_payload,
|
|
127
|
+
e,
|
|
128
|
+
exc_info=True,
|
|
129
|
+
)
|
|
130
|
+
return None
|