virid-core 0.1.0__tar.gz
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.
- virid_core-0.1.0/.gitignore +4 -0
- virid_core-0.1.0/PKG-INFO +4 -0
- virid_core-0.1.0/pyproject.toml +13 -0
- virid_core-0.1.0/src/virid/core/__init__.py +18 -0
- virid_core-0.1.0/src/virid/core/app.py +145 -0
- virid_core-0.1.0/src/virid/core/container.py +56 -0
- virid_core-0.1.0/src/virid/core/core/__init__.py +10 -0
- virid_core-0.1.0/src/virid/core/core/dispatcher.py +281 -0
- virid_core-0.1.0/src/virid/core/core/engine.py +83 -0
- virid_core-0.1.0/src/virid/core/core/interface.py +59 -0
- virid_core-0.1.0/src/virid/core/core/io.py +62 -0
- virid_core-0.1.0/src/virid/core/core/message.py +46 -0
- virid_core-0.1.0/src/virid/core/core/registry.py +63 -0
- virid_core-0.1.0/src/virid/core/core/stage.py +75 -0
- virid_core-0.1.0/src/virid/core/decorators/__init__.py +6 -0
- virid_core-0.1.0/src/virid/core/decorators/decorator.py +208 -0
- virid_core-0.1.0/src/virid/core/util.py +111 -0
- virid_core-0.1.0/tests/core/throughput.py +75 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "virid-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
requires-python = ">=3.10"
|
|
5
|
+
dependencies = []
|
|
6
|
+
|
|
7
|
+
[tool.hatch.build.targets.wheel]
|
|
8
|
+
packages = ["src/virid"] # 告诉 Hatch 顶层是个命名空间
|
|
9
|
+
layout = "src" # 核心:使用标准的 src 布局逻辑
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
from .app import ViridApp,ViridPlugin
|
|
7
|
+
from .core import *
|
|
8
|
+
from .decorators import *
|
|
9
|
+
from .util import toggle_utils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_virid(
|
|
13
|
+
enable_logging: bool = True,
|
|
14
|
+
) -> ViridApp:
|
|
15
|
+
from .app import virid_app
|
|
16
|
+
|
|
17
|
+
toggle_utils(enable_logging)
|
|
18
|
+
return virid_app
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from .core import Engine
|
|
9
|
+
from .container import Container
|
|
10
|
+
from typing import Type, Callable, TypeVar, overload, Any, Protocol
|
|
11
|
+
from .core.message import BaseMessage, EventMessage, SingleMessage
|
|
12
|
+
from .core.interface import TickHook, ExecuteHook, Middleware
|
|
13
|
+
from .core.io import MessageWriter
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
F = TypeVar("F", bound=EventMessage, contravariant=True)
|
|
17
|
+
H = TypeVar("H", bound=SingleMessage, contravariant=True)
|
|
18
|
+
O = TypeVar("O", contravariant=True)
|
|
19
|
+
|
|
20
|
+
installed_plugins = set()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ViridPlugin(Protocol[O]):
|
|
24
|
+
name: str
|
|
25
|
+
|
|
26
|
+
def install(self, app: ViridApp, options: O) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ViridApp:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.engine = Engine()
|
|
32
|
+
self.container = Container()
|
|
33
|
+
self.activate: list[Callable] = [lambda x: x]
|
|
34
|
+
|
|
35
|
+
def on_activate(self, activate: Callable, front: bool = False) -> None:
|
|
36
|
+
if front:
|
|
37
|
+
self.activate.insert(0, activate)
|
|
38
|
+
else:
|
|
39
|
+
self.activate.append(activate)
|
|
40
|
+
|
|
41
|
+
def get(self, identifier: Type[T]) -> T:
|
|
42
|
+
return self.container.get(identifier, self.activate)
|
|
43
|
+
|
|
44
|
+
def bind(self, identifier, singleton=True):
|
|
45
|
+
self.container.bind(identifier, singleton)
|
|
46
|
+
|
|
47
|
+
def tick(self):
|
|
48
|
+
self.engine.tick()
|
|
49
|
+
|
|
50
|
+
def register(
|
|
51
|
+
self, message_class: Type[BaseMessage], system_fn: Callable, priority: int = 0
|
|
52
|
+
):
|
|
53
|
+
self.engine.register(message_class, system_fn, priority)
|
|
54
|
+
|
|
55
|
+
def on_before_tick(self, hook: TickHook, front: bool = False):
|
|
56
|
+
self.engine.on_before_tick(hook, front)
|
|
57
|
+
|
|
58
|
+
def on_after_tick(self, hook: TickHook, front: bool = False):
|
|
59
|
+
self.engine.on_after_tick(hook, front)
|
|
60
|
+
|
|
61
|
+
@overload
|
|
62
|
+
def on_before_execute(
|
|
63
|
+
self,
|
|
64
|
+
message_type: Type[F],
|
|
65
|
+
hook: ExecuteHook[F],
|
|
66
|
+
front: bool = False,
|
|
67
|
+
) -> None: ...
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def on_before_execute(
|
|
71
|
+
self,
|
|
72
|
+
message_type: Type[H],
|
|
73
|
+
hook: ExecuteHook[list[H]],
|
|
74
|
+
front: bool = False,
|
|
75
|
+
) -> None: ...
|
|
76
|
+
|
|
77
|
+
@overload
|
|
78
|
+
def on_before_execute(
|
|
79
|
+
self,
|
|
80
|
+
message_type: Type[BaseMessage],
|
|
81
|
+
hook: ExecuteHook[list[SingleMessage] | EventMessage],
|
|
82
|
+
front: bool = False,
|
|
83
|
+
) -> None: ...
|
|
84
|
+
|
|
85
|
+
def on_before_execute(
|
|
86
|
+
self, message_type: Any, hook: Any, front: bool = False
|
|
87
|
+
) -> None:
|
|
88
|
+
self.engine.on_before_execute(message_type, hook, front)
|
|
89
|
+
|
|
90
|
+
@overload
|
|
91
|
+
def on_after_execute(
|
|
92
|
+
self,
|
|
93
|
+
message_type: Type[F],
|
|
94
|
+
hook: ExecuteHook[F],
|
|
95
|
+
front: bool = False,
|
|
96
|
+
) -> None: ...
|
|
97
|
+
|
|
98
|
+
@overload
|
|
99
|
+
def on_after_execute(
|
|
100
|
+
self,
|
|
101
|
+
message_type: Type[H],
|
|
102
|
+
hook: ExecuteHook[list[H]],
|
|
103
|
+
front: bool = False,
|
|
104
|
+
) -> None: ...
|
|
105
|
+
|
|
106
|
+
@overload
|
|
107
|
+
def on_after_execute(
|
|
108
|
+
self,
|
|
109
|
+
message_type: Type[BaseMessage],
|
|
110
|
+
hook: ExecuteHook[list[SingleMessage] | EventMessage],
|
|
111
|
+
front: bool = False,
|
|
112
|
+
) -> None: ...
|
|
113
|
+
|
|
114
|
+
def on_after_execute(
|
|
115
|
+
self, message_type: Any, hook: Any, front: bool = False
|
|
116
|
+
) -> None:
|
|
117
|
+
self.engine.on_after_execute(message_type, hook, front)
|
|
118
|
+
|
|
119
|
+
def use_middleware(self, middleware: Middleware):
|
|
120
|
+
self.engine.use_middleware(middleware)
|
|
121
|
+
|
|
122
|
+
def use(self, plugin: ViridPlugin[O], options: O) -> ViridApp:
|
|
123
|
+
if plugin.name in installed_plugins:
|
|
124
|
+
MessageWriter.warn(
|
|
125
|
+
f"[Virid Plugin] Duplicate Installation: Plugin {plugin.name} has already been installed."
|
|
126
|
+
)
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
plugin.install(self, options)
|
|
131
|
+
installed_plugins.add(plugin.name)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
MessageWriter.error(
|
|
134
|
+
e, f"[Virid Container] Activation Hook Failed: {plugin.name}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
virid_app = ViridApp()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
from .util import register_base_handlers
|
|
144
|
+
|
|
145
|
+
register_base_handlers(virid_app)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
from typing import Type, TypeVar, Callable, Any, Optional
|
|
7
|
+
from .core.io import MessageWriter
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Container:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self._bindings: dict[Type[Any], tuple[Type[Any], bool]] = {}
|
|
15
|
+
self._singletons: dict[Type[Any], Any] = {}
|
|
16
|
+
|
|
17
|
+
def bind(self, identifier: Type[T], singleton: bool = False) -> None:
|
|
18
|
+
"""
|
|
19
|
+
注册绑定。
|
|
20
|
+
- to: 映射到的目标类。如果不传,默认绑定到自身(toSelf)
|
|
21
|
+
- singleton: 是否为单例。默认为 False (transient)
|
|
22
|
+
"""
|
|
23
|
+
if getattr(identifier, "__virid_component__", False) is True or getattr(identifier, "__virid_controller__", False) is True: # type: ignore
|
|
24
|
+
self._bindings[identifier] = (identifier, singleton)
|
|
25
|
+
|
|
26
|
+
else:
|
|
27
|
+
MessageWriter.error(
|
|
28
|
+
TypeError(
|
|
29
|
+
f"[Virid Container] Invalid Component: The Class {identifier.__name__} whit not @Component decorator."
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def get(self, identifier: Type[T], on_activate: list[Callable]) -> T:
|
|
34
|
+
"""获取实例,执行激活流水线"""
|
|
35
|
+
if identifier not in self._bindings:
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
f"[Virid Container] Unbound: No binding found for {identifier.__name__}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
target_ctor, is_singleton = self._bindings[identifier]
|
|
41
|
+
|
|
42
|
+
# 如果是单例
|
|
43
|
+
if is_singleton:
|
|
44
|
+
if identifier not in self._singletons:
|
|
45
|
+
self._singletons[identifier] = self.activate(target_ctor(), on_activate)
|
|
46
|
+
return self._singletons[identifier]
|
|
47
|
+
|
|
48
|
+
# 如果是多例
|
|
49
|
+
return self.activate(target_ctor(), on_activate)
|
|
50
|
+
|
|
51
|
+
def activate(self, component: Any, on_activate: list[Callable]):
|
|
52
|
+
result = component
|
|
53
|
+
for hook in on_activate:
|
|
54
|
+
result = hook(result)
|
|
55
|
+
|
|
56
|
+
return result
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import Callable, Type
|
|
9
|
+
from .interface import (
|
|
10
|
+
SystemTask,
|
|
11
|
+
TickHook,
|
|
12
|
+
ExecuteHook,
|
|
13
|
+
TickHookContext,
|
|
14
|
+
ExecuteHookContext,
|
|
15
|
+
)
|
|
16
|
+
from .io import MessageWriter
|
|
17
|
+
from .message import BaseMessage, SingleMessage, EventMessage
|
|
18
|
+
from .stage import Stage
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExecutionTask:
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
system_fn: Callable,
|
|
26
|
+
message: EventMessage | list[SingleMessage],
|
|
27
|
+
priority: int,
|
|
28
|
+
hook_context: ExecuteHookContext,
|
|
29
|
+
):
|
|
30
|
+
self.message = message
|
|
31
|
+
self.system_fn = system_fn
|
|
32
|
+
self.priority = priority
|
|
33
|
+
self.hook_context = hook_context
|
|
34
|
+
|
|
35
|
+
def trigger_hook(
|
|
36
|
+
self,
|
|
37
|
+
hooks: list[
|
|
38
|
+
tuple[
|
|
39
|
+
type[BaseMessage],
|
|
40
|
+
ExecuteHook,
|
|
41
|
+
]
|
|
42
|
+
],
|
|
43
|
+
success: bool,
|
|
44
|
+
):
|
|
45
|
+
# 根据是不是数组来取self.message的第一个
|
|
46
|
+
message = self.message[0] if isinstance(self.message, list) else self.message
|
|
47
|
+
try:
|
|
48
|
+
for hook in hooks:
|
|
49
|
+
if isinstance(message, hook[0]):
|
|
50
|
+
hook[1](self.message, self.hook_context, success)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
MessageWriter.error(e, f"[Virid Hook] System Execute Hook Error.\n")
|
|
53
|
+
|
|
54
|
+
def execute(
|
|
55
|
+
self,
|
|
56
|
+
before_execute_hooks: list[
|
|
57
|
+
tuple[
|
|
58
|
+
type[BaseMessage],
|
|
59
|
+
ExecuteHook,
|
|
60
|
+
]
|
|
61
|
+
],
|
|
62
|
+
after_execute_hooks: list[
|
|
63
|
+
tuple[
|
|
64
|
+
type[BaseMessage],
|
|
65
|
+
ExecuteHook,
|
|
66
|
+
]
|
|
67
|
+
],
|
|
68
|
+
):
|
|
69
|
+
success = True
|
|
70
|
+
self.trigger_hook(before_execute_hooks, success)
|
|
71
|
+
try:
|
|
72
|
+
self.system_fn(self.message)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
success = False
|
|
75
|
+
self.trigger_hook(after_execute_hooks, success)
|
|
76
|
+
# 重新丢出错误
|
|
77
|
+
raise e
|
|
78
|
+
|
|
79
|
+
self.trigger_hook(after_execute_hooks, success)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Dispatcher:
|
|
83
|
+
def __init__(self):
|
|
84
|
+
self.stage = Stage()
|
|
85
|
+
self.event_queue: list[EventMessage] = []
|
|
86
|
+
self.dirty_signal_types: dict[Type[SingleMessage], None] = {}
|
|
87
|
+
|
|
88
|
+
self.is_running = False
|
|
89
|
+
self.internal_depth = 0
|
|
90
|
+
self.tick_counter = 0
|
|
91
|
+
|
|
92
|
+
# 两个tick hook
|
|
93
|
+
self.before_tick_hooks: list[TickHook] = []
|
|
94
|
+
self.after_tick_hooks: list[TickHook] = []
|
|
95
|
+
self.tick_payload = {}
|
|
96
|
+
|
|
97
|
+
# 两个execute hook
|
|
98
|
+
self.before_execute_hooks: list[
|
|
99
|
+
tuple[
|
|
100
|
+
type[BaseMessage],
|
|
101
|
+
ExecuteHook,
|
|
102
|
+
]
|
|
103
|
+
] = []
|
|
104
|
+
self.after_execute_hooks: list[
|
|
105
|
+
tuple[
|
|
106
|
+
type[BaseMessage],
|
|
107
|
+
ExecuteHook,
|
|
108
|
+
]
|
|
109
|
+
] = []
|
|
110
|
+
|
|
111
|
+
def add_before_tick_hook(self, hook: TickHook, front: bool = False):
|
|
112
|
+
if front:
|
|
113
|
+
self.before_tick_hooks.insert(0, hook)
|
|
114
|
+
else:
|
|
115
|
+
self.before_tick_hooks.append(hook)
|
|
116
|
+
|
|
117
|
+
def add_after_tick_hook(self, hook: TickHook, front: bool = False):
|
|
118
|
+
if front:
|
|
119
|
+
self.after_tick_hooks.insert(0, hook)
|
|
120
|
+
else:
|
|
121
|
+
self.after_tick_hooks.append(hook)
|
|
122
|
+
|
|
123
|
+
def add_before_execute_hook(
|
|
124
|
+
self, message_type: Type[BaseMessage], hook: ExecuteHook, front: bool = False
|
|
125
|
+
):
|
|
126
|
+
if front:
|
|
127
|
+
self.before_execute_hooks.insert(0, (message_type, hook))
|
|
128
|
+
else:
|
|
129
|
+
self.before_execute_hooks.append((message_type, hook))
|
|
130
|
+
|
|
131
|
+
def add_after_execute_hook(
|
|
132
|
+
self, message_type: Type[BaseMessage], hook: ExecuteHook, front: bool = False
|
|
133
|
+
):
|
|
134
|
+
if front:
|
|
135
|
+
self.after_execute_hooks.insert(0, (message_type, hook))
|
|
136
|
+
else:
|
|
137
|
+
self.after_execute_hooks.append((message_type, hook))
|
|
138
|
+
|
|
139
|
+
def mark_dirty(self, message: BaseMessage):
|
|
140
|
+
self.stage.push(message)
|
|
141
|
+
# 根据消息类型放进不同的池子里
|
|
142
|
+
if isinstance(message, SingleMessage):
|
|
143
|
+
self.dirty_signal_types[type(message)] = None
|
|
144
|
+
elif isinstance(message, EventMessage):
|
|
145
|
+
self.event_queue.append(message)
|
|
146
|
+
|
|
147
|
+
def tick(self, system_task_map: dict[Type[BaseMessage], list[SystemTask]]):
|
|
148
|
+
|
|
149
|
+
if self.is_running or (
|
|
150
|
+
len(self.dirty_signal_types) == 0 and len(self.event_queue) == 0
|
|
151
|
+
):
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if self.internal_depth == 0:
|
|
155
|
+
self.tick_payload = {}
|
|
156
|
+
self.execute_hooks(self.before_tick_hooks)
|
|
157
|
+
|
|
158
|
+
if self.internal_depth > 100:
|
|
159
|
+
self.internal_depth = 0
|
|
160
|
+
self.dirty_signal_types.clear()
|
|
161
|
+
self.event_queue.clear()
|
|
162
|
+
self.stage.reset()
|
|
163
|
+
MessageWriter.error(
|
|
164
|
+
RuntimeError(
|
|
165
|
+
"[Virid Dispatcher] Internal depth exceeded 100. Possible infinite loop detected. 💥."
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
self.is_running = True
|
|
171
|
+
self.internal_depth += 1
|
|
172
|
+
|
|
173
|
+
# 正式开始任务
|
|
174
|
+
signal_snapshot = set()
|
|
175
|
+
event_snapshot = list()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
signal_snapshot, event_snapshot = self.prepare_snapshot()
|
|
179
|
+
tasks = self.collect_tasks(
|
|
180
|
+
event_snapshot,
|
|
181
|
+
signal_snapshot,
|
|
182
|
+
system_task_map,
|
|
183
|
+
)
|
|
184
|
+
self.execute_tasks(tasks)
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
MessageWriter.error(e)
|
|
188
|
+
|
|
189
|
+
finally:
|
|
190
|
+
self.clear()
|
|
191
|
+
self.is_running = False
|
|
192
|
+
if len(self.dirty_signal_types) > 0 or len(self.event_queue) > 0:
|
|
193
|
+
self.tick(system_task_map)
|
|
194
|
+
else:
|
|
195
|
+
self.internal_depth = 0
|
|
196
|
+
self.execute_hooks(self.after_tick_hooks)
|
|
197
|
+
self.tick_counter += 1
|
|
198
|
+
|
|
199
|
+
def collect_tasks(
|
|
200
|
+
self,
|
|
201
|
+
event_snapshot: list[EventMessage],
|
|
202
|
+
signal_snapshot: list[Type[SingleMessage]],
|
|
203
|
+
system_task_map: dict[Type[BaseMessage], list[SystemTask]],
|
|
204
|
+
):
|
|
205
|
+
tasks: list[ExecutionTask] = []
|
|
206
|
+
# 处理Event消息
|
|
207
|
+
for msg in event_snapshot:
|
|
208
|
+
for system_task in system_task_map.get(type(msg), []):
|
|
209
|
+
tasks.append(
|
|
210
|
+
ExecutionTask(
|
|
211
|
+
system_task.system_fn,
|
|
212
|
+
msg,
|
|
213
|
+
system_task.priority,
|
|
214
|
+
ExecuteHookContext(
|
|
215
|
+
tick=self.tick_counter,
|
|
216
|
+
context=system_task.system_fn.system_context, # type: ignore
|
|
217
|
+
payload={},
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# 处理Signal消息
|
|
223
|
+
for msg_cls in signal_snapshot:
|
|
224
|
+
for system_task in system_task_map.get(msg_cls, []):
|
|
225
|
+
tasks.append(
|
|
226
|
+
ExecutionTask(
|
|
227
|
+
system_task.system_fn,
|
|
228
|
+
self.stage.peek_signal(msg_cls),
|
|
229
|
+
system_task.priority,
|
|
230
|
+
ExecuteHookContext(
|
|
231
|
+
tick=self.tick_counter,
|
|
232
|
+
context=system_task.system_fn.system_context, # type: ignore
|
|
233
|
+
payload={},
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return tasks
|
|
239
|
+
|
|
240
|
+
def execute_tasks(
|
|
241
|
+
self,
|
|
242
|
+
tasks: list[ExecutionTask],
|
|
243
|
+
):
|
|
244
|
+
# 按照优先级排序
|
|
245
|
+
tasks.sort(key=lambda task: task.priority, reverse=True)
|
|
246
|
+
|
|
247
|
+
for task in tasks:
|
|
248
|
+
try:
|
|
249
|
+
task.execute(self.before_execute_hooks, self.after_execute_hooks)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
MessageWriter.error(
|
|
252
|
+
e,
|
|
253
|
+
f"[virid Dispatcher]: System Error. \n"
|
|
254
|
+
+ f"SystemLocation: {task.system_fn.__name__} \n"
|
|
255
|
+
+ f"MessageName: {type(task.message).__name__} \n"
|
|
256
|
+
+ f"MessageData: {task.message} \n",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def prepare_snapshot(self) -> tuple[list[type[SingleMessage]], list[EventMessage]]:
|
|
260
|
+
self.stage.flip()
|
|
261
|
+
signal_snapshot = list(self.dirty_signal_types.keys())
|
|
262
|
+
event_snapshot = self.event_queue[:]
|
|
263
|
+
self.dirty_signal_types.clear()
|
|
264
|
+
self.event_queue.clear()
|
|
265
|
+
return signal_snapshot, event_snapshot
|
|
266
|
+
|
|
267
|
+
def clear(
|
|
268
|
+
self,
|
|
269
|
+
):
|
|
270
|
+
self.stage.clear_signal()
|
|
271
|
+
self.stage.clear_event()
|
|
272
|
+
|
|
273
|
+
def execute_hooks(self, tick_hooks: list[TickHook]):
|
|
274
|
+
hooks_context = TickHookContext(
|
|
275
|
+
tick=self.tick_counter, time=time.time(), payload=self.tick_payload
|
|
276
|
+
)
|
|
277
|
+
try:
|
|
278
|
+
for hook in tick_hooks:
|
|
279
|
+
hook(hooks_context)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
MessageWriter.error(e, f"[Virid Dispatcher]: Tick Hook Error.\n")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
from typing import Callable, Type
|
|
7
|
+
from .message import BaseMessage
|
|
8
|
+
from .io import MessageWriter, activate_publisher
|
|
9
|
+
from .registry import Registry
|
|
10
|
+
from .dispatcher import Dispatcher
|
|
11
|
+
from .interface import TickHook, ExecuteHook, Middleware
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Engine:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.registry = Registry()
|
|
17
|
+
self.dispatcher = Dispatcher()
|
|
18
|
+
self.middlewares: list[Middleware] = []
|
|
19
|
+
activate_publisher(self)
|
|
20
|
+
|
|
21
|
+
def use_middleware(self, middleware: Middleware):
|
|
22
|
+
self.middlewares.append(middleware)
|
|
23
|
+
|
|
24
|
+
def dispatch(self, message: BaseMessage):
|
|
25
|
+
"""将消息放入缓冲区,等待调度器处理。"""
|
|
26
|
+
|
|
27
|
+
# 检查消息类型
|
|
28
|
+
if not isinstance(message, BaseMessage):
|
|
29
|
+
MessageWriter.error(
|
|
30
|
+
TypeError(
|
|
31
|
+
f"[Virid Dispatch] TypeError: Only instances of BaseMessage can be dispatched, got: {type(message).__name__}"
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def final_action() -> None:
|
|
36
|
+
# 检查是否有对应的处理函数
|
|
37
|
+
message_class = type(message)
|
|
38
|
+
if message_class not in self.registry.system_task_map:
|
|
39
|
+
MessageWriter.warn(
|
|
40
|
+
f"[Virid Dispatch] No System Found: No system function is registered for message: {message_class.__name__}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.dispatcher.mark_dirty(message)
|
|
44
|
+
|
|
45
|
+
self.pipeline(message, final_action)
|
|
46
|
+
|
|
47
|
+
def tick(self):
|
|
48
|
+
"""调度器调用,处理缓冲区中的消息。"""
|
|
49
|
+
self.dispatcher.tick(self.registry.system_task_map)
|
|
50
|
+
|
|
51
|
+
def register(
|
|
52
|
+
self,
|
|
53
|
+
message_class: Type[BaseMessage],
|
|
54
|
+
system_fn: Callable,
|
|
55
|
+
priority: int = 0,
|
|
56
|
+
):
|
|
57
|
+
"""注册System并返回一个无参的卸载函数。"""
|
|
58
|
+
return self.registry.register(message_class, system_fn, priority)
|
|
59
|
+
|
|
60
|
+
def pipeline(self, message: BaseMessage, final_action: Callable[[], None]):
|
|
61
|
+
def next_step(idx):
|
|
62
|
+
if idx < len(self.middlewares):
|
|
63
|
+
self.middlewares[idx](message, lambda: next_step(idx + 1))
|
|
64
|
+
else:
|
|
65
|
+
final_action()
|
|
66
|
+
|
|
67
|
+
next_step(0)
|
|
68
|
+
|
|
69
|
+
def on_before_tick(self, hook: TickHook, front: bool = False):
|
|
70
|
+
self.dispatcher.add_before_tick_hook(hook, front)
|
|
71
|
+
|
|
72
|
+
def on_after_tick(self, hook: TickHook, front: bool = False):
|
|
73
|
+
self.dispatcher.add_after_tick_hook(hook, front)
|
|
74
|
+
|
|
75
|
+
def on_before_execute(
|
|
76
|
+
self, message_type: Type[BaseMessage], hook: ExecuteHook, front: bool = False
|
|
77
|
+
):
|
|
78
|
+
self.dispatcher.add_before_execute_hook(message_type, hook, front)
|
|
79
|
+
|
|
80
|
+
def on_after_execute(
|
|
81
|
+
self, message_type: Type[BaseMessage], hook: ExecuteHook, front: bool = False
|
|
82
|
+
):
|
|
83
|
+
self.dispatcher.add_after_execute_hook(message_type, hook, front)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Callable, TypeVar
|
|
8
|
+
from .message import BaseMessage
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class SystemContext:
|
|
13
|
+
"""
|
|
14
|
+
系统上下文,包含系统执行时的相关信息
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
params: list[Any]
|
|
18
|
+
message_type: type[BaseMessage]
|
|
19
|
+
method_name: str
|
|
20
|
+
original_method: Callable
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SystemTask:
|
|
25
|
+
"""
|
|
26
|
+
系统任务,包含系统函数和相关信息
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
system_fn: Callable
|
|
30
|
+
priority: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class TickHookContext:
|
|
35
|
+
"""
|
|
36
|
+
TickHook 上下文,包含 tickHook 的相关信息
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
tick: int
|
|
40
|
+
time: float
|
|
41
|
+
payload: dict[str, Any]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ExecuteHookContext:
|
|
46
|
+
"""
|
|
47
|
+
ExecuteHook 上下文,包含 executeHook 的相关信息
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
tick: int
|
|
51
|
+
context: SystemContext
|
|
52
|
+
payload: dict[str, Any]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
ExecuteHook = Callable[[T, ExecuteHookContext, bool], None]
|
|
56
|
+
|
|
57
|
+
TickHook = Callable[[TickHookContext], None]
|
|
58
|
+
|
|
59
|
+
Middleware = Callable[[BaseMessage, Callable[[], None]], None]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
from typing import Type, TypeVar, Any
|
|
7
|
+
from .message import BaseMessage, ErrorMessage, WarnMessage, InfoMessage
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=BaseMessage)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
publisher: Any = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def activate_publisher(dispatcher_instance: Any):
|
|
16
|
+
"""供Dispatcher初始化时调用,延迟初始化"""
|
|
17
|
+
global publisher
|
|
18
|
+
publisher = dispatcher_instance
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def publish(message: BaseMessage) -> None:
|
|
22
|
+
if publisher is None:
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
"[Virid MessageWriter] RuntimeError: No active dispatcher found. Please ensure that a dispatcher is activated before dispatching messages."
|
|
25
|
+
)
|
|
26
|
+
publisher.dispatch(message)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MessageWriter:
|
|
30
|
+
"""
|
|
31
|
+
消息写入器。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def write(target: Type[T] | T, *args, **kwargs) -> None:
|
|
36
|
+
"""
|
|
37
|
+
核心写入入口
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(target, type) and issubclass(target, BaseMessage):
|
|
40
|
+
# 动态实例化,完美透传所有位置参数与关键字参数
|
|
41
|
+
instance = target(*args, **kwargs)
|
|
42
|
+
elif isinstance(target, BaseMessage):
|
|
43
|
+
instance = target
|
|
44
|
+
else:
|
|
45
|
+
raise TypeError(
|
|
46
|
+
f"[Virid MessageWriter] TypeError: Only BaseMessage subclasses or instances are allowed to be written, got: {type(target).__name__}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 统一收拢到惰性分发代理
|
|
50
|
+
publish(instance)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def error(error: Exception, context: str = "") -> None:
|
|
54
|
+
MessageWriter.write(ErrorMessage(error=error, context=context))
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def warn(context: str) -> None:
|
|
58
|
+
MessageWriter.write(WarnMessage(context=context))
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def info(context: str) -> None:
|
|
62
|
+
MessageWriter.write(InfoMessage(context=context))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TypeVar, Callable, ParamSpec
|
|
8
|
+
|
|
9
|
+
# 定义参数规格变量和类型变量
|
|
10
|
+
P = ParamSpec("P")
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class BaseMessage:
|
|
16
|
+
@classmethod
|
|
17
|
+
def send(cls: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> None:
|
|
18
|
+
# 这里的 cls 实际上就是构造函数本身
|
|
19
|
+
instance = cls(*args, **kwargs)
|
|
20
|
+
from .io import MessageWriter
|
|
21
|
+
|
|
22
|
+
MessageWriter.write(instance) # type: ignore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class SingleMessage(BaseMessage): ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class EventMessage(BaseMessage): ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class InfoMessage(EventMessage):
|
|
35
|
+
context: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class WarnMessage(EventMessage):
|
|
40
|
+
context: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ErrorMessage(EventMessage):
|
|
45
|
+
error: Exception
|
|
46
|
+
context: str = ""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
from typing import Type, Callable
|
|
7
|
+
from .message import BaseMessage
|
|
8
|
+
from .io import MessageWriter
|
|
9
|
+
from .interface import SystemTask
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Registry:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.system_task_map: dict[Type[BaseMessage], list[SystemTask]] = {}
|
|
15
|
+
|
|
16
|
+
def register(
|
|
17
|
+
self,
|
|
18
|
+
message_class: Type[BaseMessage],
|
|
19
|
+
system_fn: Callable,
|
|
20
|
+
priority: int = 0,
|
|
21
|
+
) -> Callable[[], None]:
|
|
22
|
+
"""
|
|
23
|
+
注册系统函数并返回一个无参的卸载函数。
|
|
24
|
+
"""
|
|
25
|
+
# 初始化消息桶
|
|
26
|
+
if message_class not in self.system_task_map:
|
|
27
|
+
self.system_task_map[message_class] = []
|
|
28
|
+
|
|
29
|
+
systems = self.system_task_map[message_class]
|
|
30
|
+
|
|
31
|
+
# 检查重复注册
|
|
32
|
+
if system_fn in systems:
|
|
33
|
+
func_name = getattr(system_fn, "__name__", "Anonymous")
|
|
34
|
+
MessageWriter.warn(
|
|
35
|
+
f"[virid Warn] System function is already registered for this message type!\n"
|
|
36
|
+
f"Message Class: {message_class.__name__}\n"
|
|
37
|
+
f"Function Name: {func_name}\n"
|
|
38
|
+
)
|
|
39
|
+
return lambda: None
|
|
40
|
+
|
|
41
|
+
# 追加注册到桶中
|
|
42
|
+
systems.append(SystemTask(system_fn=system_fn, priority=priority))
|
|
43
|
+
|
|
44
|
+
# 返回一个闭包卸载函数
|
|
45
|
+
def unregister() -> None:
|
|
46
|
+
current_systems = self.system_task_map.get(message_class)
|
|
47
|
+
if current_systems is not None:
|
|
48
|
+
# 寻找匹配的系统索引(对标 JS 的 findIndex)
|
|
49
|
+
index = next(
|
|
50
|
+
(
|
|
51
|
+
i
|
|
52
|
+
for i, s in enumerate(current_systems)
|
|
53
|
+
if s.system_fn == system_fn
|
|
54
|
+
),
|
|
55
|
+
-1,
|
|
56
|
+
)
|
|
57
|
+
if index != -1:
|
|
58
|
+
current_systems.pop(index)
|
|
59
|
+
# 如果这个消息类型没有任何系统监听了,彻底删除 key,保持内存干净
|
|
60
|
+
if len(current_systems) == 0:
|
|
61
|
+
del self.system_task_map[message_class]
|
|
62
|
+
|
|
63
|
+
return unregister
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
from typing import Type
|
|
7
|
+
from .message import BaseMessage, SingleMessage, EventMessage
|
|
8
|
+
from .io import MessageWriter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Stage:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
# SingleMessage缓冲池
|
|
14
|
+
self._signal_active: dict[Type[SingleMessage], list[SingleMessage]] = {}
|
|
15
|
+
self._signal_staging: dict[Type[SingleMessage], list[SingleMessage]] = {}
|
|
16
|
+
|
|
17
|
+
# EventMessage缓冲池
|
|
18
|
+
self._event_active: list[EventMessage] = []
|
|
19
|
+
self._event_staging: list[EventMessage] = []
|
|
20
|
+
|
|
21
|
+
def push(self, event: BaseMessage):
|
|
22
|
+
"""根据消息继承范式,物理隔离分流"""
|
|
23
|
+
if isinstance(event, SingleMessage):
|
|
24
|
+
msg_cls = type(event)
|
|
25
|
+
if msg_cls not in self._signal_staging:
|
|
26
|
+
self._signal_staging[msg_cls] = []
|
|
27
|
+
self._signal_staging[msg_cls].append(event)
|
|
28
|
+
|
|
29
|
+
elif isinstance(event, EventMessage):
|
|
30
|
+
self._event_staging.append(event)
|
|
31
|
+
|
|
32
|
+
else:
|
|
33
|
+
MessageWriter.error(
|
|
34
|
+
TypeError(
|
|
35
|
+
f"[Virid Buffer] TypeError: Message {type(event).__name__} must inherit from SingleMessage or EventMessage"
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def flip(self):
|
|
40
|
+
"""
|
|
41
|
+
翻转双缓冲区:
|
|
42
|
+
"""
|
|
43
|
+
# 物理隔离:Active 指向当前轮的快照,Staging 重置为全新容器
|
|
44
|
+
self._signal_active = self._signal_staging
|
|
45
|
+
self._signal_staging = {}
|
|
46
|
+
|
|
47
|
+
self._event_active = self._event_staging
|
|
48
|
+
self._event_staging = []
|
|
49
|
+
|
|
50
|
+
def peek_signal(self, msg_cls: Type[SingleMessage]) -> list[SingleMessage]:
|
|
51
|
+
"""
|
|
52
|
+
获取指定消息类型的缓冲池中的消息
|
|
53
|
+
"""
|
|
54
|
+
return self._signal_active.get(msg_cls, [])
|
|
55
|
+
|
|
56
|
+
def clear_signal(self) -> None:
|
|
57
|
+
"""
|
|
58
|
+
清空指定消息类型的缓冲池
|
|
59
|
+
"""
|
|
60
|
+
self._signal_active.clear()
|
|
61
|
+
|
|
62
|
+
def clear_event(self) -> None:
|
|
63
|
+
"""
|
|
64
|
+
清空事件消息缓冲池
|
|
65
|
+
"""
|
|
66
|
+
self._event_active.clear()
|
|
67
|
+
|
|
68
|
+
def reset(self):
|
|
69
|
+
"""
|
|
70
|
+
重置整个缓冲区,清空所有消息
|
|
71
|
+
"""
|
|
72
|
+
self._signal_active = {}
|
|
73
|
+
self._signal_staging = {}
|
|
74
|
+
self._event_active = []
|
|
75
|
+
self._event_staging = []
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
import inspect
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import Any, Callable, Type, Optional, get_origin, get_args
|
|
9
|
+
|
|
10
|
+
from ..core.interface import SystemContext
|
|
11
|
+
from ..core.message import BaseMessage, EventMessage, SingleMessage
|
|
12
|
+
from ..core.io import MessageWriter
|
|
13
|
+
from ..app import virid_app
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def handle_result(res: Any) -> None:
|
|
17
|
+
"""统一处理返回值,支持链式反应,平铺列表投递"""
|
|
18
|
+
if res is None:
|
|
19
|
+
return
|
|
20
|
+
# 如果返回的是列表/元组,平铺处理
|
|
21
|
+
messages = res if isinstance(res, (list, tuple)) else [res]
|
|
22
|
+
|
|
23
|
+
for m in messages:
|
|
24
|
+
if isinstance(m, BaseMessage):
|
|
25
|
+
MessageWriter.write(m)
|
|
26
|
+
else:
|
|
27
|
+
MessageWriter.warn(
|
|
28
|
+
f"[virid HandleResult] Invalid Return Type: Expected BaseMessage or List[BaseMessage], got {type(m).__name__}. Ignored."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def system(
|
|
33
|
+
message_type: Optional[Type[BaseMessage]] = None,
|
|
34
|
+
priority: int = 0,
|
|
35
|
+
singleton: bool = True,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
系统装饰器
|
|
39
|
+
:param message_type: 监听的消息类型 (可选,如果不传会自动从参数的类型注解推断)
|
|
40
|
+
:param priority: 优先级,数值越大越早执行
|
|
41
|
+
:param singleton: 是否为单例模式。True 传入单条消息;False 传入整个消息列表
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# 提前校验 decorator 参数
|
|
45
|
+
if message_type is not None and not (
|
|
46
|
+
isinstance(message_type, type) and issubclass(message_type, BaseMessage)
|
|
47
|
+
):
|
|
48
|
+
raise TypeError(
|
|
49
|
+
f"[Virid System] TypeError: @System requires a subclass of BaseMessage, got: {message_type}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def decorator(func: Callable) -> Callable:
|
|
53
|
+
sig = inspect.signature(func)
|
|
54
|
+
params = list(sig.parameters.values())
|
|
55
|
+
|
|
56
|
+
if not params:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"[Virid System] Parameter Loss: '{func.__name__}' must have at least one parameter for receiving messages!"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 支持泛型解包的参数解析器
|
|
62
|
+
msg_param_name = None
|
|
63
|
+
inferred_msg_type = None
|
|
64
|
+
|
|
65
|
+
for param in params:
|
|
66
|
+
annotation = param.annotation
|
|
67
|
+
if not annotation or annotation == inspect.Parameter.empty:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
target_type = None
|
|
71
|
+
origin = get_origin(annotation)
|
|
72
|
+
|
|
73
|
+
# 如果是泛型列表(如 list[DecreaseMessage] 或 List[DecreaseMessage])
|
|
74
|
+
if origin is list:
|
|
75
|
+
args = get_args(annotation)
|
|
76
|
+
if (
|
|
77
|
+
args
|
|
78
|
+
and isinstance(args[0], type)
|
|
79
|
+
and issubclass(args[0], BaseMessage)
|
|
80
|
+
):
|
|
81
|
+
target_type = args[0] # 提取出里面的 DecreaseMessage 真实类型
|
|
82
|
+
|
|
83
|
+
# 普通类型
|
|
84
|
+
elif isinstance(annotation, type) and issubclass(annotation, BaseMessage):
|
|
85
|
+
target_type = annotation
|
|
86
|
+
|
|
87
|
+
# 如果找到了消息参数
|
|
88
|
+
if target_type is not None:
|
|
89
|
+
if msg_param_name is not None:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"[Virid System] Multiple Messages: '{func.__name__}' cannot have multiple BaseMessage parameters."
|
|
92
|
+
)
|
|
93
|
+
msg_param_name = param.name
|
|
94
|
+
inferred_msg_type = target_type
|
|
95
|
+
|
|
96
|
+
# 确定最终的消息类型 (装饰器明确指定优先 > 参数类型推断)
|
|
97
|
+
final_message_type = message_type or inferred_msg_type
|
|
98
|
+
|
|
99
|
+
if final_message_type is None:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"[Virid System] Parameter Loss: Cannot infer message type for '{func.__name__}'. "
|
|
102
|
+
f"Declare via @System(MyMessage) or type hint (e.g., def my_system(msg: MyMessage):)."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# 运行时警告:类型冲突
|
|
106
|
+
if (
|
|
107
|
+
message_type
|
|
108
|
+
and inferred_msg_type
|
|
109
|
+
and not issubclass(inferred_msg_type, message_type)
|
|
110
|
+
):
|
|
111
|
+
MessageWriter.warn(
|
|
112
|
+
f"[virid System] Type Mismatch in '{func.__name__}': Decorator expects {message_type.__name__}, "
|
|
113
|
+
f"but parameter expects {inferred_msg_type.__name__}."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 闭包缓存变量
|
|
117
|
+
is_initialized = False
|
|
118
|
+
cached_components: list[Any] = [None] * len(params)
|
|
119
|
+
|
|
120
|
+
# 根据 singleton 开关,精准重组参数负载
|
|
121
|
+
def build_args(message: Any) -> list:
|
|
122
|
+
"""构建函数入参:将消息实体和可能存在的依赖注入按顺序排好"""
|
|
123
|
+
nonlocal is_initialized, cached_components
|
|
124
|
+
|
|
125
|
+
is_incoming_list = isinstance(message, list)
|
|
126
|
+
|
|
127
|
+
# 加工最终要塞给函数对应的参数负载
|
|
128
|
+
if singleton:
|
|
129
|
+
# 单例模式:函数只想要单条消息
|
|
130
|
+
true_message = message[-1] if is_incoming_list else message
|
|
131
|
+
|
|
132
|
+
# 运行时类型校验
|
|
133
|
+
if not isinstance(true_message, final_message_type):
|
|
134
|
+
raise TypeError(
|
|
135
|
+
f"[virid System] Type Mismatch: Expected {final_message_type.__name__}, but received {type(true_message).__name__}"
|
|
136
|
+
)
|
|
137
|
+
payload = true_message
|
|
138
|
+
else:
|
|
139
|
+
# 批处理模式:函数想要一个消息列表
|
|
140
|
+
true_messages_list = message if is_incoming_list else [message]
|
|
141
|
+
|
|
142
|
+
# 运行时对列表内的元素进行类型校验
|
|
143
|
+
if true_messages_list and not isinstance(
|
|
144
|
+
true_messages_list[0], final_message_type
|
|
145
|
+
):
|
|
146
|
+
raise TypeError(
|
|
147
|
+
f"[virid System] Type Mismatch: Expected elements of {final_message_type.__name__}, but received {type(true_messages_list[0]).__name__}"
|
|
148
|
+
)
|
|
149
|
+
payload = true_messages_list
|
|
150
|
+
|
|
151
|
+
# 首次命中去容器捞取并缓存所有 Component 单例
|
|
152
|
+
if not is_initialized:
|
|
153
|
+
for idx, param in enumerate(params):
|
|
154
|
+
if param.name != msg_param_name:
|
|
155
|
+
# 只有在第一次运行时,才去全局容器里捞
|
|
156
|
+
inject_instance = virid_app.get(param.annotation)
|
|
157
|
+
if inject_instance is None:
|
|
158
|
+
raise RuntimeError(
|
|
159
|
+
f"[virid System] Unknown Inject Data Types: '{param.name}' ({param.annotation.__name__}) "
|
|
160
|
+
f"is not registered in the container for '{func.__name__}'!"
|
|
161
|
+
)
|
|
162
|
+
cached_components[idx] = inject_instance
|
|
163
|
+
# 标记初始化完成
|
|
164
|
+
is_initialized = True
|
|
165
|
+
|
|
166
|
+
# 高频重组参数
|
|
167
|
+
call_args = []
|
|
168
|
+
for idx, param in enumerate(params):
|
|
169
|
+
if param.name == msg_param_name:
|
|
170
|
+
call_args.append(payload)
|
|
171
|
+
else:
|
|
172
|
+
# 直接从闭包里的数组中拿
|
|
173
|
+
call_args.append(cached_components[idx])
|
|
174
|
+
|
|
175
|
+
return call_args
|
|
176
|
+
|
|
177
|
+
# 包装函数 (同步)
|
|
178
|
+
@wraps(func)
|
|
179
|
+
def wrapped_system(message: EventMessage | list[SingleMessage]):
|
|
180
|
+
# 动态组装参数
|
|
181
|
+
call_args = build_args(message)
|
|
182
|
+
result = func(*call_args)
|
|
183
|
+
# 解析并处理返回值(支持链式反应)
|
|
184
|
+
handle_result(result)
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
# 给包装后的函数挂载上下文信息
|
|
188
|
+
wrapped_system.system_context: SystemContext = { # type: ignore
|
|
189
|
+
"params": [p.annotation for p in params],
|
|
190
|
+
"message_type": final_message_type,
|
|
191
|
+
"method_name": func.__name__,
|
|
192
|
+
"original_method": func,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# 自动向全局调度中心登记
|
|
196
|
+
virid_app.register(final_message_type, wrapped_system, priority)
|
|
197
|
+
|
|
198
|
+
return wrapped_system
|
|
199
|
+
|
|
200
|
+
return decorator
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def component():
|
|
204
|
+
def bind_component(cls):
|
|
205
|
+
cls.__virid_component__ = True
|
|
206
|
+
return cls
|
|
207
|
+
|
|
208
|
+
return bind_component
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .core.message import ErrorMessage, InfoMessage, WarnMessage
|
|
8
|
+
from .core.interface import SystemContext
|
|
9
|
+
from logging import getLogger
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
switch = True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def toggle_utils(val: bool) -> None:
|
|
16
|
+
global switch
|
|
17
|
+
switch = val
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ViridFormatter(logging.Formatter):
|
|
21
|
+
# 颜色与样式转义码
|
|
22
|
+
RESET = "\x1b[0m"
|
|
23
|
+
BOLD = "\x1b[1m"
|
|
24
|
+
RED = "\x1b[31m"
|
|
25
|
+
GREEN = "\x1b[32m"
|
|
26
|
+
YELLOW = "\x1b[33m"
|
|
27
|
+
MAGENTA = "\x1b[35m"
|
|
28
|
+
CYAN = "\x1b[36m"
|
|
29
|
+
GRAY = "\x1b[90m"
|
|
30
|
+
|
|
31
|
+
def format(self, record):
|
|
32
|
+
if record.levelno == logging.INFO:
|
|
33
|
+
header = f"{self.GREEN}{self.BOLD} ✔ [Virid Info] {self.RESET}"
|
|
34
|
+
record.msg = (
|
|
35
|
+
f"{header}{self.GRAY}Global Info Caught:{self.RESET}\n"
|
|
36
|
+
f" {self.GREEN}Details:{self.RESET} {record.msg}"
|
|
37
|
+
)
|
|
38
|
+
elif record.levelno == logging.WARNING:
|
|
39
|
+
header = f"{self.YELLOW}{self.BOLD} ⚠ [Virid Warn] {self.RESET}"
|
|
40
|
+
context = f"{self.CYAN}{record.msg}{self.RESET}"
|
|
41
|
+
record.msg = (
|
|
42
|
+
f"{header}{self.GRAY}Global Warn Caught:{self.RESET}\n"
|
|
43
|
+
f" {self.YELLOW}Context:{self.RESET} {context}"
|
|
44
|
+
)
|
|
45
|
+
elif record.levelno == logging.ERROR:
|
|
46
|
+
header = f"{self.RED}{self.BOLD} ✖ [Virid Error] {self.RESET}"
|
|
47
|
+
|
|
48
|
+
if "context" in getattr(record, "msg_type", ""):
|
|
49
|
+
record.msg = f" {self.RED}Context:{self.RESET} {self.MAGENTA}{record.msg}{self.RESET}"
|
|
50
|
+
else:
|
|
51
|
+
record.msg = (
|
|
52
|
+
f"{header}{self.GRAY}Global Error Caught:{self.RESET}\n"
|
|
53
|
+
f" {self.RED}Details:{self.RESET} {record.msg}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return super().format(record)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
logging.basicConfig(level=logging.INFO)
|
|
60
|
+
handler = logging.root.handlers[0]
|
|
61
|
+
handler.setFormatter(ViridFormatter("%(message)s"))
|
|
62
|
+
logger = getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def error(message: ErrorMessage) -> None:
|
|
66
|
+
if switch is False:
|
|
67
|
+
return
|
|
68
|
+
logger.error(message.error, extra={"msg_type": "error"})
|
|
69
|
+
logger.error(message.context, extra={"msg_type": "context"})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
error.system_context: SystemContext = { # type: ignore
|
|
73
|
+
"params": [ErrorMessage],
|
|
74
|
+
"message_type": ErrorMessage,
|
|
75
|
+
"method_name": error.__name__,
|
|
76
|
+
"original_method": error,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def info(message: InfoMessage) -> None:
|
|
81
|
+
if switch is False:
|
|
82
|
+
return
|
|
83
|
+
logger.info(message.context)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
info.system_context: SystemContext = { # type: ignore
|
|
87
|
+
"params": [InfoMessage],
|
|
88
|
+
"message_type": InfoMessage,
|
|
89
|
+
"method_name": info.__name__,
|
|
90
|
+
"original_method": info,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def warn(message: WarnMessage) -> None:
|
|
95
|
+
if switch is False:
|
|
96
|
+
return
|
|
97
|
+
logger.warning(message.context)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
warn.system_context: SystemContext = { # type: ignore
|
|
101
|
+
"params": [WarnMessage],
|
|
102
|
+
"message_type": WarnMessage,
|
|
103
|
+
"method_name": warn.__name__,
|
|
104
|
+
"original_method": warn,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def register_base_handlers(virid) -> None:
|
|
109
|
+
virid.register(ErrorMessage, error)
|
|
110
|
+
virid.register(InfoMessage, info)
|
|
111
|
+
virid.register(WarnMessage, warn)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026-present Ailrid.
|
|
3
|
+
Licensed under the Apache License, Version 2.0.
|
|
4
|
+
Project: Virid
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from virid.core import (
|
|
10
|
+
component,
|
|
11
|
+
EventMessage,
|
|
12
|
+
system,
|
|
13
|
+
create_virid,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
virid = create_virid()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@component()
|
|
20
|
+
@dataclass
|
|
21
|
+
class BenchCounter:
|
|
22
|
+
value: int = 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
virid.bind(BenchCounter)
|
|
26
|
+
|
|
27
|
+
class BenchMessage(EventMessage): ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# 用来在外部验证系统确实执行了正确次数的全局计数器
|
|
31
|
+
actual_run_count = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@system(message_type=BenchMessage)
|
|
35
|
+
def benchmark(counter: BenchCounter) -> None:
|
|
36
|
+
global actual_run_count
|
|
37
|
+
counter.value += 1
|
|
38
|
+
actual_run_count += 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
# 压测规模:跑 200,000 次完整的 [Send -> Tick -> DI -> Execute] 大循环
|
|
43
|
+
TOTAL_ITERATIONS = 200_000
|
|
44
|
+
|
|
45
|
+
print("=" * 60)
|
|
46
|
+
print(" 开始 Virid 框架全链路集成压测 (Full Loop Benchmark)...")
|
|
47
|
+
print(f" 压测规模: {TOTAL_ITERATIONS:,} 次 完整框架调度闭环")
|
|
48
|
+
print("=" * 60)
|
|
49
|
+
|
|
50
|
+
# 预热第一帧
|
|
51
|
+
BenchMessage.send()
|
|
52
|
+
virid.tick()
|
|
53
|
+
|
|
54
|
+
# 重置计数器,开始正式全链路计时
|
|
55
|
+
actual_run_count = 0
|
|
56
|
+
start_time = time.perf_counter()
|
|
57
|
+
|
|
58
|
+
# 核心吞吐量高频循环
|
|
59
|
+
for _ in range(TOTAL_ITERATIONS):
|
|
60
|
+
BenchMessage.send()
|
|
61
|
+
virid.tick()
|
|
62
|
+
|
|
63
|
+
end_time = time.perf_counter()
|
|
64
|
+
|
|
65
|
+
# 性能结果统计
|
|
66
|
+
elapsed_time = end_time - start_time
|
|
67
|
+
throughput = TOTAL_ITERATIONS / elapsed_time
|
|
68
|
+
|
|
69
|
+
print(f"【压测结果】")
|
|
70
|
+
print(f" 完整跑完耗时 : {elapsed_time:.4f} 秒")
|
|
71
|
+
print(f" 框架全链路吞吐: {throughput:,.2f} 次全循环/秒 (Ticks/sec)")
|
|
72
|
+
print("-" * 60)
|
|
73
|
+
print(f"【确定性状态校验】")
|
|
74
|
+
print(f" 系统实际触发次数: {actual_run_count:,} 次 (预期: {TOTAL_ITERATIONS:,})")
|
|
75
|
+
print("=" * 60)
|