@agentunion/kite 1.0.7 → 1.3.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 +208 -0
- package/README.md +48 -0
- package/cli.js +1 -1
- package/extensions/agents/__init__.py +1 -0
- package/extensions/agents/assistant/__init__.py +1 -0
- package/extensions/agents/assistant/entry.py +329 -0
- package/extensions/agents/assistant/module.md +22 -0
- package/extensions/agents/assistant/server.py +197 -0
- package/extensions/channels/__init__.py +1 -0
- package/extensions/channels/acp_channel/__init__.py +1 -0
- package/extensions/channels/acp_channel/entry.py +329 -0
- package/extensions/channels/acp_channel/module.md +22 -0
- package/extensions/channels/acp_channel/server.py +197 -0
- package/extensions/event_hub_bench/entry.py +624 -379
- package/extensions/event_hub_bench/module.md +2 -1
- package/extensions/services/backup/__init__.py +1 -0
- package/extensions/services/backup/entry.py +508 -0
- package/extensions/services/backup/module.md +22 -0
- package/extensions/services/model_service/__init__.py +1 -0
- package/extensions/services/model_service/entry.py +508 -0
- package/extensions/services/model_service/module.md +22 -0
- package/extensions/services/watchdog/entry.py +468 -102
- package/extensions/services/watchdog/module.md +3 -0
- package/extensions/services/watchdog/monitor.py +170 -69
- package/extensions/services/web/__init__.py +1 -0
- package/extensions/services/web/config.yaml +149 -0
- package/extensions/services/web/entry.py +390 -0
- package/extensions/services/web/module.md +24 -0
- package/extensions/services/web/routes/__init__.py +1 -0
- package/extensions/services/web/routes/routes_call.py +189 -0
- package/extensions/services/web/routes/routes_config.py +512 -0
- package/extensions/services/web/routes/routes_contacts.py +98 -0
- package/extensions/services/web/routes/routes_devlog.py +99 -0
- package/extensions/services/web/routes/routes_phone.py +81 -0
- package/extensions/services/web/routes/routes_sms.py +48 -0
- package/extensions/services/web/routes/routes_stats.py +17 -0
- package/extensions/services/web/routes/routes_voicechat.py +554 -0
- package/extensions/services/web/routes/schemas.py +216 -0
- package/extensions/services/web/server.py +375 -0
- package/extensions/services/web/static/css/style.css +1064 -0
- package/extensions/services/web/static/index.html +1445 -0
- package/extensions/services/web/static/js/app.js +4671 -0
- package/extensions/services/web/vendor/__init__.py +1 -0
- package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
- package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
- package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
- package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
- package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
- package/extensions/services/web/vendor/config.py +139 -0
- package/extensions/services/web/vendor/conversation/asr.py +936 -0
- package/extensions/services/web/vendor/conversation/engine.py +548 -0
- package/extensions/services/web/vendor/conversation/llm.py +534 -0
- package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
- package/extensions/services/web/vendor/conversation/tts.py +322 -0
- package/extensions/services/web/vendor/conversation/vad.py +138 -0
- package/extensions/services/web/vendor/storage/__init__.py +1 -0
- package/extensions/services/web/vendor/storage/identity.py +312 -0
- package/extensions/services/web/vendor/storage/store.py +507 -0
- package/extensions/services/web/vendor/task/manager.py +864 -0
- package/extensions/services/web/vendor/task/models.py +45 -0
- package/extensions/services/web/vendor/task/webhook.py +263 -0
- package/extensions/services/web/vendor/tools/registry.py +321 -0
- package/kernel/__init__.py +0 -0
- package/kernel/entry.py +407 -0
- package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
- package/kernel/module.md +33 -0
- package/{core/registry/store.py → kernel/registry_store.py} +23 -8
- package/kernel/rpc_router.py +388 -0
- package/kernel/server.py +267 -0
- package/launcher/__init__.py +10 -0
- package/launcher/__main__.py +6 -0
- package/launcher/count_lines.py +258 -0
- package/launcher/entry.py +1778 -0
- package/launcher/logging_setup.py +289 -0
- package/{core/launcher → launcher}/module_scanner.py +11 -6
- package/launcher/process_manager.py +880 -0
- package/main.py +11 -210
- package/package.json +6 -9
- package/__init__.py +0 -1
- package/__main__.py +0 -15
- package/core/event_hub/BENCHMARK.md +0 -94
- package/core/event_hub/bench.py +0 -459
- package/core/event_hub/bench_extreme.py +0 -308
- package/core/event_hub/bench_perf.py +0 -350
- package/core/event_hub/entry.py +0 -157
- package/core/event_hub/module.md +0 -20
- package/core/event_hub/server.py +0 -206
- package/core/launcher/entry.py +0 -1158
- package/core/launcher/process_manager.py +0 -470
- package/core/registry/entry.py +0 -110
- package/core/registry/module.md +0 -30
- package/core/registry/server.py +0 -289
- package/extensions/services/watchdog/server.py +0 -167
- /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
- /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
- /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
- /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
- /package/{core/event_hub → kernel}/dedup.py +0 -0
- /package/{core/event_hub → kernel}/router.py +0 -0
- /package/{core/launcher → launcher}/module.md +0 -0
package/kernel/entry.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kernel entry point.
|
|
3
|
+
Reads boot_info token + tokens dict from stdin, starts unified WebSocket server,
|
|
4
|
+
outputs port via stdout, self-registers, publishes module.ready.
|
|
5
|
+
|
|
6
|
+
Stdin protocol:
|
|
7
|
+
Line 1: {"token": "launcher_kite_token"} (boot_info)
|
|
8
|
+
Line 2: {"kite": "tokens", "tokens": {"mod1":"tok1", ...}} (token mapping)
|
|
9
|
+
|
|
10
|
+
Stdout protocol:
|
|
11
|
+
{"kite": "port", "port": N} (Kernel port)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import builtins
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import socket
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import traceback
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
|
|
25
|
+
import uvicorn
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── Module configuration ──
|
|
29
|
+
MODULE_NAME = "kernel"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fmt_elapsed(t0: float) -> str:
|
|
33
|
+
"""Format elapsed time since t0: <1s -> 'NNNms', >=1s -> 'N.Ns', >=10s -> 'NNs'."""
|
|
34
|
+
d = time.monotonic() - t0
|
|
35
|
+
if d < 1:
|
|
36
|
+
return f"{d * 1000:.0f}ms"
|
|
37
|
+
if d < 10:
|
|
38
|
+
return f"{d:.1f}s"
|
|
39
|
+
return f"{d:.0f}s"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
|
|
43
|
+
|
|
44
|
+
class _SafeWriter:
|
|
45
|
+
"""Wraps a stream to silently swallow BrokenPipeError on write/flush."""
|
|
46
|
+
def __init__(self, stream):
|
|
47
|
+
self._stream = stream
|
|
48
|
+
|
|
49
|
+
def write(self, s):
|
|
50
|
+
try:
|
|
51
|
+
self._stream.write(s)
|
|
52
|
+
except (BrokenPipeError, OSError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def flush(self):
|
|
56
|
+
try:
|
|
57
|
+
self._stream.flush()
|
|
58
|
+
except (BrokenPipeError, OSError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def __getattr__(self, name):
|
|
62
|
+
return getattr(self._stream, name)
|
|
63
|
+
|
|
64
|
+
sys.stdout = _SafeWriter(sys.stdout)
|
|
65
|
+
sys.stderr = _SafeWriter(sys.stderr)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ── Timestamped print + log file writer ──
|
|
69
|
+
# Independent implementation per module (no shared code dependency)
|
|
70
|
+
|
|
71
|
+
_builtin_print = builtins.print
|
|
72
|
+
_start_ts = time.monotonic()
|
|
73
|
+
_last_ts = time.monotonic()
|
|
74
|
+
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
75
|
+
_log_lock = threading.Lock()
|
|
76
|
+
_log_latest_path = None
|
|
77
|
+
_log_daily_path = None
|
|
78
|
+
_log_daily_date = ""
|
|
79
|
+
_log_dir = None
|
|
80
|
+
_crash_log_path = None
|
|
81
|
+
|
|
82
|
+
def _strip_ansi(s: str) -> str:
|
|
83
|
+
return _ANSI_RE.sub("", s)
|
|
84
|
+
|
|
85
|
+
def _resolve_daily_log_path():
|
|
86
|
+
"""Resolve daily log path based on current date."""
|
|
87
|
+
global _log_daily_path, _log_daily_date
|
|
88
|
+
if not _log_dir:
|
|
89
|
+
return
|
|
90
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
91
|
+
if today == _log_daily_date and _log_daily_path:
|
|
92
|
+
return
|
|
93
|
+
month_dir = os.path.join(_log_dir, today[:7])
|
|
94
|
+
os.makedirs(month_dir, exist_ok=True)
|
|
95
|
+
_log_daily_path = os.path.join(month_dir, f"{today}.log")
|
|
96
|
+
_log_daily_date = today
|
|
97
|
+
|
|
98
|
+
def _write_log(plain_line: str):
|
|
99
|
+
"""Write a plain-text line to both latest.log and daily log."""
|
|
100
|
+
with _log_lock:
|
|
101
|
+
if _log_latest_path:
|
|
102
|
+
try:
|
|
103
|
+
with open(_log_latest_path, "a", encoding="utf-8") as f:
|
|
104
|
+
f.write(plain_line)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
_resolve_daily_log_path()
|
|
108
|
+
if _log_daily_path:
|
|
109
|
+
try:
|
|
110
|
+
with open(_log_daily_path, "a", encoding="utf-8") as f:
|
|
111
|
+
f.write(plain_line)
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
|
|
116
|
+
"""Write crash record to crashes.jsonl + daily crash archive."""
|
|
117
|
+
record = {
|
|
118
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
119
|
+
"module": MODULE_NAME,
|
|
120
|
+
"thread": thread_name or threading.current_thread().name,
|
|
121
|
+
"exception_type": exc_type.__name__ if exc_type else "Unknown",
|
|
122
|
+
"exception_message": str(exc_value),
|
|
123
|
+
"traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
124
|
+
"severity": severity,
|
|
125
|
+
"handled": handled,
|
|
126
|
+
"process_id": os.getpid(),
|
|
127
|
+
"platform": sys.platform,
|
|
128
|
+
"runtime_version": f"Python {sys.version.split()[0]}",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if exc_tb:
|
|
132
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
133
|
+
if tb_entries:
|
|
134
|
+
last = tb_entries[-1]
|
|
135
|
+
record["context"] = {
|
|
136
|
+
"function": last.name,
|
|
137
|
+
"file": os.path.basename(last.filename),
|
|
138
|
+
"line": last.lineno,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
line = json.dumps(record, ensure_ascii=False) + "\n"
|
|
142
|
+
|
|
143
|
+
# 1. Write to crashes.jsonl (current run)
|
|
144
|
+
if _crash_log_path:
|
|
145
|
+
try:
|
|
146
|
+
with open(_crash_log_path, "a", encoding="utf-8") as f:
|
|
147
|
+
f.write(line)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
# 2. Write to daily crash archive
|
|
152
|
+
if _log_dir:
|
|
153
|
+
try:
|
|
154
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
155
|
+
archive_dir = os.path.join(_log_dir, "crashes", today[:7])
|
|
156
|
+
os.makedirs(archive_dir, exist_ok=True)
|
|
157
|
+
archive_path = os.path.join(archive_dir, f"{today}.jsonl")
|
|
158
|
+
with open(archive_path, "a", encoding="utf-8") as f:
|
|
159
|
+
f.write(line)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
def _print_crash_summary(exc_type, exc_tb, thread_name=None):
|
|
164
|
+
"""Print crash summary to console (red highlight)."""
|
|
165
|
+
RED = "\033[91m"
|
|
166
|
+
RESET = "\033[0m"
|
|
167
|
+
|
|
168
|
+
if exc_tb:
|
|
169
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
170
|
+
if tb_entries:
|
|
171
|
+
last = tb_entries[-1]
|
|
172
|
+
location = f"{os.path.basename(last.filename)}:{last.lineno}"
|
|
173
|
+
else:
|
|
174
|
+
location = "unknown"
|
|
175
|
+
else:
|
|
176
|
+
location = "unknown"
|
|
177
|
+
|
|
178
|
+
prefix = f"[{MODULE_NAME}]"
|
|
179
|
+
if thread_name:
|
|
180
|
+
_builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
|
|
181
|
+
f"{exc_type.__name__} in {location}{RESET}")
|
|
182
|
+
else:
|
|
183
|
+
_builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
|
|
184
|
+
if _crash_log_path:
|
|
185
|
+
_builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
|
|
186
|
+
|
|
187
|
+
def _setup_exception_hooks():
|
|
188
|
+
"""Set up global exception hooks (sys.excepthook + threading.excepthook)."""
|
|
189
|
+
_orig_excepthook = sys.excepthook
|
|
190
|
+
|
|
191
|
+
def _excepthook(exc_type, exc_value, exc_tb):
|
|
192
|
+
_write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
|
|
193
|
+
_print_crash_summary(exc_type, exc_tb)
|
|
194
|
+
_orig_excepthook(exc_type, exc_value, exc_tb)
|
|
195
|
+
|
|
196
|
+
sys.excepthook = _excepthook
|
|
197
|
+
|
|
198
|
+
if hasattr(threading, "excepthook"):
|
|
199
|
+
def _thread_excepthook(args):
|
|
200
|
+
_write_crash(args.exc_type, args.exc_value, args.exc_traceback,
|
|
201
|
+
thread_name=args.thread.name if args.thread else "unknown",
|
|
202
|
+
severity="error", handled=False)
|
|
203
|
+
_print_crash_summary(args.exc_type, args.exc_traceback,
|
|
204
|
+
thread_name=args.thread.name if args.thread else None)
|
|
205
|
+
|
|
206
|
+
threading.excepthook = _thread_excepthook
|
|
207
|
+
|
|
208
|
+
def _tprint(*args, **kwargs):
|
|
209
|
+
"""Timestamped print that adds [timestamp] HH:MM:SS.mmm +delta prefix."""
|
|
210
|
+
global _last_ts
|
|
211
|
+
now = time.monotonic()
|
|
212
|
+
elapsed = now - _start_ts
|
|
213
|
+
delta = now - _last_ts
|
|
214
|
+
_last_ts = now
|
|
215
|
+
|
|
216
|
+
# Format elapsed (cumulative time since start)
|
|
217
|
+
if elapsed < 1:
|
|
218
|
+
elapsed_str = f"{elapsed * 1000:.0f}ms"
|
|
219
|
+
elif elapsed < 100:
|
|
220
|
+
elapsed_str = f"{elapsed:.1f}s"
|
|
221
|
+
else:
|
|
222
|
+
elapsed_str = f"{elapsed:.0f}s"
|
|
223
|
+
|
|
224
|
+
# Format delta (time since last print)
|
|
225
|
+
if delta < 0.001:
|
|
226
|
+
delta_str = ""
|
|
227
|
+
elif delta < 1:
|
|
228
|
+
delta_str = f"+{delta * 1000:.0f}ms"
|
|
229
|
+
elif delta < 100:
|
|
230
|
+
delta_str = f"+{delta:.1f}s"
|
|
231
|
+
else:
|
|
232
|
+
delta_str = f"+{delta:.0f}s"
|
|
233
|
+
|
|
234
|
+
# Current time
|
|
235
|
+
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
236
|
+
|
|
237
|
+
# Print to console (original behavior)
|
|
238
|
+
_builtin_print(*args, **kwargs)
|
|
239
|
+
|
|
240
|
+
# Write to log files with timestamp prefix
|
|
241
|
+
if _log_latest_path or _log_daily_path:
|
|
242
|
+
sep = kwargs.get("sep", " ")
|
|
243
|
+
end = kwargs.get("end", "\n")
|
|
244
|
+
text = sep.join(str(a) for a in args)
|
|
245
|
+
prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
|
|
246
|
+
_write_log(prefix + _strip_ansi(text) + end)
|
|
247
|
+
|
|
248
|
+
builtins.print = _tprint
|
|
249
|
+
|
|
250
|
+
# Ensure project root is on sys.path (set by main.py or cli.js)
|
|
251
|
+
_this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
252
|
+
_project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(_this_dir))
|
|
253
|
+
if _project_root not in sys.path:
|
|
254
|
+
sys.path.insert(0, _project_root)
|
|
255
|
+
|
|
256
|
+
from kernel.server import KernelServer
|
|
257
|
+
|
|
258
|
+
|
|
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
|
+
def _bind_port(preferred: int, host: str) -> int:
|
|
281
|
+
"""Try preferred port first, fall back to OS-assigned."""
|
|
282
|
+
if preferred:
|
|
283
|
+
try:
|
|
284
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
285
|
+
s.bind((host, preferred))
|
|
286
|
+
return preferred
|
|
287
|
+
except OSError:
|
|
288
|
+
pass
|
|
289
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
290
|
+
s.bind((host, 0))
|
|
291
|
+
return s.getsockname()[1]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _read_stdin_kite_messages(expected: set[str], timeout: float = 10) -> dict[str, dict]:
|
|
295
|
+
"""Read multiple structured kite messages from stdin until all expected types received.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
expected: set of kite message types to wait for (e.g. {"tokens"})
|
|
299
|
+
timeout: total timeout in seconds
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
dict mapping kite type -> message dict. Missing types are absent from the result.
|
|
303
|
+
"""
|
|
304
|
+
collected: dict[str, dict] = {}
|
|
305
|
+
remaining = set(expected)
|
|
306
|
+
|
|
307
|
+
def _read():
|
|
308
|
+
while remaining:
|
|
309
|
+
try:
|
|
310
|
+
line = sys.stdin.readline().strip()
|
|
311
|
+
if not line:
|
|
312
|
+
return # stdin closed
|
|
313
|
+
msg = json.loads(line)
|
|
314
|
+
if isinstance(msg, dict) and "kite" in msg:
|
|
315
|
+
kite_type = msg["kite"]
|
|
316
|
+
collected[kite_type] = msg
|
|
317
|
+
remaining.discard(kite_type)
|
|
318
|
+
except Exception:
|
|
319
|
+
return # parse error or stdin closed
|
|
320
|
+
|
|
321
|
+
t = threading.Thread(target=_read, daemon=True)
|
|
322
|
+
t.start()
|
|
323
|
+
t.join(timeout=timeout)
|
|
324
|
+
return collected
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def main():
|
|
328
|
+
# Initialize log file paths
|
|
329
|
+
global _log_dir, _log_latest_path, _crash_log_path
|
|
330
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
331
|
+
if module_data:
|
|
332
|
+
_log_dir = os.path.join(module_data, "log")
|
|
333
|
+
os.makedirs(_log_dir, exist_ok=True)
|
|
334
|
+
suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
|
|
335
|
+
|
|
336
|
+
# latest.log — truncate on each startup
|
|
337
|
+
_log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
|
|
338
|
+
try:
|
|
339
|
+
with open(_log_latest_path, "w", encoding="utf-8") as f:
|
|
340
|
+
pass # truncate
|
|
341
|
+
except Exception:
|
|
342
|
+
_log_latest_path = None
|
|
343
|
+
|
|
344
|
+
# crashes.jsonl — truncate on each startup
|
|
345
|
+
_crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
|
|
346
|
+
try:
|
|
347
|
+
with open(_crash_log_path, "w", encoding="utf-8") as f:
|
|
348
|
+
pass # truncate
|
|
349
|
+
except Exception:
|
|
350
|
+
_crash_log_path = None
|
|
351
|
+
|
|
352
|
+
_resolve_daily_log_path()
|
|
353
|
+
|
|
354
|
+
# Setup exception hooks
|
|
355
|
+
_setup_exception_hooks()
|
|
356
|
+
|
|
357
|
+
_t0 = time.monotonic()
|
|
358
|
+
|
|
359
|
+
# Kite environment
|
|
360
|
+
is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
361
|
+
|
|
362
|
+
if is_debug:
|
|
363
|
+
print("[kernel] 调试模式已启用 (KITE_DEBUG=1),接受所有令牌")
|
|
364
|
+
|
|
365
|
+
# Step 1: Read config from own module.md
|
|
366
|
+
md_config = _read_module_md()
|
|
367
|
+
advertise_ip = md_config["advertise_ip"]
|
|
368
|
+
preferred_port = md_config["preferred_port"]
|
|
369
|
+
|
|
370
|
+
# Step 2: Generate launcher token
|
|
371
|
+
import secrets
|
|
372
|
+
launcher_token = secrets.token_urlsafe(32)
|
|
373
|
+
|
|
374
|
+
# Step 3: Create KernelServer with launcher_token
|
|
375
|
+
server = KernelServer(launcher_token=launcher_token, advertise_ip=advertise_ip)
|
|
376
|
+
|
|
377
|
+
# Step 4: Bind port
|
|
378
|
+
bind_host = advertise_ip
|
|
379
|
+
port = _bind_port(preferred_port, bind_host)
|
|
380
|
+
server.port = port
|
|
381
|
+
|
|
382
|
+
# Step 5: Output port + launcher_token via stdout
|
|
383
|
+
print(json.dumps({"kite": "port", "port": port, "token": launcher_token}), flush=True)
|
|
384
|
+
|
|
385
|
+
print(f"[kernel] 启动中 {bind_host}:{port} ({_fmt_elapsed(_t0)})")
|
|
386
|
+
|
|
387
|
+
# Step 6: Self-register in registry (direct memory write)
|
|
388
|
+
server.self_register()
|
|
389
|
+
|
|
390
|
+
# Step 7: Start uvicorn (module.ready will be published when Launcher subscribes)
|
|
391
|
+
try:
|
|
392
|
+
config = uvicorn.Config(server.app, host=bind_host, port=port, log_level="warning")
|
|
393
|
+
uvi_server = uvicorn.Server(config)
|
|
394
|
+
server._uvicorn_server = uvi_server
|
|
395
|
+
|
|
396
|
+
# module.ready is now published when Launcher subscribes (not at startup)
|
|
397
|
+
# This ensures Launcher is connected and subscribed before receiving the event
|
|
398
|
+
|
|
399
|
+
uvi_server.run()
|
|
400
|
+
except Exception as e:
|
|
401
|
+
_write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
|
|
402
|
+
_print_crash_summary(type(e), e.__traceback__)
|
|
403
|
+
sys.exit(1)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
if __name__ == "__main__":
|
|
407
|
+
main()
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
Kernel Event Hub logic.
|
|
3
|
+
Connection management, subscription matching, event routing (validate -> dedup -> route),
|
|
4
|
+
per-subscriber delivery queues.
|
|
5
|
+
|
|
6
|
+
Adapted from core/event_hub/hub.py for the merged Kernel module:
|
|
7
|
+
- Removed handle_message, _handle_event, _send_ack, _send_error (moved to RpcRouter)
|
|
8
|
+
- Added publish_event() for RPC-driven event publishing
|
|
9
|
+
- Added publish_internal() for Kernel-originated events (e.g. module.offline)
|
|
10
|
+
- Event delivery uses JSON-RPC 2.0 Notification format
|
|
5
11
|
"""
|
|
6
12
|
|
|
7
13
|
import asyncio
|
|
@@ -14,7 +20,7 @@ try:
|
|
|
14
20
|
except ImportError:
|
|
15
21
|
orjson = None
|
|
16
22
|
|
|
17
|
-
from
|
|
23
|
+
from starlette.websockets import WebSocket
|
|
18
24
|
|
|
19
25
|
from .dedup import EventDedup
|
|
20
26
|
from .router import match_parts
|
|
@@ -31,6 +37,7 @@ def _loads(raw: str):
|
|
|
31
37
|
return orjson.loads(raw)
|
|
32
38
|
return json.loads(raw)
|
|
33
39
|
|
|
40
|
+
|
|
34
41
|
QUEUE_MAXSIZE = 10000
|
|
35
42
|
|
|
36
43
|
|
|
@@ -75,14 +82,14 @@ class EventHub:
|
|
|
75
82
|
self._senders[module_id] = asyncio.create_task(
|
|
76
83
|
self._sender_loop(module_id, ws, q)
|
|
77
84
|
)
|
|
78
|
-
print(f"[
|
|
85
|
+
print(f"[kernel] {module_id} connected")
|
|
79
86
|
|
|
80
87
|
async def _close_old(self, module_id: str, ws: WebSocket):
|
|
81
88
|
try:
|
|
82
89
|
await ws.close(code=4000, reason="replaced by new connection")
|
|
83
90
|
except Exception:
|
|
84
91
|
pass
|
|
85
|
-
print(f"[
|
|
92
|
+
print(f"[kernel] Closed old connection for {module_id}")
|
|
86
93
|
|
|
87
94
|
def remove_connection(self, module_id: str):
|
|
88
95
|
"""Clean up on disconnect."""
|
|
@@ -93,7 +100,7 @@ class EventHub:
|
|
|
93
100
|
task = self._senders.pop(module_id, None)
|
|
94
101
|
if task:
|
|
95
102
|
task.cancel()
|
|
96
|
-
print(f"[
|
|
103
|
+
print(f"[kernel] {module_id} disconnected")
|
|
97
104
|
|
|
98
105
|
# ── Sender loop (per-subscriber) ──
|
|
99
106
|
|
|
@@ -109,7 +116,7 @@ class EventHub:
|
|
|
109
116
|
await ws.send_text(raw)
|
|
110
117
|
self._cnt_routed += 1
|
|
111
118
|
except Exception:
|
|
112
|
-
print(f"[
|
|
119
|
+
print(f"[kernel] Send failed to {mid}, closing sender")
|
|
113
120
|
break
|
|
114
121
|
except asyncio.CancelledError:
|
|
115
122
|
pass
|
|
@@ -122,77 +129,72 @@ class EventHub:
|
|
|
122
129
|
self.subscriptions[module_id].update(
|
|
123
130
|
(p, tuple(p.split("."))) for p in events
|
|
124
131
|
)
|
|
125
|
-
print(f"[
|
|
132
|
+
print(f"[kernel] {module_id} subscribed: {events}")
|
|
126
133
|
|
|
127
134
|
def handle_unsubscribe(self, module_id: str, events: list[str]):
|
|
128
135
|
subs = self.subscriptions.get(module_id)
|
|
129
136
|
if subs:
|
|
130
137
|
to_remove = {item for item in subs if item[0] in events}
|
|
131
138
|
subs.difference_update(to_remove)
|
|
132
|
-
print(f"[
|
|
133
|
-
|
|
134
|
-
# ── Main message handler ──
|
|
135
|
-
|
|
136
|
-
async def handle_message(self, module_id: str, ws: WebSocket, raw: str):
|
|
137
|
-
try:
|
|
138
|
-
msg = _loads(raw)
|
|
139
|
-
except Exception:
|
|
140
|
-
await self._send_error(ws, "Invalid JSON")
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
msg_type = msg.get("type", "")
|
|
144
|
-
|
|
145
|
-
if msg_type == "subscribe":
|
|
146
|
-
events = msg.get("events", [])
|
|
147
|
-
if isinstance(events, list) and events:
|
|
148
|
-
self.handle_subscribe(module_id, events)
|
|
149
|
-
return
|
|
139
|
+
print(f"[kernel] {module_id} unsubscribed: {events}")
|
|
150
140
|
|
|
151
|
-
|
|
152
|
-
events = msg.get("events", [])
|
|
153
|
-
if isinstance(events, list) and events:
|
|
154
|
-
self.handle_unsubscribe(module_id, events)
|
|
155
|
-
return
|
|
141
|
+
# ── Event publishing (called by RpcRouter) ──
|
|
156
142
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await self._send_error(ws, f"Unknown message type: {msg_type}")
|
|
162
|
-
|
|
163
|
-
# ── Event processing ──
|
|
164
|
-
|
|
165
|
-
async def _handle_event(self, module_id: str, ws: WebSocket, msg: dict):
|
|
166
|
-
"""Validate → dedup → auto-fill → route → ack."""
|
|
143
|
+
def publish_event(self, sender_id: str, event_id: str, event_type: str,
|
|
144
|
+
data: dict = None, echo: bool = False) -> dict:
|
|
145
|
+
"""Publish an event from a module. Called by RpcRouter for event.publish RPC.
|
|
146
|
+
Returns {ok: True} on success, or error dict if validation fails."""
|
|
167
147
|
self._cnt_received += 1
|
|
168
148
|
|
|
169
|
-
event_id = msg.get("event_id")
|
|
170
|
-
event_type = msg.get("event")
|
|
171
149
|
if not event_id or not event_type:
|
|
172
150
|
self._cnt_errors += 1
|
|
173
|
-
|
|
174
|
-
return
|
|
151
|
+
return {"ok": False, "error": "Missing required field: event_id or event"}
|
|
175
152
|
|
|
176
153
|
if self.dedup.is_duplicate(event_id):
|
|
177
154
|
self._cnt_dedup += 1
|
|
178
|
-
|
|
179
|
-
|
|
155
|
+
return {"ok": True} # silently accept duplicates
|
|
156
|
+
|
|
157
|
+
msg = {
|
|
158
|
+
"jsonrpc": "2.0",
|
|
159
|
+
"method": "event",
|
|
160
|
+
"params": {
|
|
161
|
+
"event_id": event_id,
|
|
162
|
+
"event": event_type,
|
|
163
|
+
"source": sender_id,
|
|
164
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
165
|
+
"data": data or {},
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
self._route_event(sender_id, msg, event_type, echo)
|
|
170
|
+
return {"ok": True}
|
|
180
171
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
172
|
+
def publish_internal(self, event_type: str, data: dict):
|
|
173
|
+
"""Publish a Kernel-originated event (e.g. module.offline, module.registered).
|
|
174
|
+
No dedup check — internal events are unique by nature."""
|
|
175
|
+
import uuid
|
|
176
|
+
event_id = str(uuid.uuid4())
|
|
177
|
+
self._cnt_received += 1
|
|
178
|
+
|
|
179
|
+
msg = {
|
|
180
|
+
"jsonrpc": "2.0",
|
|
181
|
+
"method": "event",
|
|
182
|
+
"params": {
|
|
183
|
+
"event_id": event_id,
|
|
184
|
+
"event": event_type,
|
|
185
|
+
"source": "kernel",
|
|
186
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
187
|
+
"data": data,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
185
190
|
|
|
186
|
-
|
|
187
|
-
await self._send_ack(ws, event_id)
|
|
191
|
+
self._route_event("kernel", msg, event_type, echo=False)
|
|
188
192
|
|
|
189
193
|
# ── Routing ──
|
|
190
194
|
|
|
191
|
-
|
|
195
|
+
def _route_event(self, sender_id: str, msg: dict, event_type: str, echo: bool):
|
|
192
196
|
"""Enqueue event to all matching subscribers' delivery queues."""
|
|
193
|
-
event_type = msg["event"]
|
|
194
197
|
e_parts = tuple(event_type.split("."))
|
|
195
|
-
echo = msg.get("echo", False)
|
|
196
198
|
raw = None # lazy serialization
|
|
197
199
|
|
|
198
200
|
for mid, patterns in self.subscriptions.items():
|
|
@@ -207,26 +209,12 @@ class EventHub:
|
|
|
207
209
|
try:
|
|
208
210
|
queue.put_nowait(raw)
|
|
209
211
|
except asyncio.QueueFull:
|
|
210
|
-
|
|
212
|
+
# Best-effort: drop if queue is full and we can't await
|
|
213
|
+
# (we're not in an async context in _route_event)
|
|
214
|
+
pass
|
|
211
215
|
self._cnt_queued += 1
|
|
212
216
|
break
|
|
213
217
|
|
|
214
|
-
# ── Helpers ──
|
|
215
|
-
|
|
216
|
-
@staticmethod
|
|
217
|
-
async def _send_ack(ws: WebSocket, event_id: str):
|
|
218
|
-
try:
|
|
219
|
-
await ws.send_text('{"type":"ack","event_id":"' + event_id + '"}')
|
|
220
|
-
except Exception:
|
|
221
|
-
pass
|
|
222
|
-
|
|
223
|
-
@staticmethod
|
|
224
|
-
async def _send_error(ws: WebSocket, message: str):
|
|
225
|
-
try:
|
|
226
|
-
await ws.send_json({"type": "error", "message": message})
|
|
227
|
-
except Exception:
|
|
228
|
-
pass
|
|
229
|
-
|
|
230
218
|
# ── Stats ──
|
|
231
219
|
|
|
232
220
|
def _counters_dict(self) -> dict:
|
package/kernel/module.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kernel
|
|
3
|
+
display_name: Kernel
|
|
4
|
+
type: infrastructure
|
|
5
|
+
state: enabled
|
|
6
|
+
runtime: python
|
|
7
|
+
entry: entry.py
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Kernel
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
## Responsibilities
|
|
15
|
+
|
|
16
|
+
- **Service Registry**: Module registration, heartbeat TTL, glob lookup, dot-path queries
|
|
17
|
+
- **Event Routing**: NATS-style wildcard subscriptions, per-subscriber queues, 1h dedup
|
|
18
|
+
- **RPC Routing**: JSON-RPC 2.0 dispatch for builtin methods + cross-module forwarding
|
|
19
|
+
- **Token Verification**: In-memory token→module_id resolution (no cross-process HTTP)
|
|
20
|
+
|
|
21
|
+
## Protocol
|
|
22
|
+
|
|
23
|
+
Single WebSocket endpoint: `ws://127.0.0.1:{port}/ws?token={TOKEN}&id={MODULE_ID}`
|
|
24
|
+
|
|
25
|
+
Three frame types on the wire:
|
|
26
|
+
- **RPC Request**: `{jsonrpc:"2.0", id, method, params}` — client→Kernel or Kernel→client (forwarded)
|
|
27
|
+
- **RPC Response**: `{jsonrpc:"2.0", id, result}` or `{jsonrpc:"2.0", id, error}` — response to request
|
|
28
|
+
- **Event Notification**: `{jsonrpc:"2.0", method:"event", params:{event_id, event, source, timestamp, data}}` — no id, no response
|
|
29
|
+
|
|
30
|
+
## HTTP Endpoints (debug only)
|
|
31
|
+
|
|
32
|
+
- `GET /health` — combined registry + event hub health
|
|
33
|
+
- `GET /stats` — connections, subscriptions, counters
|