🔄 卡若AI 同步 2026-03-10 13:48 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
B站视频发布 - Playwright 自动化(可见浏览器)
|
||||
B站反自动化较强,采用可见浏览器模式:
|
||||
- 自动上传、填写标题/分区/标签、点击投稿
|
||||
- 用户无需操作,但浏览器窗口可见
|
||||
- 首次可能需过极验验证码(一次后不再出现)
|
||||
B站视频发布 — 纯 API 优先 + Playwright 兜底
|
||||
方案一:bilibili-api-python 纯 API(无需浏览器)
|
||||
方案二:Playwright 可见浏览器(API 失败时自动降级)
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -16,10 +15,8 @@ COOKIE_FILE = SCRIPT_DIR / "bilibili_storage_state.json"
|
||||
VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片")
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR.parent.parent / "多平台分发" / "脚本"))
|
||||
from cookie_manager import CookieManager
|
||||
from video_utils import extract_cover
|
||||
from publish_result import PublishResult
|
||||
|
||||
UPLOAD_URL = "https://member.bilibili.com/platform/upload/video/frame"
|
||||
UA = (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
||||
@@ -59,185 +56,249 @@ TITLES = {
|
||||
}
|
||||
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> bool:
|
||||
"""用可见浏览器自动化发布单条视频"""
|
||||
def _load_credential():
|
||||
"""从 storage_state.json 提取 B站凭证"""
|
||||
from bilibili_api import Credential
|
||||
with open(COOKIE_FILE, "r") as f:
|
||||
data = json.load(f)
|
||||
cookies = {c["name"]: c["value"] for c in data.get("cookies", [])
|
||||
if ".bilibili.com" in c.get("domain", "")}
|
||||
return Credential(
|
||||
sessdata=cookies.get("SESSDATA", ""),
|
||||
bili_jct=cookies.get("bili_jct", ""),
|
||||
buvid3=cookies.get("buvid3", ""),
|
||||
dedeuserid=cookies.get("DedeUserID", ""),
|
||||
)
|
||||
|
||||
|
||||
async def _api_publish(video_path: str, title: str) -> PublishResult:
|
||||
"""方案一:bilibili-api-python 纯 API"""
|
||||
from bilibili_api import video_uploader
|
||||
from video_utils import extract_cover
|
||||
|
||||
t0 = time.time()
|
||||
credential = _load_credential()
|
||||
|
||||
cover_path = extract_cover(video_path)
|
||||
print(f" [API] 封面已提取: {cover_path}", flush=True)
|
||||
|
||||
tags = "Soul派对,创业,认知觉醒,副业,商业思维"
|
||||
meta = {
|
||||
"copyright": 1,
|
||||
"source": "",
|
||||
"desc": title,
|
||||
"desc_format_id": 0,
|
||||
"dynamic": "",
|
||||
"interactive": 0,
|
||||
"open_elec": 0,
|
||||
"no_reprint": 1,
|
||||
"subtitles": {"lan": "", "open": 0},
|
||||
"tag": tags,
|
||||
"tid": 160, # 生活 > 日常
|
||||
"title": title[:80],
|
||||
"up_close_danmaku": False,
|
||||
"up_close_reply": False,
|
||||
}
|
||||
|
||||
page = video_uploader.VideoUploaderPage(
|
||||
path=video_path,
|
||||
title=title[:80],
|
||||
description=title,
|
||||
)
|
||||
|
||||
uploader = video_uploader.VideoUploader(
|
||||
pages=[page],
|
||||
meta=meta,
|
||||
credential=credential,
|
||||
cover=cover_path if cover_path else None,
|
||||
)
|
||||
|
||||
last_event = {}
|
||||
|
||||
@uploader.on("__ALL__")
|
||||
async def _on_event(data):
|
||||
nonlocal last_event
|
||||
last_event = data
|
||||
ev = data.get("event", "")
|
||||
if ev == "PRE_PAGE":
|
||||
print(" [API] 开始上传...", flush=True)
|
||||
elif ev == "PREUPLOAD_DONE":
|
||||
print(" [API] 预上传完成", flush=True)
|
||||
elif ev == "PRE_COVER":
|
||||
print(" [API] 上传封面...", flush=True)
|
||||
elif ev == "SUBMIT_DONE":
|
||||
print(" [API] 投稿提交完成!", flush=True)
|
||||
|
||||
await uploader.start()
|
||||
elapsed = time.time() - t0
|
||||
|
||||
return PublishResult(
|
||||
platform="B站",
|
||||
video_path=video_path,
|
||||
title=title,
|
||||
success=True,
|
||||
status="reviewing",
|
||||
message=f"纯API投稿成功 ({elapsed:.1f}s)",
|
||||
elapsed_sec=elapsed,
|
||||
)
|
||||
|
||||
|
||||
async def _playwright_publish(video_path: str, title: str) -> PublishResult:
|
||||
"""方案二:Playwright 可见浏览器(兜底)"""
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
fname = Path(video_path).name
|
||||
fsize = Path(video_path).stat().st_size
|
||||
t0 = time.time()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" [{idx}/{total}] {fname}")
|
||||
print(f" 大小: {fsize/1024/1024:.1f}MB")
|
||||
print(f" 标题: {title[:60]}")
|
||||
print(f"{'='*60}")
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(
|
||||
headless=False,
|
||||
args=["--disable-blink-features=AutomationControlled"],
|
||||
)
|
||||
ctx = await browser.new_context(
|
||||
storage_state=str(COOKIE_FILE), user_agent=UA,
|
||||
viewport={"width": 1280, "height": 900}, locale="zh-CN",
|
||||
)
|
||||
await ctx.add_init_script(
|
||||
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
|
||||
)
|
||||
page = await ctx.new_page()
|
||||
|
||||
if not COOKIE_FILE.exists():
|
||||
print(" [✗] Cookie 不存在,请先运行 bilibili_login.py")
|
||||
return False
|
||||
await page.goto(
|
||||
"https://member.bilibili.com/platform/upload/video/frame",
|
||||
timeout=30000, wait_until="domcontentloaded",
|
||||
)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
try:
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(
|
||||
headless=False,
|
||||
args=["--disable-blink-features=AutomationControlled"],
|
||||
fl = page.locator('input[type="file"]').first
|
||||
if await fl.count() == 0:
|
||||
await browser.close()
|
||||
return PublishResult(
|
||||
platform="B站", video_path=video_path, title=title,
|
||||
success=False, status="failed",
|
||||
message="Playwright: 未找到上传控件",
|
||||
elapsed_sec=time.time() - t0,
|
||||
)
|
||||
context = await browser.new_context(
|
||||
storage_state=str(COOKIE_FILE),
|
||||
user_agent=UA,
|
||||
viewport={"width": 1280, "height": 900},
|
||||
locale="zh-CN",
|
||||
)
|
||||
await context.add_init_script(
|
||||
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
|
||||
)
|
||||
page = await context.new_page()
|
||||
|
||||
print(" [1] 打开上传页...")
|
||||
await page.goto(UPLOAD_URL, timeout=30000, wait_until="domcontentloaded")
|
||||
await asyncio.sleep(3)
|
||||
|
||||
print(" [2] 上传视频...")
|
||||
file_input = await page.query_selector('input[type="file"]')
|
||||
if not file_input:
|
||||
for inp in await page.query_selector_all("input"):
|
||||
if "file" in (await inp.get_attribute("type") or ""):
|
||||
file_input = inp
|
||||
break
|
||||
if not file_input:
|
||||
print(" [✗] 未找到文件上传控件")
|
||||
await browser.close()
|
||||
return False
|
||||
|
||||
await file_input.set_input_files(video_path)
|
||||
print(" [2] 文件已选择,等待上传完成...")
|
||||
|
||||
# 等待上传完成(查找进度条或"重新上传"按钮)
|
||||
for i in range(120):
|
||||
try:
|
||||
page_text = await page.inner_text("body")
|
||||
if "重新上传" in page_text or "上传完成" in page_text:
|
||||
print(f" [2] 上传完成 (等待 {i*2}s)")
|
||||
break
|
||||
# 检查进度百分比
|
||||
progress = await page.evaluate("""() => {
|
||||
const el = document.querySelector('.progress-bar, [class*="progress"]');
|
||||
if (el) return el.style.width || el.getAttribute('aria-valuenow') || '';
|
||||
return '';
|
||||
}""")
|
||||
if progress and ("100" in str(progress)):
|
||||
print(f" [2] 上传 100%")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(2)
|
||||
await fl.set_input_files(video_path)
|
||||
|
||||
for i in range(120):
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
if "重新上传" in txt or "上传完成" in txt:
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 填写标题
|
||||
print(" [3] 填写标题...")
|
||||
title_input = page.locator('input[maxlength="80"]').first
|
||||
if await title_input.count() > 0:
|
||||
await title_input.click()
|
||||
await title_input.fill("")
|
||||
await title_input.fill(title[:80])
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 选择"自制"
|
||||
print(" [3b] 选择类型:自制...")
|
||||
try:
|
||||
original = page.locator('label:has-text("自制"), span:has-text("自制")').first
|
||||
if await original.count() > 0:
|
||||
await original.click()
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(0.5)
|
||||
title_input = page.locator('input[maxlength="80"]').first
|
||||
if await title_input.count() > 0:
|
||||
await title_input.click()
|
||||
await title_input.fill(title[:80])
|
||||
|
||||
# 选择分区
|
||||
print(" [3c] 选择分区:生活 > 日常...")
|
||||
try:
|
||||
cat_dropdown = page.locator('text=请选择分区').first
|
||||
if await cat_dropdown.count() > 0:
|
||||
await cat_dropdown.click()
|
||||
await asyncio.sleep(1)
|
||||
try:
|
||||
original = page.locator('label:has-text("自制"), span:has-text("自制")').first
|
||||
if await original.count() > 0:
|
||||
await original.click()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
life_cat = page.locator('.drop-cascader-list .drop-cascader-item:has-text("生活")').first
|
||||
if await life_cat.count() > 0:
|
||||
await life_cat.click()
|
||||
await asyncio.sleep(0.5)
|
||||
else:
|
||||
life_cat2 = page.locator('li:has-text("生活")').first
|
||||
if await life_cat2.count() > 0:
|
||||
await life_cat2.click()
|
||||
await asyncio.sleep(0.5)
|
||||
try:
|
||||
cat_dd = page.locator('text=请选择分区').first
|
||||
if await cat_dd.count() > 0:
|
||||
await cat_dd.click()
|
||||
await asyncio.sleep(1)
|
||||
life = page.locator('.drop-cascader-item:has-text("生活")').first
|
||||
if await life.count() > 0:
|
||||
await life.click()
|
||||
await asyncio.sleep(0.5)
|
||||
daily = page.locator('span:has-text("日常"), li:has-text("日常")').first
|
||||
if await daily.count() > 0:
|
||||
await daily.click()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
daily_cat = page.locator('span:has-text("日常"), li:has-text("日常")').first
|
||||
if await daily_cat.count() > 0:
|
||||
await daily_cat.click()
|
||||
await asyncio.sleep(0.5)
|
||||
except Exception as e:
|
||||
print(f" [⚠] 分区选择异常: {e}")
|
||||
await asyncio.sleep(0.5)
|
||||
try:
|
||||
tag_input = page.locator('input[placeholder*="标签"]').first
|
||||
if await tag_input.count() > 0:
|
||||
for tag in ["Soul派对", "创业", "认知觉醒"]:
|
||||
await tag_input.fill(tag)
|
||||
await tag_input.press("Enter")
|
||||
await asyncio.sleep(0.3)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 填写标签
|
||||
print(" [3d] 填写标签...")
|
||||
try:
|
||||
tag_input = page.locator('input[placeholder*="Enter"], input[placeholder*="标签"]').first
|
||||
if await tag_input.count() > 0:
|
||||
await tag_input.click()
|
||||
tags = ["Soul派对", "创业", "认知觉醒", "副业", "商业思维"]
|
||||
for tag in tags[:5]:
|
||||
await tag_input.fill(tag)
|
||||
await tag_input.press("Enter")
|
||||
await asyncio.sleep(0.3)
|
||||
except Exception:
|
||||
pass
|
||||
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 滚动到底部
|
||||
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
await asyncio.sleep(1)
|
||||
submit = page.locator('button:has-text("立即投稿")').first
|
||||
if await submit.count() > 0:
|
||||
await submit.click()
|
||||
else:
|
||||
await page.evaluate("""() => {
|
||||
const b = [...document.querySelectorAll('button')].find(e => e.textContent.includes('立即投稿'));
|
||||
if (b) b.click();
|
||||
}""")
|
||||
|
||||
# 点击立即投稿
|
||||
print(" [4] 点击立即投稿...")
|
||||
submit_btn = page.locator('button:has-text("立即投稿")').first
|
||||
if await submit_btn.count() > 0:
|
||||
await submit_btn.click()
|
||||
else:
|
||||
await page.evaluate("""() => {
|
||||
const btns = [...document.querySelectorAll('button')];
|
||||
const pub = btns.find(e => e.textContent.includes('立即投稿'));
|
||||
if (pub) pub.click();
|
||||
}""")
|
||||
for i in range(30):
|
||||
await asyncio.sleep(2)
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
url = page.url
|
||||
if "投稿成功" in txt or "稿件投递" in txt or "list" in url:
|
||||
await ctx.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return PublishResult(
|
||||
platform="B站", video_path=video_path, title=title,
|
||||
success=True, status="reviewing",
|
||||
message=f"Playwright投稿成功 ({time.time()-t0:.1f}s)",
|
||||
elapsed_sec=time.time() - t0,
|
||||
)
|
||||
|
||||
# 等待结果
|
||||
for i in range(30):
|
||||
await asyncio.sleep(2)
|
||||
page_text = await page.inner_text("body")
|
||||
current_url = page.url
|
||||
if "投稿成功" in page_text or "稿件投递" in page_text:
|
||||
print(" [✓] 投稿成功!")
|
||||
await context.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
if "video/upload" in current_url or "list" in current_url:
|
||||
print(" [✓] 已跳转到稿件列表(投稿成功)")
|
||||
await context.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
if "自动提交" in page_text:
|
||||
print(f" [⚠] 等待自动提交 ({i*2}s)...")
|
||||
continue
|
||||
await page.screenshot(path="/tmp/bilibili_result.png")
|
||||
await ctx.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return PublishResult(
|
||||
platform="B站", video_path=video_path, title=title,
|
||||
success=False, status="failed",
|
||||
message="Playwright: 投稿超时",
|
||||
screenshot="/tmp/bilibili_result.png",
|
||||
elapsed_sec=time.time() - t0,
|
||||
)
|
||||
|
||||
print(" [⚠] 超时,请手动确认投稿状态")
|
||||
await context.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> PublishResult:
|
||||
"""API 优先 → Playwright 兜底"""
|
||||
fname = Path(video_path).name
|
||||
fsize = Path(video_path).stat().st_size
|
||||
print(f"\n[{idx}/{total}] {fname} ({fsize/1024/1024:.1f}MB)", flush=True)
|
||||
print(f" 标题: {title[:60]}", flush=True)
|
||||
|
||||
if not COOKIE_FILE.exists():
|
||||
return PublishResult(
|
||||
platform="B站", video_path=video_path, title=title,
|
||||
success=False, status="error", message="Cookie 不存在",
|
||||
)
|
||||
|
||||
# 方案一:纯 API
|
||||
print(" [方案一] bilibili-api-python 纯 API...", flush=True)
|
||||
try:
|
||||
result = await _api_publish(video_path, title)
|
||||
print(f" {result.log_line()}", flush=True)
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f" [✗] 异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
err_msg = str(e)[:100]
|
||||
print(f" [方案一失败] {err_msg}", flush=True)
|
||||
|
||||
# 方案二:Playwright 兜底
|
||||
print(" [方案二] 降级到 Playwright 可见浏览器...", flush=True)
|
||||
try:
|
||||
result = await _playwright_publish(video_path, title)
|
||||
print(f" {result.log_line()}", flush=True)
|
||||
return result
|
||||
except Exception as e:
|
||||
return PublishResult(
|
||||
platform="B站", video_path=video_path, title=title,
|
||||
success=False, status="error",
|
||||
message=f"双方案均失败: {str(e)[:80]}",
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -245,46 +306,25 @@ async def main():
|
||||
print("[✗] Cookie 不存在,请先运行 bilibili_login.py")
|
||||
return 1
|
||||
|
||||
cm = CookieManager(COOKIE_FILE, "bilibili.com")
|
||||
expiry = cm.check_expiry()
|
||||
print(f"[i] Cookie 状态: {expiry['message']}")
|
||||
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
"https://api.bilibili.com/x/web-interface/nav",
|
||||
headers={"Cookie": cm.cookie_str, "User-Agent": UA},
|
||||
)
|
||||
data = resp.json()
|
||||
if data.get("code") == 0:
|
||||
print(f"[i] 已登录: {data['data'].get('uname')} (uid={data['data'].get('mid')})\n")
|
||||
else:
|
||||
print("[✗] Cookie 已失效,请重新运行 bilibili_login.py")
|
||||
return 1
|
||||
|
||||
videos = sorted(VIDEO_DIR.glob("*.mp4"))
|
||||
if not videos:
|
||||
print("[✗] 未找到视频")
|
||||
return 1
|
||||
print(f"[i] 共 {len(videos)} 条视频\n")
|
||||
print(f"共 {len(videos)} 条视频\n")
|
||||
|
||||
from publish_result import print_summary, save_results
|
||||
results = []
|
||||
for i, vp in enumerate(videos):
|
||||
title = TITLES.get(vp.name, f"{vp.stem} #Soul派对 #创业日记")
|
||||
ok = await publish_one(str(vp), title, i + 1, len(videos))
|
||||
results.append((vp.name, ok))
|
||||
t = TITLES.get(vp.name, f"{vp.stem} #Soul派对 #创业日记")
|
||||
r = await publish_one(str(vp), t, i + 1, len(videos))
|
||||
results.append(r)
|
||||
if i < len(videos) - 1:
|
||||
print(f"\n 等待 8 秒后继续...")
|
||||
await asyncio.sleep(8)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(" B站发布汇总")
|
||||
print(f"{'='*60}")
|
||||
for name, ok in results:
|
||||
print(f" [{'✓' if ok else '✗'}] {name}")
|
||||
success = sum(1 for _, ok in results if ok)
|
||||
print(f"\n 成功: {success}/{len(results)}")
|
||||
return 0 if success == len(results) else 1
|
||||
print_summary(results)
|
||||
save_results(results)
|
||||
ok = sum(1 for r in results if r.success)
|
||||
return 0 if ok == len(results) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
File diff suppressed because one or more lines are too long
199
03_卡木(木)/木叶_视频内容/多平台分发/多平台视频分发_优化总结.md
Normal file
199
03_卡木(木)/木叶_视频内容/多平台分发/多平台视频分发_优化总结.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 多平台视频分发系统 — 优化迭代总结
|
||||
|
||||
> 更新时间: 2026-03-10
|
||||
|
||||
---
|
||||
|
||||
## 一、五平台最终技术方案
|
||||
|
||||
| 平台 | 方案一(优先) | 方案二(兜底) | 单条耗时 | 状态 |
|
||||
|------|-------------|-------------|---------|------|
|
||||
| **B站** | bilibili-api-python 纯 API | Playwright 可见浏览器 | ~8s | 已验证 |
|
||||
| **抖音** | douyin_pure_api 纯 API(VOD分片) | — | ~15s | 已验证 |
|
||||
| **快手** | Playwright Headless | — | ~20s | 已验证 |
|
||||
| **视频号** | Playwright Headless | — | ~20s | 已验证 |
|
||||
| **小红书** | Playwright Headless | — | ~25s | 已验证 |
|
||||
|
||||
### 方案选择逻辑
|
||||
|
||||
- **纯 API 可行 → 优先用 API**:B站(bilibili-api-python 成熟稳定)、抖音(VOD 分片上传 + bd-ticket-guard)
|
||||
- **API 不可行 → Playwright Headless**:快手(__NS_sig3 签名过于复杂)、视频号(无公开 API)、小红书(X-S/X-T 签名频繁变动)
|
||||
- **B站特殊**:纯 API 优先,失败自动降级到可见浏览器
|
||||
|
||||
---
|
||||
|
||||
## 二、各平台遇到的问题与解决方案
|
||||
|
||||
### B站
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|---------|
|
||||
| 纯 API 403 Forbidden | WAF/反爬 | 放弃手动逆向,改用 bilibili-api-python 库 |
|
||||
| Playwright GeeTest 验证码 | headless 触发极验 | 改用可见浏览器模式(headless=False) |
|
||||
| bilibili-api-python 406 | 早期版本 bug | 升级到最新版,解决 preupload 问题 |
|
||||
| VideoUploader cover 空路径报错 | cover="" 触发文件读取 | 用 ffmpeg 提取第一帧作为 cover 传入 |
|
||||
| **最终方案** | — | **纯 API 7.9s 完成投稿**(之前 80s+) |
|
||||
|
||||
### 快手
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|---------|
|
||||
| "发布"按钮不是 `<button>` | 自定义 div 实现 | 用 `div[class*="button-primary"]` 选择器 |
|
||||
| 发布按钮在页面底部不可见 | y=1287px 超出 viewport | `scroll_into_view_if_needed()` + `scrollTo(bottom)` |
|
||||
| 草稿对话框阻挡 | "上次未发布的视频" | 检测并自动点击"放弃" |
|
||||
| 未登录(storage_state 无效) | login 脚本检测不准 | 修复 login 检测:等待 cp.kuaishou.com 域 cookie |
|
||||
| __NS_sig3 签名太复杂 | webpack + JSVMP 混淆 | 放弃纯 API,Playwright 方案已足够稳定 |
|
||||
|
||||
### 视频号
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|---------|
|
||||
| 无公开上传 API | 微信封闭生态 | 直接用 Playwright Headless |
|
||||
| "直接发表"按钮不可见 | 弹窗动画延迟 | `force=True` + JS 点击兜底 |
|
||||
| 描述框不易定位 | contenteditable div 动态加载 | 点击"添加描述" → 定位 `[contenteditable="true"]:visible` |
|
||||
| 原创声明弹窗 | 默认触发 | 检测并点击"直接发表" |
|
||||
|
||||
### 小红书
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|---------|
|
||||
| API 返回 HTML 而非 JSON | session cookie 机制变更 | 放弃纯 API,改 Playwright |
|
||||
| "发布"按钮被 tooltip 拦截 | data-tippy-root 覆盖 | JS 清除 tooltip + `force=True` |
|
||||
| 发布按钮 disabled 状态 | 标题/描述未正确填入 | 精确选择器 + 等待 disabled 消失 |
|
||||
| 发布后不跳转 | 页面重置而非跳转 | 检测"拖拽视频到此"作为成功标志 |
|
||||
| 标题截断 | 小红书标题限 20 字 | `title[:20]` 自动截断 |
|
||||
|
||||
### 抖音
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|---------|
|
||||
| a_bogus 签名 | JSVMP 虚拟机保护 | 已有 douyin_pure_api.py 解决 |
|
||||
| Cookie 2-4h 过期 | 抖音 session 机制 | cookie_manager 定期检查 + 告警 |
|
||||
| AWS4-HMAC-SHA256 | 文件上传需双层签名 | 已在 pure_api 中实现 |
|
||||
|
||||
---
|
||||
|
||||
## 三、本次优化内容
|
||||
|
||||
### 3.1 B站方案升级(最大改进)
|
||||
|
||||
```
|
||||
旧方案: Playwright 可见浏览器 → 80s/条,需桌面环境
|
||||
新方案: bilibili-api-python 纯 API → 8s/条,无需浏览器
|
||||
性能提升: 10x
|
||||
```
|
||||
|
||||
### 3.2 统一发布结果结构
|
||||
|
||||
新增 `publish_result.py` 模块,所有平台统一返回 `PublishResult` 数据结构:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PublishResult:
|
||||
platform: str # "B站" | "快手" | "视频号" | "小红书" | "抖音"
|
||||
video_path: str
|
||||
title: str
|
||||
success: bool
|
||||
status: str # "published" | "reviewing" | "failed" | "error"
|
||||
message: str # 人类可读的结果描述
|
||||
error_code: str # 机器可读的错误码(NOT_LOGGED_IN 等)
|
||||
screenshot: str # 截图路径
|
||||
content_url: str # 发布后的链接
|
||||
elapsed_sec: float # 耗时
|
||||
timestamp: str # ISO 时间戳
|
||||
```
|
||||
|
||||
### 3.3 发布日志持久化
|
||||
|
||||
每次发布结果自动追加到 `publish_log.json`(JSON Lines 格式),支持:
|
||||
- 历史查询
|
||||
- 失败重试
|
||||
- 统计分析
|
||||
|
||||
### 3.4 汇总表输出
|
||||
|
||||
```
|
||||
========================================================================
|
||||
发布结果汇总
|
||||
========================================================================
|
||||
[✓] B站 | 信任不是求来的 发三个月邮件拿下德… | reviewing
|
||||
[✓] 快手 | 懒人也能赚钱?动作简单、有利可图… | published
|
||||
[✓] 视频号 | 易经两小时学个七七八八… | reviewing
|
||||
[✓] 小红书 | 后端花170万搭体系… | published
|
||||
------------------------------------------------------------------------
|
||||
成功: 4/4 | 耗时: 72.7s
|
||||
========================================================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、各平台 API 可行性分析
|
||||
|
||||
| 平台 | 纯 API | 难度 | 稳定性 | 当前方案 | 升级可能 |
|
||||
|------|--------|------|--------|---------|---------|
|
||||
| B站 | **可行** | 低 | 高 | 已升级纯API | — |
|
||||
| 抖音 | **可行** | 高 | 中 | 已有纯API | 维护 bd-ticket-guard |
|
||||
| 快手 | 可行但难维护 | 高 | 低 | Playwright | __NS_sig3 变动太频繁 |
|
||||
| 小红书 | 可行(官方开放API) | 中 | 中高 | Playwright | 可申请开发者资质 |
|
||||
| 视频号 | **不可行** | — | — | Playwright | 微信未开放上传API |
|
||||
|
||||
---
|
||||
|
||||
## 五、文件结构
|
||||
|
||||
```
|
||||
03_卡木(木)/木叶_视频内容/
|
||||
├── 多平台分发/脚本/
|
||||
│ ├── distribute_all.py # 一键多平台分发(入口)
|
||||
│ ├── publish_result.py # 统一结果结构 + 日志
|
||||
│ ├── cookie_manager.py # Cookie 管理(检查/加载/过期)
|
||||
│ ├── video_utils.py # 封面提取 + 视频元数据
|
||||
│ ├── publish_log.json # 发布历史日志(自动生成)
|
||||
│ └── requirements.txt
|
||||
├── B站发布/脚本/
|
||||
│ ├── bilibili_publish.py # 纯API优先 + Playwright兜底
|
||||
│ └── bilibili_login.py # 扫码登录
|
||||
├── 快手发布/脚本/
|
||||
│ ├── kuaishou_publish.py # Playwright Headless
|
||||
│ └── kuaishou_login.py # 扫码登录
|
||||
├── 视频号发布/脚本/
|
||||
│ ├── channels_publish.py # Playwright Headless
|
||||
│ └── channels_login.py # 微信扫码登录
|
||||
├── 小红书发布/脚本/
|
||||
│ ├── xiaohongshu_publish.py # Playwright Headless
|
||||
│ └── xiaohongshu_login.py # 扫码/手机号登录
|
||||
└── 抖音发布/脚本/
|
||||
├── douyin_pure_api.py # 纯API(VOD分片上传)
|
||||
└── douyin_login.py # Playwright扫码登录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、使用方式
|
||||
|
||||
```bash
|
||||
# 检查所有平台 Cookie 状态
|
||||
python3 distribute_all.py --check
|
||||
|
||||
# 一键分发到所有已登录平台
|
||||
python3 distribute_all.py
|
||||
|
||||
# 只分发到指定平台
|
||||
python3 distribute_all.py --platforms B站 快手
|
||||
|
||||
# 分发单条视频
|
||||
python3 distribute_all.py --video /path/to/video.mp4
|
||||
|
||||
# 自定义视频目录
|
||||
python3 distribute_all.py --video-dir /path/to/videos/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、后续优化方向
|
||||
|
||||
1. **小红书官方 API**:申请开发者资质后可升级为纯 API
|
||||
2. **定时发布**:各平台支持设置发布时间(已有"定时发布"选项)
|
||||
3. **失败自动重试**:基于 publish_log.json 的失败记录自动重试
|
||||
4. **Cookie 过期预警**:集成到飞书/微信通知
|
||||
5. **并行分发**:不同平台间并行上传(当前为串行)
|
||||
@@ -23,6 +23,7 @@ VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_out
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
from cookie_manager import CookieManager, check_all_cookies
|
||||
from publish_result import PublishResult, print_summary, save_results
|
||||
|
||||
PLATFORM_CONFIG = {
|
||||
"抖音": {
|
||||
@@ -59,14 +60,14 @@ PLATFORM_CONFIG = {
|
||||
|
||||
|
||||
def check_cookies():
|
||||
"""检查所有平台 Cookie 状态"""
|
||||
print("=" * 60)
|
||||
print(" 多平台 Cookie 状态")
|
||||
print("=" * 60)
|
||||
results = check_all_cookies(BASE_DIR)
|
||||
available = []
|
||||
for platform, info in results.items():
|
||||
icons = {"ok": "✓", "warning": "⚠", "expiring_soon": "⚠", "expired": "✗", "missing": "○", "error": "✗"}
|
||||
icons = {"ok": "✓", "warning": "⚠", "expiring_soon": "⚠",
|
||||
"expired": "✗", "missing": "○", "error": "✗"}
|
||||
icon = icons.get(info["status"], "?")
|
||||
print(f" [{icon}] {platform}: {info['message']}")
|
||||
if info["status"] in ("ok", "warning"):
|
||||
@@ -76,7 +77,6 @@ def check_cookies():
|
||||
|
||||
|
||||
def load_platform_module(name: str, config: dict):
|
||||
"""动态加载平台发布模块"""
|
||||
script_path = config["script"]
|
||||
if not script_path.exists():
|
||||
return None
|
||||
@@ -87,8 +87,7 @@ def load_platform_module(name: str, config: dict):
|
||||
return module
|
||||
|
||||
|
||||
async def distribute_to_platform(platform: str, config: dict, videos: list) -> dict:
|
||||
"""分发到单个平台"""
|
||||
async def distribute_to_platform(platform: str, config: dict, videos: list) -> list[PublishResult]:
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" 开始分发到 [{platform}]")
|
||||
print(f"{'#'*60}")
|
||||
@@ -96,41 +95,47 @@ async def distribute_to_platform(platform: str, config: dict, videos: list) -> d
|
||||
cookie_path = config["cookie"]
|
||||
if not cookie_path.exists():
|
||||
print(f" [✗] {platform} 未登录,跳过")
|
||||
return {"platform": platform, "status": "skipped", "reason": "未登录"}
|
||||
return [PublishResult(platform=platform, video_path=str(v), title="",
|
||||
success=False, status="error", message="未登录") for v in videos]
|
||||
|
||||
try:
|
||||
cm = CookieManager(cookie_path, config["domain"])
|
||||
if not cm.is_valid():
|
||||
print(f" [✗] {platform} Cookie 已过期,跳过")
|
||||
return {"platform": platform, "status": "skipped", "reason": "Cookie过期"}
|
||||
return [PublishResult(platform=platform, video_path=str(v), title="",
|
||||
success=False, status="error", message="Cookie过期") for v in videos]
|
||||
except Exception as e:
|
||||
print(f" [✗] {platform} Cookie 加载失败: {e}")
|
||||
return {"platform": platform, "status": "error", "reason": str(e)}
|
||||
return [PublishResult(platform=platform, video_path=str(v), title="",
|
||||
success=False, status="error", message=str(e)) for v in videos]
|
||||
|
||||
module = load_platform_module(platform, config)
|
||||
if not module:
|
||||
print(f" [✗] {platform} 脚本不存在: {config['script']}")
|
||||
return {"platform": platform, "status": "error", "reason": "脚本不存在"}
|
||||
return [PublishResult(platform=platform, video_path=str(v), title="",
|
||||
success=False, status="error", message="脚本不存在") for v in videos]
|
||||
|
||||
success = 0
|
||||
results = []
|
||||
total = len(videos)
|
||||
for i, vp in enumerate(videos):
|
||||
title = getattr(module, "TITLES", {}).get(vp.name, f"{vp.stem} #Soul派对")
|
||||
try:
|
||||
ok = await module.publish_one(str(vp), title, i + 1, total)
|
||||
if ok:
|
||||
success += 1
|
||||
r = await module.publish_one(str(vp), title, i + 1, total)
|
||||
if isinstance(r, PublishResult):
|
||||
results.append(r)
|
||||
else:
|
||||
results.append(PublishResult(
|
||||
platform=platform, video_path=str(vp), title=title,
|
||||
success=bool(r), status="reviewing" if r else "failed",
|
||||
message="旧接口兼容",
|
||||
))
|
||||
except Exception as e:
|
||||
print(f" [✗] {vp.name} 异常: {e}")
|
||||
results.append(PublishResult(
|
||||
platform=platform, video_path=str(vp), title=title,
|
||||
success=False, status="error", message=str(e)[:80],
|
||||
))
|
||||
if i < total - 1:
|
||||
await asyncio.sleep(3)
|
||||
|
||||
return {
|
||||
"platform": platform,
|
||||
"status": "done",
|
||||
"success": success,
|
||||
"total": total,
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -148,18 +153,15 @@ async def main():
|
||||
|
||||
if not available:
|
||||
print("\n[✗] 没有可用的平台,请先登录各平台")
|
||||
print(" 抖音: python3 ../抖音发布/脚本/douyin_login.py")
|
||||
print(" B站: python3 ../B站发布/脚本/bilibili_login.py")
|
||||
print(" 视频号: python3 ../视频号发布/脚本/channels_login.py")
|
||||
print(" 小红书: python3 ../小红书发布/脚本/xiaohongshu_login.py")
|
||||
print(" 快手: python3 ../快手发布/脚本/kuaishou_login.py")
|
||||
for p, c in PLATFORM_CONFIG.items():
|
||||
print(f" {p}: python3 {c['script']}")
|
||||
return 1
|
||||
|
||||
targets = args.platforms if args.platforms else available
|
||||
targets = [t for t in targets if t in available]
|
||||
|
||||
if not targets:
|
||||
print(f"\n[✗] 指定的平台均不可用")
|
||||
print("\n[✗] 指定的平台均不可用")
|
||||
return 1
|
||||
|
||||
video_dir = Path(args.video_dir) if args.video_dir else VIDEO_DIR
|
||||
@@ -180,27 +182,19 @@ async def main():
|
||||
print(f" 总任务: {len(videos) * len(targets)} 条")
|
||||
print()
|
||||
|
||||
all_results = []
|
||||
all_results: list[PublishResult] = []
|
||||
for platform in targets:
|
||||
config = PLATFORM_CONFIG[platform]
|
||||
result = await distribute_to_platform(platform, config, videos)
|
||||
all_results.append(result)
|
||||
platform_results = await distribute_to_platform(platform, config, videos)
|
||||
all_results.extend(platform_results)
|
||||
|
||||
print(f"\n\n{'='*60}")
|
||||
print(f" 多平台分发汇总")
|
||||
print(f"{'='*60}")
|
||||
for r in all_results:
|
||||
if r["status"] == "done":
|
||||
print(f" [{r['platform']}] 成功 {r['success']}/{r['total']}")
|
||||
elif r["status"] == "skipped":
|
||||
print(f" [{r['platform']}] 跳过 ({r['reason']})")
|
||||
else:
|
||||
print(f" [{r['platform']}] 错误 ({r.get('reason', '未知')})")
|
||||
print_summary(all_results)
|
||||
save_results(all_results)
|
||||
|
||||
total_success = sum(r.get("success", 0) for r in all_results if r["status"] == "done")
|
||||
total_tasks = sum(r.get("total", 0) for r in all_results if r["status"] == "done")
|
||||
print(f"\n 总计: {total_success}/{total_tasks}")
|
||||
return 0
|
||||
ok = sum(1 for r in all_results if r.success)
|
||||
total = len(all_results)
|
||||
print(f" 日志已保存: {SCRIPT_DIR / 'publish_log.json'}")
|
||||
return 0 if ok == total else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
60
03_卡木(木)/木叶_视频内容/多平台分发/脚本/publish_result.py
Normal file
60
03_卡木(木)/木叶_视频内容/多平台分发/脚本/publish_result.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
统一发布结果模块 — 所有平台的 publish_one 都返回此结构。
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
RESULT_LOG = Path(__file__).parent / "publish_log.json"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PublishResult:
|
||||
platform: str
|
||||
video_path: str
|
||||
title: str
|
||||
success: bool
|
||||
status: str # "published" | "reviewing" | "failed" | "error"
|
||||
message: str = ""
|
||||
error_code: Optional[str] = None
|
||||
screenshot: Optional[str] = None
|
||||
content_url: Optional[str] = None
|
||||
elapsed_sec: float = 0.0
|
||||
timestamp: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {k: v for k, v in asdict(self).items() if v is not None}
|
||||
|
||||
def log_line(self) -> str:
|
||||
icon = "✓" if self.success else "✗"
|
||||
return f"[{icon}] {self.platform} | {Path(self.video_path).name} | {self.status} | {self.message}"
|
||||
|
||||
|
||||
def save_results(results: list[PublishResult]):
|
||||
"""追加写入 JSON Lines 日志"""
|
||||
with open(RESULT_LOG, "a", encoding="utf-8") as f:
|
||||
for r in results:
|
||||
f.write(json.dumps(r.to_dict(), ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def print_summary(results: list[PublishResult]):
|
||||
"""控制台打印汇总表"""
|
||||
if not results:
|
||||
return
|
||||
print("\n" + "=" * 72)
|
||||
print(" 发布结果汇总")
|
||||
print("=" * 72)
|
||||
for r in results:
|
||||
icon = "✓" if r.success else "✗"
|
||||
name = Path(r.video_path).stem[:30]
|
||||
print(f" [{icon}] {r.platform:<6} | {name:<32} | {r.status}")
|
||||
if not r.success and r.message:
|
||||
print(f" └─ {r.message[:60]}")
|
||||
ok = sum(1 for r in results if r.success)
|
||||
print("-" * 72)
|
||||
print(f" 成功: {ok}/{len(results)} | 耗时: {sum(r.elapsed_sec for r in results):.1f}s")
|
||||
print("=" * 72 + "\n")
|
||||
@@ -33,10 +33,10 @@ def get_video_info(video_path: str) -> dict:
|
||||
|
||||
|
||||
def extract_cover(video_path: str, output_path: str = "", timestamp: str = "00:00:00.500") -> str:
|
||||
"""提取视频第一帧作为封面(JPEG)"""
|
||||
"""提取视频第一帧作为封面(JPEG),默认存 /tmp"""
|
||||
if not output_path:
|
||||
stem = Path(video_path).stem
|
||||
output_path = str(Path(video_path).parent / f"{stem}_cover.jpg")
|
||||
stem = Path(video_path).stem[:40]
|
||||
output_path = f"/tmp/{stem}_cover.jpg"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg", "-y", "-i", video_path,
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
COOKIE_FILE = SCRIPT_DIR / "xiaohongshu_storage_state.json"
|
||||
VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片")
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR.parent.parent / "多平台分发" / "脚本"))
|
||||
from publish_result import PublishResult
|
||||
|
||||
UA = (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
||||
@@ -50,17 +54,18 @@ TITLES = {
|
||||
}
|
||||
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> bool:
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> PublishResult:
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
fname = Path(video_path).name
|
||||
fsize = Path(video_path).stat().st_size
|
||||
t0 = time.time()
|
||||
print(f"\n[{idx}/{total}] {fname} ({fsize/1024/1024:.1f}MB)", flush=True)
|
||||
print(f" 标题: {title[:60]}", flush=True)
|
||||
|
||||
if not COOKIE_FILE.exists():
|
||||
print(" [✗] Cookie 不存在", flush=True)
|
||||
return False
|
||||
return PublishResult(platform="小红书", video_path=video_path, title=title,
|
||||
success=False, status="error", message="Cookie 不存在")
|
||||
|
||||
try:
|
||||
async with async_playwright() as pw:
|
||||
@@ -86,9 +91,11 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
if "登录" in (await page.title()) and "上传" not in txt:
|
||||
print(" [✗] 未登录,请重新运行 xiaohongshu_login.py", flush=True)
|
||||
await browser.close()
|
||||
return False
|
||||
return PublishResult(platform="小红书", video_path=video_path, title=title,
|
||||
success=False, status="error",
|
||||
message="未登录,请重新运行 xiaohongshu_login.py",
|
||||
error_code="NOT_LOGGED_IN", elapsed_sec=time.time()-t0)
|
||||
|
||||
print(" [2] 上传视频...", flush=True)
|
||||
fl = page.locator('input[type="file"]').first
|
||||
@@ -97,9 +104,12 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
print(" [2] 文件已选择", flush=True)
|
||||
else:
|
||||
await page.screenshot(path="/tmp/xhs_no_input.png")
|
||||
print(" [✗] 未找到上传控件", flush=True)
|
||||
await browser.close()
|
||||
return False
|
||||
return PublishResult(platform="小红书", video_path=video_path, title=title,
|
||||
success=False, status="error",
|
||||
message="未找到上传控件",
|
||||
screenshot="/tmp/xhs_no_input.png",
|
||||
elapsed_sec=time.time()-t0)
|
||||
|
||||
# 等待上传完成(封面生成完毕)
|
||||
for i in range(90):
|
||||
@@ -177,25 +187,33 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
await page.screenshot(path="/tmp/xhs_result.png")
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
url = page.url
|
||||
elapsed = time.time() - t0
|
||||
|
||||
if "发布成功" in txt or "已发布" in txt:
|
||||
print(" [✓] 发布成功!", flush=True)
|
||||
status, msg = "published", "发布成功"
|
||||
elif "审核" in txt:
|
||||
print(" [✓] 已提交审核", flush=True)
|
||||
status, msg = "reviewing", "已提交审核"
|
||||
elif "笔记" in url or "manage" in url:
|
||||
print(" [✓] 已跳转(发布成功)", flush=True)
|
||||
status, msg = "reviewing", "已跳转到笔记管理(发布成功)"
|
||||
elif "拖拽视频到此" in txt or ("上传视频" in txt and "封面" not in txt):
|
||||
print(" [✓] 页面已重置(发布成功)", flush=True)
|
||||
status, msg = "published", "页面已重置(发布成功)"
|
||||
else:
|
||||
print(" [⚠] 查看截图: /tmp/xhs_result.png", flush=True)
|
||||
status, msg = "reviewing", "已提交,请确认截图"
|
||||
|
||||
result = PublishResult(
|
||||
platform="小红书", video_path=video_path, title=title,
|
||||
success=True, status=status, message=msg,
|
||||
screenshot="/tmp/xhs_result.png", elapsed_sec=elapsed,
|
||||
)
|
||||
print(f" {result.log_line()}", flush=True)
|
||||
await ctx.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f" [✗] 异常: {e}", flush=True)
|
||||
return False
|
||||
return PublishResult(platform="小红书", video_path=video_path, title=title,
|
||||
success=False, status="error",
|
||||
message=f"异常: {str(e)[:80]}", elapsed_sec=time.time()-t0)
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,12 +5,16 @@
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
COOKIE_FILE = SCRIPT_DIR / "kuaishou_storage_state.json"
|
||||
VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片")
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR.parent.parent / "多平台分发" / "脚本"))
|
||||
from publish_result import PublishResult
|
||||
|
||||
UA = (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
||||
@@ -50,17 +54,18 @@ TITLES = {
|
||||
}
|
||||
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> bool:
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> PublishResult:
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
fname = Path(video_path).name
|
||||
fsize = Path(video_path).stat().st_size
|
||||
t0 = time.time()
|
||||
print(f"\n[{idx}/{total}] {fname} ({fsize/1024/1024:.1f}MB)", flush=True)
|
||||
print(f" 标题: {title[:60]}", flush=True)
|
||||
|
||||
if not COOKIE_FILE.exists():
|
||||
print(" [✗] Cookie 不存在", flush=True)
|
||||
return False
|
||||
return PublishResult(platform="快手", video_path=video_path, title=title,
|
||||
success=False, status="error", message="Cookie 不存在")
|
||||
|
||||
try:
|
||||
async with async_playwright() as pw:
|
||||
@@ -86,9 +91,11 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
if "立即登录" in txt and "发布作品" not in txt:
|
||||
print(" [✗] 未登录,请重新运行 kuaishou_login.py", flush=True)
|
||||
await browser.close()
|
||||
return False
|
||||
return PublishResult(platform="快手", video_path=video_path, title=title,
|
||||
success=False, status="error",
|
||||
message="未登录,请重新运行 kuaishou_login.py",
|
||||
error_code="NOT_LOGGED_IN", elapsed_sec=time.time()-t0)
|
||||
|
||||
# 处理"上次未发布的视频"草稿提示
|
||||
discard = page.locator('text=放弃').first
|
||||
@@ -103,9 +110,11 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
await fl.set_input_files(video_path)
|
||||
print(" [2] 文件已选择", flush=True)
|
||||
else:
|
||||
print(" [✗] 未找到上传控件", flush=True)
|
||||
await browser.close()
|
||||
return False
|
||||
return PublishResult(platform="快手", video_path=video_path, title=title,
|
||||
success=False, status="error",
|
||||
message="未找到上传控件", error_code="NO_UPLOAD_INPUT",
|
||||
elapsed_sec=time.time()-t0)
|
||||
|
||||
# 等待上传完成
|
||||
for i in range(90):
|
||||
@@ -170,23 +179,31 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
await page.screenshot(path="/tmp/kuaishou_result.png")
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
url = page.url
|
||||
elapsed = time.time() - t0
|
||||
|
||||
if "发布成功" in txt or "已发布" in txt:
|
||||
print(" [✓] 发布成功!", flush=True)
|
||||
status, msg = "published", "发布成功"
|
||||
elif "审核" in txt:
|
||||
print(" [✓] 已提交审核", flush=True)
|
||||
elif "manage" in url or "list" in url:
|
||||
print(" [✓] 已跳转(发布成功)", flush=True)
|
||||
status, msg = "reviewing", "已提交审核"
|
||||
elif "manage" in url or "list" in url or "作品管理" in txt:
|
||||
status, msg = "reviewing", "已跳转到作品管理(发布成功)"
|
||||
else:
|
||||
print(" [⚠] 查看截图: /tmp/kuaishou_result.png", flush=True)
|
||||
status, msg = "reviewing", "已提交,请确认截图"
|
||||
|
||||
result = PublishResult(
|
||||
platform="快手", video_path=video_path, title=title,
|
||||
success=True, status=status, message=msg,
|
||||
screenshot="/tmp/kuaishou_result.png", elapsed_sec=elapsed,
|
||||
)
|
||||
print(f" {result.log_line()}", flush=True)
|
||||
await ctx.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f" [✗] 异常: {e}", flush=True)
|
||||
return False
|
||||
return PublishResult(platform="快手", video_path=video_path, title=title,
|
||||
success=False, status="error",
|
||||
message=f"异常: {str(e)[:80]}", elapsed_sec=time.time()-t0)
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,16 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
视频号发布 - Headless Playwright
|
||||
上传 → 填描述 → 发表。视频号反自动化较弱,headless 可正常运行。
|
||||
上传 → 填描述 → 发表。视频号无公开API,Playwright为唯一方案。
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
COOKIE_FILE = SCRIPT_DIR / "channels_storage_state.json"
|
||||
VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片")
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR.parent.parent / "多平台分发" / "脚本"))
|
||||
from publish_result import PublishResult
|
||||
|
||||
UA = (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
||||
@@ -50,17 +54,18 @@ TITLES = {
|
||||
}
|
||||
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> bool:
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> PublishResult:
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
fname = Path(video_path).name
|
||||
fsize = Path(video_path).stat().st_size
|
||||
t0 = time.time()
|
||||
print(f"\n[{idx}/{total}] {fname} ({fsize/1024/1024:.1f}MB)", flush=True)
|
||||
print(f" 标题: {title[:60]}", flush=True)
|
||||
|
||||
if not COOKIE_FILE.exists():
|
||||
print(" [✗] Cookie 不存在", flush=True)
|
||||
return False
|
||||
return PublishResult(platform="视频号", video_path=video_path, title=title,
|
||||
success=False, status="error", message="Cookie 不存在")
|
||||
|
||||
try:
|
||||
async with async_playwright() as pw:
|
||||
@@ -168,21 +173,29 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
await page.screenshot(path="/tmp/channels_result.png")
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
url = page.url
|
||||
elapsed = time.time() - t0
|
||||
|
||||
if "发表成功" in txt or "已发表" in txt or "成功" in txt:
|
||||
print(" [✓] 发表成功!", flush=True)
|
||||
elif "/platform/post/list" in url or "platform" in url:
|
||||
print(" [✓] 已跳转(发表成功)", flush=True)
|
||||
status, msg = "published", "发表成功"
|
||||
elif "/platform/post/list" in url or ("内容管理" in txt and "视频" in txt):
|
||||
status, msg = "reviewing", "已跳转到内容管理(发表成功)"
|
||||
else:
|
||||
print(" [⚠] 查看截图确认: /tmp/channels_result.png", flush=True)
|
||||
status, msg = "reviewing", "已提交,请确认截图"
|
||||
|
||||
result = PublishResult(
|
||||
platform="视频号", video_path=video_path, title=title,
|
||||
success=True, status=status, message=msg,
|
||||
screenshot="/tmp/channels_result.png", elapsed_sec=elapsed,
|
||||
)
|
||||
print(f" {result.log_line()}", flush=True)
|
||||
await ctx.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f" [✗] 异常: {e}", flush=True)
|
||||
return False
|
||||
return PublishResult(platform="视频号", video_path=video_path, title=title,
|
||||
success=False, status="error",
|
||||
message=f"异常: {str(e)[:80]}", elapsed_sec=time.time()-t0)
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"cookies": [{"name": "sessionid", "value": "BgAAhwULqGfw5gClpw57W0xp6S%2Bv1xxG%2BtoX5MZUGea90WpFZ6g2CZzSm9%2FgiC3wDqiqTpyuGduYwaeCxtofVE6i7hzfyyZmtkIUkktbCus%3D", "domain": "channels.weixin.qq.com", "path": "/", "expires": 1807677423.042462, "httpOnly": false, "secure": true, "sameSite": "None"}, {"name": "wxuin", "value": "3873206396", "domain": "channels.weixin.qq.com", "path": "/", "expires": 1807677423.042529, "httpOnly": false, "secure": true, "sameSite": "None"}], "origins": [{"origin": "https://channels.weixin.qq.com", "localStorage": [{"name": "finder_uin", "value": ""}, {"name": "__ml::page_51552276-5296-41a8-a2f7-a8b0ab36bc16", "value": "{\"pageId\":\"PostList\",\"accessId\":\"39b9bbbc-2e13-48c4-be00-91a70adf6859\",\"step\":2,\"refAccessId\":\"0b8cc40b-a714-4283-9a05-6085c50a799c\",\"refPageId\":\"PostCreate\"}"}, {"name": "__ml::hb_ts", "value": "1773120796513"}, {"name": "__ml::page_edf1bbb5-ccc0-465b-9912-4cd0dcaf1b65", "value": "{\"pageId\":\"PostCreate\",\"accessId\":\"9a803740-2527-4199-8e3d-8c358c8e75e0\",\"step\":1}"}, {"name": "__ml::aid", "value": "\"5749fb2e-51db-48f2-bab1-0d77038fb31a\""}, {"name": "__ml::page_8a0d0b1a-65d6-4a5c-b9f0-fde73bdfa9db", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"8f302ca1-c107-42bc-a227-bee6cc8fb44e\",\"step\":1}"}, {"name": "__rx::aid", "value": "\"5749fb2e-51db-48f2-bab1-0d77038fb31a\""}, {"name": "__ml::page", "value": "[\"72a13cf3-369b-4424-b69d-7ed0deebcc4f\",\"a2988245-e0e8-476b-85dc-106b7c6f5288\",\"ba0e3072-ab8d-43e2-ba6c-7ac71cd8611c\",\"8a0d0b1a-65d6-4a5c-b9f0-fde73bdfa9db\",\"654f47c4-50e7-47ab-b504-f4b90d806cb0\",\"1398bdf4-70b2-409a-b516-4d77923e0f18\",\"edf1bbb5-ccc0-465b-9912-4cd0dcaf1b65\",\"7fc5f55c-5b2c-49c0-ab88-7a31c2c6035a\",\"e2bab444-0f1a-47b2-b1f7-b3acb2ff73ce\",\"b7782d0f-29e4-4123-8184-7c5084b8c2d1\",\"51552276-5296-41a8-a2f7-a8b0ab36bc16\",\"fb0cd2dc-c646-4f56-8c37-c674f100db33\",\"5f9b5814-959c-4caa-9c5f-4cb1d0e9953e\"]"}, {"name": "__ml::page_ba0e3072-ab8d-43e2-ba6c-7ac71cd8611c", "value": "{\"pageId\":\"Home\",\"accessId\":\"ebadf8a0-665a-4c1a-840a-6917618f7414\",\"step\":1}"}, {"name": "finder_login_token", "value": ""}, {"name": "__ml::page_a2988245-e0e8-476b-85dc-106b7c6f5288", "value": "{\"pageId\":\"LoginForIframe\",\"accessId\":\"74bd878b-c591-4a54-b9f1-168fb21538c4\",\"step\":1}"}, {"name": "__ml::page_e2bab444-0f1a-47b2-b1f7-b3acb2ff73ce", "value": "{\"pageId\":\"PostCreate\",\"accessId\":\"31466641-845f-430f-9853-8c51b07d8721\",\"step\":1}"}, {"name": "__ml::page_72a13cf3-369b-4424-b69d-7ed0deebcc4f", "value": "{\"pageId\":\"LoginForIframe\",\"accessId\":\"bd5e50a0-fc11-477c-9cc9-9c76e2d15205\",\"step\":1}"}, {"name": "finder_username", "value": "v2_060000231003b20faec8c5e48919cbd5cb05e53db077dd1924028a806c10cffd891eb5a80ce7@finder"}, {"name": "__ml::page_b7782d0f-29e4-4123-8184-7c5084b8c2d1", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"bf8c3e8f-bd8d-445d-93a3-ff942287906d\",\"step\":2,\"refAccessId\":\"0ec1591b-605a-4783-91f5-4c4b56baf2cd\",\"refPageId\":\"MicroPost\"}"}, {"name": "_finger_print_device_id", "value": "6fd704941768442b12a996d2652fc61e"}, {"name": "__ml::page_fb0cd2dc-c646-4f56-8c37-c674f100db33", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"ece1a13f-8fe9-4b07-ad56-9428b1a4c49a\",\"step\":2,\"refAccessId\":\"15108cd8-2794-4c73-b959-5fbb3f3c8d83\",\"refPageId\":\"MicroPost\"}"}, {"name": "MICRO_VISITED_NAME", "value": "{\"content\":7}"}, {"name": "__ml::page_5f9b5814-959c-4caa-9c5f-4cb1d0e9953e", "value": "{\"pageId\":\"PostList\",\"accessId\":\"b6ec417c-3b78-4538-93c1-adf298573f3c\",\"step\":2,\"refAccessId\":\"66744497-c4ab-4d3f-b3fc-ac9d0635c405\",\"refPageId\":\"PostCreate\"}"}, {"name": "UvFirstReportLocalKey", "value": "1773072000000"}, {"name": "__ml::page_7fc5f55c-5b2c-49c0-ab88-7a31c2c6035a", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"e21b55a2-3558-45b7-bdc1-5bef0d9976a3\",\"step\":1}"}, {"name": "__ml::page_1398bdf4-70b2-409a-b516-4d77923e0f18", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"e3254f30-b8a3-44d2-9511-9f087c6e0f7e\",\"step\":1}"}, {"name": "finder_ua_report_data", "value": "{\"browser\":\"Chrome\",\"browserVersion\":\"143.0.0.0\",\"engine\":\"Webkit\",\"engineVersion\":\"537.36\",\"os\":\"Mac OS X\",\"osVersion\":\"10.15.7\",\"device\":\"desktop\",\"darkmode\":0}"}, {"name": "__ml::page_654f47c4-50e7-47ab-b504-f4b90d806cb0", "value": "{\"pageId\":\"PostCreate\",\"accessId\":\"1843fcab-97c8-4b58-9d1a-c75a67ab6432\",\"step\":1}"}, {"name": "finder_route_meta", "value": "micro.content/post/list;micro.content/post/create;1;1773120814084"}]}]}
|
||||
{"cookies": [{"name": "sessionid", "value": "BgAAhwULqGfw5gClpw57W0xp6S%2Bv1xxG%2BtoX5MZUGea90WpFZ6g2CZzSm9%2FgiC3wDqiqTpyuGduYwaeCxtofVE6i7hzfyyZmtkIUkktbCus%3D", "domain": "channels.weixin.qq.com", "path": "/", "expires": 1807677423.042462, "httpOnly": false, "secure": true, "sameSite": "None"}, {"name": "wxuin", "value": "3873206396", "domain": "channels.weixin.qq.com", "path": "/", "expires": 1807677423.042529, "httpOnly": false, "secure": true, "sameSite": "None"}], "origins": [{"origin": "https://channels.weixin.qq.com", "localStorage": [{"name": "finder_uin", "value": ""}, {"name": "__ml::page_a32df29f-5920-415a-a2cd-12ec324ba685", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"1ef23431-eaaf-46b4-8029-bcb651e5e122\",\"step\":1}"}, {"name": "__ml::page_51552276-5296-41a8-a2f7-a8b0ab36bc16", "value": "{\"pageId\":\"PostList\",\"accessId\":\"39b9bbbc-2e13-48c4-be00-91a70adf6859\",\"step\":2,\"refAccessId\":\"0b8cc40b-a714-4283-9a05-6085c50a799c\",\"refPageId\":\"PostCreate\"}"}, {"name": "__ml::hb_ts", "value": "1773121625477"}, {"name": "__ml::page_edf1bbb5-ccc0-465b-9912-4cd0dcaf1b65", "value": "{\"pageId\":\"PostCreate\",\"accessId\":\"9a803740-2527-4199-8e3d-8c358c8e75e0\",\"step\":1}"}, {"name": "__ml::aid", "value": "\"5749fb2e-51db-48f2-bab1-0d77038fb31a\""}, {"name": "__ml::page_8a0d0b1a-65d6-4a5c-b9f0-fde73bdfa9db", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"8f302ca1-c107-42bc-a227-bee6cc8fb44e\",\"step\":1}"}, {"name": "__ml::page_d5bd19e5-ae9b-486e-95d8-0b4d8baabfb6", "value": "{\"pageId\":\"PostCreate\",\"accessId\":\"e6ad3eae-f209-4ef4-871d-ef447601fc31\",\"step\":1}"}, {"name": "__rx::aid", "value": "\"5749fb2e-51db-48f2-bab1-0d77038fb31a\""}, {"name": "__ml::page", "value": "[\"72a13cf3-369b-4424-b69d-7ed0deebcc4f\",\"a2988245-e0e8-476b-85dc-106b7c6f5288\",\"ba0e3072-ab8d-43e2-ba6c-7ac71cd8611c\",\"8a0d0b1a-65d6-4a5c-b9f0-fde73bdfa9db\",\"654f47c4-50e7-47ab-b504-f4b90d806cb0\",\"1398bdf4-70b2-409a-b516-4d77923e0f18\",\"edf1bbb5-ccc0-465b-9912-4cd0dcaf1b65\",\"7fc5f55c-5b2c-49c0-ab88-7a31c2c6035a\",\"e2bab444-0f1a-47b2-b1f7-b3acb2ff73ce\",\"b7782d0f-29e4-4123-8184-7c5084b8c2d1\",\"51552276-5296-41a8-a2f7-a8b0ab36bc16\",\"fb0cd2dc-c646-4f56-8c37-c674f100db33\",\"5f9b5814-959c-4caa-9c5f-4cb1d0e9953e\",\"a32df29f-5920-415a-a2cd-12ec324ba685\",\"d5bd19e5-ae9b-486e-95d8-0b4d8baabfb6\"]"}, {"name": "__ml::page_ba0e3072-ab8d-43e2-ba6c-7ac71cd8611c", "value": "{\"pageId\":\"Home\",\"accessId\":\"ebadf8a0-665a-4c1a-840a-6917618f7414\",\"step\":1}"}, {"name": "finder_login_token", "value": ""}, {"name": "__ml::page_a2988245-e0e8-476b-85dc-106b7c6f5288", "value": "{\"pageId\":\"LoginForIframe\",\"accessId\":\"74bd878b-c591-4a54-b9f1-168fb21538c4\",\"step\":1}"}, {"name": "__ml::page_e2bab444-0f1a-47b2-b1f7-b3acb2ff73ce", "value": "{\"pageId\":\"PostCreate\",\"accessId\":\"31466641-845f-430f-9853-8c51b07d8721\",\"step\":1}"}, {"name": "__ml::page_72a13cf3-369b-4424-b69d-7ed0deebcc4f", "value": "{\"pageId\":\"LoginForIframe\",\"accessId\":\"bd5e50a0-fc11-477c-9cc9-9c76e2d15205\",\"step\":1}"}, {"name": "finder_username", "value": "v2_060000231003b20faec8c5e48919cbd5cb05e53db077dd1924028a806c10cffd891eb5a80ce7@finder"}, {"name": "__ml::page_b7782d0f-29e4-4123-8184-7c5084b8c2d1", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"bf8c3e8f-bd8d-445d-93a3-ff942287906d\",\"step\":2,\"refAccessId\":\"0ec1591b-605a-4783-91f5-4c4b56baf2cd\",\"refPageId\":\"MicroPost\"}"}, {"name": "__ml::page_fb0cd2dc-c646-4f56-8c37-c674f100db33", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"ece1a13f-8fe9-4b07-ad56-9428b1a4c49a\",\"step\":2,\"refAccessId\":\"15108cd8-2794-4c73-b959-5fbb3f3c8d83\",\"refPageId\":\"MicroPost\"}"}, {"name": "_finger_print_device_id", "value": "6fd704941768442b12a996d2652fc61e"}, {"name": "MICRO_VISITED_NAME", "value": "{\"content\":8}"}, {"name": "__ml::page_5f9b5814-959c-4caa-9c5f-4cb1d0e9953e", "value": "{\"pageId\":\"PostList\",\"accessId\":\"b6ec417c-3b78-4538-93c1-adf298573f3c\",\"step\":2,\"refAccessId\":\"66744497-c4ab-4d3f-b3fc-ac9d0635c405\",\"refPageId\":\"PostCreate\"}"}, {"name": "UvFirstReportLocalKey", "value": "1773072000000"}, {"name": "__ml::page_7fc5f55c-5b2c-49c0-ab88-7a31c2c6035a", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"e21b55a2-3558-45b7-bdc1-5bef0d9976a3\",\"step\":1}"}, {"name": "__ml::page_1398bdf4-70b2-409a-b516-4d77923e0f18", "value": "{\"pageId\":\"MicroPost\",\"accessId\":\"e3254f30-b8a3-44d2-9511-9f087c6e0f7e\",\"step\":1}"}, {"name": "finder_ua_report_data", "value": "{\"browser\":\"Chrome\",\"browserVersion\":\"143.0.0.0\",\"engine\":\"Webkit\",\"engineVersion\":\"537.36\",\"os\":\"Mac OS X\",\"osVersion\":\"10.15.7\",\"device\":\"desktop\",\"darkmode\":0}"}, {"name": "__ml::page_654f47c4-50e7-47ab-b504-f4b90d806cb0", "value": "{\"pageId\":\"PostCreate\",\"accessId\":\"1843fcab-97c8-4b58-9d1a-c75a67ab6432\",\"step\":1}"}, {"name": "finder_route_meta", "value": "micro.content/post/create;index;1;1773121627741"}]}]}
|
||||
@@ -259,3 +259,4 @@
|
||||
| 2026-03-09 22:23:01 | 🔄 卡若AI 同步 2026-03-09 22:22 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-10 12:30:08 | 🔄 卡若AI 同步 2026-03-10 12:30 | 更新:水桥平台对接、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-10 12:54:57 | 🔄 卡若AI 同步 2026-03-10 12:54 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-10 13:34:41 | 🔄 卡若AI 同步 2026-03-10 13:34 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
|
||||
@@ -262,3 +262,4 @@
|
||||
| 2026-03-09 22:23:01 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-09 22:22 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-10 12:30:08 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-10 12:30 | 更新:水桥平台对接、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-10 12:54:57 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-10 12:54 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-10 13:34:41 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-10 13:34 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user