@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 =
|
|
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.
|
|
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.
|
|
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
|
|
372
|
-
|
|
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 };
|