@lightcone-ai/daemon 0.9.77 → 0.9.78

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.
@@ -37,7 +37,14 @@ const CREATOR_HOME_URL = 'https://creator.douyin.com/creator-micro/home';
37
37
  const IMAGE_PUBLISH_URL = 'https://creator.douyin.com/creator-micro/content/upload-image-text';
38
38
  const VIDEO_PUBLISH_URL = 'https://creator.douyin.com/creator-micro/content/upload';
39
39
 
40
- const FILE_INPUT_SELECTOR = 'input[type="file"]';
40
+ const FILE_INPUT_SELECTOR = [
41
+ 'input[type="file"][accept*="image"]',
42
+ 'input[type="file"][accept*=".jpg"]',
43
+ 'input[type="file"][accept*=".jpeg"]',
44
+ 'input[type="file"][accept*=".png"]',
45
+ 'input[type="file"][accept*=".webp"]',
46
+ 'input[type="file"]',
47
+ ].join(', ');
41
48
  const TITLE_SELECTOR = 'input[placeholder*="标题"], textarea[placeholder*="标题"]';
42
49
  const CONTENT_SELECTOR = '[contenteditable="true"], textarea[placeholder*="描述"], textarea[placeholder*="作品描述"], [placeholder*="添加作品描述"]';
43
50
 
