@lightcone-ai/daemon 0.15.59 → 0.15.61
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 +72 -0
- package/src/browser-login.js +84 -7
- package/src/connection.js +15 -2
- package/src/index.js +1 -0
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import {
|
|
4
4
|
existsSync,
|
|
5
|
+
cpSync,
|
|
5
6
|
mkdirSync,
|
|
6
7
|
readFileSync,
|
|
7
8
|
readdirSync,
|
|
@@ -179,6 +180,48 @@ export class AgentManager {
|
|
|
179
180
|
return `${workspaceId ?? ''}:${agentId}`;
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Snapshot of agents this daemon process is currently managing. Sent to the
|
|
185
|
+
* server alongside 'ready' so the server can reconcile lifecycle state
|
|
186
|
+
* after a reconnect — agents previously on this machine but absent from
|
|
187
|
+
* the inventory get reset from stale `crashed unreachable` back to a clean
|
|
188
|
+
* `standby reachable available` baseline (machine is healthy, agent will
|
|
189
|
+
* be spawned on demand).
|
|
190
|
+
*
|
|
191
|
+
* Each entry mirrors the daemon-internal agent state machine:
|
|
192
|
+
* - 'starting': spawn in progress (entry exists in this.starting)
|
|
193
|
+
* - 'running': child process is alive (this.agents has it)
|
|
194
|
+
* The server treats whatever is NOT in this list as 'standby' (spawnable
|
|
195
|
+
* but not currently running on this machine).
|
|
196
|
+
*/
|
|
197
|
+
getAgentInventory() {
|
|
198
|
+
const inventory = [];
|
|
199
|
+
for (const key of this.starting) {
|
|
200
|
+
const colonIdx = key.indexOf(':');
|
|
201
|
+
const workspaceId = colonIdx === -1 ? null : (key.slice(0, colonIdx) || null);
|
|
202
|
+
const agentId = colonIdx === -1 ? key : key.slice(colonIdx + 1);
|
|
203
|
+
inventory.push({
|
|
204
|
+
agentId,
|
|
205
|
+
workspaceId,
|
|
206
|
+
runtimeState: 'starting',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
for (const [key, agent] of this.agents.entries()) {
|
|
210
|
+
const colonIdx = key.indexOf(':');
|
|
211
|
+
const workspaceId = agent.workspaceId
|
|
212
|
+
?? (colonIdx === -1 ? null : (key.slice(0, colonIdx) || null));
|
|
213
|
+
const agentId = agent.agentId
|
|
214
|
+
?? (colonIdx === -1 ? key : key.slice(colonIdx + 1));
|
|
215
|
+
inventory.push({
|
|
216
|
+
agentId,
|
|
217
|
+
workspaceId,
|
|
218
|
+
runtimeState: 'running',
|
|
219
|
+
sessionId: agent.sessionId ?? null,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return inventory;
|
|
223
|
+
}
|
|
224
|
+
|
|
182
225
|
handle(msg, connection) {
|
|
183
226
|
switch (msg.type) {
|
|
184
227
|
case 'agent:start': return this._startAgent(msg, connection);
|
|
@@ -188,6 +231,7 @@ export class AgentManager {
|
|
|
188
231
|
case 'agent:reprobe': return this._reprobeCapability(msg, connection);
|
|
189
232
|
case 'publish:job': return this._handlePublishJob(msg, connection);
|
|
190
233
|
case 'browser:start_login': return this._startBrowserLogin(msg, connection);
|
|
234
|
+
case 'browser:bind_profile': return this._bindBrowserProfile(msg, connection);
|
|
191
235
|
case 'browser:stop_login': return this._stopBrowserLogin(msg);
|
|
192
236
|
case 'policy_invalidate': return this._handlePolicyInvalidate(msg, connection);
|
|
193
237
|
case 'credential_revoked': return this._handleCredentialRevoked(msg, connection);
|
|
@@ -1464,6 +1508,34 @@ export class AgentManager {
|
|
|
1464
1508
|
}
|
|
1465
1509
|
}
|
|
1466
1510
|
|
|
1511
|
+
_bindBrowserProfile(msg, connection) {
|
|
1512
|
+
const platform = String(msg.platform ?? 'xhs').trim() || 'xhs';
|
|
1513
|
+
const credentialId = String(msg.credentialId ?? '').trim();
|
|
1514
|
+
const sourceProfileDir = String(msg.sourceProfileDir ?? '').trim();
|
|
1515
|
+
if (!credentialId || !sourceProfileDir) {
|
|
1516
|
+
connection.send({
|
|
1517
|
+
type: 'browser:login_error',
|
|
1518
|
+
platform,
|
|
1519
|
+
error: 'browser profile bind missing credentialId/sourceProfileDir',
|
|
1520
|
+
});
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const targetProfileDir = path.join(homedir(), '.lightcone', 'chrome-profiles', `cred-${credentialId}`);
|
|
1524
|
+
try {
|
|
1525
|
+
if (!existsSync(sourceProfileDir)) {
|
|
1526
|
+
throw new Error(`source profile does not exist: ${sourceProfileDir}`);
|
|
1527
|
+
}
|
|
1528
|
+
rmSync(targetProfileDir, { recursive: true, force: true });
|
|
1529
|
+
mkdirSync(path.dirname(targetProfileDir), { recursive: true });
|
|
1530
|
+
cpSync(sourceProfileDir, targetProfileDir, { recursive: true });
|
|
1531
|
+
console.log(`[AgentManager] Bound browser profile platform=${platform} credential=${credentialId}`);
|
|
1532
|
+
connection.send({ type: 'browser:profile_bound', platform, credentialId });
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
console.error(`[AgentManager] Failed to bind browser profile (${platform}):`, err.message);
|
|
1535
|
+
connection.send({ type: 'browser:login_error', platform, error: `profile_bind_failed:${err.message}` });
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1467
1539
|
_stopBrowserLogin(msg) {
|
|
1468
1540
|
const platform = msg.platform ?? 'xhs';
|
|
1469
1541
|
console.log(`[AgentManager] Stopping browser login for platform=${platform}`);
|
package/src/browser-login.js
CHANGED
|
@@ -461,6 +461,77 @@ export class BrowserLoginSession {
|
|
|
461
461
|
return true;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
async _extractAccountIdentity(cookies = []) {
|
|
465
|
+
if (this._platform !== 'xhs') return null;
|
|
466
|
+
const result = await this.send('Runtime.evaluate', {
|
|
467
|
+
expression: `(() => {
|
|
468
|
+
const out = { externalAccountId: '', externalDisplayName: '', externalAvatarUrl: '' };
|
|
469
|
+
const seen = new Set();
|
|
470
|
+
const candidates = [];
|
|
471
|
+
const push = (value, source) => {
|
|
472
|
+
if (value == null) return;
|
|
473
|
+
const text = String(value).trim();
|
|
474
|
+
if (!text || text.length < 3 || text.length > 160 || seen.has(text)) return;
|
|
475
|
+
seen.add(text);
|
|
476
|
+
candidates.push({ value: text, source });
|
|
477
|
+
};
|
|
478
|
+
const visit = (obj, source, depth = 0) => {
|
|
479
|
+
if (!obj || depth > 5) return;
|
|
480
|
+
if (typeof obj === 'string') {
|
|
481
|
+
const trimmed = obj.trim();
|
|
482
|
+
if (!trimmed) return;
|
|
483
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
484
|
+
try { visit(JSON.parse(trimmed), source, depth + 1); } catch {}
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (typeof obj !== 'object') return;
|
|
489
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
490
|
+
const normalizedKey = String(key).toLowerCase();
|
|
491
|
+
if (['userid', 'user_id', 'user id', 'creatorid', 'creator_id', 'redid', 'red_id', 'authorid', 'author_id', 'accountid', 'account_id'].includes(normalizedKey)) {
|
|
492
|
+
push(value, source + ':' + key);
|
|
493
|
+
}
|
|
494
|
+
if (!out.externalDisplayName && ['nickname', 'nick_name', 'name', 'username', 'user_name'].includes(normalizedKey) && typeof value === 'string') {
|
|
495
|
+
out.externalDisplayName = value.trim().slice(0, 80);
|
|
496
|
+
}
|
|
497
|
+
if (!out.externalAvatarUrl && ['avatar', 'avatarurl', 'avatar_url', 'image'].includes(normalizedKey) && typeof value === 'string' && /^https?:\\/\\//.test(value)) {
|
|
498
|
+
out.externalAvatarUrl = value.trim();
|
|
499
|
+
}
|
|
500
|
+
if (value && typeof value === 'object') visit(value, source + ':' + key, depth + 1);
|
|
501
|
+
else if (typeof value === 'string' && value.length < 5000) visit(value, source + ':' + key, depth + 1);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
for (const storage of [localStorage, sessionStorage]) {
|
|
505
|
+
for (let i = 0; i < storage.length; i += 1) {
|
|
506
|
+
const key = storage.key(i);
|
|
507
|
+
const value = storage.getItem(key);
|
|
508
|
+
const lk = String(key || '').toLowerCase();
|
|
509
|
+
if (/(user|account|creator|red)/.test(lk)) {
|
|
510
|
+
push(value, 'storage:' + key);
|
|
511
|
+
visit(value, 'storage:' + key);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const key of ['__INITIAL_STATE__', '__INITIAL_DATA__', '__REDUX_STATE__']) {
|
|
516
|
+
if (window[key]) visit(window[key], 'window:' + key);
|
|
517
|
+
}
|
|
518
|
+
out.externalAccountId = candidates[0]?.value || '';
|
|
519
|
+
out.identitySource = candidates[0]?.source || '';
|
|
520
|
+
return out;
|
|
521
|
+
})()`,
|
|
522
|
+
returnByValue: true,
|
|
523
|
+
}, 5000);
|
|
524
|
+
const value = result?.result?.value ?? {};
|
|
525
|
+
const externalAccountId = String(value.externalAccountId ?? '').trim();
|
|
526
|
+
if (!externalAccountId) return null;
|
|
527
|
+
return {
|
|
528
|
+
externalAccountId,
|
|
529
|
+
externalDisplayName: String(value.externalDisplayName ?? '').trim(),
|
|
530
|
+
externalAvatarUrl: String(value.externalAvatarUrl ?? '').trim(),
|
|
531
|
+
identitySource: String(value.identitySource ?? (value.externalAccountId ? 'page' : 'session_cookie')).trim(),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
464
535
|
_startPolling(connection, baselineSession) {
|
|
465
536
|
let _screenshotInProgress = false;
|
|
466
537
|
this._screenshotTimer = setInterval(async () => {
|
|
@@ -485,10 +556,11 @@ export class BrowserLoginSession {
|
|
|
485
556
|
try {
|
|
486
557
|
const cookieResult = await this.send('Network.getAllCookies', {});
|
|
487
558
|
const baseDomain = new URL(this._config.loginUrl).hostname.split('.').slice(-2).join('.');
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
559
|
+
const cookies = (cookieResult.cookies ?? []).filter(c =>
|
|
560
|
+
c.domain.includes(baseDomain)
|
|
561
|
+
);
|
|
562
|
+
var accountIdentity = await this._extractAccountIdentity(cookies);
|
|
563
|
+
writeFileSync(path.join(this._profileDir, 'cookies.json'), JSON.stringify(cookies));
|
|
492
564
|
console.log(`[BrowserLogin][${this._platform}] Saved ${cookies.length} cookies to cookies.json`);
|
|
493
565
|
if (this._platform === 'wechat_mp') {
|
|
494
566
|
const hrefResult = await this.send('Runtime.evaluate', {
|
|
@@ -515,9 +587,14 @@ export class BrowserLoginSession {
|
|
|
515
587
|
closePublisherSession(this._platform);
|
|
516
588
|
console.log(`[BrowserLogin][${this._platform}] Closed stale publisher Chrome session after re-login`);
|
|
517
589
|
} catch {}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
590
|
+
await this.close();
|
|
591
|
+
connection.send({
|
|
592
|
+
type: 'browser:login_complete',
|
|
593
|
+
platform: this._platform,
|
|
594
|
+
profileDir: this._profileDir,
|
|
595
|
+
accountIdentity,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
521
598
|
} catch (err) {
|
|
522
599
|
console.error(`[BrowserLogin][${this._platform}] Login check error:`, err.message);
|
|
523
600
|
}
|
package/src/connection.js
CHANGED
|
@@ -45,10 +45,18 @@ function parseOptionalDeviceHints() {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export class DaemonConnection {
|
|
48
|
-
constructor({ serverUrl, machineApiKey, onMessage }) {
|
|
48
|
+
constructor({ serverUrl, machineApiKey, onMessage, getAgentInventory }) {
|
|
49
|
+
if (typeof getAgentInventory !== 'function') {
|
|
50
|
+
throw new TypeError('DaemonConnection: getAgentInventory callback is required');
|
|
51
|
+
}
|
|
49
52
|
this.serverUrl = serverUrl.replace(/^http/, 'ws');
|
|
50
53
|
this.machineApiKey = machineApiKey;
|
|
51
54
|
this.onMessage = onMessage;
|
|
55
|
+
// Snapshot of agents this daemon is currently managing, sent at 'ready'
|
|
56
|
+
// time. The server uses it to reconcile stale lifecycle state after
|
|
57
|
+
// reconnects (see src/daemon/index.js 'ready' handler — agents on this
|
|
58
|
+
// machine but missing from inventory get reset from stale crashed→standby).
|
|
59
|
+
this.getAgentInventory = getAgentInventory;
|
|
52
60
|
this.ws = null;
|
|
53
61
|
this.reconnectDelay = RECONNECT_INITIAL;
|
|
54
62
|
this.stopped = false;
|
|
@@ -111,7 +119,11 @@ export class DaemonConnection {
|
|
|
111
119
|
...parseOptionalDeviceHints(),
|
|
112
120
|
};
|
|
113
121
|
|
|
114
|
-
|
|
122
|
+
const agentInventory = this.getAgentInventory();
|
|
123
|
+
console.log(
|
|
124
|
+
`[Connection] Ready — host=${hostname} runtimes=[${runtimes.join(',')}] v${DAEMON_VERSION}`
|
|
125
|
+
+ ` inventory=${agentInventory.length}`
|
|
126
|
+
);
|
|
115
127
|
this.send({
|
|
116
128
|
type: 'ready',
|
|
117
129
|
hostname,
|
|
@@ -119,6 +131,7 @@ export class DaemonConnection {
|
|
|
119
131
|
runtimes,
|
|
120
132
|
daemonVersion: DAEMON_VERSION,
|
|
121
133
|
deviceHints,
|
|
134
|
+
agentInventory,
|
|
122
135
|
});
|
|
123
136
|
}
|
|
124
137
|
|
package/src/index.js
CHANGED