重构无人直播助手:内嵌 youLIVE、摄像头直推、推流诊断与 UI 焕新

将 README 改写为产品文档;移除微信/EasyTier/工作台等遗留模块;新增 youLIVE 目标端监控、摄像头推流、质量守卫与转播历史能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric
2026-07-07 20:50:44 -05:00
parent d28e64f4ef
commit d61526698d
140 changed files with 24209 additions and 3721 deletions

View File

@@ -11,6 +11,7 @@ from pathlib import Path
from typing import Any, Callable, Awaitable
from web2_youtube_ini import parse_youtube_ini, serialize_youtube_ini
from web2_config_governance import backup_config_file
# 与 web2_client_overview 一致的去重键
def _norm_douyin_url(url: str) -> str:
@@ -182,9 +183,11 @@ def list_channels_for_pro(config_dir: Path) -> dict[str, Any]:
"name": name,
"douyinUrl": durl,
"keyPreview": kp,
"youtubeWatchUrl": get_youtube_watch_url(config_dir, cid),
"configured": bool(kp or durl),
}
)
return {"ok": True, "channels": out}
return {"ok": True, "channels": out, "singleYoutubeWatchUrl": get_single_youtube_watch_url(config_dir)}
def _mask_key_preview(key: str) -> str:
@@ -219,6 +222,79 @@ def _all_keys_for_channel(item: dict) -> list[str]:
return [k] if k else []
def get_single_youtube_watch_url(config_dir: Path) -> str:
doc, _ = load_channels_document(config_dir)
if doc is None:
return ""
return str(doc.get("singleYoutubeWatchUrl") or doc.get("single_youtube_watch_url") or "").strip()
def get_youtube_watch_url(config_dir: Path, channel_id: str) -> str:
cid = (channel_id or "").strip()
if not cid or cid == "single":
return get_single_youtube_watch_url(config_dir)
ch = get_channel_by_id(config_dir, cid)
if not ch:
return ""
return str(ch.get("youtubeWatchUrl") or ch.get("youtube_watch_url") or "").strip()
def set_youtube_watch_url(config_dir: Path, channel_id: str, url: str) -> tuple[bool, str]:
url = (url or "").strip()
cid = (channel_id or "").strip()
if not cid:
return False, "缺少 channelId"
config_dir = Path(config_dir)
if cid == "single":
doc, errs = load_channels_document(config_dir)
if doc is None:
ensure_default_relay_config(config_dir)
doc, _ = load_channels_document(config_dir)
if doc is None:
return False, errs[0] if errs else "无法读取配置"
doc["singleYoutubeWatchUrl"] = url
channels_json_path(config_dir).write_text(
json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8"
)
return True, "已保存"
doc, errs = load_channels_document(config_dir)
if doc is None:
return False, errs[0] if errs else "无法读取配置"
arr = doc.get("channels")
if not isinstance(arr, list):
return False, "channels 无效"
for item in arr:
if not isinstance(item, dict):
continue
if str(item.get("id") or "").strip() != cid:
continue
item["youtubeWatchUrl"] = url
channels_json_path(config_dir).write_text(
json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8"
)
return True, "已保存"
return False, "未找到该频道"
def watch_url_for_relay_process(config_dir: Path, process: str) -> str:
prefix = "youtube2__"
if process.startswith(prefix):
return get_youtube_watch_url(config_dir, process[len(prefix) :])
return get_single_youtube_watch_url(config_dir)
def channel_configured_for_monitor(config_dir: Path, channel_id: str) -> bool:
ch = get_channel_by_id(config_dir, channel_id)
if not ch:
return False
if str(ch.get("youtubeKey") or ch.get("key") or "").strip():
return True
if str(ch.get("douyinUrl") or ch.get("douyin_url") or "").strip():
return True
url_text = read_url_config_for_channel(config_dir, channel_id)
return bool(extract_url_lines_from_ini(url_text))
def get_channel_detail(config_dir: Path, channel_id: str) -> dict[str, Any] | None:
ch = get_channel_by_id(config_dir, channel_id)
if not ch:
@@ -240,6 +316,7 @@ def get_channel_detail(config_dir: Path, channel_id: str) -> dict[str, Any] | No
"youtubeKey": active,
"youtubeKeys": keys,
"youtubeActiveIndex": ai,
"youtubeWatchUrl": get_youtube_watch_url(config_dir, str(ch.get("id") or "").strip()),
}
@@ -376,6 +453,8 @@ def write_url_config_for_channel(config_dir: Path, channel_id: str, content: str
return False, errs[0]
p = url_config_path_for_channel(config_dir, channel_id)
p.parent.mkdir(parents=True, exist_ok=True)
rel = f"relay_pro/{p.name}"
backup_config_file(config_dir, rel, reason="relay_pro_url")
p.write_text(content, encoding="utf-8")
return True, "保存成功"