@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 +1 -1
- package/src/agent-manager.js +4 -3
- package/src/browser-login.js +81 -36
- package/src/index.js +16 -2
- package/src/profile-lock.js +39 -3
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -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) {
|
package/src/browser-login.js
CHANGED
|
@@ -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
|
|
231
|
-
|
|
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.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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(); });
|
package/src/profile-lock.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|