dsh-douyin-agent 1.4.5 → 1.4.7
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/autonomous-watch.mjs +7 -1
- package/chat-browser.mjs +56 -12
- package/package.json +1 -1
- package/runtime-paths.mjs +14 -1
package/autonomous-watch.mjs
CHANGED
|
@@ -192,7 +192,13 @@ export class AutonomousWatch {
|
|
|
192
192
|
}, 500); this.heartbeat.unref?.();
|
|
193
193
|
const remaining = this.videoLimits.state().session.expiresAt - this.now();
|
|
194
194
|
this.deadline = setTimeout(() => this.abort.abort(failure('本轮刷视频时间已用完。', 'limited')), Math.max(1, Math.min(remaining, 2_147_483_647))); this.deadline.unref?.();
|
|
195
|
-
this.completion = Promise.resolve().then(() => this.run()).catch(error => {
|
|
195
|
+
this.completion = Promise.resolve().then(() => this.run()).catch(error => {
|
|
196
|
+
this.onEvent({ type: 'watch.failed', message: redact(error.message || String(error), [this.config.apiKey]) });
|
|
197
|
+
// 2026-10-31:启动阶段就失败(最常见是没找到 Node.js,专用浏览器根本没起来)时,
|
|
198
|
+
// 界面上的轮次秒数还在走、却什么都不会发生。这里把这一轮直接结束掉,别让它空转。
|
|
199
|
+
if (!this.track) { try { this.videoLimits.endSession({ explicit: true, reason: '本轮启动失败,已自动结束。' }); } catch { /* 结束不掉也不影响报错 */ } }
|
|
200
|
+
return { running: false, status: 'failed', reason: redact(error.message || String(error), [this.config.apiKey]) };
|
|
201
|
+
}).then(result => { this.notifyRoundEnd(result); return result; }).finally(() => { this.completion = null; });
|
|
196
202
|
return this.state();
|
|
197
203
|
} catch (error) {
|
|
198
204
|
clearInterval(this.heartbeat); clearTimeout(this.deadline); this.running = false;
|
package/chat-browser.mjs
CHANGED
|
@@ -9,6 +9,43 @@ export { MessageIdentityTracker } from './message-tracker.mjs';
|
|
|
9
9
|
const HOME = 'https://www.douyin.com/?recommend=1&from_nav=1';
|
|
10
10
|
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
11
11
|
const stop = signal => signal?.throwIfAborted();
|
|
12
|
+
// 2026-10-31:读「我的关注」名单。原来的实现依赖抖音的哈希类名(.OlxToPIh / .Zce7SWd3),
|
|
13
|
+
// 改版后名单还会开在新标签页,导致 followed 全是 null、设置页的许可好友勾选框全灰。
|
|
14
|
+
// 现在按行读、按按钮文字判关系:类名再变也不影响;新标签页由 verifyFollowing 负责接管。
|
|
15
|
+
const readFollowingRows = () => {
|
|
16
|
+
const root = document.querySelector('[data-e2e="user-fans-container"]') || document.body;
|
|
17
|
+
const order = ['相互关注', '已关注', '关注', '回关'];
|
|
18
|
+
const pick = row => {
|
|
19
|
+
const button = [...row.querySelectorAll('button')].find(item => (item.textContent || '').trim());
|
|
20
|
+
if (!button) return null;
|
|
21
|
+
const text = (button.textContent || '').trim();
|
|
22
|
+
const label = order.find(item => text.startsWith(item));
|
|
23
|
+
if (!label) return null;
|
|
24
|
+
const names = [...row.querySelectorAll('a')].map(anchor => (anchor.textContent || '').trim()).filter(Boolean);
|
|
25
|
+
if (!names.length) return null;
|
|
26
|
+
return { displayName: names[0], mutual: label === '相互关注', followed: label !== '关注' && label !== '回关' };
|
|
27
|
+
};
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const row of root.querySelectorAll('.OlxToPIh, li')) {
|
|
30
|
+
const item = pick(row);
|
|
31
|
+
if (item) out.push(item);
|
|
32
|
+
}
|
|
33
|
+
if (!out.length) {
|
|
34
|
+
// 类名全换了也能用:从按钮往上找能读出名字的那一层当行。
|
|
35
|
+
for (const button of root.querySelectorAll('button')) {
|
|
36
|
+
const text = (button.textContent || '').trim();
|
|
37
|
+
if (!order.some(item => text.startsWith(item))) continue;
|
|
38
|
+
let node = button.parentElement;
|
|
39
|
+
for (let depth = 0; depth < 5 && node && node !== root; depth += 1) {
|
|
40
|
+
const item = pick(node);
|
|
41
|
+
if (item) { out.push(item); break; }
|
|
42
|
+
node = node.parentElement;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
return out.filter(item => (seen.has(item.displayName) ? false : (seen.add(item.displayName), true)));
|
|
48
|
+
};
|
|
12
49
|
const readableDraft = value => value.replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/g, '').trim();
|
|
13
50
|
const localDay = value => new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(value));
|
|
14
51
|
|
|
@@ -283,20 +320,27 @@ export class ChatBrowser {
|
|
|
283
320
|
await page.goto(target.href, { waitUntil: 'domcontentloaded' });
|
|
284
321
|
await page.locator('[data-e2e="user-info-follow"]').waitFor({ state: 'visible', timeout: 10_000 });
|
|
285
322
|
await page.waitForTimeout(800);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
323
|
+
stop(signal);
|
|
324
|
+
// 2026-10-31:改版后点「关注」有时把名单开在**新标签页**,原来只在当前页找,永远找不到。
|
|
325
|
+
// 这里两条路都走:等 popup(新标签页),没有 popup 就读当前页;读不到再点一次,最多三次。
|
|
326
|
+
let rows = [];
|
|
327
|
+
for (let attempt = 0; attempt < 3 && !rows.length; attempt += 1) {
|
|
289
328
|
stop(signal);
|
|
290
|
-
|
|
291
|
-
await
|
|
329
|
+
const popupPromise = page.waitForEvent('popup', { timeout: 4000 }).catch(() => null);
|
|
330
|
+
await page.locator('[data-e2e="user-info-follow"]').click({ timeout: 5000 }).catch(() => {});
|
|
331
|
+
const popup = await popupPromise;
|
|
332
|
+
const view = popup || page;
|
|
333
|
+
if (popup) {
|
|
334
|
+
await popup.waitForLoadState('domcontentloaded').catch(() => {});
|
|
335
|
+
await popup.waitForTimeout(1200);
|
|
336
|
+
}
|
|
337
|
+
await view.locator('[data-e2e="user-fans-container"]').first().waitFor({ state: 'visible', timeout: 8000 }).catch(() => {});
|
|
338
|
+
rows = await view.evaluate(readFollowingRows).catch(() => []);
|
|
339
|
+
if (!rows.length) rows = await page.evaluate(readFollowingRows).catch(() => []);
|
|
340
|
+
if (popup) await popup.close().catch(() => {});
|
|
292
341
|
}
|
|
293
|
-
if (!
|
|
294
|
-
|
|
295
|
-
stop(signal);
|
|
296
|
-
this.following = await page.evaluate(() => [...document.querySelectorAll('[data-e2e="user-fans-container"] .OlxToPIh')].map(row => ({
|
|
297
|
-
displayName: row.querySelector('.Zce7SWd3 a')?.textContent.trim(),
|
|
298
|
-
mutual: [...row.querySelectorAll('button')].some(button => button.textContent.trim() === '相互关注'),
|
|
299
|
-
})));
|
|
342
|
+
if (!rows.length) throw new Error('本人的关注列表未就绪,未确认联系人关系。');
|
|
343
|
+
this.following = rows;
|
|
300
344
|
await page.goto(HOME, { waitUntil: 'domcontentloaded' });
|
|
301
345
|
await this.dialog(page, signal);
|
|
302
346
|
}
|
package/package.json
CHANGED
package/runtime-paths.mjs
CHANGED
|
@@ -99,8 +99,21 @@ export function resolveNodeExecutable({ env = process.env, executable = process.
|
|
|
99
99
|
const fixedLocations = process.platform === 'win32'
|
|
100
100
|
? ['ProgramFiles', 'ProgramFiles(x86)', 'LOCALAPPDATA'].filter(key => typeof env[key] === 'string' && path.isAbsolute(env[key])).map(key => path.join(env[key], 'Programs', 'nodejs', nodeName)).concat(['ProgramFiles', 'ProgramFiles(x86)'].filter(key => typeof env[key] === 'string' && path.isAbsolute(env[key])).map(key => path.join(env[key], 'nodejs', nodeName)))
|
|
101
101
|
: ['/usr/local/bin', '/usr/bin', '/opt/homebrew/bin'].map(directory => path.join(directory, nodeName));
|
|
102
|
+
// 2026-10-31:DSH 0.2.1-rc.5 把 Node 放在 resources\runtime\primary-runtime\dependencies\node\bin\node.exe,
|
|
103
|
+
// 和插件原来找的 resources\runtime\node\node.exe 不是同一个目录——使用者就是这样五处全落空、浏览器起不来。
|
|
104
|
+
const runtimeCandidates = (() => {
|
|
105
|
+
if (!resourcesPath || !path.isAbsolute(resourcesPath)) return [];
|
|
106
|
+
const root = path.join(resourcesPath, 'runtime');
|
|
107
|
+
const found = [path.join(root, 'node', nodeName)];
|
|
108
|
+
try {
|
|
109
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
110
|
+
if (entry.isDirectory()) found.push(path.join(root, entry.name, 'dependencies', 'node', 'bin', nodeName));
|
|
111
|
+
}
|
|
112
|
+
} catch { /* 没有 runtime 目录就只留默认那条 */ }
|
|
113
|
+
return found;
|
|
114
|
+
})();
|
|
102
115
|
const candidates = [
|
|
103
|
-
...
|
|
116
|
+
...runtimeCandidates,
|
|
104
117
|
path.join(sourceRoot, 'desktop', '.cache', 'generated', 'node', nodeName),
|
|
105
118
|
...runtimeNodes,
|
|
106
119
|
...fixedLocations,
|