@agentunion/kite 1.0.6 → 1.0.7
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/cli.js +127 -25
- package/core/event_hub/entry.py +105 -61
- package/core/event_hub/module.md +0 -1
- package/core/event_hub/server.py +96 -28
- package/core/launcher/entry.py +477 -290
- package/core/launcher/module_scanner.py +10 -9
- package/core/launcher/process_manager.py +120 -96
- package/core/registry/entry.py +66 -30
- package/core/registry/server.py +47 -14
- package/core/registry/store.py +6 -1
- package/{core → extensions}/event_hub_bench/entry.py +17 -9
- package/{core → extensions}/event_hub_bench/module.md +2 -1
- package/extensions/services/watchdog/entry.py +11 -7
- package/extensions/services/watchdog/server.py +1 -1
- package/main.py +204 -4
- package/package.json +11 -2
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/data_dir.cpython-313.pyc +0 -0
- package/core/data_dir.py +0 -62
- package/core/event_hub/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/bench.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/bench_perf.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/dedup.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/hub.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/router.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/server.cpython-313.pyc +0 -0
- package/core/event_hub/bench_results/.gitkeep +0 -0
- package/core/event_hub/bench_results/2026-02-28_13-26-48.json +0 -51
- package/core/event_hub/bench_results/2026-02-28_13-44-45.json +0 -51
- package/core/event_hub/bench_results/2026-02-28_13-45-39.json +0 -51
- package/core/launcher/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/module_scanner.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/process_manager.cpython-313.pyc +0 -0
- package/core/launcher/data/log/lifecycle.jsonl +0 -1158
- package/core/launcher/data/token.txt +0 -1
- package/core/registry/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/server.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/store.cpython-313.pyc +0 -0
- package/core/registry/data/port.txt +0 -1
- package/core/registry/data/port_484.txt +0 -1
- package/extensions/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/entry.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/monitor.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/server.cpython-313.pyc +0 -0
package/core/registry/server.py
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
Registry HTTP server.
|
|
3
3
|
7 endpoints: /modules, /lookup, /get/{path}, /tokens, /verify, /query, /health.
|
|
4
4
|
All endpoints except /health require Bearer token auth.
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
Delayed ready mechanism (mechanism 7):
|
|
7
|
+
Registry does NOT send module.ready immediately after HTTP starts.
|
|
8
|
+
When Event Hub registers (POST /modules with metadata.ws_endpoint),
|
|
9
|
+
Registry connects to Event Hub WS and then sends module.ready.
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
import asyncio
|
|
@@ -20,15 +24,19 @@ from .store import RegistryStore
|
|
|
20
24
|
class RegistryServer:
|
|
21
25
|
"""FastAPI-based Registry HTTP server."""
|
|
22
26
|
|
|
23
|
-
def __init__(self, store: RegistryStore, launcher_token: str = ""):
|
|
27
|
+
def __init__(self, store: RegistryStore, launcher_token: str = "", advertise_ip: str = "127.0.0.1"):
|
|
24
28
|
self.store = store
|
|
25
29
|
self.launcher_token = launcher_token
|
|
30
|
+
self.advertise_ip = advertise_ip
|
|
31
|
+
self.port: int = 0 # set by entry.py before uvicorn.run
|
|
26
32
|
self.app = self._create_app()
|
|
27
33
|
self._ttl_task: asyncio.Task | None = None
|
|
28
34
|
# Event Hub WebSocket
|
|
29
35
|
self._event_hub_ws_url: str = ""
|
|
30
36
|
self._ws: object | None = None
|
|
31
37
|
self._ws_task: asyncio.Task | None = None
|
|
38
|
+
self._event_hub_connected = False
|
|
39
|
+
self._ready_sent = False
|
|
32
40
|
|
|
33
41
|
def _extract_token(self, request: Request) -> str:
|
|
34
42
|
"""Extract Bearer token from Authorization header."""
|
|
@@ -51,13 +59,12 @@ class RegistryServer:
|
|
|
51
59
|
if not self.store.is_launcher(token):
|
|
52
60
|
raise HTTPException(status_code=403, detail="Only Launcher may call this endpoint")
|
|
53
61
|
|
|
54
|
-
# ── Event Hub connection ──
|
|
62
|
+
# ── Event Hub connection + delayed ready ──
|
|
55
63
|
|
|
56
64
|
async def _try_connect_event_hub(self):
|
|
57
|
-
"""
|
|
58
|
-
if self.
|
|
65
|
+
"""Event Hub just registered — connect to it and send module.ready."""
|
|
66
|
+
if self._event_hub_connected:
|
|
59
67
|
return
|
|
60
|
-
# Look up Event Hub ws_endpoint from our own store
|
|
61
68
|
eh = self.store.modules.get("event_hub")
|
|
62
69
|
if not eh:
|
|
63
70
|
return
|
|
@@ -78,17 +85,24 @@ class RegistryServer:
|
|
|
78
85
|
except Exception as e:
|
|
79
86
|
print(f"[registry] Event Hub connection error: {e}")
|
|
80
87
|
self._ws = None
|
|
88
|
+
self._event_hub_connected = False
|
|
81
89
|
await asyncio.sleep(5)
|
|
82
90
|
|
|
83
91
|
async def _ws_connect(self):
|
|
84
|
-
"""Single WebSocket session."""
|
|
85
|
-
# Use registry's own per-module token
|
|
86
|
-
# to avoid conflicting with Launcher's connection (same launcher_token → same module_id)
|
|
92
|
+
"""Single WebSocket session. On first connect, send module.ready."""
|
|
93
|
+
# Use registry's own per-module token to avoid conflicting with Launcher's connection
|
|
87
94
|
token = self.store.token_map.get("registry", "") or self.launcher_token
|
|
88
95
|
ws_url = f"{self._event_hub_ws_url}?token={token}"
|
|
89
|
-
async with websockets.connect(ws_url) as ws:
|
|
96
|
+
async with websockets.connect(ws_url, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
|
|
90
97
|
self._ws = ws
|
|
98
|
+
self._event_hub_connected = True
|
|
91
99
|
print("[registry] Connected to Event Hub")
|
|
100
|
+
|
|
101
|
+
# Send module.ready on first successful connection (delayed ready mechanism)
|
|
102
|
+
if not self._ready_sent:
|
|
103
|
+
self._ready_sent = True
|
|
104
|
+
await self._send_module_ready()
|
|
105
|
+
|
|
92
106
|
async for raw in ws:
|
|
93
107
|
try:
|
|
94
108
|
msg = json.loads(raw)
|
|
@@ -98,6 +112,26 @@ class RegistryServer:
|
|
|
98
112
|
if msg_type == "error":
|
|
99
113
|
print(f"[registry] Event Hub error: {msg.get('message')}")
|
|
100
114
|
|
|
115
|
+
async def _send_module_ready(self):
|
|
116
|
+
"""Send module.ready event to Event Hub. Launcher is listening for this."""
|
|
117
|
+
from datetime import datetime, timezone
|
|
118
|
+
msg = {
|
|
119
|
+
"type": "event",
|
|
120
|
+
"event_id": str(uuid.uuid4()),
|
|
121
|
+
"event": "module.ready",
|
|
122
|
+
"source": "registry",
|
|
123
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
124
|
+
"data": {
|
|
125
|
+
"module_id": "registry",
|
|
126
|
+
"api_endpoint": f"http://{self.advertise_ip}:{self.port}",
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
try:
|
|
130
|
+
await self._ws.send(json.dumps(msg))
|
|
131
|
+
print("[registry] Sent module.ready")
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"[registry] Failed to send module.ready: {e}")
|
|
134
|
+
|
|
101
135
|
async def _publish_event(self, event_type: str, data: dict):
|
|
102
136
|
"""Publish event to Event Hub. Best-effort, no-op if not connected."""
|
|
103
137
|
if not self._ws:
|
|
@@ -153,8 +187,9 @@ class RegistryServer:
|
|
|
153
187
|
if result.get("ok"):
|
|
154
188
|
mid = body["module_id"]
|
|
155
189
|
await server._publish_event("module.registered", {"module_id": mid})
|
|
156
|
-
# If Event Hub just registered,
|
|
157
|
-
|
|
190
|
+
# If Event Hub just registered, connect and send module.ready
|
|
191
|
+
ws_endpoint = (body.get("metadata") or {}).get("ws_endpoint")
|
|
192
|
+
if ws_endpoint and mid == "event_hub":
|
|
158
193
|
await server._try_connect_event_hub()
|
|
159
194
|
return result
|
|
160
195
|
|
|
@@ -222,8 +257,6 @@ class RegistryServer:
|
|
|
222
257
|
return {"ok": False}
|
|
223
258
|
|
|
224
259
|
# ── 6. POST /query (stub) ──
|
|
225
|
-
# TODO: implement LLM semantic query per design §5.1
|
|
226
|
-
# accept {"question": "..."}, search registry with LLM, return matched modules/tools
|
|
227
260
|
|
|
228
261
|
@app.post("/query")
|
|
229
262
|
async def query(request: Request):
|
package/core/registry/store.py
CHANGED
|
@@ -5,6 +5,7 @@ No persistence — Registry crash triggers Kite full restart, all modules re-reg
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import fnmatch
|
|
8
|
+
import os
|
|
8
9
|
import time
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
@@ -18,11 +19,15 @@ class RegistryStore:
|
|
|
18
19
|
self.heartbeats: dict[str, float] = {} # module_id -> last heartbeat timestamp
|
|
19
20
|
self.ttl = 60 # seconds before marking offline
|
|
20
21
|
self.heartbeat_interval = 30
|
|
22
|
+
self.is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
21
23
|
|
|
22
24
|
# ── Token verification ──
|
|
23
25
|
|
|
24
26
|
def verify_token(self, token: str) -> str | None:
|
|
25
|
-
"""Return module_id if token is valid, None otherwise.
|
|
27
|
+
"""Return module_id if token is valid, None otherwise.
|
|
28
|
+
In debug mode (KITE_DEBUG=1), any non-empty token is accepted."""
|
|
29
|
+
if self.is_debug and token:
|
|
30
|
+
return "debug"
|
|
26
31
|
if token == self.launcher_token:
|
|
27
32
|
return "launcher"
|
|
28
33
|
for mid, tok in self.token_map.items():
|
|
@@ -8,7 +8,6 @@ import json
|
|
|
8
8
|
import os
|
|
9
9
|
import platform
|
|
10
10
|
import sys
|
|
11
|
-
import threading
|
|
12
11
|
import time
|
|
13
12
|
from datetime import datetime, timezone
|
|
14
13
|
|
|
@@ -18,12 +17,16 @@ import uuid
|
|
|
18
17
|
import httpx
|
|
19
18
|
import websockets
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
_project_root = os.path.dirname(os.path.dirname(_this_dir))
|
|
20
|
+
_project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
23
21
|
if _project_root not in sys.path:
|
|
24
22
|
sys.path.insert(0, _project_root)
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
# Results go to KITE_MODULE_DATA (set by Launcher per module)
|
|
25
|
+
# Fallback to source tree for standalone runs
|
|
26
|
+
RESULTS_DIR = os.environ.get("KITE_MODULE_DATA") or os.path.join(
|
|
27
|
+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
|
28
|
+
"core", "event_hub", "bench_results",
|
|
29
|
+
)
|
|
27
30
|
TAG = "[event_hub_bench]"
|
|
28
31
|
|
|
29
32
|
|
|
@@ -34,7 +37,7 @@ def _discover_hub(registry_url: str, token: str, timeout: float = 30) -> str | N
|
|
|
34
37
|
while time.time() < deadline:
|
|
35
38
|
try:
|
|
36
39
|
resp = httpx.get(
|
|
37
|
-
f"{registry_url}/
|
|
40
|
+
f"{registry_url}/get/event_hub",
|
|
38
41
|
headers=headers, timeout=5,
|
|
39
42
|
)
|
|
40
43
|
if resp.status_code == 200:
|
|
@@ -335,20 +338,25 @@ async def _run(ws_url: str, token: str):
|
|
|
335
338
|
|
|
336
339
|
|
|
337
340
|
def main():
|
|
338
|
-
# Read boot_info from stdin
|
|
341
|
+
# Read boot_info from stdin (only token)
|
|
339
342
|
token = ""
|
|
340
|
-
registry_port = 0
|
|
341
343
|
try:
|
|
342
344
|
line = sys.stdin.readline().strip()
|
|
343
345
|
if line:
|
|
344
346
|
boot = json.loads(line)
|
|
345
347
|
token = boot.get("token", "")
|
|
346
|
-
registry_port = boot.get("registry_port", 0)
|
|
347
348
|
except Exception:
|
|
348
349
|
pass
|
|
349
350
|
|
|
351
|
+
# Read registry_port from environment variable
|
|
352
|
+
registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
|
|
353
|
+
|
|
350
354
|
if not token or not registry_port:
|
|
351
|
-
print(f"{TAG} ERROR: Missing token or
|
|
355
|
+
print(f"{TAG} ERROR: Missing token or KITE_REGISTRY_PORT")
|
|
356
|
+
sys.exit(1)
|
|
357
|
+
|
|
358
|
+
if os.environ.get("KITE_DEBUG") != "1":
|
|
359
|
+
print(f"{TAG} ERROR: Benchmark requires KITE_DEBUG=1 (multiple clients share one token)")
|
|
352
360
|
sys.exit(1)
|
|
353
361
|
|
|
354
362
|
registry_url = f"http://127.0.0.1:{registry_port}"
|
|
@@ -18,8 +18,9 @@ subscriptions: []
|
|
|
18
18
|
- 通过 Registry 发现 Event Hub,以真实模块身份连接
|
|
19
19
|
- 多线程模拟多个 publisher/subscriber 压测 hub
|
|
20
20
|
- 监控 hub 进程 CPU/内存占用
|
|
21
|
-
- 结果保存到 `
|
|
21
|
+
- 结果保存到 `KITE_MODULE_DATA/`
|
|
22
22
|
- 测试完成后自动退出
|
|
23
|
+
- 需要 `KITE_DEBUG=1`(多客户端共用一个 token)
|
|
23
24
|
|
|
24
25
|
启用:将 `state` 改为 `enabled`
|
|
25
26
|
停用:将 `state` 改为 `disabled`
|
|
@@ -11,9 +11,8 @@ import sys
|
|
|
11
11
|
import httpx
|
|
12
12
|
import uvicorn
|
|
13
13
|
|
|
14
|
-
# Ensure project root is on sys.path
|
|
15
|
-
|
|
16
|
-
_project_root = os.path.dirname(os.path.dirname(os.path.dirname(_this_dir)))
|
|
14
|
+
# Ensure project root is on sys.path (set by main.py or cli.js)
|
|
15
|
+
_project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
17
16
|
if _project_root not in sys.path:
|
|
18
17
|
sys.path.insert(0, _project_root)
|
|
19
18
|
|
|
@@ -93,20 +92,25 @@ def _get_event_hub_ws(token: str, registry_url: str) -> str:
|
|
|
93
92
|
|
|
94
93
|
|
|
95
94
|
def main():
|
|
96
|
-
#
|
|
95
|
+
# Kite environment
|
|
96
|
+
kite_instance = os.environ.get("KITE_INSTANCE", "")
|
|
97
|
+
is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
98
|
+
|
|
99
|
+
# Read boot_info from stdin (only token)
|
|
97
100
|
token = ""
|
|
98
|
-
registry_port = 0
|
|
99
101
|
try:
|
|
100
102
|
line = sys.stdin.readline().strip()
|
|
101
103
|
if line:
|
|
102
104
|
boot_info = json.loads(line)
|
|
103
105
|
token = boot_info.get("token", "")
|
|
104
|
-
registry_port = boot_info.get("registry_port", 0)
|
|
105
106
|
except Exception:
|
|
106
107
|
pass
|
|
107
108
|
|
|
109
|
+
# Read registry_port from environment variable
|
|
110
|
+
registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
|
|
111
|
+
|
|
108
112
|
if not token or not registry_port:
|
|
109
|
-
print("[watchdog] ERROR: Missing token or
|
|
113
|
+
print("[watchdog] ERROR: Missing token or KITE_REGISTRY_PORT")
|
|
110
114
|
sys.exit(1)
|
|
111
115
|
|
|
112
116
|
print(f"[watchdog] Token received ({len(token)} chars), registry port: {registry_port}")
|
|
@@ -93,7 +93,7 @@ class WatchdogServer:
|
|
|
93
93
|
async def _ws_connect(self):
|
|
94
94
|
"""Single WebSocket session: connect, subscribe, receive loop."""
|
|
95
95
|
url = f"{self.event_hub_ws}?token={self.token}"
|
|
96
|
-
async with websockets.connect(url) as ws:
|
|
96
|
+
async with websockets.connect(url, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
|
|
97
97
|
self._ws = ws
|
|
98
98
|
print("[watchdog] Connected to Event Hub")
|
|
99
99
|
|
package/main.py
CHANGED
|
@@ -1,16 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kite development entry point.
|
|
3
|
+
Loads .env, sets KITE_* defaults, parses --debug, starts Launcher.
|
|
4
|
+
"""
|
|
5
|
+
import atexit
|
|
6
|
+
import builtins
|
|
7
|
+
import os
|
|
1
8
|
import secrets
|
|
2
9
|
import sys
|
|
3
|
-
import
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
# ── Timestamped print with delta + color ──
|
|
15
|
+
# Covers the entire Launcher process. Child processes (registry, event_hub) are
|
|
16
|
+
# separate PIDs — unaffected. Their stdout is relayed via ProcessManager._read_stdout
|
|
17
|
+
# → print(), so timestamps & deltas are added at the relay point automatically.
|
|
18
|
+
|
|
19
|
+
_builtin_print = builtins.print
|
|
20
|
+
_last_ts = time.monotonic()
|
|
21
|
+
|
|
22
|
+
# ANSI escape codes
|
|
23
|
+
_DIM = "\033[90m" # gray — fast, normal
|
|
24
|
+
_GREEN = "\033[32m" # green — noticeable but expected (subprocess launch, network wait)
|
|
25
|
+
_RED = "\033[91m" # red — abnormally slow, likely a problem
|
|
26
|
+
_RESET = "\033[0m"
|
|
27
|
+
|
|
28
|
+
# ── Log file handles ──
|
|
29
|
+
# Initialized lazily after KITE_MODULE_DATA is resolved (see _init_log_files)
|
|
30
|
+
_log_lock = threading.Lock()
|
|
31
|
+
_log_latest = None # file handle: latest.log (cleared each startup)
|
|
32
|
+
_log_daily = None # file handle: {YYYY-MM}/{YYYY-MM-DD}.log (append)
|
|
33
|
+
_log_daily_date = "" # current date string to detect day rollover
|
|
34
|
+
|
|
35
|
+
# Strip ANSI escape sequences for plain-text log files
|
|
36
|
+
import re
|
|
37
|
+
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
38
|
+
|
|
39
|
+
def _strip_ansi(s: str) -> str:
|
|
40
|
+
return _ANSI_RE.sub("", s)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _init_log_files():
|
|
44
|
+
"""Initialize log file handles. Called after KITE_MODULE_DATA is set."""
|
|
45
|
+
global _log_latest, _log_daily, _log_daily_date
|
|
46
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
47
|
+
if not module_data:
|
|
48
|
+
return
|
|
49
|
+
log_dir = os.path.join(module_data, "log")
|
|
50
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
# latest.log — truncate on each startup
|
|
53
|
+
try:
|
|
54
|
+
_log_latest = open(os.path.join(log_dir, "latest.log"), "w", encoding="utf-8")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
_builtin_print(f"[launcher] 警告: 无法打开 latest.log: {e}")
|
|
57
|
+
|
|
58
|
+
# daily log — append
|
|
59
|
+
_open_daily_log(log_dir)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _open_daily_log(log_dir: str = None):
|
|
63
|
+
"""Open (or rotate) the daily log file based on current date."""
|
|
64
|
+
global _log_daily, _log_daily_date
|
|
65
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
66
|
+
if today == _log_daily_date and _log_daily:
|
|
67
|
+
return
|
|
68
|
+
if _log_daily:
|
|
69
|
+
try:
|
|
70
|
+
_log_daily.close()
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
if not log_dir:
|
|
74
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
75
|
+
if not module_data:
|
|
76
|
+
return
|
|
77
|
+
log_dir = os.path.join(module_data, "log")
|
|
78
|
+
month_dir = os.path.join(log_dir, today[:7]) # YYYY-MM
|
|
79
|
+
os.makedirs(month_dir, exist_ok=True)
|
|
80
|
+
try:
|
|
81
|
+
_log_daily = open(os.path.join(month_dir, f"{today}.log"), "a", encoding="utf-8")
|
|
82
|
+
_log_daily_date = today
|
|
83
|
+
except Exception as e:
|
|
84
|
+
_builtin_print(f"[launcher] 警告: 无法打开每日日志: {e}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _write_log(plain_line: str):
|
|
88
|
+
"""Write a plain-text line to both latest.log and daily log."""
|
|
89
|
+
global _log_daily
|
|
90
|
+
with _log_lock:
|
|
91
|
+
if _log_latest:
|
|
92
|
+
try:
|
|
93
|
+
_log_latest.write(plain_line)
|
|
94
|
+
_log_latest.flush()
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
# Check daily rotation
|
|
98
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
99
|
+
if today != _log_daily_date:
|
|
100
|
+
_open_daily_log()
|
|
101
|
+
if _log_daily:
|
|
102
|
+
try:
|
|
103
|
+
_log_daily.write(plain_line)
|
|
104
|
+
_log_daily.flush()
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _close_log_files():
|
|
110
|
+
"""Flush and close log files on exit."""
|
|
111
|
+
global _log_latest, _log_daily
|
|
112
|
+
for f in (_log_latest, _log_daily):
|
|
113
|
+
if f:
|
|
114
|
+
try:
|
|
115
|
+
f.flush()
|
|
116
|
+
f.close()
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
_log_latest = None
|
|
120
|
+
_log_daily = None
|
|
121
|
+
|
|
122
|
+
atexit.register(_close_log_files)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _tprint(*args, **kwargs):
|
|
126
|
+
global _last_ts
|
|
127
|
+
now = time.monotonic()
|
|
128
|
+
delta = now - _last_ts
|
|
129
|
+
_last_ts = now
|
|
130
|
+
|
|
131
|
+
# Timestamp
|
|
132
|
+
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
133
|
+
|
|
134
|
+
# Format delta string
|
|
135
|
+
if delta < 1:
|
|
136
|
+
delta_str = f"+{delta * 1000:.0f}ms"
|
|
137
|
+
elif delta < 100:
|
|
138
|
+
delta_str = f"+{delta:.1f}s"
|
|
139
|
+
else:
|
|
140
|
+
delta_str = f"+{delta:.0f}s"
|
|
141
|
+
|
|
142
|
+
# Color: gray < 1s, green 1–5s, red > 5s
|
|
143
|
+
if delta >= 5:
|
|
144
|
+
color = _RED
|
|
145
|
+
elif delta >= 1:
|
|
146
|
+
color = _GREEN
|
|
147
|
+
else:
|
|
148
|
+
color = _DIM
|
|
149
|
+
|
|
150
|
+
prefix = f"{ts} {color}{delta_str:>8}{_RESET} "
|
|
151
|
+
_builtin_print(prefix, end="")
|
|
152
|
+
_builtin_print(*args, **kwargs)
|
|
153
|
+
|
|
154
|
+
# Write to log files (plain text, no ANSI)
|
|
155
|
+
if _log_latest or _log_daily:
|
|
156
|
+
sep = kwargs.get("sep", " ")
|
|
157
|
+
end = kwargs.get("end", "\n")
|
|
158
|
+
text = sep.join(str(a) for a in args)
|
|
159
|
+
plain_prefix = f"{ts} {delta_str:>8} "
|
|
160
|
+
_write_log(plain_prefix + text + end)
|
|
161
|
+
|
|
162
|
+
builtins.print = _tprint
|
|
163
|
+
|
|
164
|
+
# Load .env (development convenience, not required in production)
|
|
165
|
+
try:
|
|
166
|
+
from dotenv import load_dotenv
|
|
167
|
+
load_dotenv()
|
|
168
|
+
except ImportError:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
# Resolve project root (directory containing this file)
|
|
172
|
+
_project_root = os.path.dirname(os.path.abspath(__file__))
|
|
173
|
+
|
|
174
|
+
# Home base for Kite data
|
|
175
|
+
_home = os.environ.get("HOME") or os.environ.get("USERPROFILE") or os.path.expanduser("~")
|
|
176
|
+
_kite_home = os.path.join(_home, ".kite")
|
|
177
|
+
|
|
178
|
+
# Set KITE_* defaults (only if not already set by cli.js or .env)
|
|
179
|
+
_defaults = {
|
|
180
|
+
"KITE_PROJECT": _project_root,
|
|
181
|
+
"KITE_CWD": os.getcwd(),
|
|
182
|
+
"KITE_WORKSPACE": os.path.join(_kite_home, "workspace"),
|
|
183
|
+
"KITE_DATA": os.path.join(_kite_home, "data"),
|
|
184
|
+
"KITE_MODULES": os.path.join(_kite_home, "modules"),
|
|
185
|
+
"KITE_REPO": os.path.join(_kite_home, "repo"),
|
|
186
|
+
"KITE_ENV": "development",
|
|
187
|
+
}
|
|
188
|
+
for key, value in _defaults.items():
|
|
189
|
+
if not os.environ.get(key):
|
|
190
|
+
os.environ[key] = value
|
|
191
|
+
|
|
192
|
+
# Parse --debug flag
|
|
193
|
+
if "--debug" in sys.argv:
|
|
194
|
+
os.environ["KITE_DEBUG"] = "1"
|
|
195
|
+
sys.argv.remove("--debug")
|
|
4
196
|
|
|
5
|
-
sys.path
|
|
197
|
+
# Ensure project root is on sys.path
|
|
198
|
+
sys.path.insert(0, os.environ["KITE_PROJECT"])
|
|
6
199
|
|
|
7
200
|
from core.launcher.entry import Launcher
|
|
8
201
|
|
|
9
202
|
|
|
10
203
|
def main():
|
|
11
204
|
token = secrets.token_hex(32)
|
|
12
|
-
print(f"[main] KITE_TOKEN
|
|
13
|
-
|
|
205
|
+
print(f"[main] KITE_TOKEN 已生成 ({len(token)} 字符)")
|
|
206
|
+
env = os.environ.get("KITE_ENV", "development")
|
|
207
|
+
debug = os.environ.get("KITE_DEBUG") == "1"
|
|
208
|
+
print(f"[main] 环境={env}, 调试={debug}, 项目={os.environ['KITE_PROJECT']}")
|
|
209
|
+
launcher = Launcher(kite_token=token)
|
|
210
|
+
# KITE_MODULE_DATA is now set — initialize log files
|
|
211
|
+
_init_log_files()
|
|
212
|
+
print(f"[main] 日志文件已初始化: {os.environ.get('KITE_MODULE_DATA', '')}/log/")
|
|
213
|
+
launcher.run()
|
|
14
214
|
|
|
15
215
|
|
|
16
216
|
if __name__ == "__main__":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentunion/kite",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Kite framework launcher — start Kite from anywhere",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kite": "./cli.js"
|
|
@@ -11,7 +11,16 @@
|
|
|
11
11
|
"__main__.py",
|
|
12
12
|
"__init__.py",
|
|
13
13
|
"core/**",
|
|
14
|
-
"extensions/**"
|
|
14
|
+
"extensions/**",
|
|
15
|
+
"!**/__pycache__",
|
|
16
|
+
"!**/__pycache__/**",
|
|
17
|
+
"!**/*.pyc",
|
|
18
|
+
"!**/data",
|
|
19
|
+
"!**/data/**",
|
|
20
|
+
"!**/bench_results",
|
|
21
|
+
"!**/bench_results/**",
|
|
22
|
+
"!core/event_hub_bench",
|
|
23
|
+
"!core/event_hub_bench/**"
|
|
15
24
|
],
|
|
16
25
|
"engines": {
|
|
17
26
|
"node": ">=16"
|
|
Binary file
|
|
Binary file
|
package/core/data_dir.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Data directory management for Kite.
|
|
3
|
-
When installed globally via npm, use ~/.kite/ for runtime data.
|
|
4
|
-
When running locally, use project_root/core/*/data/.
|
|
5
|
-
"""
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def get_data_root() -> str:
|
|
11
|
-
"""Get the root directory for runtime data.
|
|
12
|
-
|
|
13
|
-
Returns:
|
|
14
|
-
- ~/.kite/ if installed globally (no write permission to install dir)
|
|
15
|
-
- project_root if running locally (development mode)
|
|
16
|
-
"""
|
|
17
|
-
# Check if we're in a global npm install (no write permission)
|
|
18
|
-
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
19
|
-
test_file = os.path.join(project_root, "core", "launcher", "data", ".write_test")
|
|
20
|
-
|
|
21
|
-
try:
|
|
22
|
-
os.makedirs(os.path.dirname(test_file), exist_ok=True)
|
|
23
|
-
with open(test_file, "w") as f:
|
|
24
|
-
f.write("test")
|
|
25
|
-
os.remove(test_file)
|
|
26
|
-
# Write permission OK, use project_root
|
|
27
|
-
return project_root
|
|
28
|
-
except (OSError, PermissionError):
|
|
29
|
-
# No write permission, use user home
|
|
30
|
-
home = os.path.expanduser("~")
|
|
31
|
-
kite_home = os.path.join(home, ".kite")
|
|
32
|
-
os.makedirs(kite_home, exist_ok=True)
|
|
33
|
-
return kite_home
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def get_launcher_data_dir() -> str:
|
|
37
|
-
"""Get launcher data directory."""
|
|
38
|
-
root = get_data_root()
|
|
39
|
-
if root.endswith(".kite"):
|
|
40
|
-
# User home mode: ~/.kite/launcher/
|
|
41
|
-
return os.path.join(root, "launcher")
|
|
42
|
-
else:
|
|
43
|
-
# Project mode: project_root/core/launcher/data/
|
|
44
|
-
return os.path.join(root, "core", "launcher", "data")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def get_registry_data_dir() -> str:
|
|
48
|
-
"""Get registry data directory."""
|
|
49
|
-
root = get_data_root()
|
|
50
|
-
if root.endswith(".kite"):
|
|
51
|
-
return os.path.join(root, "registry")
|
|
52
|
-
else:
|
|
53
|
-
return os.path.join(root, "core", "registry", "data")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def get_event_hub_data_dir() -> str:
|
|
57
|
-
"""Get event hub data directory."""
|
|
58
|
-
root = get_data_root()
|
|
59
|
-
if root.endswith(".kite"):
|
|
60
|
-
return os.path.join(root, "event_hub")
|
|
61
|
-
else:
|
|
62
|
-
return os.path.join(root, "core", "event_hub", "data")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"timestamp": "2026-02-28T13:26:48.644977",
|
|
3
|
-
"env": {
|
|
4
|
-
"platform": "win32",
|
|
5
|
-
"python": "3.13.12"
|
|
6
|
-
},
|
|
7
|
-
"throughput": {
|
|
8
|
-
"events": 10000,
|
|
9
|
-
"send_rate": 13121,
|
|
10
|
-
"client_recv": 10000,
|
|
11
|
-
"hub_queued": 10000,
|
|
12
|
-
"hub_routed": 10000,
|
|
13
|
-
"send_time": 0.76,
|
|
14
|
-
"recv_time": 1.59
|
|
15
|
-
},
|
|
16
|
-
"latency": {
|
|
17
|
-
"samples": 200,
|
|
18
|
-
"avg_ms": 0.69,
|
|
19
|
-
"p50_ms": 0.65,
|
|
20
|
-
"p95_ms": 0.92,
|
|
21
|
-
"p99_ms": 1.27
|
|
22
|
-
},
|
|
23
|
-
"fanout_1": {
|
|
24
|
-
"subs": 1,
|
|
25
|
-
"events": 2000,
|
|
26
|
-
"send_rate": 17292,
|
|
27
|
-
"avg_recv": 2000,
|
|
28
|
-
"min_recv": 2000
|
|
29
|
-
},
|
|
30
|
-
"fanout_10": {
|
|
31
|
-
"subs": 10,
|
|
32
|
-
"events": 2000,
|
|
33
|
-
"send_rate": 18074,
|
|
34
|
-
"avg_recv": 2000,
|
|
35
|
-
"min_recv": 2000
|
|
36
|
-
},
|
|
37
|
-
"fanout_50": {
|
|
38
|
-
"subs": 50,
|
|
39
|
-
"events": 2000,
|
|
40
|
-
"send_rate": 6138,
|
|
41
|
-
"avg_recv": 2000,
|
|
42
|
-
"min_recv": 2000
|
|
43
|
-
},
|
|
44
|
-
"hub_counters": {
|
|
45
|
-
"events_received": 16200,
|
|
46
|
-
"events_routed": 132200,
|
|
47
|
-
"events_queued": 132200,
|
|
48
|
-
"events_deduplicated": 0,
|
|
49
|
-
"errors": 0
|
|
50
|
-
}
|
|
51
|
-
}
|