@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.
Files changed (3) hide show
  1. package/cli.js +112 -58
  2. package/launcher/entry.py +48 -1
  3. 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
- const child = spawn(
256
- python,
257
- [path.join(versionDir, 'main.py'), ...pythonArgs],
258
- { cwd: versionDir, env, stdio: 'inherit' }
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.on('error', err => {
262
- console.error(`[kite] 启动 Python 失败: ${err.message}`);
263
- console.error(`[kite] 请确认 Python 3.8+ 已安装`);
264
- console.error(`[kite] 或运行: npm install 重新准备环境`);
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
- markEnvFailed('python_launch_failed');
268
- process.exit(1);
269
- });
270
-
271
- child.on('exit', (code, signal) => {
272
- // 如果是异常退出,检查是否是环境问题
273
- if (code !== 0 && code !== null) {
274
- console.error(`[kite] Python 进程异常退出,退出码: ${code}`);
275
-
276
- // 读取 Kernel 日志,检查是否是依赖问题
277
- // KITE_INSTANCE_DIR Launcher 设置,格式为 ~/.kite/workspace/Kite
278
- const workspaceDir = path.join(kiteHome, 'workspace');
279
- const instanceDir = path.join(workspaceDir, 'Kite');
280
- const kernelLog = path.join(instanceDir, 'kernel', 'log', 'latest.log');
281
-
282
- let isDependencyIssue = false;
283
-
284
- if (fs.existsSync(kernelLog)) {
285
- try {
286
- const log = fs.readFileSync(kernelLog, 'utf-8');
287
-
288
- // 检测依赖相关错误
289
- const depErrors = [
290
- 'ImportError',
291
- 'ModuleNotFoundError',
292
- 'not installed',
293
- 'cannot parse module.md',
294
- 'No module named',
295
- 'Failed to import'
296
- ];
297
-
298
- for (const errorPattern of depErrors) {
299
- if (log.includes(errorPattern)) {
300
- isDependencyIssue = true;
301
- console.error(`[kite] 检测到依赖问题: ${errorPattern}`);
302
- break;
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
- if (isDependencyIssue || code === 1 || code === 127) {
312
- console.error(`[kite] 下次启动将重新检查环境`);
313
- markEnvFailed(isDependencyIssue ? 'dependency_import_error' : 'python_runtime_error');
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
- process.exit(code ?? 0);
318
- });
370
+ process.exit(code ?? 0);
371
+ });
319
372
 
320
- for (const sig of ['SIGINT', 'SIGTERM']) {
321
- process.on(sig, () => child.kill(sig));
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentunion/kite",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Kite framework launcher — start Kite from anywhere",
5
5
  "bin": {
6
6
  "kite": "./cli.js"