@lightcone-ai/daemon 0.9.61 → 0.9.63
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 +56 -20
- 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
|
@@ -23,13 +23,29 @@ export const PLATFORM_CONFIGS = {
|
|
|
23
23
|
qrTabSelector: ['.qrcode-box', '[class*="qrcode"]', '[class*="scan-login"]', '[class*="qr-login"]'],
|
|
24
24
|
qrFallbackScript: `
|
|
25
25
|
const loginBox = document.querySelector('.login-box-container') || document.body;
|
|
26
|
-
const
|
|
27
|
-
const
|
|
26
|
+
const boxRect = loginBox.getBoundingClientRect();
|
|
27
|
+
const visible = (e) => {
|
|
28
28
|
const r = e.getBoundingClientRect();
|
|
29
29
|
const cs = getComputedStyle(e);
|
|
30
|
-
return r.width >=
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
return r.width >= 35 && r.width <= 110
|
|
31
|
+
&& r.height >= 35 && r.height <= 110
|
|
32
|
+
&& r.left >= boxRect.left
|
|
33
|
+
&& r.top >= boxRect.top
|
|
34
|
+
&& r.right <= boxRect.right + 4
|
|
35
|
+
&& r.bottom <= boxRect.bottom + 4
|
|
36
|
+
&& cs.display !== 'none'
|
|
37
|
+
&& cs.visibility !== 'hidden';
|
|
38
|
+
};
|
|
39
|
+
const elements = [...loginBox.querySelectorAll('img, canvas, svg, [role="img"], div, span, button')].filter(visible);
|
|
40
|
+
const corner = elements
|
|
41
|
+
.map(e => ({ e, r: e.getBoundingClientRect() }))
|
|
42
|
+
.filter(({ r }) => r.top <= boxRect.top + 120 && r.right >= boxRect.right - 140)
|
|
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
|
+
}
|
|
33
49
|
return null;
|
|
34
50
|
`,
|
|
35
51
|
getSessionValue: (cookies) =>
|
|
@@ -119,6 +135,7 @@ export class BrowserLoginSession {
|
|
|
119
135
|
this._loginCheckTimer = null;
|
|
120
136
|
this._profileDir = null;
|
|
121
137
|
this._profileLock = null;
|
|
138
|
+
this._closing = null;
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
async start(connection, userId) {
|
|
@@ -212,7 +229,7 @@ export class BrowserLoginSession {
|
|
|
212
229
|
if (this._config.qrTabSelector) {
|
|
213
230
|
try {
|
|
214
231
|
const selectors = this._config.qrTabSelector;
|
|
215
|
-
await this.send('Runtime.evaluate', {
|
|
232
|
+
const qrResult = await this.send('Runtime.evaluate', {
|
|
216
233
|
expression: `(function() {
|
|
217
234
|
const sels = ${JSON.stringify(selectors)};
|
|
218
235
|
for (const s of sels) {
|
|
@@ -228,8 +245,11 @@ export class BrowserLoginSession {
|
|
|
228
245
|
})()`,
|
|
229
246
|
returnByValue: true,
|
|
230
247
|
});
|
|
248
|
+
console.log(`[BrowserLogin][${this._platform}] QR switch result: ${qrResult.result?.value ?? 'not-found'}`);
|
|
231
249
|
await sleep(1000);
|
|
232
|
-
} catch {
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.error(`[BrowserLogin][${this._platform}] QR switch failed: ${err.message}`);
|
|
252
|
+
}
|
|
233
253
|
}
|
|
234
254
|
|
|
235
255
|
// Record baseline session cookie value.
|
|
@@ -325,7 +345,7 @@ export class BrowserLoginSession {
|
|
|
325
345
|
console.error(`[BrowserLogin][${this._platform}] Failed to save cookies: ${err.message}`);
|
|
326
346
|
}
|
|
327
347
|
connection.send({ type: 'browser:login_complete', platform: this._platform, profileDir: this._profileDir });
|
|
328
|
-
this.close();
|
|
348
|
+
await this.close();
|
|
329
349
|
}
|
|
330
350
|
} catch (err) {
|
|
331
351
|
console.error(`[BrowserLogin][${this._platform}] Login check error:`, err.message);
|
|
@@ -348,13 +368,17 @@ export class BrowserLoginSession {
|
|
|
348
368
|
}
|
|
349
369
|
|
|
350
370
|
async close() {
|
|
351
|
-
this.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
371
|
+
if (this._closing) return this._closing;
|
|
372
|
+
this._closing = (async () => {
|
|
373
|
+
this._stopTimers();
|
|
374
|
+
// Use CDP Browser.close for graceful shutdown so Chrome flushes cookies to disk
|
|
375
|
+
try { await this.send('Browser.close', {}, 3000); } catch {}
|
|
376
|
+
await sleep(1000);
|
|
377
|
+
if (this._ws) { try { this._ws.close(); } catch {} this._ws = null; }
|
|
378
|
+
if (this._proc) { try { this._proc.kill('SIGKILL'); } catch {} this._proc = null; }
|
|
379
|
+
if (this._profileLock) { this._profileLock.release(); this._profileLock = null; }
|
|
380
|
+
})();
|
|
381
|
+
return this._closing;
|
|
358
382
|
}
|
|
359
383
|
}
|
|
360
384
|
|
|
@@ -366,14 +390,26 @@ export function getSession(platform) { return _sessions.get(platform) ?? null; }
|
|
|
366
390
|
|
|
367
391
|
export async function startSession(platform, connection, userId) {
|
|
368
392
|
const existing = _sessions.get(platform);
|
|
369
|
-
if (existing) { existing.close(); _sessions.delete(platform); }
|
|
393
|
+
if (existing) { await existing.close(); _sessions.delete(platform); }
|
|
370
394
|
const session = new BrowserLoginSession(platform);
|
|
371
395
|
_sessions.set(platform, session);
|
|
372
|
-
|
|
373
|
-
|
|
396
|
+
try {
|
|
397
|
+
await session.start(connection, userId);
|
|
398
|
+
return session;
|
|
399
|
+
} catch (err) {
|
|
400
|
+
_sessions.delete(platform);
|
|
401
|
+
await session.close();
|
|
402
|
+
throw err;
|
|
403
|
+
}
|
|
374
404
|
}
|
|
375
405
|
|
|
376
|
-
export function stopSession(platform) {
|
|
406
|
+
export async function stopSession(platform) {
|
|
377
407
|
const session = _sessions.get(platform);
|
|
378
|
-
if (session) { session.close(); _sessions.delete(platform); }
|
|
408
|
+
if (session) { await session.close(); _sessions.delete(platform); }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function stopAllSessions() {
|
|
412
|
+
const sessions = [..._sessions.values()];
|
|
413
|
+
_sessions.clear();
|
|
414
|
+
await Promise.allSettled(sessions.map(session => session.close()));
|
|
379
415
|
}
|
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 {
|