@agentunion/kite 1.3.1 → 1.4.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/CHANGELOG.md +287 -1
- package/cli.js +76 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +263 -197
- package/extensions/channels/acp_channel/entry.py +111 -1
- package/extensions/channels/acp_channel/module.md +23 -22
- package/extensions/channels/acp_channel/server.py +263 -197
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +408 -72
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/model_service/entry.py +255 -71
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/watchdog/entry.py +344 -90
- package/extensions/services/watchdog/monitor.py +237 -21
- package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
- package/extensions/services/web/config_example.py +35 -0
- package/extensions/services/web/config_loader.py +110 -0
- package/extensions/services/web/entry.py +114 -26
- package/extensions/services/web/module.md +35 -24
- package/extensions/services/web/pairing.py +250 -0
- package/extensions/services/web/pairing_codes.jsonl +16 -0
- package/extensions/services/web/relay.py +643 -0
- package/extensions/services/web/relay_config.json5 +67 -0
- package/extensions/services/web/routes/routes_management_ws.py +127 -0
- package/extensions/services/web/routes/routes_rpc.py +89 -0
- package/extensions/services/web/routes/routes_test.py +61 -0
- package/extensions/services/web/server.py +445 -99
- package/extensions/services/web/static/css/style.css +138 -2
- package/extensions/services/web/static/index.html +295 -2
- package/extensions/services/web/static/js/app.js +1579 -5
- package/extensions/services/web/static/js/kernel-client-example.js +161 -0
- package/extensions/services/web/static/js/kernel-client.js +383 -0
- package/extensions/services/web/static/js/registry-tests.js +558 -0
- package/extensions/services/web/static/js/token-manager.js +175 -0
- package/extensions/services/web/static/pairing.html +248 -0
- package/extensions/services/web/static/test_registry.html +262 -0
- package/extensions/services/web/web_config.json5 +29 -0
- package/kernel/entry.py +120 -32
- package/kernel/event_hub.py +159 -16
- package/kernel/module.md +36 -33
- package/kernel/registry_store.py +70 -20
- package/kernel/rpc_router.py +134 -57
- package/kernel/server.py +292 -15
- package/kite_cli/__init__.py +3 -0
- package/kite_cli/__main__.py +5 -0
- package/kite_cli/commands/__init__.py +1 -0
- package/kite_cli/commands/clean.py +101 -0
- package/kite_cli/commands/doctor.py +35 -0
- package/kite_cli/commands/history.py +111 -0
- package/kite_cli/commands/info.py +96 -0
- package/kite_cli/commands/install.py +313 -0
- package/kite_cli/commands/list.py +143 -0
- package/kite_cli/commands/log.py +81 -0
- package/kite_cli/commands/rollback.py +88 -0
- package/kite_cli/commands/search.py +73 -0
- package/kite_cli/commands/uninstall.py +85 -0
- package/kite_cli/commands/update.py +118 -0
- package/kite_cli/core/__init__.py +1 -0
- package/kite_cli/core/checker.py +142 -0
- package/kite_cli/core/dependency.py +229 -0
- package/kite_cli/core/downloader.py +209 -0
- package/kite_cli/core/install_info.py +40 -0
- package/kite_cli/core/tool_installer.py +397 -0
- package/kite_cli/core/validator.py +78 -0
- package/kite_cli/main.py +289 -0
- package/kite_cli/utils/__init__.py +1 -0
- package/kite_cli/utils/i18n.py +252 -0
- package/kite_cli/utils/interactive.py +63 -0
- package/kite_cli/utils/operation_log.py +77 -0
- package/kite_cli/utils/paths.py +34 -0
- package/kite_cli/utils/version.py +308 -0
- package/launcher/count_lines.py +34 -0
- package/launcher/entry.py +905 -166
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +37 -37
- package/launcher/process_manager.py +12 -1
- package/package.json +2 -1
- package/scripts/plan_manager.py +315 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Web 服务配置
|
|
3
|
+
server: {
|
|
4
|
+
host: "0.0.0.0",
|
|
5
|
+
port: 18766,
|
|
6
|
+
ssl: false,
|
|
7
|
+
// SSL 证书路径(如果启用 SSL)
|
|
8
|
+
ssl_cert: null,
|
|
9
|
+
ssl_key: null
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
// 静态文件配置
|
|
13
|
+
static: {
|
|
14
|
+
directory: "static",
|
|
15
|
+
cache_max_age: 3600 // 秒
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
// CORS 配置
|
|
19
|
+
cors: {
|
|
20
|
+
enabled: false,
|
|
21
|
+
origins: ["*"]
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// 日志配置
|
|
25
|
+
logging: {
|
|
26
|
+
level: "INFO",
|
|
27
|
+
format: "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/kernel/entry.py
CHANGED
|
@@ -26,7 +26,113 @@ import uvicorn
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
# ── Module configuration ──
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
def _load_module_config() -> dict:
|
|
31
|
+
"""Load module configuration from module.md frontmatter.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dict with keys: name, preferred_port, advertise_ip
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
SystemExit: If module.md is invalid or name is non-compliant
|
|
38
|
+
"""
|
|
39
|
+
_this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
40
|
+
module_md = os.path.join(_this_dir, "module.md")
|
|
41
|
+
|
|
42
|
+
# Calculate relative path for error messages
|
|
43
|
+
project_root = os.environ.get("KITE_PROJECT", "")
|
|
44
|
+
if project_root and _this_dir.startswith(project_root):
|
|
45
|
+
rel_path = os.path.relpath(_this_dir, project_root)
|
|
46
|
+
else:
|
|
47
|
+
rel_path = _this_dir
|
|
48
|
+
|
|
49
|
+
# Default values (will be overridden if valid config exists)
|
|
50
|
+
result = {
|
|
51
|
+
"name": "",
|
|
52
|
+
"preferred_port": 0,
|
|
53
|
+
"advertise_ip": "127.0.0.1"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Check if module.md exists
|
|
57
|
+
if not os.path.exists(module_md):
|
|
58
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
59
|
+
print(f" Path: {rel_path}/module.md")
|
|
60
|
+
print(f" Reason: File not found")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
with open(module_md, encoding="utf-8") as f:
|
|
65
|
+
text = f.read()
|
|
66
|
+
|
|
67
|
+
# Extract YAML frontmatter (between --- markers)
|
|
68
|
+
import re
|
|
69
|
+
m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
70
|
+
if not m:
|
|
71
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
72
|
+
print(f" Path: {rel_path}/module.md")
|
|
73
|
+
print(f" Reason: Missing YAML frontmatter")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
# Parse YAML frontmatter
|
|
77
|
+
try:
|
|
78
|
+
import yaml
|
|
79
|
+
fm = yaml.safe_load(m.group(1)) or {}
|
|
80
|
+
except ImportError:
|
|
81
|
+
print(f"[{rel_path}] ERROR: PyYAML not installed, cannot parse module.md")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
85
|
+
print(f" Path: {rel_path}/module.md")
|
|
86
|
+
print(f" Reason: YAML parse error: {e}")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
# Validate 'name' field (required)
|
|
90
|
+
if "name" not in fm:
|
|
91
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
92
|
+
print(f" Path: {rel_path}/module.md")
|
|
93
|
+
print(f" Reason: Missing 'name' field")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
raw_name = str(fm["name"]).strip()
|
|
97
|
+
|
|
98
|
+
if not raw_name:
|
|
99
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
100
|
+
print(f" Path: {rel_path}/module.md")
|
|
101
|
+
print(f" Reason: Empty module name")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
# Validate name characters
|
|
105
|
+
sanitized = re.sub(r'[^a-zA-Z0-9_\-]', '', raw_name)
|
|
106
|
+
|
|
107
|
+
if sanitized != raw_name:
|
|
108
|
+
invalid_chars = ''.join(sorted(set(c for c in raw_name if c not in sanitized)))
|
|
109
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
110
|
+
print(f" Path: {rel_path}/module.md")
|
|
111
|
+
print(f" Reason: Invalid characters in name '{raw_name}': {repr(invalid_chars)}")
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
result["name"] = sanitized
|
|
115
|
+
|
|
116
|
+
# Extract optional fields
|
|
117
|
+
if "preferred_port" in fm:
|
|
118
|
+
try:
|
|
119
|
+
result["preferred_port"] = int(fm["preferred_port"])
|
|
120
|
+
except (ValueError, TypeError):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
if "advertise_ip" in fm:
|
|
124
|
+
result["advertise_ip"] = str(fm["advertise_ip"])
|
|
125
|
+
|
|
126
|
+
except SystemExit:
|
|
127
|
+
raise # Re-raise exit to prevent catching by outer except
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"[{rel_path}] ERROR: Failed to read module.md: {e}")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
_module_config = _load_module_config()
|
|
135
|
+
MODULE_NAME = _module_config["name"]
|
|
30
136
|
|
|
31
137
|
|
|
32
138
|
def _fmt_elapsed(t0: float) -> str:
|
|
@@ -256,27 +362,6 @@ if _project_root not in sys.path:
|
|
|
256
362
|
from kernel.server import KernelServer
|
|
257
363
|
|
|
258
364
|
|
|
259
|
-
def _read_module_md() -> dict:
|
|
260
|
-
"""Read preferred_port and advertise_ip from own module.md."""
|
|
261
|
-
md_path = os.path.join(_this_dir, "module.md")
|
|
262
|
-
result = {"preferred_port": 0, "advertise_ip": "127.0.0.1"}
|
|
263
|
-
try:
|
|
264
|
-
with open(md_path, "r", encoding="utf-8") as f:
|
|
265
|
-
text = f.read()
|
|
266
|
-
m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
267
|
-
if m:
|
|
268
|
-
try:
|
|
269
|
-
import yaml
|
|
270
|
-
fm = yaml.safe_load(m.group(1)) or {}
|
|
271
|
-
except ImportError:
|
|
272
|
-
fm = {}
|
|
273
|
-
result["preferred_port"] = int(fm.get("preferred_port", 0))
|
|
274
|
-
result["advertise_ip"] = fm.get("advertise_ip", "127.0.0.1")
|
|
275
|
-
except Exception:
|
|
276
|
-
pass
|
|
277
|
-
return result
|
|
278
|
-
|
|
279
|
-
|
|
280
365
|
def _bind_port(preferred: int, host: str) -> int:
|
|
281
366
|
"""Try preferred port first, fall back to OS-assigned."""
|
|
282
367
|
if preferred:
|
|
@@ -284,11 +369,15 @@ def _bind_port(preferred: int, host: str) -> int:
|
|
|
284
369
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
285
370
|
s.bind((host, preferred))
|
|
286
371
|
return preferred
|
|
287
|
-
except OSError:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
372
|
+
except OSError as e:
|
|
373
|
+
print(f"\033[31m[kernel] ✖ 指定端口 {preferred} 绑定失败: {e},将使用系统随机端口\033[0m")
|
|
374
|
+
try:
|
|
375
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
376
|
+
s.bind((host, 0))
|
|
377
|
+
return s.getsockname()[1]
|
|
378
|
+
except OSError as e:
|
|
379
|
+
print(f"\033[31m[kernel] ✖ 端口绑定完全失败 ({host}): {e}\033[0m")
|
|
380
|
+
raise
|
|
292
381
|
|
|
293
382
|
|
|
294
383
|
def _read_stdin_kite_messages(expected: set[str], timeout: float = 10) -> dict[str, dict]:
|
|
@@ -362,17 +451,16 @@ def main():
|
|
|
362
451
|
if is_debug:
|
|
363
452
|
print("[kernel] 调试模式已启用 (KITE_DEBUG=1),接受所有令牌")
|
|
364
453
|
|
|
365
|
-
# Step 1:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
preferred_port = md_config["preferred_port"]
|
|
454
|
+
# Step 1: Use cached module config (already loaded at module level)
|
|
455
|
+
advertise_ip = _module_config["advertise_ip"]
|
|
456
|
+
preferred_port = _module_config["preferred_port"]
|
|
369
457
|
|
|
370
458
|
# Step 2: Generate launcher token
|
|
371
459
|
import secrets
|
|
372
460
|
launcher_token = secrets.token_urlsafe(32)
|
|
373
461
|
|
|
374
462
|
# Step 3: Create KernelServer with launcher_token
|
|
375
|
-
server = KernelServer(launcher_token=launcher_token, advertise_ip=advertise_ip)
|
|
463
|
+
server = KernelServer(launcher_token=launcher_token, advertise_ip=advertise_ip, module_id=MODULE_NAME)
|
|
376
464
|
|
|
377
465
|
# Step 4: Bind port
|
|
378
466
|
bind_host = advertise_ip
|
package/kernel/event_hub.py
CHANGED
|
@@ -40,6 +40,12 @@ def _loads(raw: str):
|
|
|
40
40
|
|
|
41
41
|
QUEUE_MAXSIZE = 10000
|
|
42
42
|
|
|
43
|
+
# System events that are auto-broadcast to ALL connected modules (no subscription needed)
|
|
44
|
+
SYSTEM_EVENTS = {"module.offline", "module.ready", "module.shutdown"}
|
|
45
|
+
|
|
46
|
+
# Callback execution pool size (max concurrent callback tasks)
|
|
47
|
+
CALLBACK_POOL_SIZE = 100
|
|
48
|
+
|
|
43
49
|
|
|
44
50
|
class EventHub:
|
|
45
51
|
|
|
@@ -63,6 +69,13 @@ class EventHub:
|
|
|
63
69
|
self._cnt_errors = 0
|
|
64
70
|
self._start_time = time.time()
|
|
65
71
|
|
|
72
|
+
# Internal event callbacks (module_id -> callback)
|
|
73
|
+
# 用于没有 WebSocket 连接的模块(如 Kernel)通过回调接收事件
|
|
74
|
+
self._internal_callbacks: dict[str, callable] = {}
|
|
75
|
+
|
|
76
|
+
# Queue overflow tracking (per module)
|
|
77
|
+
self._queue_overflow: dict[str, int] = {} # module_id -> overflow count
|
|
78
|
+
|
|
66
79
|
# ── Connection lifecycle ──
|
|
67
80
|
|
|
68
81
|
def add_connection(self, module_id: str, ws: WebSocket):
|
|
@@ -116,13 +129,80 @@ class EventHub:
|
|
|
116
129
|
await ws.send_text(raw)
|
|
117
130
|
self._cnt_routed += 1
|
|
118
131
|
except Exception:
|
|
119
|
-
|
|
132
|
+
# Connection closed (normal during shutdown) — exit silently
|
|
120
133
|
break
|
|
121
134
|
except asyncio.CancelledError:
|
|
122
135
|
pass
|
|
123
136
|
|
|
137
|
+
async def _callback_sender_loop(self, mid: str, callback: callable, queue: asyncio.Queue):
|
|
138
|
+
"""回调专用的发送循环(类似 _sender_loop,但调用回调而不是发送 WS)
|
|
139
|
+
|
|
140
|
+
从队列取出 JSON-RPC 2.0 Notification,解析后调用回调函数。
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
while True:
|
|
144
|
+
raw = await queue.get()
|
|
145
|
+
try:
|
|
146
|
+
msg = _loads(raw)
|
|
147
|
+
params = msg.get("params", {})
|
|
148
|
+
event_type = params.get("event", "")
|
|
149
|
+
data = params.get("data", {})
|
|
150
|
+
await callback(event_type, data)
|
|
151
|
+
self._cnt_routed += 1
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"[kernel] Internal callback error for {mid} on {event_type}: {e}")
|
|
154
|
+
import traceback
|
|
155
|
+
traceback.print_exc()
|
|
156
|
+
except asyncio.CancelledError:
|
|
157
|
+
pass
|
|
158
|
+
|
|
124
159
|
# ── Subscribe / Unsubscribe ──
|
|
125
160
|
|
|
161
|
+
def register_internal_callback(self, module_id: str, callback: callable):
|
|
162
|
+
"""注册内部事件回调(用于 Kernel 自身接收事件)
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
module_id: 模块 ID(通常是 "kernel")
|
|
166
|
+
callback: 异步回调函数 async def callback(event_type: str, data: dict)
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
TypeError: 如果 callback 不是 callable
|
|
170
|
+
|
|
171
|
+
Note:
|
|
172
|
+
只注册回调和创建队列,不创建 task。
|
|
173
|
+
需要在事件循环启动后调用 start_internal_senders() 来创建 sender tasks。
|
|
174
|
+
"""
|
|
175
|
+
if not callable(callback):
|
|
176
|
+
raise TypeError(f"callback must be callable, got {type(callback)}")
|
|
177
|
+
|
|
178
|
+
# 存储回调函数引用
|
|
179
|
+
self._internal_callbacks[module_id] = callback
|
|
180
|
+
|
|
181
|
+
# 创建队列(如果还没有)
|
|
182
|
+
if module_id not in self._queues:
|
|
183
|
+
q = asyncio.Queue(maxsize=QUEUE_MAXSIZE)
|
|
184
|
+
self._queues[module_id] = q
|
|
185
|
+
|
|
186
|
+
print(f"[kernel] Registered internal callback for {module_id}")
|
|
187
|
+
|
|
188
|
+
def start_internal_senders(self):
|
|
189
|
+
"""启动所有已注册的内部回调的 sender tasks(必须在事件循环中调用)"""
|
|
190
|
+
for module_id, callback in self._internal_callbacks.items():
|
|
191
|
+
if module_id not in self._senders:
|
|
192
|
+
q = self._queues[module_id]
|
|
193
|
+
self._senders[module_id] = asyncio.create_task(
|
|
194
|
+
self._callback_sender_loop(module_id, callback, q)
|
|
195
|
+
)
|
|
196
|
+
print(f"[kernel] Started internal sender for {module_id}")
|
|
197
|
+
|
|
198
|
+
def unregister_internal_callback(self, module_id: str):
|
|
199
|
+
"""注销内部事件回调"""
|
|
200
|
+
self._internal_callbacks.pop(module_id, None)
|
|
201
|
+
self._queues.pop(module_id, None)
|
|
202
|
+
task = self._senders.pop(module_id, None)
|
|
203
|
+
if task:
|
|
204
|
+
task.cancel()
|
|
205
|
+
|
|
126
206
|
def handle_subscribe(self, module_id: str, events: list[str]):
|
|
127
207
|
if module_id not in self.subscriptions:
|
|
128
208
|
self.subscriptions[module_id] = set()
|
|
@@ -131,28 +211,32 @@ class EventHub:
|
|
|
131
211
|
)
|
|
132
212
|
print(f"[kernel] {module_id} subscribed: {events}")
|
|
133
213
|
|
|
134
|
-
def handle_unsubscribe(self, module_id: str, events: list[str]):
|
|
214
|
+
def handle_unsubscribe(self, module_id: str, events: list[str]) -> dict:
|
|
215
|
+
"""Unsubscribe from events. Returns dict with unsubscribed events and count."""
|
|
135
216
|
subs = self.subscriptions.get(module_id)
|
|
217
|
+
unsubscribed = []
|
|
136
218
|
if subs:
|
|
137
219
|
to_remove = {item for item in subs if item[0] in events}
|
|
220
|
+
unsubscribed = [item[0] for item in to_remove]
|
|
138
221
|
subs.difference_update(to_remove)
|
|
139
222
|
print(f"[kernel] {module_id} unsubscribed: {events}")
|
|
223
|
+
return {"unsubscribed": unsubscribed, "count": len(unsubscribed)}
|
|
140
224
|
|
|
141
225
|
# ── Event publishing (called by RpcRouter) ──
|
|
142
226
|
|
|
143
227
|
def publish_event(self, sender_id: str, event_id: str, event_type: str,
|
|
144
228
|
data: dict = None, echo: bool = False) -> dict:
|
|
145
229
|
"""Publish an event from a module. Called by RpcRouter for event.publish RPC.
|
|
146
|
-
Returns
|
|
230
|
+
Returns empty dict on success, raises exception on validation failure."""
|
|
147
231
|
self._cnt_received += 1
|
|
148
232
|
|
|
149
233
|
if not event_id or not event_type:
|
|
150
234
|
self._cnt_errors += 1
|
|
151
|
-
|
|
235
|
+
raise ValueError("Missing required field: event_id or event")
|
|
152
236
|
|
|
153
237
|
if self.dedup.is_duplicate(event_id):
|
|
154
238
|
self._cnt_dedup += 1
|
|
155
|
-
return {
|
|
239
|
+
return {} # silently accept duplicates
|
|
156
240
|
|
|
157
241
|
msg = {
|
|
158
242
|
"jsonrpc": "2.0",
|
|
@@ -167,11 +251,17 @@ class EventHub:
|
|
|
167
251
|
}
|
|
168
252
|
|
|
169
253
|
self._route_event(sender_id, msg, event_type, echo)
|
|
170
|
-
return {
|
|
254
|
+
return {}
|
|
171
255
|
|
|
172
|
-
def publish_internal(self, event_type: str, data: dict):
|
|
256
|
+
def publish_internal(self, event_type: str, data: dict, source: str):
|
|
173
257
|
"""Publish a Kernel-originated event (e.g. module.offline, module.registered).
|
|
174
|
-
No dedup check — internal events are unique by nature.
|
|
258
|
+
No dedup check — internal events are unique by nature.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
event_type: 事件类型(如 "module.ready")
|
|
262
|
+
data: 事件数据
|
|
263
|
+
source: 事件来源模块 ID(必填,通常是调用者的 module_id)
|
|
264
|
+
"""
|
|
175
265
|
import uuid
|
|
176
266
|
event_id = str(uuid.uuid4())
|
|
177
267
|
self._cnt_received += 1
|
|
@@ -182,21 +272,71 @@ class EventHub:
|
|
|
182
272
|
"params": {
|
|
183
273
|
"event_id": event_id,
|
|
184
274
|
"event": event_type,
|
|
185
|
-
"source":
|
|
275
|
+
"source": source,
|
|
186
276
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
187
277
|
"data": data,
|
|
188
278
|
},
|
|
189
279
|
}
|
|
190
280
|
|
|
191
|
-
self._route_event(
|
|
281
|
+
self._route_event(source, msg, event_type, echo=False)
|
|
192
282
|
|
|
193
283
|
# ── Routing ──
|
|
194
284
|
|
|
195
285
|
def _route_event(self, sender_id: str, msg: dict, event_type: str, echo: bool):
|
|
196
|
-
"""Enqueue event to all matching subscribers' delivery queues.
|
|
286
|
+
"""Enqueue event to all matching subscribers' delivery queues.
|
|
287
|
+
System events (module.offline, module.ready, module.shutdown) are auto-broadcast
|
|
288
|
+
to ALL connected modules, regardless of subscription.
|
|
289
|
+
|
|
290
|
+
Exception: module.shutdown with a target module_id is sent ONLY to that module.
|
|
291
|
+
"""
|
|
197
292
|
e_parts = tuple(event_type.split("."))
|
|
198
293
|
raw = None # lazy serialization
|
|
199
|
-
|
|
294
|
+
params = msg.get("params", {})
|
|
295
|
+
data = params.get("data", {})
|
|
296
|
+
|
|
297
|
+
# System events → broadcast to all connected modules (unless targeted)
|
|
298
|
+
if event_type in SYSTEM_EVENTS:
|
|
299
|
+
# Check if this is a targeted shutdown (has module_id in data)
|
|
300
|
+
if event_type == "module.shutdown":
|
|
301
|
+
target_module = data.get("module_id", "")
|
|
302
|
+
|
|
303
|
+
# Targeted shutdown → send only to target module
|
|
304
|
+
if target_module:
|
|
305
|
+
queue = self._queues.get(target_module)
|
|
306
|
+
if queue:
|
|
307
|
+
if raw is None:
|
|
308
|
+
raw = _dumps(msg)
|
|
309
|
+
try:
|
|
310
|
+
queue.put_nowait(raw)
|
|
311
|
+
self._cnt_queued += 1
|
|
312
|
+
except asyncio.QueueFull:
|
|
313
|
+
# Queue 满了,记录溢出
|
|
314
|
+
self._queue_overflow[target_module] = self._queue_overflow.get(target_module, 0) + 1
|
|
315
|
+
overflow_count = self._queue_overflow[target_module]
|
|
316
|
+
# 每 100 次溢出打印一次警告
|
|
317
|
+
if overflow_count % 100 == 1:
|
|
318
|
+
print(f"[kernel] Warning: Queue full for {target_module}, dropped {overflow_count} events")
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
# Broadcast system events (no target or non-shutdown events)
|
|
322
|
+
for mid, queue in self._queues.items():
|
|
323
|
+
if mid == sender_id and not echo:
|
|
324
|
+
continue
|
|
325
|
+
if raw is None:
|
|
326
|
+
raw = _dumps(msg)
|
|
327
|
+
try:
|
|
328
|
+
queue.put_nowait(raw)
|
|
329
|
+
self._cnt_queued += 1
|
|
330
|
+
except asyncio.QueueFull:
|
|
331
|
+
# Queue 满了,记录溢出
|
|
332
|
+
self._queue_overflow[mid] = self._queue_overflow.get(mid, 0) + 1
|
|
333
|
+
overflow_count = self._queue_overflow[mid]
|
|
334
|
+
if overflow_count % 100 == 1:
|
|
335
|
+
print(f"[kernel] Warning: Queue full for {mid}, dropped {overflow_count} events")
|
|
336
|
+
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Normal events → subscription-based routing
|
|
200
340
|
for mid, patterns in self.subscriptions.items():
|
|
201
341
|
if mid == sender_id and not echo:
|
|
202
342
|
continue
|
|
@@ -208,11 +348,14 @@ class EventHub:
|
|
|
208
348
|
raw = _dumps(msg)
|
|
209
349
|
try:
|
|
210
350
|
queue.put_nowait(raw)
|
|
351
|
+
self._cnt_queued += 1
|
|
211
352
|
except asyncio.QueueFull:
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
353
|
+
# Queue 满了,记录溢出
|
|
354
|
+
self._queue_overflow[mid] = self._queue_overflow.get(mid, 0) + 1
|
|
355
|
+
overflow_count = self._queue_overflow[mid]
|
|
356
|
+
# 每 100 次溢出打印一次警告
|
|
357
|
+
if overflow_count % 100 == 1:
|
|
358
|
+
print(f"[kernel] Warning: Queue full for {mid}, dropped {overflow_count} events")
|
|
216
359
|
break
|
|
217
360
|
|
|
218
361
|
# ── Stats ──
|
package/kernel/module.md
CHANGED
|
@@ -1,33 +1,36 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: kernel
|
|
3
|
-
display_name:
|
|
4
|
-
type: infrastructure
|
|
5
|
-
state: enabled
|
|
6
|
-
runtime: python
|
|
7
|
-
entry: entry.py
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
---
|
|
2
|
+
name: kernel
|
|
3
|
+
display_name: Kite内核
|
|
4
|
+
type: infrastructure
|
|
5
|
+
state: enabled
|
|
6
|
+
runtime: python
|
|
7
|
+
entry: entry.py
|
|
8
|
+
version: 1.0.1
|
|
9
|
+
advertise_ip: 127.0.0.1
|
|
10
|
+
monitor: true
|
|
11
|
+
preferred_port: 0
|
|
12
|
+
---
|
|
13
|
+
# Kernel
|
|
14
|
+
|
|
15
|
+
Unified infrastructure module that merges Registry (service discovery) and Event Hub (event routing) into a single process with a WebSocket JSON-RPC 2.0 interface.
|
|
16
|
+
|
|
17
|
+
## Responsibilities
|
|
18
|
+
|
|
19
|
+
- **Service Registry**: Module registration, heartbeat TTL, glob lookup, dot-path queries
|
|
20
|
+
- **Event Routing**: NATS-style wildcard subscriptions, per-subscriber queues, 1h dedup
|
|
21
|
+
- **RPC Routing**: JSON-RPC 2.0 dispatch for builtin methods + cross-module forwarding
|
|
22
|
+
- **Token Verification**: In-memory token→module_id resolution (no cross-process HTTP)
|
|
23
|
+
|
|
24
|
+
## Protocol
|
|
25
|
+
|
|
26
|
+
Single WebSocket endpoint: `ws://127.0.0.1:{port}/ws?token={TOKEN}&id={MODULE_ID}`
|
|
27
|
+
|
|
28
|
+
Three frame types on the wire:
|
|
29
|
+
- **RPC Request**: `{jsonrpc:"2.0", id, method, params}` — client→Kernel or Kernel→client (forwarded)
|
|
30
|
+
- **RPC Response**: `{jsonrpc:"2.0", id, result}` or `{jsonrpc:"2.0", id, error}` — response to request
|
|
31
|
+
- **Event Notification**: `{jsonrpc:"2.0", method:"event", params:{event_id, event, source, timestamp, data}}` — no id, no response
|
|
32
|
+
|
|
33
|
+
## HTTP Endpoints (debug only)
|
|
34
|
+
|
|
35
|
+
- `GET /health` — combined registry + event hub health
|
|
36
|
+
- `GET /stats` — connections, subscriptions, counters
|