feat(p3): add discovery, telegram notifications, and one-click favorites

Implement WBI video search with legacy fallback, classification scoring, candidate pool, permanent seen-aid dedupe, blacklist filtering, and batched Telegram notifications. Add write client for fav deal/sort, target-folder selection, insertion position calculation, read-back verification, fav_action locking, retry on failure, ignore and blacklist actions. Wire discovery, candidate cleanup, Telegram long polling, and new discover/candidates CLI commands into the scheduler. Add migration v3 for candidate duration and index stats.
This commit is contained in:
kairee committed 2026-10-31 21:23:12 +08:00
1 parent 6a21b5d905
commit d971e718e9
31 files changed
+2886 -55

No files matched your search

+36
View File
@@ -18,6 +18,7 @@ from typing import Any, Mapping
DEFAULT_CLASSIFY: dict[str, Any] = {
"require_title": "咻咻满",
"self_mid": 37754047,
"positive": [
"reaction", "re", "反应", "看", "听", "初听", "首次",
"点评", "锐评", "路人", "震惊", "双人",
@@ -64,6 +65,11 @@ DEFAULT_CONFIG: dict[str, Any] = {
"snapshot_at": "00:01",
"candidate_ttl_days": 7,
},
"discover": {
"keywords": ["咻咻满 reaction", "咻咻满 充电"],
"pages": 2,
"window_days": 14,
},
"notify": {"min_score": 2, "max_items_per_message": 10},
"classify": DEFAULT_CLASSIFY,
"heat_weights": DEFAULT_HEAT_WEIGHTS,
@@ -148,6 +154,13 @@ class NotifyConfig:
max_items_per_message: int
@dataclass(frozen=True)
class DiscoverConfig:
keywords: tuple[str, ...]
pages: int
window_days: int
@dataclass(frozen=True)
class AppConfig:
path: Path
@@ -160,6 +173,7 @@ class AppConfig:
up_blacklist_seed: tuple[int, ...]
schedule: ScheduleConfig
notify: NotifyConfig
discover: DiscoverConfig
classify: Mapping[str, Any]
heat_weights: Mapping[str, float]
raw: Mapping[str, Any] = field(repr=False, compare=False)
@@ -367,6 +381,24 @@ def _build_config(data: Mapping[str, Any], path: Path) -> AppConfig:
),
)
discover_raw = data["discover"]
if not isinstance(discover_raw, Mapping):
raise ConfigError("discover 必须是对象")
keywords = discover_raw.get("keywords", [])
if not isinstance(keywords, list) or not keywords:
raise ConfigError("discover.keywords 必须是非空数组")
discover = DiscoverConfig(
keywords=tuple(
_as_str(item, "discover.keywords[]") for item in keywords
),
pages=_as_int(
discover_raw.get("pages", 2), "discover.pages", minimum=1, maximum=10
),
window_days=_as_int(
discover_raw.get("window_days", 14), "discover.window_days", minimum=1
),
)
classify_raw = data["classify"]
if not isinstance(classify_raw, Mapping):
raise ConfigError("classify 必须是对象")
@@ -376,6 +408,9 @@ def _build_config(data: Mapping[str, Any], path: Path) -> AppConfig:
)
if not classify["require_title"]:
raise ConfigError("classify.require_title 不能为空")
classify["self_mid"] = _as_int(
classify.get("self_mid", 37754047), "classify.self_mid", minimum=1
)
for key in ("positive", "charge", "negative"):
classify[key] = _as_str_list(classify.get(key, []), f"classify.{key}")
boost = classify.get("duration_boost", {})
@@ -407,6 +442,7 @@ def _build_config(data: Mapping[str, Any], path: Path) -> AppConfig:
up_blacklist_seed=_as_int_list(data["up_blacklist_seed"], "up_blacklist_seed"),
schedule=schedule,
notify=notify,
discover=discover,
classify=classify,
heat_weights=heat,
raw=data,