@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backup entry point.
|
|
3
|
+
Reads boot_info from stdin, registers to Registry, starts backup service.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import builtins
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import traceback
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
import websockets
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── Module configuration ──
|
|
26
|
+
MODULE_NAME = "backup"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _SafeWriter:
|
|
30
|
+
"""Wraps a stream to silently swallow BrokenPipeError on write/flush."""
|
|
31
|
+
def __init__(self, stream):
|
|
32
|
+
self._stream = stream
|
|
33
|
+
|
|
34
|
+
def write(self, s):
|
|
35
|
+
try:
|
|
36
|
+
self._stream.write(s)
|
|
37
|
+
except (BrokenPipeError, OSError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def flush(self):
|
|
41
|
+
try:
|
|
42
|
+
self._stream.flush()
|
|
43
|
+
except (BrokenPipeError, OSError):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def __getattr__(self, name):
|
|
47
|
+
return getattr(self._stream, name)
|
|
48
|
+
|
|
49
|
+
sys.stdout = _SafeWriter(sys.stdout)
|
|
50
|
+
sys.stderr = _SafeWriter(sys.stderr)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── Timestamped print + log file writer ──
|
|
54
|
+
# Independent implementation per module (no shared code dependency)
|
|
55
|
+
|
|
56
|
+
_builtin_print = builtins.print
|
|
57
|
+
_start_ts = time.monotonic()
|
|
58
|
+
_last_ts = time.monotonic()
|
|
59
|
+
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
60
|
+
_log_lock = threading.Lock()
|
|
61
|
+
_log_latest_path = None
|
|
62
|
+
_log_daily_path = None
|
|
63
|
+
_log_daily_date = ""
|
|
64
|
+
_log_dir = None
|
|
65
|
+
_crash_log_path = None
|
|
66
|
+
|
|
67
|
+
def _strip_ansi(s: str) -> str:
|
|
68
|
+
return _ANSI_RE.sub("", s)
|
|
69
|
+
|
|
70
|
+
def _resolve_daily_log_path():
|
|
71
|
+
"""Resolve daily log path based on current date."""
|
|
72
|
+
global _log_daily_path, _log_daily_date
|
|
73
|
+
if not _log_dir:
|
|
74
|
+
return
|
|
75
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
76
|
+
if today == _log_daily_date and _log_daily_path:
|
|
77
|
+
return
|
|
78
|
+
month_dir = os.path.join(_log_dir, today[:7])
|
|
79
|
+
os.makedirs(month_dir, exist_ok=True)
|
|
80
|
+
_log_daily_path = os.path.join(month_dir, f"{today}.log")
|
|
81
|
+
_log_daily_date = today
|
|
82
|
+
|
|
83
|
+
def _write_log(plain_line: str):
|
|
84
|
+
"""Write a plain-text line to both latest.log and daily log."""
|
|
85
|
+
with _log_lock:
|
|
86
|
+
if _log_latest_path:
|
|
87
|
+
try:
|
|
88
|
+
with open(_log_latest_path, "a", encoding="utf-8") as f:
|
|
89
|
+
f.write(plain_line)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
_resolve_daily_log_path()
|
|
93
|
+
if _log_daily_path:
|
|
94
|
+
try:
|
|
95
|
+
with open(_log_daily_path, "a", encoding="utf-8") as f:
|
|
96
|
+
f.write(plain_line)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
|
|
101
|
+
"""Write crash record to crashes.jsonl + daily crash archive."""
|
|
102
|
+
record = {
|
|
103
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
104
|
+
"module": MODULE_NAME,
|
|
105
|
+
"thread": thread_name or threading.current_thread().name,
|
|
106
|
+
"exception_type": exc_type.__name__ if exc_type else "Unknown",
|
|
107
|
+
"exception_message": str(exc_value),
|
|
108
|
+
"traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
109
|
+
"severity": severity,
|
|
110
|
+
"handled": handled,
|
|
111
|
+
"process_id": os.getpid(),
|
|
112
|
+
"platform": sys.platform,
|
|
113
|
+
"runtime_version": f"Python {sys.version.split()[0]}",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if exc_tb:
|
|
117
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
118
|
+
if tb_entries:
|
|
119
|
+
last = tb_entries[-1]
|
|
120
|
+
record["context"] = {
|
|
121
|
+
"function": last.name,
|
|
122
|
+
"file": os.path.basename(last.filename),
|
|
123
|
+
"line": last.lineno,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
line = json.dumps(record, ensure_ascii=False) + "\n"
|
|
127
|
+
|
|
128
|
+
if _crash_log_path:
|
|
129
|
+
try:
|
|
130
|
+
with open(_crash_log_path, "a", encoding="utf-8") as f:
|
|
131
|
+
f.write(line)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
if _log_dir:
|
|
136
|
+
try:
|
|
137
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
138
|
+
archive_dir = os.path.join(_log_dir, "crashes", today[:7])
|
|
139
|
+
os.makedirs(archive_dir, exist_ok=True)
|
|
140
|
+
archive_path = os.path.join(archive_dir, f"{today}.jsonl")
|
|
141
|
+
with open(archive_path, "a", encoding="utf-8") as f:
|
|
142
|
+
f.write(line)
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def _print_crash_summary(exc_type, exc_tb, thread_name=None):
|
|
147
|
+
"""Print crash summary to console (red highlight)."""
|
|
148
|
+
RED = "\033[91m"
|
|
149
|
+
RESET = "\033[0m"
|
|
150
|
+
|
|
151
|
+
if exc_tb:
|
|
152
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
153
|
+
if tb_entries:
|
|
154
|
+
last = tb_entries[-1]
|
|
155
|
+
location = f"{os.path.basename(last.filename)}:{last.lineno}"
|
|
156
|
+
else:
|
|
157
|
+
location = "unknown"
|
|
158
|
+
else:
|
|
159
|
+
location = "unknown"
|
|
160
|
+
|
|
161
|
+
prefix = f"[{MODULE_NAME}]"
|
|
162
|
+
if thread_name:
|
|
163
|
+
_builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
|
|
164
|
+
f"{exc_type.__name__} in {location}{RESET}")
|
|
165
|
+
else:
|
|
166
|
+
_builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
|
|
167
|
+
if _crash_log_path:
|
|
168
|
+
_builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
|
|
169
|
+
|
|
170
|
+
def _setup_exception_hooks():
|
|
171
|
+
"""Set up global exception hooks."""
|
|
172
|
+
_orig_excepthook = sys.excepthook
|
|
173
|
+
|
|
174
|
+
def _excepthook(exc_type, exc_value, exc_tb):
|
|
175
|
+
_write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
|
|
176
|
+
_print_crash_summary(exc_type, exc_tb)
|
|
177
|
+
_orig_excepthook(exc_type, exc_value, exc_tb)
|
|
178
|
+
|
|
179
|
+
sys.excepthook = _excepthook
|
|
180
|
+
|
|
181
|
+
if hasattr(threading, "excepthook"):
|
|
182
|
+
def _thread_excepthook(args):
|
|
183
|
+
_write_crash(args.exc_type, args.exc_value, args.exc_traceback,
|
|
184
|
+
thread_name=args.thread.name if args.thread else "unknown",
|
|
185
|
+
severity="error", handled=False)
|
|
186
|
+
_print_crash_summary(args.exc_type, args.exc_traceback,
|
|
187
|
+
thread_name=args.thread.name if args.thread else None)
|
|
188
|
+
|
|
189
|
+
threading.excepthook = _thread_excepthook
|
|
190
|
+
|
|
191
|
+
def _tprint(*args, **kwargs):
|
|
192
|
+
"""Timestamped print that adds [timestamp] HH:MM:SS.mmm +delta prefix."""
|
|
193
|
+
global _last_ts
|
|
194
|
+
now = time.monotonic()
|
|
195
|
+
elapsed = now - _start_ts
|
|
196
|
+
delta = now - _last_ts
|
|
197
|
+
_last_ts = now
|
|
198
|
+
|
|
199
|
+
if elapsed < 1:
|
|
200
|
+
elapsed_str = f"{elapsed * 1000:.0f}ms"
|
|
201
|
+
elif elapsed < 100:
|
|
202
|
+
elapsed_str = f"{elapsed:.1f}s"
|
|
203
|
+
else:
|
|
204
|
+
elapsed_str = f"{elapsed:.0f}s"
|
|
205
|
+
|
|
206
|
+
if delta < 0.001:
|
|
207
|
+
delta_str = ""
|
|
208
|
+
elif delta < 1:
|
|
209
|
+
delta_str = f"+{delta * 1000:.0f}ms"
|
|
210
|
+
elif delta < 100:
|
|
211
|
+
delta_str = f"+{delta:.1f}s"
|
|
212
|
+
else:
|
|
213
|
+
delta_str = f"+{delta:.0f}s"
|
|
214
|
+
|
|
215
|
+
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
216
|
+
|
|
217
|
+
_builtin_print(*args, **kwargs)
|
|
218
|
+
|
|
219
|
+
if _log_latest_path or _log_daily_path:
|
|
220
|
+
sep = kwargs.get("sep", " ")
|
|
221
|
+
end = kwargs.get("end", "\n")
|
|
222
|
+
text = sep.join(str(a) for a in args)
|
|
223
|
+
prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
|
|
224
|
+
_write_log(prefix + _strip_ansi(text) + end)
|
|
225
|
+
|
|
226
|
+
builtins.print = _tprint
|
|
227
|
+
|
|
228
|
+
# Ensure project root is on sys.path (set by main.py or cli.js)
|
|
229
|
+
_project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
230
|
+
if _project_root not in sys.path:
|
|
231
|
+
sys.path.insert(0, _project_root)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _fmt_elapsed(t0: float) -> str:
|
|
236
|
+
d = time.monotonic() - t0
|
|
237
|
+
if d < 1:
|
|
238
|
+
return f"{d * 1000:.0f}ms"
|
|
239
|
+
if d < 10:
|
|
240
|
+
return f"{d:.1f}s"
|
|
241
|
+
return f"{d:.0f}s"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict | None:
|
|
245
|
+
"""Read a single kite message of expected type from stdin with timeout."""
|
|
246
|
+
result = [None]
|
|
247
|
+
|
|
248
|
+
def _read():
|
|
249
|
+
try:
|
|
250
|
+
line = sys.stdin.readline().strip()
|
|
251
|
+
if line:
|
|
252
|
+
msg = json.loads(line)
|
|
253
|
+
if isinstance(msg, dict) and msg.get("kite") == expected_type:
|
|
254
|
+
result[0] = msg
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
t = threading.Thread(target=_read, daemon=True)
|
|
259
|
+
t.start()
|
|
260
|
+
t.join(timeout=timeout)
|
|
261
|
+
return result[0]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Global WS reference for publish_event callback
|
|
265
|
+
_ws_global = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
async def main():
|
|
269
|
+
global _ws_global
|
|
270
|
+
# Initialize log file paths
|
|
271
|
+
global _log_dir, _log_latest_path, _crash_log_path
|
|
272
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
273
|
+
if module_data:
|
|
274
|
+
_log_dir = os.path.join(module_data, "log")
|
|
275
|
+
os.makedirs(_log_dir, exist_ok=True)
|
|
276
|
+
suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
|
|
277
|
+
|
|
278
|
+
_log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
|
|
279
|
+
try:
|
|
280
|
+
with open(_log_latest_path, "w", encoding="utf-8") as f:
|
|
281
|
+
pass
|
|
282
|
+
except Exception:
|
|
283
|
+
_log_latest_path = None
|
|
284
|
+
|
|
285
|
+
_crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
|
|
286
|
+
try:
|
|
287
|
+
with open(_crash_log_path, "w", encoding="utf-8") as f:
|
|
288
|
+
pass
|
|
289
|
+
except Exception:
|
|
290
|
+
_crash_log_path = None
|
|
291
|
+
|
|
292
|
+
_resolve_daily_log_path()
|
|
293
|
+
|
|
294
|
+
_setup_exception_hooks()
|
|
295
|
+
|
|
296
|
+
_t0 = time.monotonic()
|
|
297
|
+
|
|
298
|
+
# Read boot_info from stdin (only token)
|
|
299
|
+
token = ""
|
|
300
|
+
try:
|
|
301
|
+
line = sys.stdin.readline().strip()
|
|
302
|
+
if line:
|
|
303
|
+
boot_info = json.loads(line)
|
|
304
|
+
token = boot_info.get("token", "")
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
# Read kernel_port: env first (fast path), stdin fallback (parallel start)
|
|
309
|
+
kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
|
|
310
|
+
if not kernel_port:
|
|
311
|
+
msg = _read_stdin_kite_message("kernel_port", timeout=10)
|
|
312
|
+
if msg:
|
|
313
|
+
kernel_port = int(msg.get("kernel_port", 0))
|
|
314
|
+
|
|
315
|
+
if not token or not kernel_port:
|
|
316
|
+
print("[backup] ERROR: Missing token or kernel_port")
|
|
317
|
+
sys.exit(1)
|
|
318
|
+
|
|
319
|
+
print(f"[backup] Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
|
|
320
|
+
|
|
321
|
+
# Connect to Kernel WebSocket
|
|
322
|
+
ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=backup"
|
|
323
|
+
print(f"[backup] Connecting to Kernel: {ws_url}")
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
|
|
327
|
+
_ws_global = ws
|
|
328
|
+
print(f"[backup] Connected to Kernel ({_fmt_elapsed(_t0)})")
|
|
329
|
+
|
|
330
|
+
# Subscribe to events
|
|
331
|
+
await _rpc_call(ws, "event.subscribe", {
|
|
332
|
+
"events": [
|
|
333
|
+
"module.started",
|
|
334
|
+
"module.stopped",
|
|
335
|
+
"module.shutdown",
|
|
336
|
+
],
|
|
337
|
+
})
|
|
338
|
+
print(f"[backup] Subscribed to events ({_fmt_elapsed(_t0)})")
|
|
339
|
+
|
|
340
|
+
# Register to Kernel Registry via RPC
|
|
341
|
+
await _rpc_call(ws, "registry.register", {
|
|
342
|
+
"module_id": "backup",
|
|
343
|
+
"module_type": "service",
|
|
344
|
+
"events_publish": {
|
|
345
|
+
"backup.test": {"description": "Test event from backup module"},
|
|
346
|
+
},
|
|
347
|
+
"events_subscribe": [
|
|
348
|
+
"module.started",
|
|
349
|
+
"module.stopped",
|
|
350
|
+
"module.shutdown",
|
|
351
|
+
],
|
|
352
|
+
})
|
|
353
|
+
print(f"[backup] Registered to Kernel ({_fmt_elapsed(_t0)})")
|
|
354
|
+
|
|
355
|
+
# Publish module.ready
|
|
356
|
+
await _rpc_call(ws, "event.publish", {
|
|
357
|
+
"event_id": str(uuid.uuid4()),
|
|
358
|
+
"event": "module.ready",
|
|
359
|
+
"data": {
|
|
360
|
+
"module_id": "backup",
|
|
361
|
+
"graceful_shutdown": True,
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
print(f"[backup] module.ready published ({_fmt_elapsed(_t0)})")
|
|
365
|
+
|
|
366
|
+
# Start test event loop in background
|
|
367
|
+
test_task = asyncio.create_task(_test_event_loop(ws))
|
|
368
|
+
|
|
369
|
+
# Message loop: handle incoming RPC + events
|
|
370
|
+
async for raw in ws:
|
|
371
|
+
try:
|
|
372
|
+
msg = json.loads(raw)
|
|
373
|
+
except (json.JSONDecodeError, TypeError):
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
has_method = "method" in msg
|
|
378
|
+
has_id = "id" in msg
|
|
379
|
+
|
|
380
|
+
if has_method and not has_id:
|
|
381
|
+
# Event Notification
|
|
382
|
+
await _handle_event_notification(msg)
|
|
383
|
+
elif has_method and has_id:
|
|
384
|
+
# Incoming RPC request
|
|
385
|
+
await _handle_rpc_request(ws, msg)
|
|
386
|
+
# Ignore RPC responses (we don't await them in this simple impl)
|
|
387
|
+
except Exception as e:
|
|
388
|
+
print(f"[backup] 消息处理异常(已忽略): {e}")
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
_write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
|
|
392
|
+
_print_crash_summary(type(e), e.__traceback__)
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
async def _rpc_call(ws, method: str, params: dict = None):
|
|
397
|
+
"""Send a JSON-RPC 2.0 request (fire-and-forget, no response awaited)."""
|
|
398
|
+
msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
|
|
399
|
+
if params:
|
|
400
|
+
msg["params"] = params
|
|
401
|
+
await ws.send(json.dumps(msg))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def _publish_event(ws, event: dict):
|
|
405
|
+
"""Publish an event via RPC event.publish."""
|
|
406
|
+
await _rpc_call(ws, "event.publish", {
|
|
407
|
+
"event_id": str(uuid.uuid4()),
|
|
408
|
+
"event": event.get("event", ""),
|
|
409
|
+
"data": event.get("data", {}),
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
async def _handle_event_notification(msg: dict):
|
|
414
|
+
"""Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
|
|
415
|
+
params = msg.get("params", {})
|
|
416
|
+
event_type = params.get("event", "")
|
|
417
|
+
data = params.get("data", {})
|
|
418
|
+
|
|
419
|
+
# Special handling for module.shutdown targeting backup
|
|
420
|
+
if event_type == "module.shutdown" and data.get("module_id") == "backup":
|
|
421
|
+
await _handle_shutdown()
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
# Log other events
|
|
425
|
+
print(f"[backup] Event received: {event_type}")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
async def _handle_rpc_request(ws, msg: dict):
|
|
429
|
+
"""Handle an incoming RPC request (backup.* methods)."""
|
|
430
|
+
rpc_id = msg.get("id", "")
|
|
431
|
+
method = msg.get("method", "")
|
|
432
|
+
params = msg.get("params", {})
|
|
433
|
+
|
|
434
|
+
handlers = {
|
|
435
|
+
"health": lambda p: _rpc_health(),
|
|
436
|
+
"status": lambda p: _rpc_status(),
|
|
437
|
+
}
|
|
438
|
+
handler = handlers.get(method)
|
|
439
|
+
if handler:
|
|
440
|
+
try:
|
|
441
|
+
result = await handler(params)
|
|
442
|
+
await ws.send(json.dumps({"jsonrpc": "2.0", "id": rpc_id, "result": result}))
|
|
443
|
+
except Exception as e:
|
|
444
|
+
await ws.send(json.dumps({
|
|
445
|
+
"jsonrpc": "2.0", "id": rpc_id,
|
|
446
|
+
"error": {"code": -32603, "message": str(e)},
|
|
447
|
+
}))
|
|
448
|
+
else:
|
|
449
|
+
await ws.send(json.dumps({
|
|
450
|
+
"jsonrpc": "2.0", "id": rpc_id,
|
|
451
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
452
|
+
}))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
async def _rpc_health() -> dict:
|
|
456
|
+
"""RPC handler for backup.health."""
|
|
457
|
+
return {
|
|
458
|
+
"status": "healthy",
|
|
459
|
+
"details": {
|
|
460
|
+
"uptime_seconds": round(time.time() - _start_ts),
|
|
461
|
+
},
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
async def _rpc_status() -> dict:
|
|
466
|
+
"""RPC handler for backup.status."""
|
|
467
|
+
return {
|
|
468
|
+
"module": "backup",
|
|
469
|
+
"status": "running",
|
|
470
|
+
"uptime_seconds": round(time.time() - _start_ts),
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
async def _handle_shutdown():
|
|
475
|
+
"""Handle module.shutdown event — ack, cleanup, ready, exit."""
|
|
476
|
+
print("[backup] Received shutdown request")
|
|
477
|
+
# Step 1: Send ack
|
|
478
|
+
await _publish_event(_ws_global, {
|
|
479
|
+
"event": "module.shutdown.ack",
|
|
480
|
+
"data": {"module_id": "backup", "estimated_cleanup": 2},
|
|
481
|
+
})
|
|
482
|
+
# Step 2: Cleanup (nothing to clean up for backup)
|
|
483
|
+
# Step 3: Send ready
|
|
484
|
+
await _publish_event(_ws_global, {
|
|
485
|
+
"event": "module.shutdown.ready",
|
|
486
|
+
"data": {"module_id": "backup"},
|
|
487
|
+
})
|
|
488
|
+
print("[backup] Shutdown ready, exiting")
|
|
489
|
+
# Step 4: Exit
|
|
490
|
+
sys.exit(0)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
async def _test_event_loop(ws):
|
|
494
|
+
"""Publish a test event every 10 seconds."""
|
|
495
|
+
while True:
|
|
496
|
+
await asyncio.sleep(10)
|
|
497
|
+
await _publish_event(ws, {
|
|
498
|
+
"event": "backup.test",
|
|
499
|
+
"data": {
|
|
500
|
+
"message": "test event from backup",
|
|
501
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
print("[backup] test event published")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
if __name__ == "__main__":
|
|
508
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backup
|
|
3
|
+
display_name: Backup
|
|
4
|
+
version: "1.0"
|
|
5
|
+
type: service
|
|
6
|
+
state: enabled
|
|
7
|
+
runtime: python
|
|
8
|
+
entry: entry.py
|
|
9
|
+
events:
|
|
10
|
+
- backup.test
|
|
11
|
+
subscriptions:
|
|
12
|
+
- module.started
|
|
13
|
+
- module.stopped
|
|
14
|
+
- module.shutdown
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Backup(备份模块)
|
|
18
|
+
|
|
19
|
+
自动备份工程代码和数据的扩展模块。
|
|
20
|
+
|
|
21
|
+
- 定时备份 — 按配置周期自动备份指定目录
|
|
22
|
+
- 事件通知 — 通过 Event Hub 发布备份状态事件
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|