@lightcone-ai/daemon 0.9.10 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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}`);
@@ -366,6 +369,21 @@ export class AgentManager {
366
369
  this._flushPending(key, connection);
367
370
  }
368
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
+
369
387
  _stopAgent(agentId, teamId, connection) {
370
388
  const key = this._key(agentId, teamId);
371
389
  const agent = this.agents.get(key);
@@ -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
+ }