@lightcone-ai/daemon 0.9.67 → 0.9.69

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.
@@ -4,6 +4,14 @@
4
4
  */
5
5
 
6
6
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
7
+ function randomInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }
8
+
9
+ const PACE_SCALE = Math.max(0.2, Number(process.env.PUBLISHER_PACE_SCALE ?? '1') || 1);
10
+ async function humanPause(minMs, maxMs, label = '') {
11
+ const ms = Math.round(randomInt(minMs, maxMs) * PACE_SCALE);
12
+ if (label) console.error(`[XhsAdapter] pause ${label}: ${ms}ms`);
13
+ await sleep(ms);
14
+ }
7
15
 
8
16
  const REQUIREMENTS = {
9
17
  image_text: {
@@ -70,29 +78,34 @@ export class XhsAdapter {
70
78
  async publishImageText({ title, text, tags = [], images = [] }) {
71
79
  await this._openPublishComposer('image');
72
80
  await this._assertReadyForPublish();
81
+ await humanPause(2500, 5500, 'composer-ready');
73
82
 
74
83
  await this._waitForAny([IMAGE_FILE_INPUT_SELECTOR], 15000);
84
+ await humanPause(900, 2200, 'before-upload');
75
85
 
76
86
  // Upload images first (if any)
77
87
  if (images.length > 0) {
78
88
  await this._uploadFiles(images, 'image');
79
89
  await this._waitForUploadSettled(images.length, 90_000);
90
+ await humanPause(2500, 5500, 'after-upload');
80
91
  }
81
92
 
82
93
  // Fill title
83
94
  if (title) {
84
95
  await this._fillField(TITLE_SELECTOR, title);
96
+ await humanPause(1200, 2800, 'after-title');
85
97
  }
86
98
 
87
99
  // Fill content text + tags
88
100
  const fullText = tags.length > 0 ? `${text}\n${tags.map(t => `#${t}`).join(' ')}` : text;
89
101
  await this._fillField(CONTENT_SELECTOR, fullText);
90
102
 
91
- await sleep(1000);
103
+ await humanPause(4500, 9000, 'before-publish-check');
92
104
  await this._assertNoBlockingErrors();
93
105
  await this._assertPublishButtonReady();
94
106
 
95
107
  // Click publish button
108
+ await humanPause(1200, 3000, 'before-publish-click');
96
109
  const clicked = await this._clickByText('发布');
97
110
  if (!clicked) throw new Error('PUBLISH_FAILED: 找不到发布按钮,页面结构可能已变化');
98
111
  const result = await this._waitForPublishResult(90_000);
@@ -102,29 +115,36 @@ export class XhsAdapter {
102
115
  async publishShortVideo({ title, text, tags = [], video, cover }) {
103
116
  await this._openPublishComposer('video');
104
117
  await this._assertReadyForPublish();
118
+ await humanPause(2500, 5500, 'composer-ready');
105
119
 
106
120
  await this._waitForAny([VIDEO_FILE_INPUT_SELECTOR], 15000);
121
+ await humanPause(900, 2200, 'before-upload');
107
122
 
108
123
  // Upload video
109
124
  await this._uploadFiles([video], 'video');
110
125
  await this._waitForUploadSettled(1, 180_000);
126
+ await humanPause(3000, 6500, 'after-video-upload');
111
127
 
112
128
  if (cover) {
129
+ await humanPause(1200, 2800, 'before-cover-upload');
113
130
  await this._uploadFiles([cover], 'cover');
114
131
  await this._waitForUploadSettled(1, 60_000);
132
+ await humanPause(1800, 4200, 'after-cover-upload');
115
133
  }
116
134
 
117
135
  if (title) {
118
136
  await this._fillField(TITLE_SELECTOR, title);
137
+ await humanPause(1200, 2800, 'after-title');
119
138
  }
120
139
 
121
140
  const fullText = tags.length > 0 ? `${text}\n${tags.map(t => `#${t}`).join(' ')}` : text;
122
141
  await this._fillField(CONTENT_SELECTOR, fullText);
123
142
 
124
- await sleep(1000);
143
+ await humanPause(4500, 9000, 'before-publish-check');
125
144
  await this._assertNoBlockingErrors();
126
145
  await this._assertPublishButtonReady();
127
146
 
147
+ await humanPause(1200, 3000, 'before-publish-click');
128
148
  const clicked = await this._clickByText('发布');
129
149
  if (!clicked) throw new Error('PUBLISH_FAILED: 找不到发布按钮,页面结构可能已变化');
130
150
  const result = await this._waitForPublishResult(120_000);
@@ -141,9 +161,11 @@ export class XhsAdapter {
141
161
  await this._cdp.send('Page.navigate', { url });
142
162
  await this._waitForCreatorShell(20_000);
143
163
  await this._assertReadyForPublish();
164
+ await humanPause(1800, 4200, 'after-navigation');
144
165
 
145
166
  if (await this._hasSelector(inputSelector)) return;
146
167
 
168
+ await humanPause(1000, 2600, 'before-tab-switch');
147
169
  const clicked = await this._clickVisibleByText(targetTabText, '.creator-tab, button, [role="button"], span, div', { throwOnMissing: false });
148
170
  if (clicked) {
149
171
  await this._waitForAny([inputSelector], 15_000);
@@ -152,6 +174,7 @@ export class XhsAdapter {
152
174
 
153
175
  await this._cdp.send('Page.navigate', { url: kind === 'image' ? `${PUBLISH_URL}?from=tab_switch&target=image` : VIDEO_PUBLISH_URL });
154
176
  await this._waitForAny([inputSelector], 15_000);
177
+ await humanPause(1800, 4200, 'after-fallback-navigation');
155
178
  }
156
179
 
157
180
  async _waitForCreatorShell(timeoutMs = 20_000) {
@@ -328,6 +351,7 @@ export class XhsAdapter {
328
351
  }
329
352
 
330
353
  async _fillField(selector, value) {
354
+ await humanPause(500, 1400, 'before-focus-field');
331
355
  const result = await this._cdp.send('Runtime.evaluate', {
332
356
  expression: `
333
357
  (function() {
@@ -337,25 +361,19 @@ export class XhsAdapter {
337
361
  if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
338
362
  const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
339
363
  || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
340
- if (nativeInputValueSetter) nativeInputValueSetter.call(el, ${JSON.stringify(value)});
341
- else el.value = ${JSON.stringify(value)};
364
+ if (nativeInputValueSetter) nativeInputValueSetter.call(el, '');
365
+ else el.value = '';
342
366
  el.dispatchEvent(new Event('input', { bubbles: true }));
343
- el.dispatchEvent(new Event('change', { bubbles: true }));
367
+ el.select?.();
344
368
  } else {
345
- const value = ${JSON.stringify(value)};
346
369
  const range = document.createRange();
347
370
  range.selectNodeContents(el);
348
371
  const selection = window.getSelection();
349
372
  selection.removeAllRanges();
350
373
  selection.addRange(range);
351
- const inserted = document.execCommand?.('insertText', false, value);
352
- if (!inserted) {
353
- const html = value.split('\\n').map(line => '<p>' + line.replace(/[&<>]/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[ch])) + '</p>').join('');
354
- el.innerHTML = html || '<p><br></p>';
355
- }
356
- selection.removeAllRanges();
374
+ document.execCommand?.('delete', false, null);
375
+ el.focus();
357
376
  el.dispatchEvent(new InputEvent('input', { bubbles: true }));
358
- el.dispatchEvent(new Event('change', { bubbles: true }));
359
377
  }
360
378
  return true;
361
379
  })()
@@ -363,10 +381,36 @@ export class XhsAdapter {
363
381
  returnByValue: true,
364
382
  });
365
383
  if (result.result?.value !== true) throw new Error(`PUBLISH_FAILED: 找不到输入框:${selector}`);
366
- await sleep(300);
384
+ await this._typeTextHumanLike(value);
385
+ await this._cdp.send('Runtime.evaluate', {
386
+ expression: `
387
+ (function() {
388
+ const el = document.activeElement;
389
+ if (!el) return false;
390
+ el.dispatchEvent(new Event('change', { bubbles: true }));
391
+ el.dispatchEvent(new Event('blur', { bubbles: true }));
392
+ return true;
393
+ })()
394
+ `,
395
+ returnByValue: true,
396
+ });
397
+ await humanPause(700, 1800, 'after-fill-field');
398
+ }
399
+
400
+ async _typeTextHumanLike(value) {
401
+ const text = String(value ?? '');
402
+ let i = 0;
403
+ while (i < text.length) {
404
+ const chunkSize = randomInt(10, 26);
405
+ const chunk = text.slice(i, i + chunkSize);
406
+ await this._cdp.send('Input.insertText', { text: chunk });
407
+ i += chunk.length;
408
+ await humanPause(90, 260);
409
+ }
367
410
  }
368
411
 
369
412
  async _uploadFiles(filePaths, type = 'image') {
413
+ await humanPause(600, 1700, 'before-set-files');
370
414
  const selector = type === 'video' ? VIDEO_FILE_INPUT_SELECTOR : IMAGE_FILE_INPUT_SELECTOR;
371
415
  const result = await this._cdp.send('Runtime.evaluate', {
372
416
  expression: `document.querySelector(${JSON.stringify(selector)})`,
@@ -378,7 +422,7 @@ export class XhsAdapter {
378
422
  objectId: result.result.objectId,
379
423
  files: filePaths,
380
424
  });
381
- await sleep(500);
425
+ await humanPause(1200, 2600, 'after-set-files');
382
426
  }
383
427
 
384
428
  async _clickVisibleByText(text, selector, { throwOnMissing = true } = {}) {
@@ -405,7 +449,7 @@ export class XhsAdapter {
405
449
  });
406
450
  const clicked = result.result?.value === true;
407
451
  if (!clicked && throwOnMissing) throw new Error(`PUBLISH_FAILED: 找不到小红书发布入口:${text}`);
408
- if (clicked) await sleep(1000);
452
+ if (clicked) await humanPause(1500, 3500, `after-click-${text}`);
409
453
  return clicked;
410
454
  }
411
455
 
@@ -415,13 +459,23 @@ export class XhsAdapter {
415
459
  (function() {
416
460
  const els = [...document.querySelectorAll('button, [role="button"]')];
417
461
  const el = els.find(e => e.innerText?.trim() === ${JSON.stringify(text)} || e.textContent?.trim() === ${JSON.stringify(text)});
418
- if (el) { el.click(); return true; }
419
- return false;
462
+ if (!el) return null;
463
+ const r = el.getBoundingClientRect();
464
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
420
465
  })()
421
466
  `,
422
467
  returnByValue: true,
423
468
  });
424
- return result.result?.value === true;
469
+ const point = result.result?.value;
470
+ if (!point) return false;
471
+ await humanPause(500, 1400, `before-click-${text}`);
472
+ await this._cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: point.x, y: point.y });
473
+ await humanPause(120, 420);
474
+ await this._cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1 });
475
+ await humanPause(80, 220);
476
+ await this._cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: point.x, y: point.y, button: 'left', clickCount: 1 });
477
+ await humanPause(1500, 3500, `after-click-${text}`);
478
+ return true;
425
479
  }
426
480
 
427
481
  async _getUrl() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.67",
3
+ "version": "0.9.69",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -2,6 +2,8 @@
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
+ import { readFileSync } from 'fs';
6
+ import { extname } from 'path';
5
7
 
6
8
  const cliArgs = process.argv.slice(2);
7
9
  function getArg(name) {
@@ -17,6 +19,38 @@ const TEAM_ID = process.env.TEAM_ID || getArg('--team-id') || ''; // injec
17
19
  // Current active teamId for memory isolation (defaults to spawn-time TEAM_ID)
18
20
  let currentTeamId = TEAM_ID;
19
21
 
22
+ const WORKSPACE_BINARY_MIME = {
23
+ '.png': 'image/png',
24
+ '.jpg': 'image/jpeg',
25
+ '.jpeg': 'image/jpeg',
26
+ '.webp': 'image/webp',
27
+ '.gif': 'image/gif',
28
+ '.pdf': 'application/pdf',
29
+ };
30
+
31
+ function dataUrlSummary(content) {
32
+ if (typeof content !== 'string' || !content.startsWith('data:')) return null;
33
+ const commaIdx = content.indexOf(',');
34
+ if (commaIdx === -1) return null;
35
+ const header = content.slice(5, commaIdx);
36
+ const mime = header.split(';')[0] || 'application/octet-stream';
37
+ const isBase64 = header.split(';').includes('base64');
38
+ let bytes = null;
39
+ if (isBase64) {
40
+ const encoded = content.slice(commaIdx + 1).replace(/\s/g, '');
41
+ const padding = encoded.endsWith('==') ? 2 : encoded.endsWith('=') ? 1 : 0;
42
+ bytes = Math.floor(encoded.length * 3 / 4) - padding;
43
+ }
44
+ return { mime, bytes };
45
+ }
46
+
47
+ function formatBytes(bytes) {
48
+ if (!Number.isFinite(bytes)) return 'unknown size';
49
+ if (bytes < 1024) return `${bytes} B`;
50
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
51
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
52
+ }
53
+
20
54
  async function api(method, path, body) {
21
55
  const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${path}`;
22
56
  const res = await fetch(url, {
@@ -277,6 +311,15 @@ server.tool('read_workspace', 'Read a file from the shared team workspace (e.g.
277
311
  if (!currentTeamId) return { content: [{ type: 'text', text: 'No team context.' }] };
278
312
  try {
279
313
  const data = await api('GET', `/team-memory?path=${encodeURIComponent(path)}&teamId=${encodeURIComponent(currentTeamId)}`);
314
+ const summary = dataUrlSummary(data.content);
315
+ if (summary) {
316
+ return {
317
+ content: [{
318
+ type: 'text',
319
+ text: `${path} is a binary workspace file (${summary.mime}, ${formatBytes(summary.bytes)}). Do not read it as text. Use it by path in the Files tab, or use upload_image/read_file_base64 if you need a public URL or local base64.`,
320
+ }],
321
+ };
322
+ }
280
323
  return { content: [{ type: 'text', text: data.content }] };
281
324
  } catch (err) {
282
325
  if (err.message.includes('404')) return { content: [{ type: 'text', text: `(empty — ${path} has no content yet)` }] };
@@ -294,6 +337,19 @@ server.tool('write_workspace', 'Write a file to the shared team workspace. Use t
294
337
  return { content: [{ type: 'text', text: `Saved to team workspace: ${path}` }] };
295
338
  });
296
339
 
340
+ server.tool('write_workspace_file', 'Write a local file directly to the shared team workspace. Prefer this over write_workspace for images/PDFs/binary files so large base64 content never enters the model context.', {
341
+ file_path: z.string().describe('Absolute path to the local file, e.g. "/home/ubuntu/lightcone/public/cover.png"'),
342
+ path: z.string().describe('Destination path relative to team workspace root, e.g. "artifacts/cover.png"'),
343
+ }, async ({ file_path, path }) => {
344
+ if (!currentTeamId) return { content: [{ type: 'text', text: 'No team context.' }] };
345
+ const ext = extname(path || file_path).toLowerCase();
346
+ const mime = WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream';
347
+ const buf = readFileSync(file_path);
348
+ const content = `data:${mime};base64,${buf.toString('base64')}`;
349
+ await api('PUT', `/team-memory?path=${encodeURIComponent(path)}&teamId=${encodeURIComponent(currentTeamId)}`, { content });
350
+ return { content: [{ type: 'text', text: `Saved local file to team workspace: ${path} (${mime}, ${formatBytes(buf.length)})` }] };
351
+ });
352
+
297
353
  // ── get_credential ───────────────────────────────────────────────────────────
298
354
  server.tool('get_credential',
299
355
  'Retrieve decrypted credential fields for a platform granted to this agent (e.g. XHS_COOKIE for "xhs"). Use when you need to inject credentials into a browser session or external call.',