feat: EveryPublish Web 管理端重构与全平台发布接入

- 新增 React + TDesign 管理端(sau_web_react),替代历史 Vue 前端
- 后端接入 11 平台登录/发布(视频+图文),check_cookie 扩展全平台
- 异步发布任务 + 实时进度监控 + 发布日志
- Open API 开放体系(API Key 鉴权 + 7 个开放接口 + API 文档页)
- 账号单/批量校验与批量删除
- 全局配置(登录方式 + 发布静默模式)
- 统一浏览器为系统 Chrome,品牌文案统一为 EveryPublish
This commit is contained in:
Qiufeng
2026-08-21 14:41:47 +08:00
parent 1c66b7db4b
commit fd944b08bd
120 changed files with 6684 additions and 210 deletions
+4 -4
View File
@@ -114,7 +114,7 @@ async def cookie_auth(account_file):
return False
use_headless = os.environ.get("DOUYIN_COOKIE_AUTH_HEADLESS", "true").lower() in ("1", "true", "yes")
launch_kwargs = {"headless": use_headless, "channel": "chromium", "args": ["--no-sandbox", "--disable-blink-features=AutomationControlled"]}
launch_kwargs = {"headless": use_headless, "executable_path": LOCAL_CHROME_PATH, "args": ["--no-sandbox", "--disable-blink-features=AutomationControlled"]}
for _attempt in range(3):
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(**launch_kwargs)
@@ -276,7 +276,7 @@ async def douyin_cookie_gen(
context = browser.contexts[0] if browser.contexts else await browser.new_context()
should_close_context = False
else:
browser = await playwright.chromium.launch(headless=headless, channel="chromium")
browser = await playwright.chromium.launch(headless=headless, executable_path=LOCAL_CHROME_PATH)
context = await browser.new_context()
should_close_context = True
context = await set_init_script(context)
@@ -973,7 +973,7 @@ class DouYinVideo(DouYinBaseUploader):
await self.validate_upload_args()
douyin_logger.info(_msg("🥳", "上传前检查通过"))
browser = await playwright.chromium.launch(headless=self.headless, channel="chromium", args=["--no-sandbox", "--disable-blink-features=AutomationControlled"])
browser = await playwright.chromium.launch(headless=self.headless, executable_path=LOCAL_CHROME_PATH, args=["--no-sandbox", "--disable-blink-features=AutomationControlled"])
context = await browser.new_context(
storage_state=f"{self.account_file}",
permissions=["geolocation"],
@@ -1232,7 +1232,7 @@ class DouYinNote(DouYinBaseUploader):
await self.validate_upload_args()
douyin_logger.info(_msg("🥳", "图文上传前检查通过"))
browser = await playwright.chromium.launch(headless=self.headless, channel="chromium", args=["--no-sandbox", "--disable-blink-features=AutomationControlled"])
browser = await playwright.chromium.launch(headless=self.headless, executable_path=LOCAL_CHROME_PATH, args=["--no-sandbox", "--disable-blink-features=AutomationControlled"])
context = await browser.new_context(
storage_state=f"{self.account_file}",
permissions=["geolocation"],
+4 -4
View File
@@ -211,7 +211,7 @@ async def cookie_auth(account_file):
if LOCAL_CHROME_PATH:
browser = await playwright.chromium.launch(headless=True, executable_path=LOCAL_CHROME_PATH)
else:
browser = await playwright.chromium.launch(headless=True, channel="chromium")
browser = await playwright.chromium.launch(headless=True, executable_path=LOCAL_CHROME_PATH)
try:
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
@@ -285,7 +285,7 @@ async def get_ks_cookie(
if LOCAL_CHROME_PATH:
browser = await playwright.chromium.launch(headless=headless, executable_path=LOCAL_CHROME_PATH)
else:
browser = await playwright.chromium.launch(headless=headless, channel="chromium")
browser = await playwright.chromium.launch(headless=headless, executable_path=LOCAL_CHROME_PATH)
context = await browser.new_context()
should_close_context = True
context = await set_init_script(context)
@@ -613,7 +613,7 @@ class KSVideo(KSBaseUploader):
else:
browser = await playwright.chromium.launch(
headless=self.headless,
channel="chromium",
executable_path=LOCAL_CHROME_PATH,
)
context = await browser.new_context(storage_state=self.account_file)
context = await set_init_script(context)
@@ -863,7 +863,7 @@ class KSNote(KSBaseUploader):
else:
browser = await playwright.chromium.launch(
headless=self.headless,
channel="chromium",
executable_path=LOCAL_CHROME_PATH,
)
context = await browser.new_context(storage_state=self.account_file)
context = await set_init_script(context)
+63 -3
View File
@@ -1187,13 +1187,73 @@ class TencentNote(TencentBaseUploader):
self.image_paths = normalized_image_paths
async def switch_to_note_mode(self, page: Page) -> None:
raise NotImplementedError("请在 TencentNote.switch_to_note_mode 中补充视频号切换到图文发布模式的逻辑")
"""切换到图文发布模式。
视频号发布页(/platform/post/create)默认是视频模式,图文需切到图片模式。
优先点「发表图片」入口,再尝试页面内「图片/图文」tab,找不到则按当前页面继续。
"""
for label in ["发表图片", "发布图片", "图片动态"]:
try:
entry = page.locator("button", has_text=label).first
if await entry.count():
await entry.wait_for(state="visible", timeout=8000)
await entry.click()
await page.wait_for_timeout(1500)
tencent_logger.info(_msg("🖼️", f"已点击「{label}」切换到图文模式"))
return
except Exception:
continue
for text in ["图片", "图文"]:
try:
tab = page.locator('[role="tab"]', has_text=text).first
if await tab.count():
await tab.click(timeout=5000)
await page.wait_for_timeout(1000)
tencent_logger.info(_msg("🖼️", f"已点击「{text}」tab 切换到图文模式"))
return
except Exception:
continue
tencent_logger.warning(_msg("⚠️", "未找到明确的图文入口,按当前页面继续尝试上传图片"))
async def upload_note_images(self, page: Page) -> None:
raise NotImplementedError("请在 TencentNote.upload_note_images 中补充视频号图文图片上传逻辑")
"""上传图文图片(优先 accept 含 image 的 file input,兜底任意 file input)。"""
async def find_image_input():
for fr in page.frames:
try:
fi = fr.locator('input[type="file"][accept*="image"]')
if await fi.count():
return fi.first
except Exception:
continue
for fr in page.frames:
try:
fi = fr.locator('input[type="file"]')
if await fi.count():
return fi.first
except Exception:
continue
return None
fi = await find_image_input()
for _ in range(60):
if fi is not None:
break
await asyncio.sleep(1)
fi = await find_image_input()
if fi is None:
shot = Path(BASE_DIR) / "debug_tencent_note_no_input.png"
try:
await page.screenshot(path=str(shot), full_page=True)
tencent_logger.info(_msg("📸", f"图文失败现场已截图 {shot}; url={page.url}; frames={[fr.url[:80] for fr in page.frames]}"))
except Exception:
pass
raise RuntimeError("未找到视频号图文图片上传框")
await fi.set_input_files(self.image_paths)
tencent_logger.info(_msg("📤", f"已上传 {len(self.image_paths)} 张图文图片"))
async def fill_note_title_and_tags(self, page: Page) -> None:
raise NotImplementedError("请在 TencentNote.fill_note_title_and_tags 中补充视频号图文标题/话题填写逻辑")
"""填写图文标题和话题(复用视频的 div.input-editor 编辑区)。"""
await self.fill_title_and_tags(page)
async def fill_note_body(self, page: Page) -> None:
return None
+3 -2
View File
@@ -15,7 +15,7 @@ from utils.log import tiktok_logger
async def cookie_auth(account_file):
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS)
browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS, executable_path=LOCAL_CHROME_PATH)
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
# 创建一个新的页面
@@ -55,7 +55,8 @@ async def get_tiktok_cookie(account_file):
'args': [
'--lang en-GB',
],
'headless': LOCAL_CHROME_HEADLESS, # Set headless option here
'headless': LOCAL_CHROME_HEADLESS,
'executable_path': LOCAL_CHROME_PATH,
}
# Make sure to run headed.
browser = await playwright.chromium.launch(**options)
+4 -4
View File
@@ -182,7 +182,7 @@ async def cookie_auth(account_file):
if LOCAL_CHROME_PATH:
browser = await playwright.chromium.launch(headless=True, executable_path=LOCAL_CHROME_PATH)
else:
browser = await playwright.chromium.launch(headless=True, channel="chromium")
browser = await playwright.chromium.launch(headless=True, executable_path=LOCAL_CHROME_PATH)
try:
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
@@ -253,7 +253,7 @@ async def xiaohongshu_cookie_gen(
account_path.parent.mkdir(parents=True, exist_ok=True)
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=headless, channel="chromium")
browser = await playwright.chromium.launch(headless=headless, executable_path=LOCAL_CHROME_PATH)
context = await browser.new_context()
context = await set_init_script(context)
qrcode_path = None
@@ -702,7 +702,7 @@ class XiaoHongShuVideo(XiaoHongShuBaseUploader):
xiaohongshu_logger.info(_msg("🧍", "小人先检查 cookie、视频文件、封面和发布时间"))
await self.validate_upload_args()
xiaohongshu_logger.info(_msg("🥳", "上传前检查通过"))
browser = await playwright.chromium.launch(headless=self.headless, channel="chromium")
browser = await playwright.chromium.launch(headless=self.headless, executable_path=LOCAL_CHROME_PATH)
context = await browser.new_context(
permissions=["geolocation"],
storage_state=self.account_file,
@@ -825,7 +825,7 @@ class XiaoHongShuNote(XiaoHongShuBaseUploader):
xiaohongshu_logger.info(_msg("🧍", "小人先检查 cookie、图片和发布时间"))
await self.validate_upload_args()
xiaohongshu_logger.info(_msg("🥳", "图文上传前检查通过"))
browser = await playwright.chromium.launch(headless=self.headless, channel="chromium")
browser = await playwright.chromium.launch(headless=self.headless, executable_path=LOCAL_CHROME_PATH)
context = await browser.new_context(
permissions=["geolocation"],
storage_state=self.account_file,
+3 -3
View File
@@ -50,7 +50,7 @@ def _build_login_result(success, status, message, account_file, current_url=""):
async def cookie_auth(account_file) -> bool:
"""登录态是否仍有效:带 cookie 打开 Studio,没被踢到 Google 登录页且进入了频道页即有效。"""
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=True, channel="chrome")
browser = await playwright.chromium.launch(headless=True, executable_path=LOCAL_CHROME_PATH)
try:
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
@@ -71,7 +71,7 @@ async def youtube_cookie_gen(account_file, headless: bool = False):
"""交互式登录:开浏览器让用户登录 Google/YouTube,进入频道页后保存 storage_state。"""
async with async_playwright() as playwright:
# 登录必须显形,让用户输账号密码/二步验证
browser = await playwright.chromium.launch(headless=False, channel="chrome")
browser = await playwright.chromium.launch(headless=False, executable_path=LOCAL_CHROME_PATH)
context = await browser.new_context()
context = await set_init_script(context)
page = await context.new_page()
@@ -199,7 +199,7 @@ class YouTubeVideo(BaseVideoUploader):
async def upload(self, playwright: Playwright) -> None:
browser = await playwright.chromium.launch(
headless=self.headless, channel="chrome",
headless=self.headless, executable_path=LOCAL_CHROME_PATH,
proxy={"server": YT_PROXY} if YT_PROXY else None,
)
context = await browser.new_context(storage_state=self.account_file)