@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +48 -0
  3. package/cli.js +1 -1
  4. package/extensions/agents/__init__.py +1 -0
  5. package/extensions/agents/assistant/__init__.py +1 -0
  6. package/extensions/agents/assistant/entry.py +329 -0
  7. package/extensions/agents/assistant/module.md +22 -0
  8. package/extensions/agents/assistant/server.py +197 -0
  9. package/extensions/channels/__init__.py +1 -0
  10. package/extensions/channels/acp_channel/__init__.py +1 -0
  11. package/extensions/channels/acp_channel/entry.py +329 -0
  12. package/extensions/channels/acp_channel/module.md +22 -0
  13. package/extensions/channels/acp_channel/server.py +197 -0
  14. package/extensions/event_hub_bench/entry.py +624 -379
  15. package/extensions/event_hub_bench/module.md +2 -1
  16. package/extensions/services/backup/__init__.py +1 -0
  17. package/extensions/services/backup/entry.py +508 -0
  18. package/extensions/services/backup/module.md +22 -0
  19. package/extensions/services/model_service/__init__.py +1 -0
  20. package/extensions/services/model_service/entry.py +508 -0
  21. package/extensions/services/model_service/module.md +22 -0
  22. package/extensions/services/watchdog/entry.py +468 -102
  23. package/extensions/services/watchdog/module.md +3 -0
  24. package/extensions/services/watchdog/monitor.py +170 -69
  25. package/extensions/services/web/__init__.py +1 -0
  26. package/extensions/services/web/config.yaml +149 -0
  27. package/extensions/services/web/entry.py +390 -0
  28. package/extensions/services/web/module.md +24 -0
  29. package/extensions/services/web/routes/__init__.py +1 -0
  30. package/extensions/services/web/routes/routes_call.py +189 -0
  31. package/extensions/services/web/routes/routes_config.py +512 -0
  32. package/extensions/services/web/routes/routes_contacts.py +98 -0
  33. package/extensions/services/web/routes/routes_devlog.py +99 -0
  34. package/extensions/services/web/routes/routes_phone.py +81 -0
  35. package/extensions/services/web/routes/routes_sms.py +48 -0
  36. package/extensions/services/web/routes/routes_stats.py +17 -0
  37. package/extensions/services/web/routes/routes_voicechat.py +554 -0
  38. package/extensions/services/web/routes/schemas.py +216 -0
  39. package/extensions/services/web/server.py +375 -0
  40. package/extensions/services/web/static/css/style.css +1064 -0
  41. package/extensions/services/web/static/index.html +1445 -0
  42. package/extensions/services/web/static/js/app.js +4671 -0
  43. package/extensions/services/web/vendor/__init__.py +1 -0
  44. package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
  45. package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
  46. package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
  47. package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
  48. package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
  49. package/extensions/services/web/vendor/config.py +139 -0
  50. package/extensions/services/web/vendor/conversation/asr.py +936 -0
  51. package/extensions/services/web/vendor/conversation/engine.py +548 -0
  52. package/extensions/services/web/vendor/conversation/llm.py +534 -0
  53. package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
  54. package/extensions/services/web/vendor/conversation/tts.py +322 -0
  55. package/extensions/services/web/vendor/conversation/vad.py +138 -0
  56. package/extensions/services/web/vendor/storage/__init__.py +1 -0
  57. package/extensions/services/web/vendor/storage/identity.py +312 -0
  58. package/extensions/services/web/vendor/storage/store.py +507 -0
  59. package/extensions/services/web/vendor/task/manager.py +864 -0
  60. package/extensions/services/web/vendor/task/models.py +45 -0
  61. package/extensions/services/web/vendor/task/webhook.py +263 -0
  62. package/extensions/services/web/vendor/tools/registry.py +321 -0
  63. package/kernel/__init__.py +0 -0
  64. package/kernel/entry.py +407 -0
  65. package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
  66. package/kernel/module.md +33 -0
  67. package/{core/registry/store.py → kernel/registry_store.py} +23 -8
  68. package/kernel/rpc_router.py +388 -0
  69. package/kernel/server.py +267 -0
  70. package/launcher/__init__.py +10 -0
  71. package/launcher/__main__.py +6 -0
  72. package/launcher/count_lines.py +258 -0
  73. package/launcher/entry.py +1778 -0
  74. package/launcher/logging_setup.py +289 -0
  75. package/{core/launcher → launcher}/module_scanner.py +11 -6
  76. package/launcher/process_manager.py +880 -0
  77. package/main.py +11 -210
  78. package/package.json +6 -9
  79. package/__init__.py +0 -1
  80. package/__main__.py +0 -15
  81. package/core/event_hub/BENCHMARK.md +0 -94
  82. package/core/event_hub/bench.py +0 -459
  83. package/core/event_hub/bench_extreme.py +0 -308
  84. package/core/event_hub/bench_perf.py +0 -350
  85. package/core/event_hub/entry.py +0 -157
  86. package/core/event_hub/module.md +0 -20
  87. package/core/event_hub/server.py +0 -206
  88. package/core/launcher/entry.py +0 -1158
  89. package/core/launcher/process_manager.py +0 -470
  90. package/core/registry/entry.py +0 -110
  91. package/core/registry/module.md +0 -30
  92. package/core/registry/server.py +0 -289
  93. package/extensions/services/watchdog/server.py +0 -167
  94. /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
  95. /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
  96. /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
  97. /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
  98. /package/{core/event_hub → kernel}/dedup.py +0 -0
  99. /package/{core/event_hub → kernel}/router.py +0 -0
  100. /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}")
@@ -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()
@@ -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) |