@lightcone-ai/daemon 0.15.38 → 0.15.40

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.
@@ -123,6 +123,9 @@ export class BilibiliAdapter {
123
123
  if (!loggedIn) throw new Error('LOGIN_EXPIRED: B站登录已过期,请重新扫码连接');
124
124
 
125
125
  if (images.length > 0) {
126
+ // The file input only appears after clicking the image button in the editor toolbar
127
+ await this._clickImageToolbarButton();
128
+ await sleep(1_000);
126
129
  await this._waitForSelector(ARTICLE_IMAGE_FILE_SELECTOR, 15_000);
127
130
  await this._uploadFiles(images, 'image');
128
131
  await this._waitForUploadSettled(180_000);
@@ -326,6 +329,45 @@ export class BilibiliAdapter {
326
329
  await sleep(400);
327
330
  }
328
331
 
332
+ async _clickImageToolbarButton() {
333
+ const result = await this._cdp.send('Runtime.evaluate', {
334
+ expression: `
335
+ (function() {
336
+ const selectors = [
337
+ '.ql-image',
338
+ '[class*="toolbar-image"]',
339
+ '[class*="toolbar"] [class*="image"]',
340
+ '[class*="toolbar"] [class*="pic"]',
341
+ '[aria-label="图片"]',
342
+ '[aria-label*="插入图片"]',
343
+ '[title="图片"]',
344
+ '[title*="插入图片"]',
345
+ '[class*="bf-icon-image"]',
346
+ '[data-action="uploadImage"]',
347
+ '[data-type="image"]',
348
+ ];
349
+ for (const sel of selectors) {
350
+ const el = document.querySelector(sel);
351
+ if (el) { el.click(); return sel; }
352
+ }
353
+ // Heuristic: any toolbar button whose title/aria-label mentions image/图片
354
+ for (const el of document.querySelectorAll('[class*="toolbar"] button, [class*="toolbar"] [role="button"], [class*="toolbar"] span, [class*="editor-toolbar"] *')) {
355
+ const hint = [el.getAttribute('title'), el.getAttribute('aria-label'), el.className?.toString?.()].filter(Boolean).join(' ').toLowerCase();
356
+ if (hint.includes('image') || hint.includes('图片') || hint.includes('pic')) {
357
+ el.click();
358
+ return 'toolbar-heuristic:' + hint.slice(0, 60);
359
+ }
360
+ }
361
+ return null;
362
+ })()
363
+ `,
364
+ returnByValue: true,
365
+ });
366
+ const clicked = result.result?.value;
367
+ console.error(`[BilibiliAdapter] _clickImageToolbarButton: ${clicked ?? 'not found'}`);
368
+ return !!clicked;
369
+ }
370
+
329
371
  async _clickByTextCandidates(candidates = []) {
330
372
  for (const text of candidates) {
331
373
  const clicked = await this._clickByText(text);
@@ -128,7 +128,19 @@ export class KuaishouAdapter {
128
128
  const deadline = Date.now() + timeoutMs;
129
129
  while (Date.now() < deadline) {
130
130
  const result = await this._cdp.send('Runtime.evaluate', {
131
- expression: `!!document.querySelector(${JSON.stringify(selector)})`,
131
+ expression: `
132
+ (function() {
133
+ const sel = ${JSON.stringify(selector)};
134
+ if (document.querySelector(sel)) return true;
135
+ for (const f of document.querySelectorAll('iframe')) {
136
+ try {
137
+ const d = f.contentDocument || f.contentWindow?.document;
138
+ if (d && d.querySelector(sel)) return true;
139
+ } catch(e) {}
140
+ }
141
+ return false;
142
+ })()
143
+ `,
132
144
  returnByValue: true,
133
145
  });
134
146
  if (result.result?.value) return;
@@ -136,12 +148,16 @@ export class KuaishouAdapter {
136
148
  }
137
149
  // Dump diagnostic info to help debug page state on timeout
138
150
  try {
139
- const [urlR, titleR, bodyR, inputR, uploadR] = await Promise.all([
151
+ const [urlR, titleR, bodyR, inputR, uploadR, iframeR] = await Promise.all([
140
152
  this._cdp.send('Runtime.evaluate', { expression: 'location.href', returnByValue: true }),
141
153
  this._cdp.send('Runtime.evaluate', { expression: 'document.title', returnByValue: true }),
142
154
  this._cdp.send('Runtime.evaluate', { expression: 'document.body?.innerText?.slice(0,400)', returnByValue: true }),
143
155
  this._cdp.send('Runtime.evaluate', { expression: `!!document.querySelector('input[type="file"]')`, returnByValue: true }),
144
156
  this._cdp.send('Runtime.evaluate', { expression: `document.querySelectorAll('[class*="upload"],[class*="Upload"]').length`, returnByValue: true }),
157
+ this._cdp.send('Runtime.evaluate', {
158
+ expression: `(function(){const frames=document.querySelectorAll('iframe');let found=false;for(const f of frames){try{const d=f.contentDocument||f.contentWindow?.document;if(d&&d.querySelector('input[type="file"]')){found=true;break;}}catch(e){}}return JSON.stringify({iframes:frames.length,fileInIframe:found})})()`,
159
+ returnByValue: true,
160
+ }),
145
161
  ]);
146
162
  console.error(`[KuaishouAdapter] selector timeout diagnostics:`);
147
163
  console.error(` url=${urlR.result?.value}`);
@@ -149,6 +165,7 @@ export class KuaishouAdapter {
149
165
  console.error(` input[type=file] present=${inputR.result?.value}`);
150
166
  console.error(` upload-class elements=${uploadR.result?.value}`);
151
167
  console.error(` body text: ${bodyR.result?.value}`);
168
+ console.error(` iframe info: ${iframeR.result?.value}`);
152
169
  } catch (diagErr) {
153
170
  console.error(`[KuaishouAdapter] diagnostic failed: ${diagErr.message}`);
154
171
  }
@@ -196,12 +213,27 @@ export class KuaishouAdapter {
196
213
  }
197
214
 
198
215
  async _uploadFiles(filePaths) {
199
- // Wait up to 10s for the file input to appear (may be hidden/dynamically rendered)
216
+ // Wait up to 10s for the file input; check main document and same-origin iframes
200
217
  const deadline = Date.now() + 10000;
201
218
  let objectId = null;
202
219
  while (Date.now() < deadline) {
203
220
  const result = await this._cdp.send('Runtime.evaluate', {
204
- expression: `document.querySelector('input[type="file"]')`,
221
+ expression: `
222
+ (function() {
223
+ const el = document.querySelector('input[type="file"]');
224
+ if (el) return el;
225
+ for (const f of document.querySelectorAll('iframe')) {
226
+ try {
227
+ const d = f.contentDocument || f.contentWindow?.document;
228
+ if (d) {
229
+ const found = d.querySelector('input[type="file"]');
230
+ if (found) return found;
231
+ }
232
+ } catch(e) {}
233
+ }
234
+ return null;
235
+ })()
236
+ `,
205
237
  returnByValue: false,
206
238
  });
207
239
  if (result.result?.objectId) { objectId = result.result.objectId; break; }
@@ -93,7 +93,7 @@ const IMAGE_PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?targe
93
93
  const VIDEO_PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?source=video';
94
94
 
95
95
  const CREATOR_HOME_URL = 'https://creator.xiaohongshu.com/new/home';
96
- const IMAGE_FILE_INPUT_SELECTOR = 'input.upload-input[type="file"][accept*=".jpg"], input[type="file"][accept*=".png"], input[type="file"][accept*=".webp"]';
96
+ const IMAGE_FILE_INPUT_SELECTOR = 'input.upload-input[type="file"][accept*=".jpg"], input[type="file"][accept*="image"], input[type="file"][accept*=".jpg"], input[type="file"][accept*=".png"], input[type="file"][accept*=".webp"]';
97
97
  const VIDEO_FILE_INPUT_SELECTOR = 'input.upload-input[type="file"][accept*=".mp4"], input[type="file"][accept*=".mov"], input[type="file"][accept*=".mpeg"]';
98
98
  const TITLE_SELECTOR = 'input[placeholder*="标题"], input.d-text[type="text"]';
99
99
  const CONTENT_SELECTOR = '.ProseMirror[contenteditable="true"], [contenteditable="true"][role="textbox"], [contenteditable="true"]';
@@ -409,18 +409,38 @@ export class XhsAdapter extends PublisherAdapter {
409
409
  }
410
410
 
411
411
  async _waitForUploadSettled(expectedCount, timeoutMs) {
412
+ if (expectedCount === 0) return;
412
413
  const deadline = Date.now() + timeoutMs;
414
+ let sawProgress = false;
415
+ let stableRounds = 0;
416
+
413
417
  while (Date.now() < deadline) {
414
418
  await sleep(1000);
415
419
  await this._assertNoBlockingErrors();
416
420
  const state = await this._inspectPage();
417
421
  const text = state.text ?? '';
422
+
418
423
  if (/上传失败|上传出错|格式不支持|文件过大/.test(text)) {
419
424
  throw new Error(`UPLOAD_FAILED: ${(state.errors ?? []).join(';') || '页面提示上传失败'}`);
420
425
  }
421
- if (/上传中|处理中|转码中|正在上传|解析中/.test(text)) continue;
422
- if (state.hasPublishEditor || /图片编辑|视频编辑|笔记预览|上传成功|重新上传|更换|已上传|封面/.test(text)) return;
423
- if (expectedCount === 0) return;
426
+
427
+ const inProgress = /上传中|处理中|转码中|正在上传|解析中/.test(text);
428
+ if (inProgress) { sawProgress = true; stableRounds = 0; continue; }
429
+
430
+ // Definitive upload completion signals
431
+ if (/图片编辑|视频编辑|笔记预览|上传成功|重新上传|更换图片|已上传|封面/.test(text)) return;
432
+
433
+ stableRounds++;
434
+
435
+ // If we saw upload start and then it stopped, done (give 2 stable rounds)
436
+ if (sawProgress && stableRounds >= 2) return;
437
+
438
+ // hasPublishEditor alone is not enough — the form is present from page load.
439
+ // Only treat it as "done" after 10+ stable seconds (gives time for upload progress to appear)
440
+ if (state.hasPublishEditor && stableRounds >= 10) {
441
+ console.error('[XhsAdapter] _waitForUploadSettled: hasPublishEditor stable for 10s, proceeding');
442
+ return;
443
+ }
424
444
  }
425
445
  throw new Error(`UPLOAD_TIMEOUT: 等待小红书上传完成超时`);
426
446
  }
@@ -509,16 +529,54 @@ export class XhsAdapter extends PublisherAdapter {
509
529
  async _uploadFiles(filePaths, type = 'image') {
510
530
  await humanPause(600, 1700, 'before-set-files');
511
531
  const selector = type === 'video' ? VIDEO_FILE_INPUT_SELECTOR : IMAGE_FILE_INPUT_SELECTOR;
512
- const result = await this._cdp.send('Runtime.evaluate', {
513
- expression: `document.querySelector(${JSON.stringify(selector)})`,
532
+
533
+ // Use DOM.getDocument + DOM.querySelector for a stable nodeId
534
+ let nodeId = null;
535
+ const deadline = Date.now() + 10000;
536
+ while (Date.now() < deadline) {
537
+ try {
538
+ const docResult = await this._cdp.send('DOM.getDocument', { depth: 0 });
539
+ const qResult = await this._cdp.send('DOM.querySelector', {
540
+ nodeId: docResult.root.nodeId,
541
+ selector,
542
+ });
543
+ if (qResult.nodeId) { nodeId = qResult.nodeId; break; }
544
+ } catch (e) {}
545
+ await sleep(500);
546
+ }
547
+ if (!nodeId) throw new Error(`No ${type} file input found on page`);
548
+
549
+ // Force multiple-file support and ensure input is enabled before setting files
550
+ await this._cdp.send('Runtime.evaluate', {
551
+ expression: `
552
+ (function() {
553
+ const el = document.querySelector(${JSON.stringify(selector)});
554
+ if (el) {
555
+ el.removeAttribute('disabled');
556
+ if (!el.hasAttribute('multiple')) el.setAttribute('multiple', '');
557
+ }
558
+ })()
559
+ `,
514
560
  returnByValue: false,
515
561
  });
516
- if (!result.result?.objectId) throw new Error(`No ${type} file input found on page`);
517
562
 
518
- await this._cdp.send('DOM.setFileInputFiles', {
519
- objectId: result.result.objectId,
520
- files: filePaths,
563
+ console.error(`[XhsAdapter] _uploadFiles: type=${type} files=${filePaths.length} nodeId=${nodeId}`);
564
+ await this._cdp.send('DOM.setFileInputFiles', { nodeId, files: filePaths });
565
+
566
+ // Dispatch extra change/input events in case the Vue handler needs them
567
+ await this._cdp.send('Runtime.evaluate', {
568
+ expression: `
569
+ (function() {
570
+ const el = document.querySelector(${JSON.stringify(selector)});
571
+ if (el) {
572
+ el.dispatchEvent(new Event('change', { bubbles: true }));
573
+ el.dispatchEvent(new Event('input', { bubbles: true }));
574
+ }
575
+ })()
576
+ `,
577
+ returnByValue: false,
521
578
  });
579
+
522
580
  await humanPause(1200, 2600, 'after-set-files');
523
581
  }
524
582
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.38",
3
+ "version": "0.15.40",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {