diff --git a/docs/2026-09-07-19-52-history-event-split.md b/docs/2026-09-07-19-52-history-event-split.md new file mode 100644 index 0000000..98429fc --- /dev/null +++ b/docs/2026-09-07-19-52-history-event-split.md @@ -0,0 +1,26 @@ +# 历史事件类型拆分 + +## 修改 + +历史事件预览现在按独立事件类型展示: + +- 点赞 +- 评论 +- 收藏 +- 分享 +- 关注 +- 其他作品互动 + +后端新增 `favorite` 和 `share` 类型;平台返回的 `collect` 归一为 `favorite`。历史缓存读取时会根据原始业务数据重新归一化,旧缓存也能正确显示新类型。 + +已有只勾选“其他作品互动”的旧规则继续兼容匹配收藏和分享;新规则可单独勾选收藏或分享。 + +## 验证与部署 + +- 本地测试:57 个通过。 +- Windows 原生测试:56 个通过。 +- Windows 原生构建成功,耗时约 328 秒。 +- 已静默安装到 `C:\Users\Rogee\AppData\Local\Programs\DouyinAccounts`。 +- 最新安装包已复制到 `C:\Users\Rogee\Desktop\DouyinAccounts-0.1.8-Windows-x64-Setup.exe`。 +- 已安装可执行文件与 dist 文件 SHA256 一致:`EB2C6067091596E2E0217E3F4029227D191DE...` +- 安装后离线冒烟测试通过,未执行真实业务写操作;应用当前未运行。 diff --git a/src/account_store.py b/src/account_store.py index dc6a29f..201d683 100644 --- a/src/account_store.py +++ b/src/account_store.py @@ -12,6 +12,23 @@ from account_log import UI_LIMIT, visible from follow_user import validate_uid +NOTICE_KIND_LABELS = { + "digg": "点赞", + "follow": "关注", + "comment": "评论", + "favorite": "收藏", + "share": "分享", + "general_notice": "其他作品互动", +} +NOTICE_KINDS = tuple(NOTICE_KIND_LABELS) + + +def rule_matches_kind(kinds, kind): + return kind in kinds or ( + kind in {"favorite", "share"} and "general_notice" in kinds + ) + + def validate_rule(rule): rule = dict(rule) rule["require_follow"] = False @@ -20,9 +37,7 @@ def validate_rule(rule): rule.setdefault("work_ids", []) rule.setdefault("works_refresh_interval", 3600) kinds = rule.get("kinds", []) - if not isinstance(kinds, list) or any( - k not in ("digg", "follow", "comment", "general_notice") for k in kinds - ): + if not isinstance(kinds, list) or any(k not in NOTICE_KINDS for k in kinds): raise ValueError("请选择有效通知类型") for flag in ("enabled", "follow", "dm", "require_follow"): if type(rule.get(flag)) is not bool: @@ -83,7 +98,14 @@ def notice_work_id(notice): values = [notice] values.extend( notice.get(key) - for key in ("comment", "digg", "favorite", "collect", "general_notice") + for key in ( + "comment", + "digg", + "favorite", + "collect", + "share", + "general_notice", + ) ) for value in values: if not isinstance(value, dict): @@ -106,12 +128,23 @@ def notice_kind(notice): return "follow" if notice.get("digg"): return "digg" + if notice.get("favorite") or notice.get("collect"): + return "favorite" + if notice.get("share"): + return "share" return "general_notice" def notice_preview(notice): kind = notice_kind(notice) - detail = notice.get(kind) or notice.get("favorite") or notice.get("collect") or {} + detail = ( + notice.get(kind) + or notice.get("favorite") + or notice.get("collect") + or notice.get("share") + or notice.get("general_notice") + or {} + ) users = detail.get("from_user") or [] if isinstance(users, dict): users = [users] @@ -568,9 +601,10 @@ class Store: selected = set(rule["work_ids"]) result = [] for row in rows: + kind = notice_kind(decode(row["business"])) if ( rule["work_mode"] == "selected" - and row["kind"] != "follow" + and kind != "follow" and row["work_id"] not in selected ): continue @@ -578,7 +612,7 @@ class Store: { "nid": row["nid"], "create_time": row["create_time"], - "kind": row["kind"], + "kind": kind, "work_id": row["work_id"], "work_desc": row["work_desc"], "actor_uids": decode(row["actor_uids"]), @@ -734,7 +768,12 @@ class Store: self.cache_notices(source, [notice], origin) kind = notice_kind(notice) detail = ( - notice.get(kind) or notice.get("favorite") or notice.get("collect") or {} + notice.get(kind) + or notice.get("favorite") + or notice.get("collect") + or notice.get("share") + or notice.get("general_notice") + or {} ) users = detail.get("from_user") or [] if isinstance(users, dict): @@ -750,7 +789,10 @@ class Store: or work_id in rule["work_ids"] ) enabled = ( - rule["enabled"] and kind in rule["kinds"] and bool(targets) and work_allowed + rule["enabled"] + and rule_matches_kind(rule["kinds"], kind) + and bool(targets) + and work_allowed ) created = notice.get("create_time") or notice.get("createTime") or time.time() if type(created) not in (int, float) or created <= 0: @@ -796,7 +838,7 @@ class Store: else "规则未启用,本通知不生成任务" if not rule["enabled"] else "通知类型未勾选,本通知不生成任务" - if kind not in rule["kinds"] + if not rule_matches_kind(rule["kinds"], kind) else "未提取到有效通知来源用户,不生成任务" if not targets else "通知不属于当前选择的作品,不生成任务" @@ -808,12 +850,7 @@ class Store: nid=nid, event=event["id"], 来源="实时通知" if origin == "live" else "历史列表", - 通知类型={ - "digg": "点赞", - "comment": "评论", - "follow": "关注", - "general_notice": "其他作品互动", - }[kind], + 通知类型=NOTICE_KIND_LABELS[kind], 作品ID=work_id, 目标数量=len(targets), ) diff --git a/src/accounts_app.py b/src/accounts_app.py index c71c793..e637bdd 100644 --- a/src/accounts_app.py +++ b/src/accounts_app.py @@ -48,7 +48,7 @@ from PySide6.QtWidgets import ( from account_browser import AppLock, browser_executable, default_root, protect_root from account_engine import Engine from account_log import UI_LIMIT, DailyLog -from account_store import decode, validate_rule +from account_store import NOTICE_KIND_LABELS, decode, validate_rule IMAGE_HOST_SUFFIXES = ( @@ -595,12 +595,7 @@ class Window(QMainWindow): def history_dialog(self, payload): items = payload["items"] - labels = { - "digg": "点赞/作品互动", - "comment": "评论", - "follow": "关注", - "general_notice": "其他作品互动", - } + labels = NOTICE_KIND_LABELS win = QDialog(self) win.setWindowTitle(f"历史事件预览 — 当前可选 {len(items)} 条(尚未创建任务)") win.resize(1500, 800) @@ -1045,7 +1040,9 @@ class Window(QMainWindow): ("digg", "点赞"), ("follow", "关注"), ("comment", "评论"), - ("general_notice", "收藏/其他作品互动(仅有明确来源 UID 时)"), + ("favorite", "收藏"), + ("share", "分享"), + ("general_notice", "其他作品互动(仅有明确来源 UID 时)"), ]: check = QCheckBox(label) check.setChecked(kind in rule["kinds"]) diff --git a/src/get_notifications.py b/src/get_notifications.py index 5b9cf5c..4baf64e 100644 --- a/src/get_notifications.py +++ b/src/get_notifications.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from urllib.parse import quote, urlencode +from account_store import notice_kind from cdp_explicit import add_cdp_args, configure, evaluate_script from get_current_user import get_user_from_browser @@ -54,10 +55,14 @@ def get_notifications(max_time: int = 0) -> dict: def summarize(notice: dict) -> dict: - kind = next( - (k for k in ("digg", "follow", "comment") if notice.get(k)), "general_notice" + kind = notice_kind(notice) + detail = ( + notice.get(kind) + or notice.get("favorite") + or notice.get("collect") + or notice.get("share") + or {} ) - detail = notice.get(kind) or {} users = detail.get("from_user") or [] if isinstance(users, dict): users = [users] diff --git a/src/test_accounts.py b/src/test_accounts.py index 0e3649c..d432911 100644 --- a/src/test_accounts.py +++ b/src/test_accounts.py @@ -19,7 +19,13 @@ from account_browser import ( ) from account_engine import Engine from account_session import Session, SessionError, validate_endpoint -from account_store import DEFAULT_RULE, Store, validate_rule +from account_store import ( + DEFAULT_RULE, + Store, + notice_kind, + notice_preview, + validate_rule, +) from build_windows import extract_browser @@ -63,6 +69,19 @@ class StoreTest(unittest.TestCase): self.store.close() self.temp.cleanup() + def test_notification_kinds_split_favorite_and_share(self): + for raw_kind, expected in ( + ("digg", "digg"), + ("comment", "comment"), + ("favorite", "favorite"), + ("collect", "favorite"), + ("share", "share"), + ): + raw = work_notice("101", raw_kind, "902", "700", raw_kind) + self.assertEqual(notice_kind(raw), expected) + self.assertEqual(notice_preview(raw)["kind"], expected) + validate_rule(rule(kinds=["favorite", "share"])) + def test_defaults_validation_unique_identity(self): with self.assertRaises(sqlite3.IntegrityError): self.store.add("worker", "重复", "101")