Fix notification listener readiness and release 0.1.9
This commit is contained in:
+2
-2
@@ -1,7 +1,7 @@
|
||||
抖音账号助手 0.1.8 — Windows 10/11 x64
|
||||
抖音账号助手 0.1.9 — Windows 10/11 x64
|
||||
|
||||
【安装】
|
||||
只需运行 DouyinAccounts-0.1.8-Windows-x64-Setup.exe。
|
||||
只需运行 DouyinAccounts-0.1.9-Windows-x64-Setup.exe。
|
||||
无需安装 Python、Qt、Patchright、Node.js 或另行下载浏览器。
|
||||
安装程序不会自动启动业务;没有开机自启、自动登录或默认私信。
|
||||
默认使用随包的 fingerprint-chromium(adryfish 项目)和 Patchright 驱动,不再回退到系统 Chrome。
|
||||
|
||||
+2
-2
@@ -168,7 +168,7 @@ def main():
|
||||
break
|
||||
source_hashes = {p.as_posix(): sha256(p) for p in sorted(Path("src").glob("*.py"))}
|
||||
manifest = {
|
||||
"app_version": "0.1.8",
|
||||
"app_version": "0.1.9",
|
||||
"python": sys.version,
|
||||
"chrome": browser,
|
||||
"dependencies": versions,
|
||||
@@ -208,7 +208,7 @@ def main():
|
||||
"onedir 和离线冒烟已完成。安装 Inno Setup 6 后传 --iscc PATH 以生成最终单文件安装包。"
|
||||
)
|
||||
subprocess.run([str(iscc), str(ROOT / "installer.iss")], check=True)
|
||||
installer = ROOT / "release/DouyinAccounts-0.1.8-Windows-x64-Setup.exe"
|
||||
installer = ROOT / "release/DouyinAccounts-0.1.9-Windows-x64-Setup.exe"
|
||||
(installer.parent / "SHA256SUMS.txt").write_text(
|
||||
sha256(installer) + " " + installer.name + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 大号连接核验后整组暂停:监听脚本过期排查
|
||||
|
||||
- 调查日期:2026-10-07
|
||||
- 现象:大号反复显示“正在连接并核验身份 → 连接成功 → 整组暂停,5 秒后复查”;用户观察到浏览器页面可正常使用。
|
||||
- 本次范围:检查本地源码、离线测试与可用运行环境;未执行关注、私信、标记已读等业务操作。随后只修改了本地监听初始化的连接就绪等待与错误诊断,未部署到生产机。
|
||||
|
||||
## 结论
|
||||
|
||||
无法确认生产实例的前端 SDK 是否过期:用户补充这是部署在另一台机器上的生产实例,当前拿不到日志,也无法访问其运行时。源码审查发现一个与“身份核验成功后随即暂停”吻合的真实竞态:监听安装在注册事件监听器后立即检查 WebSocket 是否为 OPEN;若此时仍为 CONNECTING,即使它很快会打开,也会被当作失败并进入统一的 5 秒重试。已在本地修复这个竞态并增加 readyState 诊断,但这属于有依据的防御性修复,不等于已证明生产故障就是它,也不等于已确认 SDK 过期。
|
||||
|
||||
## 源码路径核对
|
||||
|
||||
1. `src/account_engine.py:425-428` 在主循环运行期间每 15 秒调用一次 `session.identity()`。
|
||||
2. 异常处理位于 `src/account_engine.py:500-503`:记录“整组暂停,5 秒后复查:<原因>”,卸载监听并暂停 5 秒,然后进入恢复流程。因此身份接口失败、通知安装失败、推送连接断开等异常都可能出现相同的重复状态。
|
||||
3. `session.identity()` 通过当前浏览器会话读取抖音个人资料并校验 UID。页面仍可浏览,只能说明页面本身可用,不能证明个人资料接口返回、绑定身份校验或通知连接均正常。
|
||||
4. `src/subscribe_notifications.py:27-31` 依赖当前前端运行时的 Webpack 模块特征(通知事件标记与 frame codec),再定位 `NoticeFrontier`。若模块特征变化、实例不存在或连接未就绪,才有理由怀疑监听器与当前前端 SDK 不兼容;连接瞬断也可能产生相似结果,必须结合实际异常区分。
|
||||
5. 当前 `src/` 中未找到“连接成功”这一状态文案。现有源码的连接/核验状态与用户看到的文字不完全一致,可能是打包版本、界面标签或运行分支不同,需先确认实际运行版本。
|
||||
|
||||
## 已做检查与限制
|
||||
|
||||
- 本地工作区此前干净;本机默认数据目录 `~/.local/share/DouyinAccounts` 不存在,也未发现本地运行中的该账号应用进程,因而没有当前运行实例的活动日志可读。
|
||||
- Windows UI 连接中未发现该应用窗口;浏览器运行时只读探测超时,未能验证受影响账号的实际前端模块或通知连接状态。
|
||||
- `python3 src/test_subscribe_notifications.py` 离线测试通过;这些测试使用桩/模拟数据,不代表受影响浏览器的实时监听已通过。
|
||||
- `python3 src/test_browser_matrix.py` 因环境缺少 `patchright` 无法运行。
|
||||
- 项目此前于 2026-09-20 记录过一次实时检查:当时当前页面的 `NoticeFrontier` / WebSocket 与通知分组仍有效;这是历史观测,不足以证明 2026-10-07 的生产实例仍兼容。
|
||||
|
||||
默认数据目录为 Windows `%LOCALAPPDATA%\DouyinAccounts` 或 Linux `~/.local/share/DouyinAccounts`;活动日志位于数据目录下的 `logs/YYYY-MM-DD.log`,UI 中的“运行状态/活动日志”也可查看同一条异常。
|
||||
|
||||
## 本地修复与验证(2026-10-07)
|
||||
|
||||
- `src/subscribe_notifications.py`:安装时若现有 `NoticeFrontier` 尚处于 CONNECTING,最多等待 5 秒的 `open` 事件;遇到 `close` 或超时仍失败,并在成功前重新核对连接实例、readyState 与 UID。未改变 SDK 模块匹配标记,也未新建站点连接。
|
||||
- `src/account_session.py`:连接仍未就绪时把 `readyState` 纳入异常信息,便于界面状态暴露 `0/2/3` 等具体值。
|
||||
- `src/test_subscribe_notifications.py`:Node 桩测试覆盖 CONNECTING→OPEN、关闭失败与监听器清理。
|
||||
- 测试:`python3 src/test_subscribe_notifications.py` 通过;`PYTHONPATH=. python3 -m unittest discover -s src -p 'test_accounts.py' -v` 48 项通过;`py_compile` 与 `git diff --check` 通过。`test_browser_matrix.py` 仍因环境缺少 `patchright` 无法运行。
|
||||
|
||||
## 后续诊断与处理顺序
|
||||
|
||||
1. 修复已随 0.1.9 安装到 Windows 101 机器;此次只打开了主窗口,没有启动账号 worker 或在真实账号浏览器上验证监听结果。用户无法获取远端日志,因此生产实例的具体根因仍未知。部署记录见 `docs/2026-10-07-22-18-windows-0-1-9-deploy.md`。
|
||||
2. 若部署后暂停消失,说明连接就绪竞态是可能原因,但仍不能单凭这一点断言 SDK 已过期。
|
||||
3. 若仍暂停,更新后的状态会显示 WebSocket `readyState`;若异常为“通知 SDK 已变化,无法订阅”或“未找到抖音运行时”,才有直接证据更新模块匹配逻辑。
|
||||
4. 若错误指向身份接口、资料解析或 UID 不一致,应检查 `get_current_user.py` 与账号绑定;不要先改通知解析器。
|
||||
5. 只在本地测试确认后再考虑部署;回归测试不发送真实关注/私信,也不标记已读。
|
||||
@@ -0,0 +1,41 @@
|
||||
# Windows x64 0.1.9 构建与部署记录
|
||||
|
||||
- 日期:2026-10-07
|
||||
- 版本:0.1.9
|
||||
- 目的:部署通知 WebSocket 就绪竞态修复,生成并安装 Windows x64 包。
|
||||
|
||||
## 连接与目标状态
|
||||
|
||||
- 连接方式:Windows UI MCP 用于查看状态和 GUI 冒烟;SSH 使用本机 SSH 配置完成源码传输、原生构建和安装。
|
||||
- 目标:Windows x64。
|
||||
- 操作前检查:桌面没有该应用窗口,也没有检测到该应用进程。默认安装目录已有文件,但当时没有运行中的主程序。
|
||||
- 安装器按原 AppId 覆盖安装;未删除或清理 `%LOCALAPPDATA%\DouyinAccounts` 用户数据目录,其目录仍存在。
|
||||
- 本记录不保存 SSH 地址、账号、密钥、Cookie、UID 或登录目录内容。
|
||||
|
||||
## 本地验证与传输
|
||||
|
||||
部署源码在本地先完成:
|
||||
|
||||
- `PYTHONPATH=. python3 -m unittest discover -s src -p 'test_accounts.py' -v`:48 项通过。
|
||||
- `python3 src/test_subscribe_notifications.py`:通过,覆盖 CONNECTING→OPEN、关闭失败和监听器清理。
|
||||
- Python 编译检查及 `git diff --check`:通过。
|
||||
- `test_browser_matrix.py` 未运行:构建环境缺少 `patchright`。
|
||||
|
||||
仅传输了 33 个构建所需源码/配置文件,不含数据库、浏览器用户目录、Cookie 或运行日志。传输归档 SHA-256:
|
||||
|
||||
`d3030c28ee890eb622837739fc458a70ba9a994159c3ced6498fd99b799d7b2f`
|
||||
|
||||
## Windows 原生构建与安装
|
||||
|
||||
- 构建在 Windows x64 上运行 `python .\build_windows.py`;Python 3.12.10,使用锁定的 fingerprint-chromium 148.0.7778.215 和 Inno Setup。
|
||||
- 构建脚本退出成功,生成的 manifest 标记版本 0.1.9;隔离的打包冒烟检查通过。
|
||||
- 安装包:`C:\Users\Public\Documents\DouyinAccountsBuild-20261007-0.1.9\release\DouyinAccounts-0.1.9-Windows-x64-Setup.exe`
|
||||
- 安装包大小:222,711,880 bytes。
|
||||
- 安装包 SHA-256:`e4f4c302fd14a04c854a3bfa03b9ae93eda0f200e7dc79d9a7c5911fa21ea9ab`
|
||||
- 使用 Inno Setup 静默安装;退出码 0。安装后程序文件位于 `%LOCALAPPDATA%\Programs\DouyinAccounts`;已安装主程序 SHA-256 与构建输出一致:`65e27b1a7e4c3817bce7744f20c0cc0889aaed760cb7e538dc5bd8eedae45b85`。
|
||||
|
||||
## GUI 冒烟与安全边界
|
||||
|
||||
- 安装后通过 Windows UI MCP 从开始菜单启动应用,主窗口成功显示。
|
||||
- 未登录账号、未恢复业务规则,也未执行关注、私信、标记已读等写操作。GUI 冒烟后未检测到 Chromium/远程调试浏览器进程,未启动账号 worker;应用目前留在主窗口供人工验收。
|
||||
- 这验证了安装包能安装、主程序能打开;没有在真实账号上验证通知监听或确认生产故障已消失。监听就绪竞态修复及其限制见 `docs/2026-10-07-20-45-main-account-listener-retry-diagnosis.md`。
|
||||
+2
-2
@@ -2,14 +2,14 @@
|
||||
[Setup]
|
||||
AppId={{C9C3EE3B-3666-4E41-A52B-BB309838F701}
|
||||
AppName=抖音账号助手
|
||||
AppVersion=0.1.8
|
||||
AppVersion=0.1.9
|
||||
DefaultDirName={localappdata}\Programs\DouyinAccounts
|
||||
DefaultGroupName=抖音账号助手
|
||||
PrivilegesRequired=lowest
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
OutputDir=release
|
||||
OutputBaseFilename=DouyinAccounts-0.1.8-Windows-x64-Setup
|
||||
OutputBaseFilename=DouyinAccounts-0.1.9-Windows-x64-Setup
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
|
||||
@@ -186,8 +186,15 @@ class Session:
|
||||
),
|
||||
main_world=True,
|
||||
)
|
||||
if not result.get("connected"):
|
||||
raise SessionError("通知连接未就绪,整组已暂停")
|
||||
if not isinstance(result, dict) or not result.get("connected"):
|
||||
ready_state = (
|
||||
result.get("readyState", "unknown")
|
||||
if isinstance(result, dict)
|
||||
else "unknown"
|
||||
)
|
||||
raise SessionError(
|
||||
f"通知 WebSocket 未就绪(readyState={ready_state}),整组已暂停"
|
||||
)
|
||||
|
||||
async def uninstall(self):
|
||||
if self.page and not self.page.is_closed():
|
||||
|
||||
@@ -16,7 +16,7 @@ from get_current_user import get_user_from_browser
|
||||
NOTICE_EFFECT_GROUPS = {"700", "960", "961"}
|
||||
|
||||
# 不改写站点 onmessage、不新建连接;退出时仅卸载自己的监听器。
|
||||
INSTALL = r"""(() => {
|
||||
INSTALL = r"""(async () => {
|
||||
if (location.origin !== 'https://www.douyin.com') throw Error('请选择已登录的抖音标签页');
|
||||
const key = KEY, uid = UID, chunks = window.webpackChunkdouyin_web;
|
||||
if (!chunks) throw Error('未找到抖音运行时');
|
||||
@@ -58,7 +58,28 @@ INSTALL = r"""(() => {
|
||||
if (state.wake) state.wake();
|
||||
};
|
||||
window[key] = state;
|
||||
return JSON.stringify({connected: f.readyState === f.OPEN});
|
||||
// Page rendering may finish before the existing notification socket opens.
|
||||
const opened = f.readyState === f.OPEN || await new Promise(resolve => {
|
||||
let settled = false;
|
||||
const finish = connected => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
f.removeEventListener('open', onOpen);
|
||||
f.removeEventListener('close', onClose);
|
||||
resolve(connected && f.readyState === f.OPEN);
|
||||
};
|
||||
const onOpen = () => finish(true);
|
||||
const onClose = () => finish(false);
|
||||
const timer = setTimeout(() => finish(false), 5000);
|
||||
f.addEventListener('open', onOpen);
|
||||
f.addEventListener('close', onClose);
|
||||
if (f.readyState === f.OPEN) finish(true);
|
||||
});
|
||||
const readyState = f.readyState;
|
||||
const connected = opened && readyState === f.OPEN
|
||||
&& C.frontierInstance === f && String(f._options.deviceID) === uid;
|
||||
return JSON.stringify({connected, readyState});
|
||||
})()"""
|
||||
|
||||
WAIT = r"""(async () => {
|
||||
|
||||
@@ -729,6 +729,16 @@ class EngineTest(unittest.IsolatedAsyncioTestCase):
|
||||
await action
|
||||
session.evaluate.assert_not_called()
|
||||
|
||||
async def test_install_reports_websocket_readiness_state(self):
|
||||
session = Session(None, "http://127.0.0.1:9222", "101")
|
||||
session.identity = AsyncMock()
|
||||
session.uninstall = AsyncMock()
|
||||
session.json = AsyncMock(
|
||||
return_value={"connected": False, "readyState": 0}
|
||||
)
|
||||
with self.assertRaisesRegex(SessionError, "readyState=0"):
|
||||
await session.install()
|
||||
|
||||
async def test_name_only_enrollment_waits_then_backfills_and_starts(self):
|
||||
ident = self.engine.store.add("main", "只填名称")
|
||||
session = AsyncMock()
|
||||
|
||||
@@ -278,7 +278,7 @@ def test_js_bridge():
|
||||
const assert = require('node:assert/strict');
|
||||
const input = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));
|
||||
const listeners = {};
|
||||
const f = {_options:{deviceID:'u'}, readyState:1, OPEN:1,
|
||||
const f = {_options:{deviceID:'u'}, readyState:0, OPEN:1,
|
||||
addEventListener(type, callback) { (listeners[type] ||= new Set()).add(callback); },
|
||||
removeEventListener(type, callback) { listeners[type].delete(callback); }
|
||||
};
|
||||
@@ -291,7 +291,15 @@ global.window = {webpackChunkdouyin_web:[]};
|
||||
window.webpackChunkdouyin_web.push = function(chunk) { chunk[2](r); return Array.prototype.push.call(this, chunk); };
|
||||
global.location = {origin:'https://www.douyin.com'};
|
||||
(async () => {
|
||||
assert.equal(JSON.parse(eval(input.install)).connected, true);
|
||||
const installing = eval(input.install);
|
||||
setTimeout(() => {
|
||||
f.readyState = 1;
|
||||
for (const callback of listeners.open) callback();
|
||||
}, 20);
|
||||
const installed = JSON.parse(await installing);
|
||||
assert.equal(installed.connected, true);
|
||||
assert.equal(installed.readyState, 1);
|
||||
assert.equal(JSON.parse(await eval(input.wait))[0].kind, 'open');
|
||||
const send = frame => {for(const callback of listeners.message) callback({data:new TextEncoder().encode(JSON.stringify(frame))});};
|
||||
send({service:999,payload:[]});
|
||||
assert.equal(window.probe.queue.length,0);
|
||||
@@ -309,7 +317,19 @@ global.location = {origin:'https://www.douyin.com'};
|
||||
assert.equal(listeners.message.size,0);
|
||||
assert.equal(listeners.open.size,0);
|
||||
assert.equal(listeners.close.size,0);
|
||||
assert.equal(f.readyState,1);
|
||||
f.readyState = 0;
|
||||
const closing = eval(input.install);
|
||||
setTimeout(() => {
|
||||
f.readyState = 3;
|
||||
for (const callback of listeners.close) callback();
|
||||
}, 20);
|
||||
const disconnected = JSON.parse(await closing);
|
||||
assert.equal(disconnected.connected, false);
|
||||
assert.equal(disconnected.readyState, 3);
|
||||
window.probe.dispose();
|
||||
assert.equal(listeners.message.size,0);
|
||||
assert.equal(listeners.open.size,0);
|
||||
assert.equal(listeners.close.size,0);
|
||||
C.frontierInstance=null;
|
||||
await assert.rejects(eval(input.wait),/登录失效/);
|
||||
console.log('JS bridge checks passed');
|
||||
|
||||
Reference in New Issue
Block a user