@@ -79,7 +86,7 @@ export class DouyinAdapter {
79
86
  await humanPause(2500, 5500, 'composer-ready');
80
87
 
81
88
  if (images.length > 0) {
82
- await this._waitForSelector(FILE_INPUT_SELECTOR, 15000);
89
+ await this._ensureUploaderReady('image');
83
90
  await humanPause(900, 2200, 'before-upload');
84
91
  await this._uploadFiles(images);
85
92
  await this._waitForUploadSettled(images.length, 120_000);
@@ -114,7 +121,7 @@ export class DouyinAdapter {
114
121
  await this._assertReadyForPublish();
115
122
  await humanPause(2500, 5500, 'composer-ready');
116
123
 
117
- await this._waitForSelector(FILE_INPUT_SELECTOR, 15000);
124
+ await this._ensureUploaderReady('video');
118
125
  await humanPause(900, 2200, 'before-upload');
119
126
  await this._uploadFiles([video]);
120
127
  await this._waitForUploadSettled(1, 180_000);
@@ -152,6 +159,7 @@ export class DouyinAdapter {
152
159
  await this._cdp.send('Page.navigate', { url });
153
160
  await this._waitForCreatorShell(20_000);
154
161
  await humanPause(1800, 4200, 'after-navigation');
162
+ await this._ensureComposer(kind);
155
163
  }
156
164
 
157
165
  async _waitForCreatorShell(timeoutMs = 20_000) {
@@ -187,6 +195,66 @@ export class DouyinAdapter {
187
195
  throw new Error(`Timeout waiting for selector: ${selector}`);
188
196
  }
189
197
 
198
+ async _ensureComposer(kind) {
199
+ if (await this._waitForUploaderOrEditor(8000)) return;
200
+
201
+ const labels = kind === 'video'
202
+ ? ['发布视频', '上传视频', '视频', '发布作品', '上传']
203
+ : ['发布图文', '上传图文', '图文', '发布图片', '上传图片', '图片', '发布作品', '上传'];
204
+
205
+ for (const label of labels) {
206
+ const clicked = await this._clickByText(label);
207
+ if (!clicked) continue;
208
+ if (await this._waitForUploaderOrEditor(12_000)) return;
209
+ }
210
+
211
+ await this._cdp.send('Page.navigate', { url: kind === 'video' ? VIDEO_PUBLISH_URL : IMAGE_PUBLISH_URL });
212
+ if (await this._waitForUploaderOrEditor(15_000)) return;
213
+
214
+ const state = await this._inspectPage();
215
+ throw new Error(`PUBLISH_FAILED: 找不到抖音${kind === 'image' ? '图文' : '视频'}上传入口。${this._formatPageHint(state)}`);
216
+ }
217
+
218
+ async _ensureUploaderReady(kind) {
219
+ if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 8000)) return;
220
+ await this._ensureComposer(kind);
221
+ if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 12_000)) return;
222
+
223
+ const labels = kind === 'video'
224
+ ? ['上传视频', '点击上传', '选择视频', '上传']
225
+ : ['上传图片', '添加图片', '点击上传', '选择图片', '上传'];
226
+ for (const label of labels) {
227
+ const clicked = await this._clickByText(label);
228
+ if (!clicked) continue;
229
+ if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 8000)) return;
230
+ }
231
+
232
+ const state = await this._inspectPage();
233
+ throw new Error(`PUBLISH_FAILED: 找不到抖音文件上传输入框。${this._formatPageHint(state)}`);
234
+ }
235
+
236
+ async _waitForUploaderOrEditor(timeoutMs) {
237
+ const deadline = Date.now() + timeoutMs;
238
+ while (Date.now() < deadline) {
239
+ const state = await this._inspectPage();
240
+ if (state.hasLoginHint) {
241
+ throw new Error(`LOGIN_EXPIRED: 当前页面 ${state.url},请重新扫码连接抖音`);
242
+ }
243
+ if (state.hasUploader || state.hasPublishEditor) return true;
244
+ await sleep(500);
245
+ }
246
+ return false;
247
+ }
248
+
249
+ async _waitForSelectorQuiet(selector, timeoutMs = 8000) {
250
+ try {
251
+ await this._waitForSelector(selector, timeoutMs);
252
+ return true;
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+
190
258
  async _inspectPage() {
191
259
  const result = await this._cdp.send('Runtime.evaluate', {
192
260
  expression: `
@@ -208,12 +276,28 @@ export class DouyinAdapter {
208
276
  const buttons = [...document.querySelectorAll('button, [role="button"]')].map(e => ({
209
277
  text: (e.innerText || e.textContent || '').trim(),
210
278
  disabled: !!e.disabled || e.getAttribute('aria-disabled') === 'true' || e.className?.toString().includes('disabled'),
211
- }));
279
+ })).filter(b => b.text).slice(0, 30);
280
+ const links = [...document.querySelectorAll('a')].map(e => ({
281
+ text: (e.innerText || e.textContent || '').trim(),
282
+ href: e.href || '',
283
+ })).filter(a => a.text).slice(0, 30);
284
+ const uploadish = [...document.querySelectorAll('input, button, [role="button"], a, div, span')]
285
+ .map(e => ({
286
+ tag: e.tagName,
287
+ text: (e.innerText || e.textContent || e.getAttribute('aria-label') || e.getAttribute('title') || '').trim(),
288
+ type: e.getAttribute('type') || '',
289
+ accept: e.getAttribute('accept') || '',
290
+ cls: e.className?.toString?.() || '',
291
+ }))
292
+ .filter(e => /上传|图文|图片|视频|作品|file|upload/i.test([e.text, e.type, e.accept, e.cls].join(' ')))
293
+ .slice(0, 40);
212
294
  return {
213
295
  url,
214
296
  text: text.slice(0, 5000),
215
297
  errors,
216
298
  buttons,
299
+ links,
300
+ uploadish,
217
301
  hasLoginHint: /登录|扫码|验证码|手机号/.test(text) && !/发布|作品管理|创作服务平台/.test(text),
218
302
  isCreatorLoggedIn: /创作服务平台|创作者中心|发布作品|作品管理|内容管理/.test(text),
219
303
  hasUploader: !!document.querySelector(${JSON.stringify(FILE_INPUT_SELECTOR)}),
@@ -224,7 +308,19 @@ export class DouyinAdapter {
224
308
  `,
225
309
  returnByValue: true,
226
310
  });
227
- return result.result?.value ?? { url: await this._getUrl(), text: '', errors: [], buttons: [] };
311
+ return result.result?.value ?? { url: await this._getUrl(), text: '', errors: [], buttons: [], links: [], uploadish: [] };
312
+ }
313
+
314
+ _formatPageHint(state) {
315
+ const buttons = (state.buttons ?? []).map(b => b.text).filter(Boolean).slice(0, 12).join(' / ');
316
+ const links = (state.links ?? []).map(a => a.text).filter(Boolean).slice(0, 12).join(' / ');
317
+ const uploadish = (state.uploadish ?? [])
318
+ .map(e => [e.tag, e.text, e.type, e.accept].filter(Boolean).join(':'))
319
+ .filter(Boolean)
320
+ .slice(0, 12)
321
+ .join(' / ');
322
+ const text = (state.text ?? '').replace(/\s+/g, ' ').slice(0, 240);
323
+ return `当前 url=${state.url}; buttons=${buttons || 'none'}; links=${links || 'none'}; uploadCandidates=${uploadish || 'none'}; text=${text || 'empty'}`;
228
324
  }
229
325
 
230
326
  async _assertReadyForPublish() {
@@ -368,8 +464,23 @@ export class DouyinAdapter {
368
464
  const result = await this._cdp.send('Runtime.evaluate', {
369
465
  expression: `
370
466
  (function() {
371
- const els = [...document.querySelectorAll('button, [role="button"]')];
372
- const el = els.find(e => e.innerText?.trim() === ${JSON.stringify(text)} || e.textContent?.trim() === ${JSON.stringify(text)} || e.innerText?.includes(${JSON.stringify(text)}));
467
+ const visible = (el) => {
468
+ const r = el.getBoundingClientRect();
469
+ const s = getComputedStyle(el);
470
+ return r.width > 0 && r.height > 0 && r.left > -1000 && s.display !== 'none' && s.visibility !== 'hidden';
471
+ };
472
+ const els = [...document.querySelectorAll('button, [role="button"], a, div, span')]
473
+ .filter(visible)
474
+ .filter(e => {
475
+ const txt = (e.innerText || e.textContent || e.getAttribute('aria-label') || e.getAttribute('title') || '').trim();
476
+ if (!txt) return false;
477
+ return txt === ${JSON.stringify(text)} || txt.includes(${JSON.stringify(text)});
478
+ });
479
+ const el = els.sort((a, b) => {
480
+ const ar = a.getBoundingClientRect();
481
+ const br = b.getBoundingClientRect();
482
+ return (ar.width * ar.height) - (br.width * br.height);
483
+ })[0];
373
484
  if (!el) return null;
374
485
  const r = el.getBoundingClientRect();
375
486
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.77",
3
+ "version": "0.9.78",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {