@agentunion/kite 1.0.5 → 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/cli.js
CHANGED
|
@@ -1,70 +1,172 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Kite CLI —
|
|
4
|
-
*
|
|
3
|
+
* Kite CLI — installer + launcher.
|
|
4
|
+
*
|
|
5
|
+
* First run: copies framework code to ~/.kite/versions/{version}/
|
|
6
|
+
* Subsequent runs: directly starts the installed version.
|
|
7
|
+
*
|
|
8
|
+
* Directory structure:
|
|
9
|
+
* ~/.kite/
|
|
10
|
+
* versions/{ver}/ — framework code (per-version, replaceable)
|
|
11
|
+
* modules/ — shared modules (all versions share, user-writable)
|
|
12
|
+
* data/ — runtime data (all versions share)
|
|
5
13
|
*
|
|
6
14
|
* Usage:
|
|
7
|
-
* kite #
|
|
15
|
+
* kite # start current version
|
|
8
16
|
* kite --cwd /some/path # override working directory
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Environment variables passed to Python:
|
|
12
|
-
* KITE_HOME — Kite framework root (where core/, extensions/ live)
|
|
13
|
-
* KITE_CWD — directory where user executed the kite command
|
|
17
|
+
* kite --use 1.0.6 # use a specific version
|
|
14
18
|
*/
|
|
15
|
-
const { spawn } = require('child_process');
|
|
19
|
+
const { spawn, execSync } = require('child_process');
|
|
16
20
|
const crypto = require('crypto');
|
|
17
21
|
const fs = require('fs');
|
|
18
22
|
const path = require('path');
|
|
19
23
|
|
|
20
|
-
//
|
|
24
|
+
// Timestamped console.log with delta + color
|
|
25
|
+
const _origLog = console.log.bind(console);
|
|
26
|
+
const _origErr = console.error.bind(console);
|
|
27
|
+
let _lastTs = Date.now();
|
|
28
|
+
const DIM = '\x1b[90m', GREEN = '\x1b[32m', RED = '\x1b[91m', RST = '\x1b[0m';
|
|
29
|
+
|
|
30
|
+
function _prefix() {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const delta = now - _lastTs;
|
|
33
|
+
_lastTs = now;
|
|
34
|
+
const d = new Date();
|
|
35
|
+
const ts = d.toTimeString().slice(0, 8) + '.' + String(d.getMilliseconds()).padStart(3, '0');
|
|
36
|
+
const ds = delta < 1000 ? `+${delta}ms` : `+${(delta / 1000).toFixed(1)}s`;
|
|
37
|
+
const color = delta >= 5000 ? RED : delta >= 1000 ? GREEN : DIM;
|
|
38
|
+
return `${ts} ${color}${ds.padStart(8)}${RST} `;
|
|
39
|
+
}
|
|
40
|
+
console.log = (...args) => _origLog(_prefix(), ...args);
|
|
41
|
+
console.error = (...args) => _origErr(_prefix(), ...args);
|
|
42
|
+
|
|
43
|
+
// ── Paths ──
|
|
44
|
+
const packageDir = fs.realpathSync(__dirname);
|
|
45
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'));
|
|
46
|
+
const version = packageJson.version;
|
|
47
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
48
|
+
const kiteHome = path.join(homeDir, '.kite');
|
|
49
|
+
const modulesDir = path.join(kiteHome, 'modules');
|
|
50
|
+
const dataDir = path.join(kiteHome, 'data');
|
|
51
|
+
|
|
52
|
+
// ── Parse CLI args ──
|
|
21
53
|
const args = process.argv.slice(2);
|
|
22
54
|
const pythonArgs = [];
|
|
23
55
|
let cwdOverride = null;
|
|
56
|
+
let useVersion = version;
|
|
24
57
|
|
|
25
58
|
for (let i = 0; i < args.length; i++) {
|
|
26
59
|
if (args[i] === '--cwd' && args[i + 1]) {
|
|
27
60
|
cwdOverride = path.resolve(args[++i]);
|
|
61
|
+
} else if (args[i] === '--use' && args[i + 1]) {
|
|
62
|
+
useVersion = args[++i];
|
|
28
63
|
} else {
|
|
29
64
|
pythonArgs.push(args[i]);
|
|
30
65
|
}
|
|
31
66
|
}
|
|
32
67
|
|
|
33
|
-
const
|
|
34
|
-
|
|
68
|
+
const versionDir = path.join(kiteHome, 'versions', useVersion);
|
|
69
|
+
|
|
70
|
+
// ── Install: copy framework to ~/.kite/versions/{ver}/ if needed ──
|
|
71
|
+
|
|
72
|
+
function copyDirSync(src, dest) {
|
|
73
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
74
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
75
|
+
const srcPath = path.join(src, entry.name);
|
|
76
|
+
const destPath = path.join(dest, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
// Skip directories that shouldn't be copied
|
|
79
|
+
if (['__pycache__', 'node_modules', '.git', 'data'].includes(entry.name)) continue;
|
|
80
|
+
copyDirSync(srcPath, destPath);
|
|
81
|
+
} else {
|
|
82
|
+
fs.copyFileSync(srcPath, destPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function install() {
|
|
88
|
+
console.log(`[kite] 安装版本 ${useVersion} 到 ${versionDir}`);
|
|
89
|
+
|
|
90
|
+
// Create shared directories
|
|
91
|
+
fs.mkdirSync(modulesDir, { recursive: true });
|
|
92
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
// Copy framework code from npm package
|
|
95
|
+
copyDirSync(packageDir, versionDir);
|
|
96
|
+
|
|
97
|
+
// Patch launcher module.md: add shared modules path to discovery
|
|
98
|
+
patchLauncherDiscovery();
|
|
99
|
+
|
|
100
|
+
console.log(`[kite] 版本 ${useVersion} 已安装`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function patchLauncherDiscovery() {
|
|
104
|
+
const mdPath = path.join(versionDir, 'core', 'launcher', 'module.md');
|
|
105
|
+
if (!fs.existsSync(mdPath)) return;
|
|
106
|
+
|
|
107
|
+
let content = fs.readFileSync(mdPath, 'utf-8');
|
|
108
|
+
|
|
109
|
+
// Already patched?
|
|
110
|
+
if (content.includes('shared_modules')) return;
|
|
111
|
+
|
|
112
|
+
// Insert shared_modules discovery entry before the closing ---
|
|
113
|
+
const entry = [
|
|
114
|
+
' shared_modules:',
|
|
115
|
+
' type: scan_dir',
|
|
116
|
+
` path: "${modulesDir.replace(/\\/g, '/')}"`,
|
|
117
|
+
' enabled: true',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
120
|
+
// Insert after the last discovery entry, before ---
|
|
121
|
+
content = content.replace(
|
|
122
|
+
/(discovery:\n(?:[\s\S]*?))(---)/,
|
|
123
|
+
`$1${entry}\n$2`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
fs.writeFileSync(mdPath, content, 'utf-8');
|
|
127
|
+
console.log(`[kite] 已补丁启动器发现路径: ${modulesDir}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Main: check version, install if needed, start ──
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(path.join(versionDir, 'main.py'))) {
|
|
133
|
+
install();
|
|
134
|
+
}
|
|
135
|
+
|
|
35
136
|
const kiteCwd = cwdOverride || process.cwd();
|
|
36
137
|
|
|
37
|
-
console.log(`[
|
|
38
|
-
console.log(`[
|
|
39
|
-
console.log(`[
|
|
138
|
+
console.log(`[kite] 版本: ${useVersion}`);
|
|
139
|
+
console.log(`[kite] 项目: ${versionDir}`);
|
|
140
|
+
console.log(`[kite] 工作目录: ${kiteCwd}`);
|
|
40
141
|
|
|
41
|
-
// Spawn Python using package mode: python -m Kite
|
|
42
|
-
// PYTHONPATH points to Kite's parent so "import Kite" / "python -m Kite" works
|
|
43
|
-
const kiteParent = path.dirname(kiteHome);
|
|
44
142
|
const env = {
|
|
45
143
|
...process.env,
|
|
46
|
-
|
|
144
|
+
KITE_PROJECT: versionDir,
|
|
47
145
|
KITE_CWD: kiteCwd,
|
|
48
|
-
|
|
146
|
+
KITE_WORKSPACE: path.join(kiteHome, 'workspace'),
|
|
147
|
+
KITE_DATA: dataDir,
|
|
148
|
+
KITE_MODULES: modulesDir,
|
|
149
|
+
KITE_REPO: path.join(kiteHome, 'repo'),
|
|
150
|
+
KITE_VERSION: useVersion,
|
|
151
|
+
KITE_ENV: 'production',
|
|
49
152
|
PYTHONUTF8: '1',
|
|
50
153
|
};
|
|
51
154
|
|
|
52
155
|
const python = process.platform === 'win32' ? 'python' : 'python3';
|
|
53
156
|
const child = spawn(
|
|
54
157
|
python,
|
|
55
|
-
[path.join(
|
|
56
|
-
{ cwd:
|
|
158
|
+
[path.join(versionDir, 'main.py'), ...pythonArgs],
|
|
159
|
+
{ cwd: versionDir, env, stdio: 'inherit' }
|
|
57
160
|
);
|
|
58
161
|
|
|
59
162
|
child.on('error', err => {
|
|
60
|
-
console.error(`[
|
|
61
|
-
console.error(`[
|
|
163
|
+
console.error(`[kite] 启动 Python 失败: ${err.message}`);
|
|
164
|
+
console.error(`[kite] 请确认 '${python}' 在 PATH 中且依赖已安装`);
|
|
62
165
|
process.exit(1);
|
|
63
166
|
});
|
|
64
167
|
|
|
65
168
|
child.on('exit', code => process.exit(code ?? 0));
|
|
66
169
|
|
|
67
|
-
// Forward signals for graceful shutdown
|
|
68
170
|
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
69
171
|
process.on(sig, () => child.kill(sig));
|
|
70
172
|
}
|
package/core/event_hub/entry.py
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Event Hub entry point.
|
|
3
|
-
Reads
|
|
3
|
+
Reads token from stdin boot_info, reads launcher_ws_token from stdin second line,
|
|
4
|
+
starts FastAPI server, outputs ws_endpoint via stdout, waits for Launcher to connect,
|
|
5
|
+
then registers to Registry after stdio disconnect.
|
|
4
6
|
"""
|
|
5
7
|
|
|
6
8
|
import json
|
|
7
9
|
import os
|
|
10
|
+
import re
|
|
8
11
|
import socket
|
|
9
12
|
import sys
|
|
13
|
+
import threading
|
|
10
14
|
|
|
11
|
-
import httpx
|
|
12
15
|
import uvicorn
|
|
13
16
|
|
|
14
|
-
# Ensure project root is on sys.path
|
|
17
|
+
# Ensure project root is on sys.path (set by main.py or cli.js)
|
|
15
18
|
_this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
-
_project_root = os.path.dirname(os.path.dirname(_this_dir))
|
|
19
|
+
_project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(_this_dir))
|
|
17
20
|
if _project_root not in sys.path:
|
|
18
21
|
sys.path.insert(0, _project_root)
|
|
19
22
|
|
|
@@ -21,91 +24,132 @@ from core.event_hub.hub import EventHub
|
|
|
21
24
|
from core.event_hub.server import EventHubServer
|
|
22
25
|
|
|
23
26
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
s.bind(("127.0.0.1", 0))
|
|
27
|
-
return s.getsockname()[1]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _register_to_registry(token: str, registry_url: str, port: int, advertise_ip: str = "127.0.0.1"):
|
|
31
|
-
"""Synchronous registration to Registry at startup."""
|
|
32
|
-
payload = {
|
|
33
|
-
"action": "register",
|
|
34
|
-
"module_id": "event_hub",
|
|
35
|
-
"module_type": "infrastructure",
|
|
36
|
-
"name": "Event Hub",
|
|
37
|
-
"api_endpoint": f"http://{advertise_ip}:{port}",
|
|
38
|
-
"health_endpoint": "/health",
|
|
39
|
-
"metadata": {
|
|
40
|
-
"ws_endpoint": f"ws://{advertise_ip}:{port}/ws",
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
44
|
-
resp = httpx.post(
|
|
45
|
-
f"{registry_url}/modules",
|
|
46
|
-
json=payload,
|
|
47
|
-
headers=headers,
|
|
48
|
-
timeout=5,
|
|
49
|
-
)
|
|
50
|
-
if resp.status_code == 200:
|
|
51
|
-
print(f"[event_hub] Registered to Registry")
|
|
52
|
-
else:
|
|
53
|
-
print(f"[event_hub] WARNING: Registry returned {resp.status_code}")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _read_test_mode() -> bool:
|
|
57
|
-
"""Read test_mode from module.md frontmatter."""
|
|
27
|
+
def _read_module_md() -> dict:
|
|
28
|
+
"""Read preferred_port, advertise_ip from own module.md."""
|
|
58
29
|
md_path = os.path.join(_this_dir, "module.md")
|
|
30
|
+
result = {"preferred_port": 0, "advertise_ip": "127.0.0.1"}
|
|
59
31
|
try:
|
|
60
32
|
with open(md_path, encoding="utf-8") as f:
|
|
61
33
|
text = f.read()
|
|
62
|
-
import re, yaml
|
|
63
34
|
m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
64
35
|
if m:
|
|
65
|
-
|
|
66
|
-
|
|
36
|
+
try:
|
|
37
|
+
import yaml
|
|
38
|
+
fm = yaml.safe_load(m.group(1)) or {}
|
|
39
|
+
except ImportError:
|
|
40
|
+
fm = {}
|
|
41
|
+
result["preferred_port"] = int(fm.get("preferred_port", 0))
|
|
42
|
+
result["advertise_ip"] = fm.get("advertise_ip", "127.0.0.1")
|
|
67
43
|
except Exception:
|
|
68
44
|
pass
|
|
69
|
-
return
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _bind_port(preferred: int, host: str) -> int:
|
|
49
|
+
"""Try preferred port first, fall back to OS-assigned."""
|
|
50
|
+
if preferred:
|
|
51
|
+
try:
|
|
52
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
53
|
+
s.bind((host, preferred))
|
|
54
|
+
return preferred
|
|
55
|
+
except OSError:
|
|
56
|
+
pass
|
|
57
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
58
|
+
s.bind((host, 0))
|
|
59
|
+
return s.getsockname()[1]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_stdin_kite_message() -> dict | None:
|
|
63
|
+
"""Read a structured kite message from stdin (second line after boot_info).
|
|
64
|
+
Uses a short timeout thread to avoid blocking forever if Launcher doesn't send.
|
|
65
|
+
"""
|
|
66
|
+
result = [None]
|
|
67
|
+
|
|
68
|
+
def _read():
|
|
69
|
+
try:
|
|
70
|
+
line = sys.stdin.readline().strip()
|
|
71
|
+
if line:
|
|
72
|
+
msg = json.loads(line)
|
|
73
|
+
if isinstance(msg, dict) and "kite" in msg:
|
|
74
|
+
result[0] = msg
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
t = threading.Thread(target=_read, daemon=True)
|
|
79
|
+
t.start()
|
|
80
|
+
t.join(timeout=5)
|
|
81
|
+
return result[0]
|
|
70
82
|
|
|
71
83
|
|
|
72
84
|
def main():
|
|
73
|
-
#
|
|
85
|
+
# Kite environment
|
|
86
|
+
kite_instance = os.environ.get("KITE_INSTANCE", "")
|
|
87
|
+
is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
88
|
+
|
|
89
|
+
# Step 1: Read token from stdin boot_info
|
|
74
90
|
token = ""
|
|
75
|
-
registry_port = 0
|
|
76
|
-
bind_host = "127.0.0.1"
|
|
77
|
-
advertise_ip = "127.0.0.1"
|
|
78
91
|
try:
|
|
79
92
|
line = sys.stdin.readline().strip()
|
|
80
93
|
if line:
|
|
81
94
|
boot_info = json.loads(line)
|
|
82
95
|
token = boot_info.get("token", "")
|
|
83
|
-
registry_port = boot_info.get("registry_port", 0)
|
|
84
|
-
bind_host = boot_info.get("bind", "127.0.0.1")
|
|
85
|
-
advertise_ip = boot_info.get("advertise_ip", "127.0.0.1")
|
|
86
96
|
except Exception:
|
|
87
97
|
pass
|
|
88
98
|
|
|
89
|
-
if not token
|
|
90
|
-
print("[event_hub]
|
|
99
|
+
if not token:
|
|
100
|
+
print("[event_hub] 错误: boot_info 中缺少令牌")
|
|
91
101
|
sys.exit(1)
|
|
92
102
|
|
|
93
|
-
|
|
103
|
+
# Step 2: Read launcher_ws_token from stdin (second line, structured kite message)
|
|
104
|
+
launcher_ws_token = ""
|
|
105
|
+
kite_msg = _read_stdin_kite_message()
|
|
106
|
+
if kite_msg and kite_msg.get("kite") == "launcher_ws_token":
|
|
107
|
+
launcher_ws_token = kite_msg.get("launcher_ws_token", "")
|
|
94
108
|
|
|
109
|
+
if launcher_ws_token:
|
|
110
|
+
print(f"[event_hub] 已收到启动器 WS 令牌 ({len(launcher_ws_token)} 字符)")
|
|
111
|
+
else:
|
|
112
|
+
print("[event_hub] 警告: 未收到 launcher_ws_token,启动器引导认证已禁用")
|
|
113
|
+
|
|
114
|
+
# Step 3: Read registry_port from environment variable
|
|
115
|
+
registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
|
|
116
|
+
if not registry_port:
|
|
117
|
+
print("[event_hub] 错误: KITE_REGISTRY_PORT 未设置")
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
print(f"[event_hub] 已收到令牌 ({len(token)} 字符),Registry 端口: {registry_port}")
|
|
121
|
+
|
|
122
|
+
# Step 4: Read config from own module.md
|
|
123
|
+
md_config = _read_module_md()
|
|
124
|
+
advertise_ip = md_config["advertise_ip"]
|
|
125
|
+
preferred_port = md_config["preferred_port"]
|
|
126
|
+
|
|
127
|
+
# Step 5: Bind port and create server
|
|
128
|
+
bind_host = advertise_ip
|
|
129
|
+
port = _bind_port(preferred_port, bind_host)
|
|
95
130
|
registry_url = f"http://127.0.0.1:{registry_port}"
|
|
96
|
-
port = _get_free_port()
|
|
97
131
|
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
if is_debug:
|
|
133
|
+
print("[event_hub] 调试模式已启用 (KITE_DEBUG=1),接受所有令牌")
|
|
100
134
|
|
|
101
|
-
# Create hub and server
|
|
102
|
-
test_mode = _read_test_mode()
|
|
103
|
-
if test_mode:
|
|
104
|
-
print("[event_hub] WARNING: test_mode enabled, all tokens accepted")
|
|
105
135
|
hub = EventHub()
|
|
106
|
-
server = EventHubServer(
|
|
136
|
+
server = EventHubServer(
|
|
137
|
+
hub,
|
|
138
|
+
own_token=token,
|
|
139
|
+
registry_url=registry_url,
|
|
140
|
+
launcher_ws_token=launcher_ws_token,
|
|
141
|
+
advertise_ip=advertise_ip,
|
|
142
|
+
port=port,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Step 6: Output ws_endpoint via stdout (Launcher reads this)
|
|
146
|
+
ws_endpoint = f"ws://{advertise_ip}:{port}/ws"
|
|
147
|
+
print(json.dumps({"kite": "ws_endpoint", "ws_endpoint": ws_endpoint}), flush=True)
|
|
107
148
|
|
|
108
|
-
|
|
149
|
+
# Step 7: Start HTTP + WS server
|
|
150
|
+
# Launcher will connect with launcher_ws_token → Event Hub sends module.ready → stdio disconnect
|
|
151
|
+
# After stdio disconnect, Event Hub registers to Registry (done by server on_launcher_connected callback)
|
|
152
|
+
print(f"[event_hub] 启动中 {bind_host}:{port}")
|
|
109
153
|
uvicorn.run(server.app, host=bind_host, port=port, log_level="warning")
|
|
110
154
|
|
|
111
155
|
|
package/core/event_hub/module.md
CHANGED
package/core/event_hub/server.py
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
Event Hub HTTP + WebSocket server.
|
|
3
3
|
FastAPI app: /ws (WebSocket), /health, /stats.
|
|
4
4
|
30s timer for heartbeat renewal + dedup cleanup.
|
|
5
|
+
|
|
6
|
+
Launcher bootstrap sequence:
|
|
7
|
+
Launcher connects with launcher_ws_token → Event Hub verifies locally →
|
|
8
|
+
sends module.ready → registers to Registry.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
11
|
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import uuid
|
|
15
|
+
from datetime import datetime, timezone
|
|
8
16
|
|
|
9
17
|
import httpx
|
|
10
18
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
@@ -14,20 +22,33 @@ from .hub import EventHub
|
|
|
14
22
|
|
|
15
23
|
class EventHubServer:
|
|
16
24
|
|
|
17
|
-
def __init__(self, hub: EventHub, own_token: str, registry_url: str,
|
|
25
|
+
def __init__(self, hub: EventHub, own_token: str, registry_url: str,
|
|
26
|
+
launcher_ws_token: str = "",
|
|
27
|
+
advertise_ip: str = "127.0.0.1", port: int = 0):
|
|
18
28
|
self.hub = hub
|
|
19
29
|
self.own_token = own_token
|
|
20
30
|
self.registry_url = registry_url
|
|
21
|
-
self.
|
|
31
|
+
self.is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
32
|
+
self.launcher_ws_token = launcher_ws_token
|
|
33
|
+
self.advertise_ip = advertise_ip
|
|
34
|
+
self.port = port
|
|
22
35
|
self._timer_task: asyncio.Task | None = None
|
|
36
|
+
self._launcher_connected = False
|
|
37
|
+
self._registered_to_registry = False
|
|
23
38
|
self.app = self._create_app()
|
|
24
39
|
|
|
25
|
-
# ── Token verification
|
|
40
|
+
# ── Token verification ──
|
|
26
41
|
|
|
27
42
|
async def _verify_token(self, token: str, module_id_hint: str = "") -> str | None:
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
"""Verify a token. Launcher's launcher_ws_token is verified locally.
|
|
44
|
+
Other tokens are verified via Registry POST /verify.
|
|
45
|
+
In debug mode (KITE_DEBUG=1), any non-empty token is accepted."""
|
|
46
|
+
if self.is_debug and token:
|
|
47
|
+
return module_id_hint or "debug"
|
|
48
|
+
# Local verification for Launcher bootstrap (before Registry registration)
|
|
49
|
+
if self.launcher_ws_token and token == self.launcher_ws_token:
|
|
50
|
+
return "launcher"
|
|
51
|
+
# Normal verification via Registry
|
|
31
52
|
try:
|
|
32
53
|
async with httpx.AsyncClient() as client:
|
|
33
54
|
resp = await client.post(
|
|
@@ -44,6 +65,62 @@ class EventHubServer:
|
|
|
44
65
|
print(f"[event_hub] Token verification failed: {e}")
|
|
45
66
|
return None
|
|
46
67
|
|
|
68
|
+
# ── Launcher bootstrap ──
|
|
69
|
+
|
|
70
|
+
async def _on_launcher_connected(self, ws: WebSocket):
|
|
71
|
+
"""Called on first Launcher WS connect. Sends module.ready, then registers to Registry."""
|
|
72
|
+
self._launcher_connected = True
|
|
73
|
+
msg = {
|
|
74
|
+
"type": "event",
|
|
75
|
+
"event_id": str(uuid.uuid4()),
|
|
76
|
+
"event": "module.ready",
|
|
77
|
+
"source": "event_hub",
|
|
78
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
79
|
+
"data": {
|
|
80
|
+
"module_id": "event_hub",
|
|
81
|
+
"ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
try:
|
|
85
|
+
await ws.send_text(json.dumps(msg))
|
|
86
|
+
print("[event_hub] Sent module.ready to Launcher")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"[event_hub] Failed to send module.ready: {e}")
|
|
89
|
+
# Register to Registry in background
|
|
90
|
+
asyncio.create_task(self._register_to_registry())
|
|
91
|
+
|
|
92
|
+
async def _register_to_registry(self):
|
|
93
|
+
"""Register to Registry. Triggered after Launcher connects."""
|
|
94
|
+
if self._registered_to_registry:
|
|
95
|
+
return
|
|
96
|
+
payload = {
|
|
97
|
+
"action": "register",
|
|
98
|
+
"module_id": "event_hub",
|
|
99
|
+
"module_type": "infrastructure",
|
|
100
|
+
"name": "Event Hub",
|
|
101
|
+
"api_endpoint": f"http://{self.advertise_ip}:{self.port}",
|
|
102
|
+
"health_endpoint": "/health",
|
|
103
|
+
"metadata": {
|
|
104
|
+
"ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
headers = {"Authorization": f"Bearer {self.own_token}"}
|
|
108
|
+
try:
|
|
109
|
+
async with httpx.AsyncClient() as client:
|
|
110
|
+
resp = await client.post(
|
|
111
|
+
f"{self.registry_url}/modules",
|
|
112
|
+
json=payload,
|
|
113
|
+
headers=headers,
|
|
114
|
+
timeout=5,
|
|
115
|
+
)
|
|
116
|
+
if resp.status_code == 200:
|
|
117
|
+
self._registered_to_registry = True
|
|
118
|
+
print("[event_hub] Registered to Registry")
|
|
119
|
+
else:
|
|
120
|
+
print(f"[event_hub] WARNING: Registry returned {resp.status_code}")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"[event_hub] WARNING: Failed to register to Registry: {e}")
|
|
123
|
+
|
|
47
124
|
# ── App factory ──
|
|
48
125
|
|
|
49
126
|
def _create_app(self) -> FastAPI:
|
|
@@ -53,14 +130,11 @@ class EventHubServer:
|
|
|
53
130
|
@app.on_event("startup")
|
|
54
131
|
async def _startup():
|
|
55
132
|
server._timer_task = asyncio.create_task(server._timer_loop())
|
|
56
|
-
server._test_task = asyncio.create_task(server._test_event_loop())
|
|
57
133
|
|
|
58
134
|
@app.on_event("shutdown")
|
|
59
135
|
async def _shutdown():
|
|
60
136
|
if server._timer_task:
|
|
61
137
|
server._timer_task.cancel()
|
|
62
|
-
if hasattr(server, '_test_task') and server._test_task:
|
|
63
|
-
server._test_task.cancel()
|
|
64
138
|
|
|
65
139
|
# ── WebSocket endpoint ──
|
|
66
140
|
|
|
@@ -70,12 +144,23 @@ class EventHubServer:
|
|
|
70
144
|
mid_hint = ws.query_params.get("id", "")
|
|
71
145
|
module_id = await server._verify_token(token, mid_hint)
|
|
72
146
|
if not module_id:
|
|
147
|
+
# Must accept before close — Starlette drops TCP without close frame otherwise,
|
|
148
|
+
# causing websockets 15.x clients to get "no close frame received or sent" errors.
|
|
149
|
+
await ws.accept()
|
|
150
|
+
print(f"[event_hub] 认证失败: token={token[:8]}... hint={mid_hint}")
|
|
73
151
|
await ws.close(code=4001, reason="Authentication failed")
|
|
74
152
|
return
|
|
75
153
|
|
|
76
154
|
await ws.accept()
|
|
77
155
|
server.hub.add_connection(module_id, ws)
|
|
78
156
|
|
|
157
|
+
# Launcher bootstrap: first connection triggers module.ready + Registry registration
|
|
158
|
+
if (module_id == "launcher"
|
|
159
|
+
and not server._launcher_connected
|
|
160
|
+
and server.launcher_ws_token
|
|
161
|
+
and token == server.launcher_ws_token):
|
|
162
|
+
await server._on_launcher_connected(ws)
|
|
163
|
+
|
|
79
164
|
try:
|
|
80
165
|
while True:
|
|
81
166
|
raw = await ws.receive_text()
|
|
@@ -104,10 +189,9 @@ class EventHubServer:
|
|
|
104
189
|
async def _timer_loop(self):
|
|
105
190
|
while True:
|
|
106
191
|
await asyncio.sleep(30)
|
|
107
|
-
# Dedup cleanup (offload to thread to avoid blocking event loop)
|
|
108
192
|
await asyncio.get_event_loop().run_in_executor(None, self.hub.dedup.cleanup)
|
|
109
|
-
|
|
110
|
-
|
|
193
|
+
if self._registered_to_registry:
|
|
194
|
+
await self._heartbeat()
|
|
111
195
|
|
|
112
196
|
async def _heartbeat(self):
|
|
113
197
|
try:
|
|
@@ -120,19 +204,3 @@ class EventHubServer:
|
|
|
120
204
|
)
|
|
121
205
|
except Exception:
|
|
122
206
|
pass
|
|
123
|
-
|
|
124
|
-
async def _test_event_loop(self):
|
|
125
|
-
"""Publish a test event every 5 seconds via internal routing."""
|
|
126
|
-
import uuid
|
|
127
|
-
from datetime import datetime, timezone
|
|
128
|
-
while True:
|
|
129
|
-
await asyncio.sleep(5)
|
|
130
|
-
msg = {
|
|
131
|
-
"type": "event",
|
|
132
|
-
"event_id": str(uuid.uuid4()),
|
|
133
|
-
"event": "event_hub.test",
|
|
134
|
-
"source": "event_hub",
|
|
135
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
136
|
-
"data": {"message": "heartbeat from event_hub"},
|
|
137
|
-
}
|
|
138
|
-
await self.hub._route_event("event_hub", msg)
|