dsh-douyin-mac 0.1.9 → 0.1.11
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/CHANGELOG.md +11 -0
- package/browser-host.mjs +9 -0
- package/chat-browser.mjs +8 -0
- package/client.js +1 -0
- package/core.mjs +13 -0
- package/diagnostics.mjs +9 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.1.11(2026-11-05)
|
|
4
|
+
|
|
5
|
+
- 登录后自动关闭“是否保存登录信息”弹层(选择取消,不保存密码),避免它挡住消息列表导致读取联系人为 0。
|
|
6
|
+
- 好友互动读取到空消息列表时给出明确提示:当前抖音没有会话,需先在网页版打开“消息”或让好友先发消息。
|
|
7
|
+
|
|
8
|
+
## 0.1.10(2026-11-05)
|
|
9
|
+
|
|
10
|
+
- 自检插件版本改为读取 `package.json`,不再固定显示上游 1.4.6。
|
|
11
|
+
- macOS 上从 Chrome 的 `Info.plist` 读取版本号,修复 `CHROME_VERSION_UNKNOWN` 警告。
|
|
12
|
+
- 调度自检区分“当前时段失败”和“历史时段失败”:历史回执不再误报 `SCHEDULE_START_FAILED`;自动启动关闭时明确提示不会到点启动。
|
|
13
|
+
|
|
3
14
|
## 0.1.9(2026-11-04)
|
|
4
15
|
|
|
5
16
|
- 新增远程短信验证:身份验证时自动打开“接收短信验证码”并请求短信,经微信推送验证界面;用户把 6 位码发回微信后由 `douyin_submit_sms` 填入提交。
|
package/browser-host.mjs
CHANGED
|
@@ -24,6 +24,15 @@ export function classifyBrowserLaunchError(error, { version } = {}) {
|
|
|
24
24
|
return { code, message: (browserVersion ? `标准路径 Chrome 版本 ${browserVersion}。` : '未读取到标准路径 Chrome 版本。') + message, browserVersion, details };
|
|
25
25
|
}
|
|
26
26
|
async function readChromeVersion(file) {
|
|
27
|
+
// macOS 上运行 Chrome 二进制可能直接拉起浏览器或无输出;优先读 Info.plist。
|
|
28
|
+
if (process.platform === 'darwin') {
|
|
29
|
+
try {
|
|
30
|
+
const plist = path.resolve(path.dirname(file), '..', 'Info.plist');
|
|
31
|
+
const result = await execute('/usr/bin/plutil', ['-extract', 'CFBundleShortVersionString', 'raw', '-o', '-', plist], { timeout: 5000, maxBuffer: 2048 });
|
|
32
|
+
const version = versionOf(result.stdout);
|
|
33
|
+
if (version) return version;
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
27
36
|
try {
|
|
28
37
|
const result = await execute(file, ['--version'], { timeout: 5000, maxBuffer: 2048 });
|
|
29
38
|
return versionOf(result.stdout);
|
package/chat-browser.mjs
CHANGED
|
@@ -974,6 +974,14 @@ export class ChatBrowser {
|
|
|
974
974
|
listContacts({ signal, verifyFollowing = this.following === null, backgroundOnly = false } = {}) {
|
|
975
975
|
return this.withPage(async page => {
|
|
976
976
|
if (backgroundOnly && await this.isForeground(page)) throw Object.assign(new Error('专用浏览器在前台,暂不切换私信页。'), { code: 'CHAT_FOREGROUND' });
|
|
977
|
+
// 登录后的“是否保存登录信息”弹层会挡住消息列表,先取消它。
|
|
978
|
+
await page.evaluate(() => {
|
|
979
|
+
if (!/是否保存登录信息/.test(document.body.innerText || '')) return false;
|
|
980
|
+
const usable = element => { const box = element.getBoundingClientRect(), style = getComputedStyle(element); return box.width > 0 && box.height > 0 && style.display !== 'none' && style.visibility !== 'hidden'; };
|
|
981
|
+
const cancel = [...document.querySelectorAll('button,div,span')].find(element => usable(element) && (element.innerText || '').trim() === '取消');
|
|
982
|
+
if (!cancel) return false;
|
|
983
|
+
cancel.click(); return true;
|
|
984
|
+
}).catch(() => false);
|
|
977
985
|
const initial = await this.dialog(page, signal, { open: !backgroundOnly });
|
|
978
986
|
if (initial.blocked) return { ...initial, contacts: [] };
|
|
979
987
|
if (verifyFollowing && !backgroundOnly) {
|
package/client.js
CHANGED
|
@@ -232,6 +232,7 @@ window.__ModuleLoader__.load({
|
|
|
232
232
|
state?.interactions?.friend?.waitReason && h('div', { style: row }, h(Button, { type: 'button', size: 'sm', variant: 'outline', disabled: !ready || busy.has('chatPrepare') || state.interactions.friend.canPrepare === false, onClick: () => { void perform('chatPrepare', () => api.request('/chat/prepare', {})); } }, busy.has('chatPrepare') || state.interactions.friend.preparing ? '正在准备…' : '准备回复通道/重新连接并重试')),
|
|
233
233
|
button('读取联系人与群聊', 'chatList', () => { void perform('chatList', async () => { const result = await api.request('/chat/contacts', {}); if (result.blocked || result.loginRequired || result.verificationRequired || !Array.isArray(result.contacts)) throw new Error('请在专用浏览器完成登录或验证。'); setContacts(result.contacts); setDiscovery(result.listDiscovery || null); return result; }); }, chat.status.kind === 'saving'),
|
|
234
234
|
note('读取范围是抖音网页版当前会话,不代表全部关注好友或所有群。要授权群聊,请先让群会话出现在抖音消息列表,再刷新。'), contacts && note(`本次读取:个人 ${contacts.filter(item => item.isGroup === false).length} 个 · 群聊 ${contacts.filter(item => item.isGroup === true).length} 个。${discovery?.scannedToEnd === false ? ` 当前列表尚未完整读取。${discovery.reason || ''}` : ''}`),
|
|
235
|
+
contacts && contacts.length === 0 && note('抖音消息列表目前是空的(页面显示“暂无会话”),所以没有可授权的个人会话或群聊;先在抖音网页版打开“消息”,或让好友先发一条消息,再点“读取联系人与群聊”。'),
|
|
235
236
|
contacts?.some(item => item.relationshipStatus === 'unavailable' || item.isGroup === true || typeof item.isGroup !== 'boolean') && toggle('授权已核对的现有会话', chat.draft.allowConversationOnly, value => { existingAuthorization.current = value; chat.edit({ allowConversationOnly: value, ...(!value ? { recipientIds: chat.draft.recipientIds.filter(id => shownContacts.some(item => item.id === id && item.followed === true && item.isGroup === false)) } : {}) }, value ? { blurOnly: true } : { immediate: true }); }),
|
|
236
237
|
...[false, true, null].flatMap(kind => { const items = shownContacts.filter(item => item.isGroup === kind || kind === null && typeof item.isGroup !== 'boolean'); return items.length ? [note(kind === false ? '个人会话' : kind === true ? '群聊(仅被 @ 或叫到响应昵称时回复)' : '旧记录类型待核对(暂不可选)'), ...items.map(item => h('label', { key: item.id, style: row }, h('input', { type: 'checkbox', 'aria-label': `许可联系人 ${item.name || item.displayName || '联系人'}`, checked: chat.draft.recipientIds.includes(item.id), disabled: !ready || !chat.draft.recipientIds.includes(item.id) && (item.notListed || !(item.followed === true && item.isGroup === false || chat.draft.allowConversationOnly === true && typeof item.isGroup === 'boolean')), onChange: event => chat.edit({ recipientIds: event.target.checked ? [...chat.draft.recipientIds, item.id] : chat.draft.recipientIds.filter(id => id !== item.id) }, { immediate: true }) }), item.name || item.displayName || '许可会话'))] : []; }),
|
|
237
238
|
area('群聊响应昵称(逗号或换行分隔)', chat, 'groupMentionNamesText', 2, true), status(chat), note(`今日回复模型请求 ${state?.chat?.daily?.requests || 0} 次 · 今日分享 ${state?.share?.daily?.attempts || 0} 次。未确认发送会暂停,不自动重发。`), ...(state?.chat?.contacts || []).filter(item => item.identityReviewRequired === true || item.recovery?.requiresUserAction === true || /登录|验证码|未授权|许可.*(?:不足|失效|撤销)|身份.*(?:变化|不一致|未通过|不明|不足)/.test(item.reason || '') || ['paused', 'delivery-uncertain'].includes(item.status) && !(item.recovery?.autoPendingCount > 0 || item.recovery?.autoRetryCount > 0)).map(item => h('div', { key: item.id }, note(item.reason || '该会话暂缓处理,状态会继续更新。'), item.identityReviewRequired === true && note('请重新读取联系人并确认许可名单;身份未通过前不会发送。'))), ...(state?.share?.unavailableRecipients || []).map(item => note(`暂不能自主分享:${item.name || '许可会话'} · ${item.reason}`)), ...reviewList('share'), h('details', null, h('summary', null, '回复处理记录'), h('pre', { style: { whiteSpace: 'pre-wrap' } }, JSON.stringify({ recovery: state?.chat?.recovery, contacts: (state?.chat?.contacts || []).map(item => ({ id: item.id, status: item.status, reason: item.reason, recovery: item.recovery, pending: Boolean(item.pending), unanswered: item.unanswered?.length || 0 })), events: state?.chat?.events || [] }, null, 2))), h('details', null, h('summary', null, '最近分享记录'), ...(state?.share?.recent || []).slice().reverse().map(item => h('div', { key: item.operationId }, note(`作品 ${item.videoId} · ${state?.share?.inProgress && state.share.activity?.operationId === item.operationId ? '正在分享' : ({ confirmed: '已确认送达', 'not-sent': '未发出', uncertain: '送达未确认', reserved: '已预留,待核对', clicking: '处理中,待核对', 'resolved-confirmed': '人工已确认送达', 'resolved-not-sent': '人工已确认未送达' }[item.status] || item.status)}`), shareFailureMessage(item) && note(`原因:${shareFailureMessage(item)}`))))),
|
package/core.mjs
CHANGED
|
@@ -586,7 +586,20 @@ export class BrowserController {
|
|
|
586
586
|
: loggedIn === false ? '未检测到抖音登录态,点“打开登录页”扫码登录。' : '无法确认登录态;可点“打开登录页”刷新后重试。';
|
|
587
587
|
return { connected: true, pageOpen: true, loggedIn, pageShowsLogin, loginRequired, verificationRequired, url: page.url(), title: state.title || '', note, at: Date.now() };
|
|
588
588
|
}
|
|
589
|
+
// 登录成功后抖音会弹“是否保存登录信息”,会挡住消息面板;这里选择“取消”避免保存密码。
|
|
590
|
+
async _dismissLoginPrompt(page) {
|
|
591
|
+
try {
|
|
592
|
+
return await page.evaluate(() => {
|
|
593
|
+
if (!/是否保存登录信息/.test(document.body.innerText || '')) return false;
|
|
594
|
+
const usable = element => { const box = element.getBoundingClientRect(), style = getComputedStyle(element); return box.width > 0 && box.height > 0 && style.display !== 'none' && style.visibility !== 'hidden'; };
|
|
595
|
+
const cancel = [...document.querySelectorAll('button,div,span')].find(element => usable(element) && (element.innerText || '').trim() === '取消');
|
|
596
|
+
if (!cancel) return false;
|
|
597
|
+
cancel.click(); return true;
|
|
598
|
+
}).catch(() => false);
|
|
599
|
+
} catch { return false; }
|
|
600
|
+
}
|
|
589
601
|
async _bringPageToFront(page) {
|
|
602
|
+
await this._dismissLoginPrompt(page);
|
|
590
603
|
try {
|
|
591
604
|
const session = await page.context().newCDPSession(page);
|
|
592
605
|
try {
|
package/diagnostics.mjs
CHANGED
|
@@ -2,12 +2,13 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
7
|
import { resolveRuntimePaths, resolveNodeExecutable } from './runtime-paths.mjs';
|
|
7
8
|
import { probeChromeInstallation, sanitizeBrowserFailure } from './browser-host.mjs';
|
|
8
9
|
|
|
9
10
|
const execute = promisify(execFile);
|
|
10
|
-
const VERSION = '
|
|
11
|
+
const VERSION = (() => { try { return JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return '0.0.0' } })();
|
|
11
12
|
const REPAIR = 'reapply_silent';
|
|
12
13
|
const resolution = (kind, steps = [], action) => ({ kind, steps, ...(action ? { action } : {}) });
|
|
13
14
|
const automatic = () => resolution('automatic', ['重新连接已核对归属的现有专用浏览器,重新应用已开启的静默设置,再回读窗口和媒体状态。'], REPAIR);
|
|
@@ -125,8 +126,13 @@ export async function runDiagnostics({ source = 'api', modelConfigured = false,
|
|
|
125
126
|
});
|
|
126
127
|
if (watchSchedule) await check('schedule', '允许时段自动启动', () => {
|
|
127
128
|
const state = watchSchedule.state();
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
const last = state.last;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const inCurrentSlot = Boolean(last && Number.isFinite(last.startAt) && Number.isFinite(last.expiresAt) && last.startAt <= now && now < last.expiresAt);
|
|
132
|
+
if (state.policy?.enabled === false) return { message: '自动启动未开启;仅设置允许时段不会到点启动。今天额度用完后也不会自动补跑。', evidence: { enabled: false } };
|
|
133
|
+
if (state.lastError && inCurrentSlot) return { warning: true, code: 'SCHEDULE_START_FAILED', message: '当前允许时段内的自动启动未完成,原调度记录已保留。', resolution: resolution('user', ['核对本报告中的模型、浏览器和运行状态;不要删除时段回执来重复启动。']) };
|
|
134
|
+
if (state.lastError) return { message: '历史时段有一次自动启动失败,已不在当前时段,不会阻止下一次启动。', evidence: { enabled: true, lastErrorAt: last?.completedAt || null } };
|
|
135
|
+
return { message: '已开启按允许时段启动;实际观看仍受模型、每日额度和允许时段约束。', evidence: { enabled: true } };
|
|
130
136
|
});
|
|
131
137
|
if (interactionStates) await check('interactions', '好友互动与发送核对', async () => {
|
|
132
138
|
const states = await interactionStates();
|