@lightcone-ai/daemon 0.9.62 → 0.9.64

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.62",
3
+ "version": "0.9.64",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import { buildSystemPrompt } from './drivers/claude.js';
6
6
  import { buildCodexSpawn, buildCodexSystemPrompt, parseCodexLine } from './drivers/codex.js';
7
7
  import { buildKimiSpawn, buildKimiInitMessages, parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
8
- import { startSession, stopSession } from './browser-login.js';
8
+ import { startSession, stopSession, stopAllSessions } from './browser-login.js';
9
9
  import { buildSkillMcpServers } from './mcp-config.js';
10
10
 
11
11
  export class AgentManager {
@@ -35,11 +35,12 @@ export class AgentManager {
35
35
  }
36
36
  }
37
37
 
38
- stopAll() {
38
+ async stopAll() {
39
39
  for (const [, agent] of this.agents) {
40
40
  if (agent.proc) agent.proc.kill();
41
41
  }
42
42
  this.agents.clear();
43
+ await stopAllSessions();
43
44
  }
44
45
 
45
46
  // ── private ───────────────────────────────────────────────────────────────
@@ -355,7 +356,7 @@ export class AgentManager {
355
356
  _stopBrowserLogin(msg) {
356
357
  const platform = msg.platform ?? 'xhs';
357
358
  console.log(`[AgentManager] Stopping browser login for platform=${platform}`);
358
- stopSession(platform);
359
+ return stopSession(platform);
359
360
  }
360
361
 
361
362
  _stopAgent(agentId, teamId, connection) {
@@ -41,11 +41,7 @@ export const PLATFORM_CONFIGS = {
41
41
  .map(e => ({ e, r: e.getBoundingClientRect() }))
42
42
  .filter(({ r }) => r.top <= boxRect.top + 120 && r.right >= boxRect.right - 140)
43
43
  .sort((a, b) => (b.r.right + boxRect.top - b.r.top) - (a.r.right + boxRect.top - a.r.top))[0]?.e;
44
- if (corner) {
45
- const target = corner.closest('button, [role="button"], a, div') || corner;
46
- target.click();
47
- return 'corner-qr-icon';
48
- }
44
+ if (corner) return { via: 'corner-qr-icon', rect: corner.getBoundingClientRect().toJSON() };
49
45
  return null;
50
46
  `,
51
47
  getSessionValue: (cookies) =>
@@ -135,6 +131,7 @@ export class BrowserLoginSession {
135
131
  this._loginCheckTimer = null;
136
132
  this._profileDir = null;
137
133
  this._profileLock = null;
134
+ this._closing = null;
138
135
  }
139
136
 
140
137
  async start(connection, userId) {
@@ -227,24 +224,8 @@ export class BrowserLoginSession {
227
224
  // Try to click the QR scan login tab if present (some platforms default to password login)
228
225
  if (this._config.qrTabSelector) {
229
226
  try {
230
- const selectors = this._config.qrTabSelector;
231
- const qrResult = await this.send('Runtime.evaluate', {
232
- expression: `(function() {
233
- const sels = ${JSON.stringify(selectors)};
234
- for (const s of sels) {
235
- const el = document.querySelector(s);
236
- if (el) { el.click(); return s; }
237
- }
238
- // Also try text-based search
239
- const all = [...document.querySelectorAll('a,button,span,div')];
240
- const el = all.find(e => e.innerText?.trim() === '扫码登录' || e.innerText?.trim() === '二维码登录');
241
- if (el) { el.click(); return 'text:' + el.innerText.trim(); }
242
- ${this._config.qrFallbackScript ?? ''}
243
- return null;
244
- })()`,
245
- returnByValue: true,
246
- });
247
- console.log(`[BrowserLogin][${this._platform}] QR switch result: ${qrResult.result?.value ?? 'not-found'}`);
227
+ const qrResult = await this._switchToQrLogin();
228
+ console.log(`[BrowserLogin][${this._platform}] QR switch result: ${qrResult?.via ?? 'not-found'}`);
248
229
  await sleep(1000);
249
230
  } catch (err) {
250
231
  console.error(`[BrowserLogin][${this._platform}] QR switch failed: ${err.message}`);
@@ -305,6 +286,54 @@ export class BrowserLoginSession {
305
286
  return result.data;
306
287
  }
307
288
 
289
+ async _switchToQrLogin() {
290
+ const selectors = this._config.qrTabSelector ?? [];
291
+ const result = await this.send('Runtime.evaluate', {
292
+ expression: `(function() {
293
+ const visible = (el) => {
294
+ const r = el.getBoundingClientRect();
295
+ const cs = getComputedStyle(el);
296
+ return r.width > 0 && r.height > 0 && cs.display !== 'none' && cs.visibility !== 'hidden';
297
+ };
298
+ const hit = (el, via) => {
299
+ if (!el) return null;
300
+ const target = el.closest('button, [role="button"], a') || el;
301
+ const rect = target.getBoundingClientRect();
302
+ if (rect.width <= 0 || rect.height <= 0) return null;
303
+ return { via, rect: rect.toJSON() };
304
+ };
305
+
306
+ const sels = ${JSON.stringify(selectors)};
307
+ for (const s of sels) {
308
+ const el = document.querySelector(s);
309
+ if (el && visible(el)) return hit(el, s);
310
+ }
311
+
312
+ const textElements = [...document.querySelectorAll('a,button,span,div')].filter(visible);
313
+ const textEl = textElements.find(e => {
314
+ const t = (e.innerText || e.textContent || '').trim();
315
+ return t === '扫码登录' || t === '二维码登录' || t === '扫码';
316
+ });
317
+ const textHit = hit(textEl, textEl ? 'text:' + (textEl.innerText || textEl.textContent || '').trim() : '');
318
+ if (textHit) return textHit;
319
+
320
+ ${this._config.qrFallbackScript ?? 'return null;'}
321
+ })()`,
322
+ returnByValue: true,
323
+ });
324
+
325
+ const hit = result.result?.value;
326
+ const rect = hit?.rect;
327
+ if (!rect) return null;
328
+
329
+ const x = Math.round(rect.left + rect.width / 2);
330
+ const y = Math.round(rect.top + rect.height / 2);
331
+ await this.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y }, 5_000);
332
+ await this.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 }, 5_000);
333
+ await this.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }, 5_000);
334
+ return { via: hit.via, x, y };
335
+ }
336
+
308
337
  async isLoggedIn(baseline) {
309
338
  const result = await this.send('Network.getAllCookies', {});
310
339
  return this._config.isLoggedIn(result.cookies ?? [], baseline);
@@ -344,7 +373,7 @@ export class BrowserLoginSession {
344
373
  console.error(`[BrowserLogin][${this._platform}] Failed to save cookies: ${err.message}`);
345
374
  }
346
375
  connection.send({ type: 'browser:login_complete', platform: this._platform, profileDir: this._profileDir });
347
- this.close();
376
+ await this.close();
348
377
  }
349
378
  } catch (err) {
350
379
  console.error(`[BrowserLogin][${this._platform}] Login check error:`, err.message);
@@ -367,13 +396,17 @@ export class BrowserLoginSession {
367
396
  }
368
397
 
369
398
  async close() {
370
- this._stopTimers();
371
- // Use CDP Browser.close for graceful shutdown so Chrome flushes cookies to disk
372
- try { await this.send('Browser.close', {}, 3000); } catch {}
373
- await sleep(1000);
374
- if (this._ws) { try { this._ws.close(); } catch {} this._ws = null; }
375
- if (this._proc) { try { this._proc.kill('SIGKILL'); } catch {} this._proc = null; }
376
- if (this._profileLock) { this._profileLock.release(); this._profileLock = null; }
399
+ if (this._closing) return this._closing;
400
+ this._closing = (async () => {
401
+ this._stopTimers();
402
+ // Use CDP Browser.close for graceful shutdown so Chrome flushes cookies to disk
403
+ try { await this.send('Browser.close', {}, 3000); } catch {}
404
+ await sleep(1000);
405
+ if (this._ws) { try { this._ws.close(); } catch {} this._ws = null; }
406
+ if (this._proc) { try { this._proc.kill('SIGKILL'); } catch {} this._proc = null; }
407
+ if (this._profileLock) { this._profileLock.release(); this._profileLock = null; }
408
+ })();
409
+ return this._closing;
377
410
  }
378
411
  }
379
412
 
@@ -385,14 +418,26 @@ export function getSession(platform) { return _sessions.get(platform) ?? null; }
385
418
 
386
419
  export async function startSession(platform, connection, userId) {
387
420
  const existing = _sessions.get(platform);
388
- if (existing) { existing.close(); _sessions.delete(platform); }
421
+ if (existing) { await existing.close(); _sessions.delete(platform); }
389
422
  const session = new BrowserLoginSession(platform);
390
423
  _sessions.set(platform, session);
391
- await session.start(connection, userId);
392
- return session;
424
+ try {
425
+ await session.start(connection, userId);
426
+ return session;
427
+ } catch (err) {
428
+ _sessions.delete(platform);
429
+ await session.close();
430
+ throw err;
431
+ }
393
432
  }
394
433
 
395
- export function stopSession(platform) {
434
+ export async function stopSession(platform) {
396
435
  const session = _sessions.get(platform);
397
- if (session) { session.close(); _sessions.delete(platform); }
436
+ if (session) { await session.close(); _sessions.delete(platform); }
437
+ }
438
+
439
+ export async function stopAllSessions() {
440
+ const sessions = [..._sessions.values()];
441
+ _sessions.clear();
442
+ await Promise.allSettled(sessions.map(session => session.close()));
398
443
  }
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ import 'dotenv/config';
3
3
  import { createRequire } from 'module';
4
4
  import { DaemonConnection } from './connection.js';
5
5
  import { AgentManager } from './agent-manager.js';
6
+ import { releaseProfileLocksForProcess } from './profile-lock.js';
6
7
 
7
8
  const { version } = createRequire(import.meta.url)('../package.json');
8
9
 
@@ -42,5 +43,18 @@ const connection = new DaemonConnection({
42
43
 
43
44
  connection.connect();
44
45
 
45
- process.on('SIGINT', () => { connection.stop(); agentManager.stopAll(); process.exit(0); });
46
- process.on('SIGTERM', () => { connection.stop(); agentManager.stopAll(); process.exit(0); });
46
+ let shuttingDown = false;
47
+ async function shutdown(signal) {
48
+ if (shuttingDown) return;
49
+ shuttingDown = true;
50
+ console.log(`[Daemon] Shutting down (${signal})`);
51
+ connection.stop();
52
+ try { await agentManager.stopAll(); } catch (err) { console.error('[Daemon] Shutdown error:', err.message); }
53
+ releaseProfileLocksForProcess();
54
+ process.exit(0);
55
+ }
56
+
57
+ process.on('SIGINT', () => { shutdown('SIGINT'); });
58
+ process.on('SIGTERM', () => { shutdown('SIGTERM'); });
59
+ process.on('SIGHUP', () => { shutdown('SIGHUP'); });
60
+ process.on('exit', () => { releaseProfileLocksForProcess(); });
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync, statSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import path from 'path';
4
+ import { randomUUID } from 'crypto';
4
5
 
5
6
  const LOCK_ROOT = path.join(homedir(), '.lightcone', 'profile-locks');
6
7
  const DEFAULT_STALE_MS = 20 * 60 * 1000;
@@ -37,6 +38,28 @@ function isStale(dir, staleMs) {
37
38
  }
38
39
  }
39
40
 
41
+ function pidIsAlive(pid) {
42
+ if (!Number.isInteger(pid) || pid <= 0) return false;
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ } catch (err) {
47
+ return err.code === 'EPERM';
48
+ }
49
+ }
50
+
51
+ function isAbandoned(dir) {
52
+ const info = lockInfo(dir);
53
+ if (!info?.pid) return false;
54
+ return !pidIsAlive(info.pid);
55
+ }
56
+
57
+ function removeLockDir(dir) {
58
+ try { rmSync(dir, { recursive: true, force: true }); } catch {}
59
+ }
60
+
61
+ const heldLocks = new Map();
62
+
40
63
  export async function acquireProfileLock(platform, profileDir, {
41
64
  owner = 'unknown',
42
65
  timeoutMs = DEFAULT_TIMEOUT_MS,
@@ -50,21 +73,26 @@ export async function acquireProfileLock(platform, profileDir, {
50
73
  while (true) {
51
74
  try {
52
75
  mkdirSync(dir);
76
+ const token = randomUUID();
53
77
  const release = () => {
54
- try { rmSync(dir, { recursive: true, force: true }); } catch {}
78
+ const info = lockInfo(dir);
79
+ heldLocks.delete(dir);
80
+ if (!info || info.token === token || info.pid === process.pid) removeLockDir(dir);
55
81
  };
56
82
  writeFileSync(path.join(dir, 'owner.json'), JSON.stringify({
57
83
  platform,
58
84
  profileDir,
59
85
  owner,
60
86
  pid: process.pid,
87
+ token,
61
88
  createdAt: new Date().toISOString(),
62
89
  }, null, 2));
90
+ heldLocks.set(dir, token);
63
91
  return { release, dir };
64
92
  } catch (err) {
65
93
  if (err.code !== 'EEXIST') throw err;
66
- if (existsSync(dir) && isStale(dir, staleMs)) {
67
- try { rmSync(dir, { recursive: true, force: true }); } catch {}
94
+ if (existsSync(dir) && (isAbandoned(dir) || isStale(dir, staleMs))) {
95
+ removeLockDir(dir);
68
96
  continue;
69
97
  }
70
98
  if (Date.now() >= deadline) {
@@ -76,6 +104,14 @@ export async function acquireProfileLock(platform, profileDir, {
76
104
  }
77
105
  }
78
106
 
107
+ export function releaseProfileLocksForProcess() {
108
+ for (const [dir, token] of heldLocks) {
109
+ const info = lockInfo(dir);
110
+ if (!info || info.token === token || info.pid === process.pid) removeLockDir(dir);
111
+ heldLocks.delete(dir);
112
+ }
113
+ }
114
+
79
115
  export async function withProfileLock(platform, profileDir, options, fn) {
80
116
  const lock = await acquireProfileLock(platform, profileDir, options);
81
117
  try {