@agentunion/kite 1.6.3 → 1.6.5
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 +112 -58
- package/launcher/entry.py +48 -1
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -55,6 +55,8 @@ const args = process.argv.slice(2);
|
|
|
55
55
|
const pythonArgs = [];
|
|
56
56
|
let cwdOverride = null;
|
|
57
57
|
let useVersion = version;
|
|
58
|
+
let isDaemon = false;
|
|
59
|
+
let isStop = false;
|
|
58
60
|
|
|
59
61
|
for (let i = 0; i < args.length; i++) {
|
|
60
62
|
if (args[i] === '--cwd' && args[i + 1]) {
|
|
@@ -62,6 +64,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
62
64
|
} else if (args[i] === '--use' && args[i + 1]) {
|
|
63
65
|
useVersion = args[++i];
|
|
64
66
|
} else {
|
|
67
|
+
if (args[i] === '--daemon' || args[i] === '-d') isDaemon = true;
|
|
68
|
+
if (args[i] === '--stop') isStop = true;
|
|
65
69
|
pythonArgs.push(args[i]);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -252,71 +256,121 @@ if (!fs.existsSync(python)) {
|
|
|
252
256
|
|
|
253
257
|
console.log(`[kite] Python: ${python}`);
|
|
254
258
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
259
|
+
// --stop: 同步执行,等待结果后退出
|
|
260
|
+
if (isStop) {
|
|
261
|
+
const child = spawn(
|
|
262
|
+
python,
|
|
263
|
+
[path.join(versionDir, 'main.py'), ...pythonArgs],
|
|
264
|
+
{ cwd: versionDir, env, stdio: 'inherit' }
|
|
265
|
+
);
|
|
266
|
+
child.on('error', err => {
|
|
267
|
+
console.error(`[kite] 执行失败: ${err.message}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
|
270
|
+
child.on('exit', code => process.exit(code ?? 0));
|
|
271
|
+
|
|
272
|
+
// --daemon / -d: 后台启动,Node 立即退出
|
|
273
|
+
} else if (isDaemon) {
|
|
274
|
+
// 日志输出到实例目录
|
|
275
|
+
const basename = path.basename(kiteCwd);
|
|
276
|
+
const instanceDir = path.join(kiteHome, 'workspace', basename || 'default');
|
|
277
|
+
const logDir = path.join(instanceDir, 'launcher', 'log');
|
|
278
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
279
|
+
const daemonLog = path.join(logDir, 'daemon.log');
|
|
280
|
+
const logFd = fs.openSync(daemonLog, 'w');
|
|
281
|
+
|
|
282
|
+
const spawnOpts = {
|
|
283
|
+
cwd: versionDir,
|
|
284
|
+
env,
|
|
285
|
+
stdio: ['ignore', logFd, logFd],
|
|
286
|
+
detached: true,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Windows: 额外设置无窗口标志
|
|
290
|
+
if (process.platform === 'win32') {
|
|
291
|
+
spawnOpts.windowsHide = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 透传给 Python 时去掉 --daemon / -d,由 Node 层处理脱离
|
|
295
|
+
const childArgs = pythonArgs.filter(a => a !== '--daemon' && a !== '-d');
|
|
296
|
+
const child = spawn(
|
|
297
|
+
python,
|
|
298
|
+
[path.join(versionDir, 'main.py'), ...childArgs],
|
|
299
|
+
spawnOpts
|
|
300
|
+
);
|
|
260
301
|
|
|
261
|
-
child.
|
|
262
|
-
console.
|
|
263
|
-
console.
|
|
264
|
-
console.
|
|
302
|
+
child.unref();
|
|
303
|
+
console.log(`[kite] 后台启动成功 (PID: ${child.pid})`);
|
|
304
|
+
console.log(`[kite] 日志: ${daemonLog}`);
|
|
305
|
+
console.log(`[kite] 停止: kite --stop`);
|
|
306
|
+
fs.closeSync(logFd);
|
|
307
|
+
process.exit(0);
|
|
265
308
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
309
|
+
// 正常前台启动
|
|
310
|
+
} else {
|
|
311
|
+
const child = spawn(
|
|
312
|
+
python,
|
|
313
|
+
[path.join(versionDir, 'main.py'), ...pythonArgs],
|
|
314
|
+
{ cwd: versionDir, env, stdio: 'inherit' }
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
child.on('error', err => {
|
|
318
|
+
console.error(`[kite] 启动 Python 失败: ${err.message}`);
|
|
319
|
+
console.error(`[kite] 请确认 Python 3.8+ 已安装`);
|
|
320
|
+
console.error(`[kite] 或运行: npm install 重新准备环境`);
|
|
321
|
+
|
|
322
|
+
// 标记环境失败
|
|
323
|
+
markEnvFailed('python_launch_failed');
|
|
324
|
+
process.exit(1);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
child.on('exit', (code, signal) => {
|
|
328
|
+
// 如果是异常退出,检查是否是环境问题
|
|
329
|
+
if (code !== 0 && code !== null) {
|
|
330
|
+
console.error(`[kite] Python 进程异常退出,退出码: ${code}`);
|
|
331
|
+
|
|
332
|
+
// 读取 Kernel 日志,检查是否是依赖问题
|
|
333
|
+
const workspaceDir = path.join(kiteHome, 'workspace');
|
|
334
|
+
const instanceDir = path.join(workspaceDir, 'Kite');
|
|
335
|
+
const kernelLog = path.join(instanceDir, 'kernel', 'log', 'latest.log');
|
|
336
|
+
|
|
337
|
+
let isDependencyIssue = false;
|
|
338
|
+
|
|
339
|
+
if (fs.existsSync(kernelLog)) {
|
|
340
|
+
try {
|
|
341
|
+
const log = fs.readFileSync(kernelLog, 'utf-8');
|
|
342
|
+
|
|
343
|
+
const depErrors = [
|
|
344
|
+
'ImportError',
|
|
345
|
+
'ModuleNotFoundError',
|
|
346
|
+
'not installed',
|
|
347
|
+
'cannot parse module.md',
|
|
348
|
+
'No module named',
|
|
349
|
+
'Failed to import'
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
for (const errorPattern of depErrors) {
|
|
353
|
+
if (log.includes(errorPattern)) {
|
|
354
|
+
isDependencyIssue = true;
|
|
355
|
+
console.error(`[kite] 检测到依赖问题: ${errorPattern}`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
303
358
|
}
|
|
359
|
+
} catch (e) {
|
|
360
|
+
// 读取日志失败,忽略
|
|
304
361
|
}
|
|
305
|
-
} catch (e) {
|
|
306
|
-
// 读取日志失败,忽略
|
|
307
362
|
}
|
|
308
|
-
}
|
|
309
363
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
364
|
+
if (isDependencyIssue || code === 1 || code === 127) {
|
|
365
|
+
console.error(`[kite] 下次启动将重新检查环境`);
|
|
366
|
+
markEnvFailed(isDependencyIssue ? 'dependency_import_error' : 'python_runtime_error');
|
|
367
|
+
}
|
|
314
368
|
}
|
|
315
|
-
}
|
|
316
369
|
|
|
317
|
-
|
|
318
|
-
});
|
|
370
|
+
process.exit(code ?? 0);
|
|
371
|
+
});
|
|
319
372
|
|
|
320
|
-
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
321
|
-
|
|
373
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
374
|
+
process.on(sig, () => child.kill(sig));
|
|
375
|
+
}
|
|
322
376
|
}
|
package/launcher/entry.py
CHANGED
|
@@ -356,12 +356,59 @@ class Launcher:
|
|
|
356
356
|
threading.Thread(target=_force, daemon=True).start()
|
|
357
357
|
|
|
358
358
|
def _setup_unix_signals(self):
|
|
359
|
-
"""Register SIGTERM/SIGINT handlers on Linux/macOS."""
|
|
359
|
+
"""Register SIGTERM/SIGINT handlers + 'q' key listener on Linux/macOS."""
|
|
360
360
|
def _handler(signum, frame):
|
|
361
361
|
self._request_shutdown(f"收到信号 {signum},正在关闭...")
|
|
362
362
|
signal.signal(signal.SIGTERM, _handler)
|
|
363
363
|
signal.signal(signal.SIGINT, _handler)
|
|
364
364
|
|
|
365
|
+
# 'q' key listener: 使用 termios 将 stdin 设为 raw 模式
|
|
366
|
+
def _listen_unix():
|
|
367
|
+
try:
|
|
368
|
+
import tty
|
|
369
|
+
import termios
|
|
370
|
+
import select
|
|
371
|
+
except ImportError:
|
|
372
|
+
return # 无 tty 支持(如 daemon 模式),跳过
|
|
373
|
+
|
|
374
|
+
fd = sys.stdin.fileno()
|
|
375
|
+
try:
|
|
376
|
+
old_settings = termios.tcgetattr(fd)
|
|
377
|
+
except termios.error:
|
|
378
|
+
return # stdin 不是终端(如重定向、daemon),跳过
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
tty.setcbreak(fd) # cbreak 模式:单字符读取,但保留 Ctrl+C 信号
|
|
382
|
+
while not self._thread_shutdown.is_set():
|
|
383
|
+
# select 超时 0.2s,避免忙等
|
|
384
|
+
rlist, _, _ = select.select([sys.stdin], [], [], 0.2)
|
|
385
|
+
if rlist:
|
|
386
|
+
ch = sys.stdin.read(1)
|
|
387
|
+
if ch == '\x1b': # ESC
|
|
388
|
+
print("[launcher] ESC 强制退出")
|
|
389
|
+
try:
|
|
390
|
+
if self._ws and self._loop:
|
|
391
|
+
import concurrent.futures
|
|
392
|
+
fut = asyncio.run_coroutine_threadsafe(
|
|
393
|
+
self._publish_event("module.exiting", {
|
|
394
|
+
"module_id": "launcher",
|
|
395
|
+
"reason": "ESC exit",
|
|
396
|
+
"action": "none",
|
|
397
|
+
}),
|
|
398
|
+
self._loop,
|
|
399
|
+
)
|
|
400
|
+
fut.result(timeout=1)
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
os._exit(0)
|
|
404
|
+
elif ch in ('q', 'Q'):
|
|
405
|
+
self._request_shutdown("收到退出请求,正在关闭...")
|
|
406
|
+
return
|
|
407
|
+
finally:
|
|
408
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
409
|
+
|
|
410
|
+
threading.Thread(target=_listen_unix, daemon=True).start()
|
|
411
|
+
|
|
365
412
|
def _setup_windows_exit(self):
|
|
366
413
|
"""SetConsoleCtrlHandler for Ctrl+C + daemon thread for 'q' key.
|
|
367
414
|
|