@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
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Manage child process lifecycle: start, stop, monitor, cleanup leftovers.
|
|
3
|
-
Cross-platform support for Windows, Linux, and macOS.
|
|
4
|
-
|
|
5
|
-
stdio lifecycle:
|
|
6
|
-
- stdin: write boot_info → keep open (Launcher may send more messages) → close after module.ready
|
|
7
|
-
- stdout: read structured messages + log lines → close after module.ready
|
|
8
|
-
- Structured messages: JSON lines with "kite" field, dispatched via callback
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
|
-
import threading
|
|
16
|
-
import time
|
|
17
|
-
from dataclasses import dataclass, asdict
|
|
18
|
-
from typing import Callable
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
IS_WINDOWS = sys.platform == "win32"
|
|
22
|
-
IS_MACOS = sys.platform == "darwin"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class ProcessRecord:
|
|
27
|
-
name: str
|
|
28
|
-
pid: int
|
|
29
|
-
cmd: list
|
|
30
|
-
module_dir: str
|
|
31
|
-
started_at: float
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class ProcessManager:
|
|
35
|
-
"""Manage child processes for all Kite modules."""
|
|
36
|
-
|
|
37
|
-
def __init__(self, kite_token: str, instance_id: str = "",
|
|
38
|
-
on_kite_message: Callable[[str, dict], None] | None = None):
|
|
39
|
-
"""
|
|
40
|
-
Args:
|
|
41
|
-
on_kite_message: callback(module_name, msg_dict) for structured stdout messages.
|
|
42
|
-
Called from stdout reader thread — must be thread-safe.
|
|
43
|
-
"""
|
|
44
|
-
self.kite_token = kite_token
|
|
45
|
-
self.instance_id = instance_id or str(os.getpid())
|
|
46
|
-
self._on_kite_message = on_kite_message
|
|
47
|
-
# Use KITE_MODULE_DATA (set by Launcher for itself)
|
|
48
|
-
self.data_dir = os.environ.get("KITE_MODULE_DATA") or os.path.join(os.environ["KITE_INSTANCE_DIR"], "launcher")
|
|
49
|
-
self.records_path = os.path.join(self.data_dir, "processes.json")
|
|
50
|
-
self._processes: dict[str, subprocess.Popen] = {} # name -> Popen
|
|
51
|
-
self._records: dict[str, ProcessRecord] = {} # name -> record
|
|
52
|
-
os.makedirs(self.data_dir, exist_ok=True)
|
|
53
|
-
|
|
54
|
-
# ── Leftover cleanup ──
|
|
55
|
-
|
|
56
|
-
def cleanup_leftovers(self):
|
|
57
|
-
"""Read processes.json in current instance dir, kill leftovers from previous runs."""
|
|
58
|
-
if not os.path.isfile(self.records_path):
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
try:
|
|
62
|
-
with open(self.records_path, "r", encoding="utf-8") as f:
|
|
63
|
-
saved = json.load(f)
|
|
64
|
-
except Exception:
|
|
65
|
-
try:
|
|
66
|
-
os.remove(self.records_path)
|
|
67
|
-
except OSError:
|
|
68
|
-
pass
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
for entry in saved:
|
|
72
|
-
pid = entry.get("pid", 0)
|
|
73
|
-
cmd = entry.get("cmd", [])
|
|
74
|
-
name = entry.get("name", "?")
|
|
75
|
-
if not pid:
|
|
76
|
-
continue
|
|
77
|
-
if self._is_pid_alive(pid) and self._cmd_matches(pid, cmd):
|
|
78
|
-
print(f"[launcher] 正在清理残留进程: {name} (PID {pid})")
|
|
79
|
-
self._force_kill(pid)
|
|
80
|
-
else:
|
|
81
|
-
print(f"[launcher] 残留进程 {name} (PID {pid}) 已不存在")
|
|
82
|
-
|
|
83
|
-
try:
|
|
84
|
-
os.remove(self.records_path)
|
|
85
|
-
except OSError:
|
|
86
|
-
pass
|
|
87
|
-
|
|
88
|
-
def _is_pid_alive(self, pid: int) -> bool:
|
|
89
|
-
"""Check if a process with given PID exists."""
|
|
90
|
-
if IS_WINDOWS:
|
|
91
|
-
try:
|
|
92
|
-
result = subprocess.run(
|
|
93
|
-
["tasklist", "/FI", f"PID eq {pid}", "/NH"],
|
|
94
|
-
capture_output=True, text=True, timeout=5,
|
|
95
|
-
)
|
|
96
|
-
return str(pid) in result.stdout
|
|
97
|
-
except Exception:
|
|
98
|
-
return False
|
|
99
|
-
else:
|
|
100
|
-
try:
|
|
101
|
-
os.kill(pid, 0)
|
|
102
|
-
return True
|
|
103
|
-
except OSError:
|
|
104
|
-
return False
|
|
105
|
-
|
|
106
|
-
def _cmd_matches(self, pid: int, expected_cmd: list) -> bool:
|
|
107
|
-
"""Verify the process command line matches what we expect (prevent killing recycled PIDs).
|
|
108
|
-
Uses multi-strategy fallback per platform for robustness.
|
|
109
|
-
"""
|
|
110
|
-
if not expected_cmd:
|
|
111
|
-
return False
|
|
112
|
-
cmdline = self._get_process_cmdline(pid)
|
|
113
|
-
if cmdline:
|
|
114
|
-
return expected_cmd[-1] in cmdline
|
|
115
|
-
# Degraded fallback: match executable image name only
|
|
116
|
-
return self._exe_name_matches(pid, expected_cmd)
|
|
117
|
-
|
|
118
|
-
def _get_process_cmdline(self, pid: int) -> str:
|
|
119
|
-
"""Get full command line of a process. Returns empty string on failure."""
|
|
120
|
-
if IS_WINDOWS:
|
|
121
|
-
for method in (self._win_powershell_cmdline, self._win_wmic_cmdline):
|
|
122
|
-
result = method(pid)
|
|
123
|
-
if result:
|
|
124
|
-
return result
|
|
125
|
-
return ""
|
|
126
|
-
elif IS_MACOS:
|
|
127
|
-
try:
|
|
128
|
-
r = subprocess.run(["ps", "-p", str(pid), "-o", "command="],
|
|
129
|
-
capture_output=True, text=True, timeout=5)
|
|
130
|
-
return r.stdout.strip()
|
|
131
|
-
except Exception:
|
|
132
|
-
return ""
|
|
133
|
-
else:
|
|
134
|
-
# Linux: /proc first, ps fallback
|
|
135
|
-
try:
|
|
136
|
-
with open(f"/proc/{pid}/cmdline", "r") as f:
|
|
137
|
-
return f.read()
|
|
138
|
-
except Exception:
|
|
139
|
-
pass
|
|
140
|
-
try:
|
|
141
|
-
r = subprocess.run(["ps", "-p", str(pid), "-o", "args="],
|
|
142
|
-
capture_output=True, text=True, timeout=5)
|
|
143
|
-
return r.stdout.strip()
|
|
144
|
-
except Exception:
|
|
145
|
-
return ""
|
|
146
|
-
|
|
147
|
-
def _win_powershell_cmdline(self, pid: int) -> str:
|
|
148
|
-
"""Windows: Get cmdline via PowerShell Get-CimInstance (modern, replaces wmic)."""
|
|
149
|
-
try:
|
|
150
|
-
r = subprocess.run(
|
|
151
|
-
["powershell", "-NoProfile", "-Command",
|
|
152
|
-
f"(Get-CimInstance Win32_Process -Filter 'ProcessId={pid}').CommandLine"],
|
|
153
|
-
capture_output=True, text=True, timeout=8,
|
|
154
|
-
)
|
|
155
|
-
return r.stdout.strip()
|
|
156
|
-
except Exception:
|
|
157
|
-
return ""
|
|
158
|
-
|
|
159
|
-
def _win_wmic_cmdline(self, pid: int) -> str:
|
|
160
|
-
"""Windows: Get cmdline via wmic (legacy fallback, deprecated but widely available)."""
|
|
161
|
-
try:
|
|
162
|
-
r = subprocess.run(
|
|
163
|
-
["wmic", "process", "where", f"ProcessId={pid}",
|
|
164
|
-
"get", "CommandLine", "/value"],
|
|
165
|
-
capture_output=True, text=True, timeout=5,
|
|
166
|
-
)
|
|
167
|
-
return r.stdout.strip()
|
|
168
|
-
except Exception:
|
|
169
|
-
return ""
|
|
170
|
-
|
|
171
|
-
def _exe_name_matches(self, pid: int, expected_cmd: list) -> bool:
|
|
172
|
-
"""Last resort: check if process image name matches expected executable."""
|
|
173
|
-
exe = os.path.basename(expected_cmd[0]).lower()
|
|
174
|
-
if exe.endswith(".exe"):
|
|
175
|
-
exe = exe[:-4]
|
|
176
|
-
if IS_WINDOWS:
|
|
177
|
-
try:
|
|
178
|
-
r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/FO", "CSV", "/NH"],
|
|
179
|
-
capture_output=True, text=True, timeout=5)
|
|
180
|
-
return exe in r.stdout.lower()
|
|
181
|
-
except Exception:
|
|
182
|
-
return False
|
|
183
|
-
else:
|
|
184
|
-
try:
|
|
185
|
-
r = subprocess.run(["ps", "-p", str(pid), "-o", "comm="],
|
|
186
|
-
capture_output=True, text=True, timeout=5)
|
|
187
|
-
return exe in r.stdout.lower()
|
|
188
|
-
except Exception:
|
|
189
|
-
return False
|
|
190
|
-
|
|
191
|
-
def _force_kill(self, pid: int):
|
|
192
|
-
"""Force kill a process by PID."""
|
|
193
|
-
try:
|
|
194
|
-
if IS_WINDOWS:
|
|
195
|
-
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
|
|
196
|
-
capture_output=True, timeout=5)
|
|
197
|
-
else:
|
|
198
|
-
import signal
|
|
199
|
-
os.kill(pid, signal.SIGKILL)
|
|
200
|
-
except Exception as e:
|
|
201
|
-
print(f"[launcher] 警告: 强制终止 PID {pid} 失败: {e}")
|
|
202
|
-
|
|
203
|
-
# ── Start module ──
|
|
204
|
-
|
|
205
|
-
def start_module(self, info, boot_info: dict = None) -> bool:
|
|
206
|
-
"""Start a module as a subprocess. Returns True on success.
|
|
207
|
-
If boot_info is provided, it is written as JSON to the child's stdin pipe.
|
|
208
|
-
stdin is kept open after boot_info for Launcher to send additional messages.
|
|
209
|
-
"""
|
|
210
|
-
from .module_scanner import ModuleInfo
|
|
211
|
-
info: ModuleInfo
|
|
212
|
-
|
|
213
|
-
if info.name in self._processes:
|
|
214
|
-
proc = self._processes[info.name]
|
|
215
|
-
if proc.poll() is None:
|
|
216
|
-
print(f"[launcher] {info.name} 已在运行 (PID {proc.pid})")
|
|
217
|
-
return True
|
|
218
|
-
|
|
219
|
-
cmd = self._build_cmd(info)
|
|
220
|
-
if not cmd:
|
|
221
|
-
print(f"[launcher] 错误: 无法构建 {info.name} 的启动命令")
|
|
222
|
-
return False
|
|
223
|
-
|
|
224
|
-
env = os.environ.copy()
|
|
225
|
-
env["PYTHONUTF8"] = "1" # Force UTF-8 in child Python processes
|
|
226
|
-
env["PYTHONUNBUFFERED"] = "1" # Disable output buffering for real-time logs
|
|
227
|
-
env["KITE_MODULE_DATA"] = os.path.join(os.environ["KITE_INSTANCE_DIR"], info.name)
|
|
228
|
-
if info.launch.env:
|
|
229
|
-
env.update(info.launch.env)
|
|
230
|
-
|
|
231
|
-
creation_flags = 0
|
|
232
|
-
if IS_WINDOWS:
|
|
233
|
-
creation_flags = subprocess.CREATE_NO_WINDOW
|
|
234
|
-
|
|
235
|
-
use_stdin = boot_info is not None and info.launch.boot_stdin
|
|
236
|
-
|
|
237
|
-
cwd = info.module_dir
|
|
238
|
-
if info.launch.cwd:
|
|
239
|
-
cwd = os.path.normpath(os.path.join(info.module_dir, info.launch.cwd))
|
|
240
|
-
|
|
241
|
-
try:
|
|
242
|
-
proc = subprocess.Popen(
|
|
243
|
-
cmd,
|
|
244
|
-
cwd=cwd,
|
|
245
|
-
env=env,
|
|
246
|
-
stdin=subprocess.PIPE if use_stdin else None,
|
|
247
|
-
stdout=subprocess.PIPE,
|
|
248
|
-
stderr=subprocess.STDOUT,
|
|
249
|
-
creationflags=creation_flags,
|
|
250
|
-
)
|
|
251
|
-
except Exception as e:
|
|
252
|
-
print(f"[launcher] 错误: 启动 {info.name} 失败: {e}")
|
|
253
|
-
return False
|
|
254
|
-
|
|
255
|
-
# Write boot_info to stdin — keep stdin open for additional messages
|
|
256
|
-
if use_stdin and proc.stdin:
|
|
257
|
-
try:
|
|
258
|
-
proc.stdin.write(json.dumps(boot_info).encode() + b"\n")
|
|
259
|
-
proc.stdin.flush()
|
|
260
|
-
except Exception as e:
|
|
261
|
-
print(f"[launcher] 警告: 向 {info.name} 写入 boot_info 失败: {e}")
|
|
262
|
-
|
|
263
|
-
self._processes[info.name] = proc
|
|
264
|
-
self._records[info.name] = ProcessRecord(
|
|
265
|
-
name=info.name,
|
|
266
|
-
pid=proc.pid,
|
|
267
|
-
cmd=cmd,
|
|
268
|
-
module_dir=info.module_dir,
|
|
269
|
-
started_at=time.time(),
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
# Daemon thread to read stdout: structured messages + log lines
|
|
273
|
-
t = threading.Thread(
|
|
274
|
-
target=self._read_stdout, args=(info.name, proc),
|
|
275
|
-
daemon=True,
|
|
276
|
-
)
|
|
277
|
-
t.start()
|
|
278
|
-
|
|
279
|
-
print(f"[launcher] 已启动 {info.name} (PID {proc.pid})")
|
|
280
|
-
return True
|
|
281
|
-
|
|
282
|
-
def write_stdin(self, name: str, data: dict) -> bool:
|
|
283
|
-
"""Write a structured JSON message to a module's stdin.
|
|
284
|
-
Used by Launcher to send additional messages after boot_info (e.g. launcher_ws_token).
|
|
285
|
-
Returns True on success.
|
|
286
|
-
"""
|
|
287
|
-
proc = self._processes.get(name)
|
|
288
|
-
if not proc or not proc.stdin or proc.stdin.closed:
|
|
289
|
-
return False
|
|
290
|
-
try:
|
|
291
|
-
proc.stdin.write(json.dumps(data).encode() + b"\n")
|
|
292
|
-
proc.stdin.flush()
|
|
293
|
-
return True
|
|
294
|
-
except Exception as e:
|
|
295
|
-
print(f"[launcher] 警告: 向 {name} 写入 stdin 失败: {e}")
|
|
296
|
-
return False
|
|
297
|
-
|
|
298
|
-
def close_stdio(self, name: str):
|
|
299
|
-
"""Close stdin and stdout for a module. Called by Launcher after module.ready → module.started.
|
|
300
|
-
stdout reader thread will exit naturally when stdout is closed.
|
|
301
|
-
"""
|
|
302
|
-
proc = self._processes.get(name)
|
|
303
|
-
if not proc:
|
|
304
|
-
return
|
|
305
|
-
if proc.stdin and not proc.stdin.closed:
|
|
306
|
-
try:
|
|
307
|
-
proc.stdin.close()
|
|
308
|
-
except Exception:
|
|
309
|
-
pass
|
|
310
|
-
if proc.stdout and not proc.stdout.closed:
|
|
311
|
-
try:
|
|
312
|
-
proc.stdout.close()
|
|
313
|
-
except Exception:
|
|
314
|
-
pass
|
|
315
|
-
|
|
316
|
-
def _build_cmd(self, info) -> list:
|
|
317
|
-
"""Build the command list based on module runtime.
|
|
318
|
-
|
|
319
|
-
For python/node: if entry starts with '-m ', use package mode (e.g. python -m pkg).
|
|
320
|
-
Otherwise use script file mode (e.g. python entry.py).
|
|
321
|
-
"""
|
|
322
|
-
if info.launch.cmd:
|
|
323
|
-
return list(info.launch.cmd)
|
|
324
|
-
|
|
325
|
-
if info.runtime == "python":
|
|
326
|
-
if info.entry.startswith("-m "):
|
|
327
|
-
return [sys.executable] + info.entry.split()
|
|
328
|
-
return [sys.executable, os.path.join(info.module_dir, info.entry)]
|
|
329
|
-
elif info.runtime == "node":
|
|
330
|
-
if info.entry.startswith("-e ") or info.entry.startswith("--eval "):
|
|
331
|
-
return ["node"] + info.entry.split()
|
|
332
|
-
return ["node", os.path.join(info.module_dir, info.entry)]
|
|
333
|
-
elif info.runtime == "binary":
|
|
334
|
-
return [os.path.join(info.module_dir, info.entry)]
|
|
335
|
-
else:
|
|
336
|
-
print(f"[launcher] 警告: 未知运行时 '{info.runtime}',模块 {info.name}")
|
|
337
|
-
return []
|
|
338
|
-
|
|
339
|
-
def _read_stdout(self, name: str, proc: subprocess.Popen):
|
|
340
|
-
"""Read subprocess stdout line by line.
|
|
341
|
-
Lines containing a JSON object with "kite" field are dispatched as structured messages.
|
|
342
|
-
All other lines are printed as log output with module name prefix.
|
|
343
|
-
"""
|
|
344
|
-
try:
|
|
345
|
-
for line in iter(proc.stdout.readline, b""):
|
|
346
|
-
text = line.decode("utf-8", errors="replace").rstrip()
|
|
347
|
-
if not text:
|
|
348
|
-
continue
|
|
349
|
-
# Try to parse as structured kite message
|
|
350
|
-
if self._on_kite_message and text.startswith("{"):
|
|
351
|
-
try:
|
|
352
|
-
msg = json.loads(text)
|
|
353
|
-
if isinstance(msg, dict) and "kite" in msg:
|
|
354
|
-
self._on_kite_message(name, msg)
|
|
355
|
-
continue
|
|
356
|
-
except (json.JSONDecodeError, KeyError):
|
|
357
|
-
pass
|
|
358
|
-
# Regular log line — avoid double prefix if module already added [name]
|
|
359
|
-
if text.startswith(f"[{name}]"):
|
|
360
|
-
print(text)
|
|
361
|
-
else:
|
|
362
|
-
print(f"[{name}] {text}")
|
|
363
|
-
except Exception:
|
|
364
|
-
pass
|
|
365
|
-
|
|
366
|
-
# ── Stop module ──
|
|
367
|
-
|
|
368
|
-
def stop_module(self, name: str, timeout: float = 5) -> bool:
|
|
369
|
-
"""Stop a module by name. Returns True if stopped successfully."""
|
|
370
|
-
proc = self._processes.get(name)
|
|
371
|
-
if not proc or proc.poll() is not None:
|
|
372
|
-
self._processes.pop(name, None)
|
|
373
|
-
self._records.pop(name, None)
|
|
374
|
-
return True
|
|
375
|
-
|
|
376
|
-
# Close stdio before terminating
|
|
377
|
-
self.close_stdio(name)
|
|
378
|
-
|
|
379
|
-
print(f"[launcher] 正在停止 {name} (PID {proc.pid})...")
|
|
380
|
-
proc.terminate()
|
|
381
|
-
try:
|
|
382
|
-
proc.wait(timeout=timeout)
|
|
383
|
-
except subprocess.TimeoutExpired:
|
|
384
|
-
print(f"[launcher] {name} 在 {timeout}s 内未退出,强制终止")
|
|
385
|
-
proc.kill()
|
|
386
|
-
try:
|
|
387
|
-
proc.wait(timeout=3)
|
|
388
|
-
except subprocess.TimeoutExpired:
|
|
389
|
-
self._force_kill(proc.pid)
|
|
390
|
-
|
|
391
|
-
self._processes.pop(name, None)
|
|
392
|
-
self._records.pop(name, None)
|
|
393
|
-
print(f"[launcher] 已停止 {name}")
|
|
394
|
-
return True
|
|
395
|
-
|
|
396
|
-
def stop_all(self, timeout: float = 10):
|
|
397
|
-
"""Stop all managed processes. Terminate first, force kill after timeout."""
|
|
398
|
-
if not self._processes:
|
|
399
|
-
return
|
|
400
|
-
|
|
401
|
-
names = list(self._processes.keys())
|
|
402
|
-
print(f"[launcher] 正在停止所有模块: {', '.join(names)}")
|
|
403
|
-
|
|
404
|
-
# Phase 1: close stdio and send terminate to all
|
|
405
|
-
for name in names:
|
|
406
|
-
self.close_stdio(name)
|
|
407
|
-
proc = self._processes.get(name)
|
|
408
|
-
if proc and proc.poll() is None:
|
|
409
|
-
proc.terminate()
|
|
410
|
-
|
|
411
|
-
# Phase 2: wait for all to exit
|
|
412
|
-
deadline = time.time() + timeout
|
|
413
|
-
for name in names:
|
|
414
|
-
proc = self._processes.get(name)
|
|
415
|
-
if not proc:
|
|
416
|
-
continue
|
|
417
|
-
remaining = max(0.1, deadline - time.time())
|
|
418
|
-
try:
|
|
419
|
-
proc.wait(timeout=remaining)
|
|
420
|
-
except subprocess.TimeoutExpired:
|
|
421
|
-
pass
|
|
422
|
-
|
|
423
|
-
# Phase 3: force kill survivors
|
|
424
|
-
for name in names:
|
|
425
|
-
proc = self._processes.get(name)
|
|
426
|
-
if proc and proc.poll() is None:
|
|
427
|
-
print(f"[launcher] 强制终止 {name} (PID {proc.pid})")
|
|
428
|
-
proc.kill()
|
|
429
|
-
try:
|
|
430
|
-
proc.wait(timeout=3)
|
|
431
|
-
except subprocess.TimeoutExpired:
|
|
432
|
-
self._force_kill(proc.pid)
|
|
433
|
-
|
|
434
|
-
self._processes.clear()
|
|
435
|
-
self._records.clear()
|
|
436
|
-
print("[launcher] 所有模块已停止")
|
|
437
|
-
|
|
438
|
-
# ── Persistence & monitoring ──
|
|
439
|
-
|
|
440
|
-
def persist_records(self):
|
|
441
|
-
"""Write current process records to processes.json."""
|
|
442
|
-
data = [asdict(r) for r in self._records.values()]
|
|
443
|
-
self._write_records_file(data)
|
|
444
|
-
|
|
445
|
-
def check_exited(self) -> list[tuple[str, int]]:
|
|
446
|
-
"""Non-blocking check for exited child processes. Returns [(name, returncode)]."""
|
|
447
|
-
exited = []
|
|
448
|
-
for name in list(self._processes.keys()):
|
|
449
|
-
proc = self._processes[name]
|
|
450
|
-
rc = proc.poll()
|
|
451
|
-
if rc is not None:
|
|
452
|
-
exited.append((name, rc))
|
|
453
|
-
self._processes.pop(name, None)
|
|
454
|
-
self._records.pop(name, None)
|
|
455
|
-
return exited
|
|
456
|
-
|
|
457
|
-
def get_record(self, name: str) -> ProcessRecord | None:
|
|
458
|
-
return self._records.get(name)
|
|
459
|
-
|
|
460
|
-
def is_running(self, name: str) -> bool:
|
|
461
|
-
proc = self._processes.get(name)
|
|
462
|
-
return proc is not None and proc.poll() is None
|
|
463
|
-
|
|
464
|
-
def _write_records_file(self, data: list):
|
|
465
|
-
"""Write data list to processes.json."""
|
|
466
|
-
try:
|
|
467
|
-
with open(self.records_path, "w", encoding="utf-8") as f:
|
|
468
|
-
json.dump(data, f, indent=2)
|
|
469
|
-
except Exception as e:
|
|
470
|
-
print(f"[launcher] 警告: 写入 processes.json 失败: {e}")
|
package/core/registry/entry.py
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Registry entry point.
|
|
3
|
-
Reads token from stdin boot_info, starts HTTP server on dynamic port,
|
|
4
|
-
outputs port via stdout structured message, waits for Event Hub to trigger module.ready.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import sys
|
|
10
|
-
import socket
|
|
11
|
-
|
|
12
|
-
import uvicorn
|
|
13
|
-
|
|
14
|
-
# Ensure project root is on sys.path (set by main.py or cli.js)
|
|
15
|
-
_this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
-
_project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(_this_dir))
|
|
17
|
-
if _project_root not in sys.path:
|
|
18
|
-
sys.path.insert(0, _project_root)
|
|
19
|
-
|
|
20
|
-
from core.registry.store import RegistryStore
|
|
21
|
-
from core.registry.server import RegistryServer
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _read_module_md() -> dict:
|
|
25
|
-
"""Read preferred_port and advertise_ip from own module.md."""
|
|
26
|
-
md_path = os.path.join(_this_dir, "module.md")
|
|
27
|
-
result = {"preferred_port": 0, "advertise_ip": "127.0.0.1"}
|
|
28
|
-
try:
|
|
29
|
-
with open(md_path, "r", encoding="utf-8") as f:
|
|
30
|
-
text = f.read()
|
|
31
|
-
import re
|
|
32
|
-
m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
33
|
-
if m:
|
|
34
|
-
try:
|
|
35
|
-
import yaml
|
|
36
|
-
fm = yaml.safe_load(m.group(1)) or {}
|
|
37
|
-
except ImportError:
|
|
38
|
-
fm = {}
|
|
39
|
-
result["preferred_port"] = int(fm.get("preferred_port", 0))
|
|
40
|
-
result["advertise_ip"] = fm.get("advertise_ip", "127.0.0.1")
|
|
41
|
-
except Exception:
|
|
42
|
-
pass
|
|
43
|
-
return result
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _bind_port(preferred: int, host: str) -> int:
|
|
47
|
-
"""Try preferred port first, fall back to OS-assigned."""
|
|
48
|
-
if preferred:
|
|
49
|
-
try:
|
|
50
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
51
|
-
s.bind((host, preferred))
|
|
52
|
-
return preferred
|
|
53
|
-
except OSError:
|
|
54
|
-
pass
|
|
55
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
56
|
-
s.bind((host, 0))
|
|
57
|
-
return s.getsockname()[1]
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def main():
|
|
61
|
-
# Kite environment
|
|
62
|
-
kite_instance = os.environ.get("KITE_INSTANCE", "")
|
|
63
|
-
is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
64
|
-
|
|
65
|
-
# Step 1: Read token from stdin boot_info (only token)
|
|
66
|
-
launcher_token = ""
|
|
67
|
-
try:
|
|
68
|
-
line = sys.stdin.readline().strip()
|
|
69
|
-
if line:
|
|
70
|
-
boot_info = json.loads(line)
|
|
71
|
-
launcher_token = boot_info.get("token", "")
|
|
72
|
-
except Exception:
|
|
73
|
-
pass
|
|
74
|
-
|
|
75
|
-
if not launcher_token:
|
|
76
|
-
print("[registry] 错误: 未通过 stdin boot_info 提供启动器令牌")
|
|
77
|
-
sys.exit(1)
|
|
78
|
-
|
|
79
|
-
print(f"[registry] 已收到启动器令牌 ({len(launcher_token)} 字符)")
|
|
80
|
-
|
|
81
|
-
if is_debug:
|
|
82
|
-
print("[registry] 调试模式已启用 (KITE_DEBUG=1),接受所有令牌")
|
|
83
|
-
|
|
84
|
-
# Step 2: Read config from own module.md
|
|
85
|
-
md_config = _read_module_md()
|
|
86
|
-
advertise_ip = md_config["advertise_ip"]
|
|
87
|
-
preferred_port = md_config["preferred_port"]
|
|
88
|
-
|
|
89
|
-
# Step 3: Create store and server
|
|
90
|
-
store = RegistryStore(launcher_token)
|
|
91
|
-
server = RegistryServer(store, launcher_token=launcher_token, advertise_ip=advertise_ip)
|
|
92
|
-
|
|
93
|
-
# Step 4: Bind port
|
|
94
|
-
bind_host = advertise_ip
|
|
95
|
-
port = _bind_port(preferred_port, bind_host)
|
|
96
|
-
|
|
97
|
-
# Step 5: Output port via stdout structured message (Launcher reads this)
|
|
98
|
-
# This message proves HTTP is about to start — Launcher uses it as readiness signal
|
|
99
|
-
print(json.dumps({"kite": "port", "port": port}), flush=True)
|
|
100
|
-
|
|
101
|
-
print(f"[registry] 启动中 {bind_host}:{port}")
|
|
102
|
-
|
|
103
|
-
# Store port and advertise_ip for module.ready event later
|
|
104
|
-
server.port = port
|
|
105
|
-
|
|
106
|
-
uvicorn.run(server.app, host=bind_host, port=port, log_level="warning")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if __name__ == "__main__":
|
|
110
|
-
main()
|
package/core/registry/module.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: registry
|
|
3
|
-
display_name: Registry
|
|
4
|
-
type: infrastructure
|
|
5
|
-
state: enabled
|
|
6
|
-
runtime: python
|
|
7
|
-
entry: entry.py
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# Registry
|
|
11
|
-
|
|
12
|
-
Kite 系统注册中心,第一个启动的核心模块。
|
|
13
|
-
|
|
14
|
-
## 职责
|
|
15
|
-
|
|
16
|
-
- 服务注册与发现(所有模块的"电话簿")
|
|
17
|
-
- 能力索引(工具、Hook、事件的聚合查询)
|
|
18
|
-
- 通用字段查询(glob 模式匹配)
|
|
19
|
-
- 心跳 TTL 管理
|
|
20
|
-
|
|
21
|
-
## API
|
|
22
|
-
|
|
23
|
-
| 方法 | 路径 | 说明 |
|
|
24
|
-
|------|------|------|
|
|
25
|
-
| POST | `/modules` | 模块生命周期(register / deregister / heartbeat) |
|
|
26
|
-
| GET | `/lookup` | 通用搜索(三参数均支持 glob) |
|
|
27
|
-
| GET | `/get/{path}` | 按路径精确读取注册内容 |
|
|
28
|
-
| POST | `/tokens` | 注册 token 映射(仅 Launcher) |
|
|
29
|
-
| POST | `/query` | LLM 自然语言查询(待实现) |
|
|
30
|
-
| GET | `/health` | 健康检查(无需 token) |
|