@lightcone-ai/daemon 0.9.9 → 0.9.11
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 +20 -0
- package/src/xhs-login.js +202 -0
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -5,6 +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 './xhs-login.js';
|
|
8
9
|
|
|
9
10
|
export class AgentManager {
|
|
10
11
|
constructor({ serverUrl, machineApiKey }) {
|
|
@@ -25,6 +26,8 @@ export class AgentManager {
|
|
|
25
26
|
case 'agent:start': return this._startAgent(msg, connection);
|
|
26
27
|
case 'agent:stop': return this._stopAgent(msg.agentId, msg.teamId, connection);
|
|
27
28
|
case 'agent:deliver': return this._deliverMessage(msg, connection);
|
|
29
|
+
case 'xhs:start_login': return this._startXhsLogin(connection);
|
|
30
|
+
case 'xhs:stop_login': return this._stopXhsLogin();
|
|
28
31
|
case 'ping': return connection.send({ type: 'pong' });
|
|
29
32
|
default:
|
|
30
33
|
console.log(`[AgentManager] Unhandled: ${msg.type}`);
|
|
@@ -233,6 +236,8 @@ export class AgentManager {
|
|
|
233
236
|
return new URL('../../mcp-servers/workspace-migrate/index.js', import.meta.url).pathname;
|
|
234
237
|
if (a === '{platform_mcp_path}')
|
|
235
238
|
return new URL('../../mcp-servers/platform/index.js', import.meta.url).pathname;
|
|
239
|
+
if (a === '{xhs_profile_dir}')
|
|
240
|
+
return path.join(homedir(), '.lightcone', 'chrome-profiles', 'xhs');
|
|
236
241
|
return a;
|
|
237
242
|
});
|
|
238
243
|
const resolvedEnv = {};
|
|
@@ -364,6 +369,21 @@ export class AgentManager {
|
|
|
364
369
|
this._flushPending(key, connection);
|
|
365
370
|
}
|
|
366
371
|
|
|
372
|
+
async _startXhsLogin(connection) {
|
|
373
|
+
console.log('[AgentManager] Starting XHS login session');
|
|
374
|
+
try {
|
|
375
|
+
await startSession(connection);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error('[AgentManager] XHS login start failed:', err.message);
|
|
378
|
+
connection.send({ type: 'xhs:login_error', error: err.message });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_stopXhsLogin() {
|
|
383
|
+
console.log('[AgentManager] Stopping XHS login session');
|
|
384
|
+
stopSession();
|
|
385
|
+
}
|
|
386
|
+
|
|
367
387
|
_stopAgent(agentId, teamId, connection) {
|
|
368
388
|
const key = this._key(agentId, teamId);
|
|
369
389
|
const agent = this.agents.get(key);
|
package/src/xhs-login.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XHS (Xiaohongshu) login session via Chrome CDP.
|
|
3
|
+
* Runs on the daemon machine (VPS) where Chrome is installed.
|
|
4
|
+
* Screenshots are sent back to the server via the daemon WebSocket connection.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import http from 'http';
|
|
10
|
+
import { WebSocket } from 'ws';
|
|
11
|
+
|
|
12
|
+
export const XHS_PROFILE_DIR = path.join(homedir(), '.lightcone', 'chrome-profiles', 'xhs');
|
|
13
|
+
|
|
14
|
+
const CDP_PORT = 9225;
|
|
15
|
+
const CHROME_BIN = process.env.CHROME_BIN ?? '/usr/bin/google-chrome';
|
|
16
|
+
const SCREENSHOT_INTERVAL_MS = 2000;
|
|
17
|
+
const LOGIN_CHECK_INTERVAL_MS = 3000;
|
|
18
|
+
|
|
19
|
+
function httpGet(url) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
http.get(url, (res) => {
|
|
22
|
+
let body = '';
|
|
23
|
+
res.on('data', d => body += d);
|
|
24
|
+
res.on('end', () => resolve(body));
|
|
25
|
+
}).on('error', reject);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise(r => setTimeout(r, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class XhsLoginSession {
|
|
34
|
+
constructor() {
|
|
35
|
+
this._proc = null;
|
|
36
|
+
this._ws = null;
|
|
37
|
+
this._nextId = 1;
|
|
38
|
+
this._pending = new Map();
|
|
39
|
+
this._screenshotTimer = null;
|
|
40
|
+
this._loginCheckTimer = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async start(connection) {
|
|
44
|
+
// Spawn Chrome
|
|
45
|
+
this._proc = spawn(CHROME_BIN, [
|
|
46
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
47
|
+
'--no-sandbox',
|
|
48
|
+
'--disable-dev-shm-usage',
|
|
49
|
+
'--headless=new',
|
|
50
|
+
`--user-data-dir=${XHS_PROFILE_DIR}`,
|
|
51
|
+
'--window-size=800,900',
|
|
52
|
+
'about:blank',
|
|
53
|
+
], {
|
|
54
|
+
stdio: 'ignore',
|
|
55
|
+
detached: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this._proc.on('exit', (code) => {
|
|
59
|
+
console.log(`[XhsLogin] Chrome exited (code=${code})`);
|
|
60
|
+
this._stopTimers();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Poll for CDP readiness
|
|
64
|
+
let ready = false;
|
|
65
|
+
for (let i = 0; i < 20; i++) {
|
|
66
|
+
await sleep(300);
|
|
67
|
+
try {
|
|
68
|
+
await httpGet(`http://localhost:${CDP_PORT}/json/version`);
|
|
69
|
+
ready = true;
|
|
70
|
+
break;
|
|
71
|
+
} catch { /* not ready yet */ }
|
|
72
|
+
}
|
|
73
|
+
if (!ready) throw new Error('Chrome CDP did not become ready in time');
|
|
74
|
+
|
|
75
|
+
// Get WebSocket debugger URL
|
|
76
|
+
const pagesJson = await httpGet(`http://localhost:${CDP_PORT}/json`);
|
|
77
|
+
const pages = JSON.parse(pagesJson);
|
|
78
|
+
const page = pages.find(p => p.type === 'page') ?? pages[0];
|
|
79
|
+
if (!page?.webSocketDebuggerUrl) throw new Error('No page websocket URL from CDP');
|
|
80
|
+
|
|
81
|
+
await this._connect(page.webSocketDebuggerUrl);
|
|
82
|
+
|
|
83
|
+
await this.send('Page.enable', {});
|
|
84
|
+
await this.send('Network.enable', {});
|
|
85
|
+
await this.send('Page.navigate', { url: 'https://www.xiaohongshu.com' });
|
|
86
|
+
|
|
87
|
+
// Start sending screenshots and checking login
|
|
88
|
+
this._startPolling(connection);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_connect(wsUrl) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const ws = new WebSocket(wsUrl);
|
|
94
|
+
ws.on('open', () => { this._ws = ws; resolve(); });
|
|
95
|
+
ws.on('message', (data) => {
|
|
96
|
+
let msg;
|
|
97
|
+
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
98
|
+
if (msg.id != null) {
|
|
99
|
+
const entry = this._pending.get(msg.id);
|
|
100
|
+
if (entry) {
|
|
101
|
+
clearTimeout(entry.timer);
|
|
102
|
+
this._pending.delete(msg.id);
|
|
103
|
+
if (msg.error) entry.reject(new Error(msg.error.message ?? 'CDP error'));
|
|
104
|
+
else entry.resolve(msg.result);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
ws.on('error', reject);
|
|
109
|
+
ws.on('close', () => {
|
|
110
|
+
for (const [, entry] of this._pending) {
|
|
111
|
+
clearTimeout(entry.timer);
|
|
112
|
+
entry.reject(new Error('WebSocket closed'));
|
|
113
|
+
}
|
|
114
|
+
this._pending.clear();
|
|
115
|
+
this._ws = null;
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
send(method, params = {}) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
if (!this._ws) return reject(new Error('WebSocket not connected'));
|
|
123
|
+
const id = this._nextId++;
|
|
124
|
+
const timer = setTimeout(() => {
|
|
125
|
+
this._pending.delete(id);
|
|
126
|
+
reject(new Error(`CDP timeout for ${method}`));
|
|
127
|
+
}, 10_000);
|
|
128
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
129
|
+
this._ws.send(JSON.stringify({ id, method, params }));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async screenshot() {
|
|
134
|
+
const result = await this.send('Page.captureScreenshot', { format: 'jpeg', quality: 70 });
|
|
135
|
+
return result.data; // base64 jpeg
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async isLoggedIn() {
|
|
139
|
+
const result = await this.send('Network.getAllCookies', {});
|
|
140
|
+
const cookies = result.cookies ?? [];
|
|
141
|
+
return cookies.some(c => c.name === 'web_session' && c.value?.length > 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_startPolling(connection) {
|
|
145
|
+
// Send screenshots periodically
|
|
146
|
+
this._screenshotTimer = setInterval(async () => {
|
|
147
|
+
try {
|
|
148
|
+
const screenshot = await this.screenshot();
|
|
149
|
+
connection.send({ type: 'xhs:screenshot', screenshot });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('[XhsLogin] Screenshot error:', err.message);
|
|
152
|
+
}
|
|
153
|
+
}, SCREENSHOT_INTERVAL_MS);
|
|
154
|
+
|
|
155
|
+
// Check login status periodically
|
|
156
|
+
this._loginCheckTimer = setInterval(async () => {
|
|
157
|
+
try {
|
|
158
|
+
const loggedIn = await this.isLoggedIn();
|
|
159
|
+
if (loggedIn) {
|
|
160
|
+
this._stopTimers();
|
|
161
|
+
connection.send({ type: 'xhs:login_complete', profileDir: XHS_PROFILE_DIR });
|
|
162
|
+
this.close();
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error('[XhsLogin] Login check error:', err.message);
|
|
166
|
+
}
|
|
167
|
+
}, LOGIN_CHECK_INTERVAL_MS);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_stopTimers() {
|
|
171
|
+
if (this._screenshotTimer) { clearInterval(this._screenshotTimer); this._screenshotTimer = null; }
|
|
172
|
+
if (this._loginCheckTimer) { clearInterval(this._loginCheckTimer); this._loginCheckTimer = null; }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
close() {
|
|
176
|
+
this._stopTimers();
|
|
177
|
+
if (this._ws) {
|
|
178
|
+
try { this._ws.close(); } catch { /* ignore */ }
|
|
179
|
+
this._ws = null;
|
|
180
|
+
}
|
|
181
|
+
if (this._proc) {
|
|
182
|
+
try { this._proc.kill('SIGKILL'); } catch { /* ignore */ }
|
|
183
|
+
this._proc = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Singleton
|
|
189
|
+
let _session = null;
|
|
190
|
+
|
|
191
|
+
export function getSession() { return _session; }
|
|
192
|
+
|
|
193
|
+
export async function startSession(connection) {
|
|
194
|
+
if (_session) { _session.close(); _session = null; }
|
|
195
|
+
_session = new XhsLoginSession();
|
|
196
|
+
await _session.start(connection);
|
|
197
|
+
return _session;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function stopSession() {
|
|
201
|
+
if (_session) { _session.close(); _session = null; }
|
|
202
|
+
}
|