🔄 卡若AI 同步 2026-03-10 13:34 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 11 个
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
- 文章已按 **写作/写作规范.md** 写好。
|
||||
- **第9章(第101场及以前)**:保存为 `9.xx 第X场|主题.md`,位于第9章目录。
|
||||
- **2026 场次(第102场及以后)**:保存为 `第X场|主题.md`,位于 `2026年/` 目录。
|
||||
- **2026 场次(第102场及以后)**:保存为 `第X场|主题.md`,位于 `2026每日派对干货/` 目录。
|
||||
|
||||
---
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
| 项目 | 值 |
|
||||
|:---|:---|
|
||||
| 第9章文章目录 | `/Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/` |
|
||||
| **2026 场次目录** | `/Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/2026年/` |
|
||||
| **2026 场次目录** | `/Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/2026每日派对干货/` |
|
||||
| 项目(含 content_upload) | `一场soul的创业实验-永平`(根目录有 `content_upload.py`) |
|
||||
| 第9章参数 | part-4, chapter-9, price 1.0 |
|
||||
| **2026每日派对干货参数** | part-2026-daily, chapter-2026-daily, id 10.xx, price 1.0 |
|
||||
@@ -39,7 +39,7 @@ python3 content_upload.py --title "第X场|标题" \
|
||||
|
||||
# 或指定 id(如 10.18)
|
||||
python3 content_upload.py --id 10.18 --title "第119场|开派对的初心是早上不影响老婆睡觉" \
|
||||
--content-file "/Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/2026年/第119场|开派对的初心是早上不影响老婆睡觉.md" \
|
||||
--content-file "/Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/2026每日派对干货/第119场|开派对的初心是早上不影响老婆睡觉.md" \
|
||||
--part part-2026-daily --chapter chapter-2026-daily --price 1.0
|
||||
```
|
||||
|
||||
|
||||
@@ -5,4 +5,6 @@
|
||||
- **链接**:https://cunkebao.feishu.cn/wiki/ZdSBwHrsGii14HkcIbccQ0flnee
|
||||
- **3 月 token**:`ZdSBwHrsGii14HkcIbccQ0flnee`(已写入 `.feishu_month_wiki_tokens.json`)
|
||||
|
||||
**日期以中国时间为准**:脚本内「今日」一律按中国时间(Asia/Shanghai)计算,保证写的是 3月10日 即中国 3 月 10 号。
|
||||
|
||||
**月份校验**:写入前会检查文档标题是否含当月(如「3月」)。若文档月份与当月不符,会提示:**请先在飞书新建当月文档,再用 `feishu_token_cli.py set-march-token <新文档token>` 后重试**(先迁一个)。
|
||||
|
||||
@@ -13,9 +13,22 @@ import json
|
||||
import subprocess
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
import time
|
||||
import re
|
||||
|
||||
# 飞书日志日期以中国时间为准
|
||||
CHINA_TZ = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
def now_china():
|
||||
"""当前时间(中国时间)"""
|
||||
return datetime.now(CHINA_TZ)
|
||||
|
||||
def get_today_date_str():
|
||||
"""今日日期字符串(中国时间),如 3月10日"""
|
||||
t = now_china()
|
||||
return f"{t.month}月{t.day}日"
|
||||
|
||||
# ============ 配置 ============
|
||||
CONFIG = {
|
||||
'APP_ID': 'cli_a48818290ef8100d',
|
||||
@@ -152,9 +165,8 @@ def get_token_silent():
|
||||
# ============ 日志写入 ============
|
||||
# 写日志前应先读 运营中枢/工作台/2026年整体目标.md,使百分比与总目标一致、上下文相关
|
||||
def get_today_tasks():
|
||||
"""获取今天的任务(可自定义修改);目标百分比以总目标为核心,见 2026年整体目标.md"""
|
||||
today = datetime.now()
|
||||
date_str = f"{today.month}月{today.day}日"
|
||||
"""获取今天的任务(可自定义修改);日期以中国时间为准;目标百分比以总目标为核心,见 2026年整体目标.md"""
|
||||
date_str = get_today_date_str()
|
||||
|
||||
# 每日固定项:开发<20%,侧重事务与方向;每晚20:00玩值电竞朋友圈已入本机日历
|
||||
tasks = [
|
||||
@@ -181,11 +193,10 @@ def get_today_tasks():
|
||||
return date_str, tasks
|
||||
|
||||
def build_blocks(date_str, tasks):
|
||||
"""构建飞书文档块(倒序:新日期在上)"""
|
||||
"""构建飞书文档块(倒序:新日期在上);callout 易触发 field validation failed,改用 text"""
|
||||
blocks = [
|
||||
{'block_type': 6, 'heading4': {'elements': [{'text_run': {'content': f'{date_str} '}}], 'style': {'align': 1}}},
|
||||
{'block_type': 19, 'callout': {'emoji_id': 'sunrise', 'background_color': 2, 'border_color': 2,
|
||||
'elements': [{'text_run': {'content': '[执行]', 'text_element_style': {'bold': True, 'text_color': 7}}}]}}
|
||||
{'block_type': 2, 'text': {'elements': [{'text_run': {'content': '[执行]', 'text_element_style': {'bold': True}}}], 'style': {}}}
|
||||
]
|
||||
|
||||
quadrant_colors = {"重要紧急": 5, "重要不紧急": 3, "不重要紧急": 6, "不重要不紧急": 4}
|
||||
@@ -229,6 +240,23 @@ def build_blocks(date_str, tasks):
|
||||
return blocks
|
||||
|
||||
|
||||
def _text_block_simple(content):
|
||||
"""极简文本块,兼容 field validation 严格校验"""
|
||||
return {'block_type': 2, 'text': {'elements': [{'text_run': {'content': content}}], 'style': {}}}
|
||||
|
||||
|
||||
def _build_blocks_simple(date_str, tasks):
|
||||
"""极简块(仅纯文本),用于 field validation failed 时回退"""
|
||||
blocks = [_text_block_simple(f'{date_str} '), _text_block_simple('[执行]')]
|
||||
for task in tasks:
|
||||
events = '、'.join(task.get('events', []))
|
||||
blocks.append(_text_block_simple(f"{task.get('person', '')}({events})"))
|
||||
for key in ('t_targets', 'n_process', 't_thoughts', 'w_work', 'f_feedback'):
|
||||
for item in task.get(key, []):
|
||||
blocks.append(_text_block_simple(f" {item}"))
|
||||
return blocks
|
||||
|
||||
|
||||
def parse_month_from_date_str(date_str):
|
||||
"""从如 '2月25日' 提取月份整数"""
|
||||
m = re.search(r'(\d+)\s*月', date_str or '')
|
||||
@@ -472,11 +500,16 @@ def write_log(token, date_str=None, tasks=None, wiki_token=None, overwrite=False
|
||||
insert_index = i + 1
|
||||
break
|
||||
|
||||
# 写入(倒序:新日期在上)
|
||||
# 写入(倒序:新日期在上);field validation failed 时尝试极简纯文本块
|
||||
content_blocks = build_blocks(date_str, tasks)
|
||||
payload = {'children': content_blocks, 'index': insert_index}
|
||||
r = requests.post(f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}/children",
|
||||
headers=headers, json={'children': content_blocks, 'index': insert_index}, timeout=30)
|
||||
|
||||
headers=headers, json=payload, timeout=30)
|
||||
if r.json().get('code') != 0 and 'field validation failed' in (r.json().get('msg') or '').lower():
|
||||
content_blocks = _build_blocks_simple(date_str, tasks)
|
||||
payload = {'children': content_blocks, 'index': insert_index}
|
||||
r = requests.post(f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}/children",
|
||||
headers=headers, json=payload, timeout=30)
|
||||
if r.json().get('code') == 0:
|
||||
print(f"✅ {date_str} 日志写入成功 -> {doc_title}")
|
||||
return True
|
||||
|
||||
@@ -3,19 +3,17 @@
|
||||
今日飞书日志(3月定制):200视频/日、工具研发10~30切片、售内容产出、李永平、年度目标百分比
|
||||
"""
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from auto_log import get_token_silent, write_log, open_result, resolve_wiki_token_for_date, CONFIG
|
||||
from auto_log import get_token_silent, write_log, open_result, resolve_wiki_token_for_date, CONFIG, get_today_date_str
|
||||
|
||||
|
||||
def build_tasks_today():
|
||||
"""今日:200视频/日、工具研发10~30切片、售内容产出、按年度目标百分比"""
|
||||
today = datetime.now()
|
||||
date_str = f"{today.month}月{today.day}日"
|
||||
"""今日:200视频/日、工具研发10~30切片、售内容产出、按年度目标百分比(日期以中国时间为准)"""
|
||||
date_str = get_today_date_str()
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -96,8 +94,7 @@ def build_tasks_today():
|
||||
|
||||
|
||||
def main():
|
||||
today = datetime.now()
|
||||
date_str = f"{today.month}月{today.day}日"
|
||||
date_str = get_today_date_str()
|
||||
print("=" * 50)
|
||||
print(f"📝 写入今日飞书日志(200视频+工具研发+售内容+年度目标%):{date_str}")
|
||||
print("=" * 50)
|
||||
|
||||
@@ -13,13 +13,12 @@ from pathlib import Path
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from auto_log import get_token_silent, write_log, open_result, resolve_wiki_token_for_date, CONFIG
|
||||
from auto_log import get_token_silent, write_log, open_result, resolve_wiki_token_for_date, CONFIG, get_today_date_str
|
||||
|
||||
|
||||
def build_tasks_today_three_focus():
|
||||
"""今日三件事 + 前面未完成;百分比以 2026年整体目标.md 为基准"""
|
||||
today = datetime.now()
|
||||
date_str = f"{today.month}月{today.day}日"
|
||||
"""今日三件事 + 前面未完成;日期以中国时间为准;百分比以 2026年整体目标.md 为基准"""
|
||||
date_str = get_today_date_str()
|
||||
# 前面未完成(延续 3 月 / 本月未闭环)
|
||||
unfinished = [
|
||||
"20 条 Soul 视频 + 20:00 发 1 条朋友圈(每日固定)",
|
||||
@@ -79,10 +78,9 @@ def main():
|
||||
parser.add_argument("--overwrite", action="store_true", help="覆盖已有当日日志")
|
||||
args = parser.parse_args()
|
||||
|
||||
today = datetime.now()
|
||||
date_str = f"{today.month}月{today.day}日"
|
||||
date_str = get_today_date_str()
|
||||
print("=" * 50)
|
||||
print(f"📝 写入今日飞书日志:{date_str}" + (" [覆盖]" if args.overwrite else ""))
|
||||
print(f"📝 写入今日飞书日志(中国时间):{date_str}" + (" [覆盖]" if args.overwrite else ""))
|
||||
print(" ① 卡若AI 完善/接口可用 ② 一场创业实验 网站/小程序上线 ③ 玩值电竞 布局 + 前面未完成")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from auto_log import get_token_silent, write_log, open_result, resolve_wiki_token_for_date, CONFIG
|
||||
from auto_log import get_token_silent, write_log, open_result, resolve_wiki_token_for_date, CONFIG, get_today_date_str
|
||||
|
||||
REF_DIR = SCRIPT_DIR.parent / "参考资料"
|
||||
|
||||
@@ -85,9 +85,8 @@ def _upload_and_insert_image(token: str, doc_id: str, image_path: Path, date_str
|
||||
|
||||
|
||||
def build_tasks_today_with_summary():
|
||||
"""今日:最近进度汇总 + 每天20切片 + 1980成交及全链路 + 目标百分比"""
|
||||
today = datetime.now()
|
||||
date_str = f"{today.month}月{today.day}日"
|
||||
"""今日:最近进度汇总 + 每天20切片 + 1980成交及全链路 + 目标百分比(日期以中国时间为准)"""
|
||||
date_str = get_today_date_str()
|
||||
# 最近进度汇总(来自全库+智能纪要 output)
|
||||
summary = [
|
||||
"【进度汇总】飞书 Token 全命令行(get/set-march-token)、今日日志三件事+未完成已固化",
|
||||
@@ -126,8 +125,7 @@ def build_tasks_today_with_summary():
|
||||
|
||||
|
||||
def main():
|
||||
today = datetime.now()
|
||||
date_str = f"{today.month}月{today.day}日"
|
||||
date_str = get_today_date_str()
|
||||
print("=" * 50)
|
||||
print(f"📝 写入今日飞书日志(进度汇总+20切片+1980全链路+百分比):{date_str}")
|
||||
print("=" * 50)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
B站视频发布 - Headless Playwright 自动化
|
||||
用 force=True 绕过 GeeTest overlay,JS 辅助操作 Vue 组件。
|
||||
B站视频发布 - Playwright 自动化(可见浏览器)
|
||||
B站反自动化较强,采用可见浏览器模式:
|
||||
- 自动上传、填写标题/分区/标签、点击投稿
|
||||
- 用户无需操作,但浏览器窗口可见
|
||||
- 首次可能需过极验验证码(一次后不再出现)
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -15,26 +17,14 @@ VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_out
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR.parent.parent / "多平台分发" / "脚本"))
|
||||
from cookie_manager import CookieManager
|
||||
from video_utils import extract_cover
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
STEALTH_JS = """
|
||||
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
||||
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
|
||||
Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh', 'en']});
|
||||
window.chrome = {runtime: {}};
|
||||
const origQuery = window.navigator.permissions.query;
|
||||
window.navigator.permissions.query = (parameters) =>
|
||||
parameters.name === 'notifications'
|
||||
? Promise.resolve({state: Notification.permission})
|
||||
: origQuery(parameters);
|
||||
"""
|
||||
|
||||
TITLES = {
|
||||
"早起不是为了开派对,是不吵老婆睡觉.mp4":
|
||||
"每天6点起床不是因为自律,是因为老婆还在睡 #Soul派对 #创业日记",
|
||||
@@ -70,7 +60,7 @@ TITLES = {
|
||||
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> bool:
|
||||
"""Headless Playwright 发布单条 B站 视频,全程 JS 操作绕过 GeeTest"""
|
||||
"""用可见浏览器自动化发布单条视频"""
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
fname = Path(video_path).name
|
||||
@@ -89,8 +79,8 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
try:
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(
|
||||
headless=True,
|
||||
args=["--disable-blink-features=AutomationControlled", "--no-sandbox"],
|
||||
headless=False,
|
||||
args=["--disable-blink-features=AutomationControlled"],
|
||||
)
|
||||
context = await browser.new_context(
|
||||
storage_state=str(COOKIE_FILE),
|
||||
@@ -98,165 +88,150 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1)
|
||||
viewport={"width": 1280, "height": 900},
|
||||
locale="zh-CN",
|
||||
)
|
||||
await context.add_init_script(STEALTH_JS)
|
||||
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(5)
|
||||
|
||||
# 清除 GeeTest overlay
|
||||
await page.evaluate("document.querySelectorAll('[class*=\"geetest\"]').forEach(el => el.remove())")
|
||||
await asyncio.sleep(3)
|
||||
|
||||
print(" [2] 上传视频...")
|
||||
file_input = await page.query_selector('input[type="file"]')
|
||||
if not file_input:
|
||||
file_input = await page.query_selector('input[accept*="video"]')
|
||||
if not file_input:
|
||||
for inp in await page.query_selector_all('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] 文件已选择,等待上传...")
|
||||
print(" [2] 文件已选择,等待上传完成...")
|
||||
|
||||
for wait_round in range(60):
|
||||
page_text = await page.inner_text("body")
|
||||
if "封面" in page_text or "分区" in page_text:
|
||||
print(" [2] 上传完成")
|
||||
break
|
||||
# 等待上传完成(查找进度条或"重新上传"按钮)
|
||||
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)
|
||||
|
||||
# 再次清除 GeeTest(可能上传后又弹出)
|
||||
await page.evaluate("document.querySelectorAll('[class*=\"geetest\"]').forEach(el => el.remove())")
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# === 全部使用 force=True 点击,绕过 overlay ===
|
||||
# 填写标题
|
||||
print(" [3] 填写标题...")
|
||||
title_input = page.locator('input[maxlength="80"]').first
|
||||
if await title_input.count() > 0:
|
||||
await title_input.click(force=True)
|
||||
await title_input.click()
|
||||
await title_input.fill("")
|
||||
await title_input.fill(title[:80])
|
||||
await asyncio.sleep(0.3)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 选择"自制"
|
||||
print(" [3b] 选择类型:自制...")
|
||||
original_label = page.locator('label:has-text("自制")').first
|
||||
if await original_label.count() > 0:
|
||||
await original_label.click(force=True)
|
||||
else:
|
||||
radio = page.locator('text=自制').first
|
||||
if await radio.count() > 0:
|
||||
await radio.click(force=True)
|
||||
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)
|
||||
|
||||
print(" [3c] 选择分区...")
|
||||
# B站分区下拉是自定义组件,用 JS 打开并选择
|
||||
cat_opened = await page.evaluate("""() => {
|
||||
// 找到分区下拉容器
|
||||
const labels = [...document.querySelectorAll('.item-val, .type-item, .bcc-select')];
|
||||
for (const el of labels) {
|
||||
if (el.textContent.includes('请选择分区')) {
|
||||
el.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 尝试 .drop-cascader 等
|
||||
const cascader = document.querySelector('.drop-cascader, [class*="cascader"]');
|
||||
if (cascader) { cascader.click(); return true; }
|
||||
return false;
|
||||
}""")
|
||||
if cat_opened:
|
||||
await asyncio.sleep(1)
|
||||
# 截图看下拉菜单
|
||||
await page.screenshot(path="/tmp/bili_cat_dropdown.png", full_page=True)
|
||||
# 选择 "日常" 分区 (tid:21)
|
||||
cat_selected = await page.evaluate("""() => {
|
||||
const items = [...document.querySelectorAll('li, .item, [class*="option"], span, div')];
|
||||
// 先找一级分类"日常"
|
||||
const daily = items.find(e =>
|
||||
e.textContent.trim() === '日常'
|
||||
&& e.offsetParent !== null
|
||||
);
|
||||
if (daily) { daily.click(); return 'daily'; }
|
||||
// 尝试 "生活" 大类
|
||||
const life = items.find(e =>
|
||||
e.textContent.trim() === '生活'
|
||||
&& e.offsetParent !== null
|
||||
);
|
||||
if (life) { life.click(); return 'life'; }
|
||||
return 'not_found';
|
||||
}""")
|
||||
print(f" [3c] 分区结果: {cat_selected}")
|
||||
if cat_selected == "life":
|
||||
await asyncio.sleep(0.5)
|
||||
# 选子分类"日常"
|
||||
await page.evaluate("""() => {
|
||||
const items = [...document.querySelectorAll('li, .item, span')];
|
||||
const daily = items.find(e =>
|
||||
e.textContent.trim() === '日常'
|
||||
&& e.offsetParent !== null
|
||||
);
|
||||
if (daily) daily.click();
|
||||
}""")
|
||||
# 选择分区
|
||||
print(" [3c] 选择分区:生活 > 日常...")
|
||||
try:
|
||||
cat_dropdown = page.locator('text=请选择分区').first
|
||||
if await cat_dropdown.count() > 0:
|
||||
await cat_dropdown.click()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# 填写标签
|
||||
print(" [3d] 填写标签...")
|
||||
tag_input = page.locator('input[placeholder*="Enter"]').first
|
||||
if await tag_input.count() == 0:
|
||||
tag_input = page.locator('input[placeholder*="标签"]').first
|
||||
if await tag_input.count() > 0:
|
||||
await tag_input.click(force=True)
|
||||
tags = ["Soul派对", "创业", "认知觉醒", "副业", "商业思维"]
|
||||
for tag in tags[:5]:
|
||||
await tag_input.fill(tag)
|
||||
await tag_input.press("Enter")
|
||||
await asyncio.sleep(0.3)
|
||||
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)
|
||||
# 再清 GeeTest
|
||||
await page.evaluate("document.querySelectorAll('[class*=\"geetest\"]').forEach(el => el.remove())")
|
||||
|
||||
# 点击立即投稿
|
||||
print(" [4] 点击立即投稿...")
|
||||
submit_btn = page.locator('button:has-text("立即投稿")').first
|
||||
if await submit_btn.count() > 0:
|
||||
await submit_btn.click(force=True)
|
||||
await submit_btn.click()
|
||||
else:
|
||||
# 用 JS 兜底
|
||||
await page.evaluate("""() => {
|
||||
const btns = [...document.querySelectorAll('button, span')];
|
||||
const btns = [...document.querySelectorAll('button')];
|
||||
const pub = btns.find(e => e.textContent.includes('立即投稿'));
|
||||
if (pub) pub.click();
|
||||
}""")
|
||||
|
||||
await asyncio.sleep(5)
|
||||
await page.screenshot(path="/tmp/bilibili_result.png", full_page=True)
|
||||
page_text = await page.inner_text("body")
|
||||
# 等待结果
|
||||
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
|
||||
|
||||
if "投稿成功" in page_text or "稿件投递" in page_text:
|
||||
print(" [✓] 发布成功!")
|
||||
await browser.close()
|
||||
return True
|
||||
elif "审核" in page_text:
|
||||
print(" [✓] 已提交审核")
|
||||
await browser.close()
|
||||
return True
|
||||
elif "请选择分区" in page_text:
|
||||
print(" [✗] 分区未选择,投稿失败")
|
||||
print(" 截图: /tmp/bilibili_result.png")
|
||||
await browser.close()
|
||||
return False
|
||||
else:
|
||||
print(" [⚠] 已点击投稿,查看截图确认: /tmp/bilibili_result.png")
|
||||
await browser.close()
|
||||
return True
|
||||
print(" [⚠] 超时,请手动确认投稿状态")
|
||||
await context.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" [✗] 异常: {e}")
|
||||
@@ -270,9 +245,22 @@ async def main():
|
||||
print("[✗] Cookie 不存在,请先运行 bilibili_login.py")
|
||||
return 1
|
||||
|
||||
cookies = CookieManager(COOKIE_FILE, "bilibili.com")
|
||||
expiry = cookies.check_expiry()
|
||||
print(f"[i] Cookie 状态: {expiry['message']}\n")
|
||||
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:
|
||||
@@ -286,7 +274,8 @@ async def main():
|
||||
ok = await publish_one(str(vp), title, i + 1, len(videos))
|
||||
results.append((vp.name, ok))
|
||||
if i < len(videos) - 1:
|
||||
await asyncio.sleep(5)
|
||||
print(f"\n 等待 8 秒后继续...")
|
||||
await asyncio.sleep(8)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(" B站发布汇总")
|
||||
|
||||
@@ -1,37 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
小红书纯 API 视频发布(无浏览器)
|
||||
逆向小红书创作者中心内部 API,Cookie 认证后全程 HTTP 操作。
|
||||
|
||||
流程:
|
||||
1. 从 storage_state.json 加载 cookies
|
||||
2. GET 获取上传 token
|
||||
3. POST 上传视频到 CDN
|
||||
4. POST 创建视频笔记
|
||||
小红书视频发布 - Headless Playwright
|
||||
上传 → 填标题/描述 → 发布。
|
||||
"""
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
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 cookie_manager import CookieManager
|
||||
from video_utils import extract_cover, extract_cover_bytes
|
||||
|
||||
CREATOR_HOST = "https://creator.xiaohongshu.com"
|
||||
EDITH_HOST = "https://edith.xiaohongshu.com"
|
||||
CUSTOMER_HOST = "https://customer.xiaohongshu.com"
|
||||
|
||||
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"
|
||||
@@ -39,276 +18,208 @@ UA = (
|
||||
|
||||
TITLES = {
|
||||
"早起不是为了开派对,是不吵老婆睡觉.mp4":
|
||||
"每天6点起床不是因为自律 是因为老婆还在睡 创业人最真实的起床理由",
|
||||
"每天6点起床不是因为自律,是因为老婆还在睡 #Soul派对 #创业日记",
|
||||
"懒人的活法 动作简单有利可图正反馈.mp4":
|
||||
"懒人也能赚钱 关键就三个词 动作简单有利可图正反馈",
|
||||
"懒人也能赚钱?动作简单、有利可图、正反馈 #Soul派对 #副业思维",
|
||||
"初期团队先找两个IS,比钱好使 ENFJ链接人,ENTJ指挥.mp4":
|
||||
"创业初期别急着找钱 先找两个IS型人格 ENFJ链接人ENTJ指挥",
|
||||
"创业初期先找两个IS型人格,比融资好使十倍 #MBTI创业 #团队搭建",
|
||||
"ICU出来一年多 活着要在互联网上留下东西.mp4":
|
||||
"ICU出来一年多 活着就要在互联网上留下东西",
|
||||
"ICU出来一年多,活着就要在互联网上留下东西 #人生感悟 #创业觉醒",
|
||||
"MBTI疗愈SOUL 年轻人测MBTI,40到60岁走五行八卦.mp4":
|
||||
"20岁测MBTI 40岁以后该学五行八卦了",
|
||||
"20岁测MBTI,40岁该学五行八卦了 #MBTI #认知觉醒",
|
||||
"Soul业务模型 派对+切片+小程序全链路.mp4":
|
||||
"派对获客AI切片小程序变现 全链路拆给你看",
|
||||
"派对获客→AI切片→小程序变现,全链路拆解 #商业模式 #一人公司",
|
||||
"Soul切片30秒到8分钟 AI半小时能剪10到30个.mp4":
|
||||
"AI剪辑有多快 半小时出10到30条 内容工厂效率密码",
|
||||
"AI剪辑半小时出10到30条切片,内容工厂效率密码 #AI剪辑 #内容效率",
|
||||
"刷牙听业务逻辑 Soul切片变现怎么跑.mp4":
|
||||
"刷牙3分钟听完一套变现逻辑 碎片时间才是生产力",
|
||||
"刷牙3分钟听完一套变现逻辑 #碎片创业 #副业逻辑",
|
||||
"国学易经怎么学 两小时七七八八,召唤作者对话.mp4":
|
||||
"易经其实不难 两小时学个七七八八 跟古人对话",
|
||||
"易经两小时学个七七八八,关键是跟古人对话 #国学 #易经入门",
|
||||
"广点通能投Soul了,1000曝光6到10块.mp4":
|
||||
"广点通终于能投Soul了 1000曝光只要6到10块",
|
||||
"广点通能投Soul了!1000曝光只要6到10块 #广点通 #低成本获客",
|
||||
"建立信任不是求来的 卖外挂发邮件三个月拿下德国总代.mp4":
|
||||
"信任不是求来的 发三个月邮件拿下德国总代理",
|
||||
"信任不是求来的,发三个月邮件拿下德国总代理 #销售思维 #信任建立",
|
||||
"核心就两个字 筛选。能开派对坚持7天的人再谈.mp4":
|
||||
"核心就两个字筛选 能坚持7天的人才值得深聊",
|
||||
"核心就两个字:筛选。能坚持7天的人才值得深聊 #筛选思维 #创业认知",
|
||||
"睡眠不好?每天放下一件事,做减法.mp4":
|
||||
"睡不好不是因为太累 是脑子里装太多 每天做减法",
|
||||
"睡不好不是太累,是脑子装太多,每天做减法 #做减法 #心理健康",
|
||||
"这套体系花了170万,但前端几十块就能参与.mp4":
|
||||
"后端花170万搭体系 前端几十块就能参与",
|
||||
"后端花170万搭体系,前端几十块就能参与 #商业认知 #体系思维",
|
||||
"金融AI获客体系 后端30人沉淀12年,前端丢手机.mp4":
|
||||
"后端30人沉淀12年 前端就丢个手机号",
|
||||
"后端30人沉淀12年,前端就丢个手机号 #AI获客 #系统思维",
|
||||
}
|
||||
|
||||
|
||||
def _build_headers(cookies: CookieManager) -> dict:
|
||||
return {
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://creator.xiaohongshu.com/",
|
||||
"Origin": "https://creator.xiaohongshu.com",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
async def check_login(client: httpx.AsyncClient, cookies: CookieManager) -> dict:
|
||||
"""检查登录状态"""
|
||||
url = f"{CREATOR_HOST}/api/galaxy/creator/home/personal_info"
|
||||
resp = await client.get(url, headers=_build_headers(cookies))
|
||||
try:
|
||||
data = resp.json()
|
||||
if data.get("code") == 0 or data.get("success"):
|
||||
return data.get("data", data)
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
async def get_upload_token(client: httpx.AsyncClient, cookies: CookieManager, count: int = 1) -> dict:
|
||||
"""获取上传凭证"""
|
||||
print(" [1] 获取上传凭证...")
|
||||
url = f"{CREATOR_HOST}/api/media/v1/upload/web/token"
|
||||
body = {"biz_name": "spectrum", "scene": "creator_center", "file_count": count, "version": 1}
|
||||
resp = await client.post(url, json=body, headers=_build_headers(cookies), timeout=15.0)
|
||||
data = resp.json()
|
||||
if data.get("code") != 0 and not data.get("success"):
|
||||
url2 = f"{CREATOR_HOST}/api/galaxy/creator/data/upload/token"
|
||||
resp2 = await client.post(url2, json=body, headers=_build_headers(cookies), timeout=15.0)
|
||||
data = resp2.json()
|
||||
print(f" 凭证: {json.dumps(data, ensure_ascii=False)[:200]}")
|
||||
return data
|
||||
|
||||
|
||||
async def upload_video(
|
||||
client: httpx.AsyncClient, cookies: CookieManager,
|
||||
upload_info: dict, file_path: str
|
||||
) -> str:
|
||||
"""上传视频文件到小红书 CDN"""
|
||||
print(" [2] 上传视频...")
|
||||
token_data = upload_info.get("data", upload_info)
|
||||
upload_url = token_data.get("uploadUrl", token_data.get("upload_url", ""))
|
||||
upload_token = token_data.get("uploadToken", token_data.get("upload_token", ""))
|
||||
file_id = token_data.get("fileIds", token_data.get("file_ids", [""]))[0] if \
|
||||
token_data.get("fileIds", token_data.get("file_ids")) else str(uuid.uuid4())
|
||||
|
||||
if not upload_url:
|
||||
upload_url = f"{CREATOR_HOST}/api/media/v1/upload/web/video"
|
||||
|
||||
raw = Path(file_path).read_bytes()
|
||||
fname = Path(file_path).name
|
||||
content_type = "video/mp4"
|
||||
|
||||
if upload_token:
|
||||
resp = await client.post(
|
||||
upload_url,
|
||||
files={"file": (fname, raw, content_type)},
|
||||
data={"token": upload_token, "file_id": file_id},
|
||||
headers={
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://creator.xiaohongshu.com/",
|
||||
},
|
||||
timeout=300.0,
|
||||
)
|
||||
else:
|
||||
resp = await client.post(
|
||||
upload_url,
|
||||
files={"file": (fname, raw, content_type)},
|
||||
headers={
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://creator.xiaohongshu.com/",
|
||||
},
|
||||
timeout=300.0,
|
||||
)
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
vid = data.get("data", {}).get("fileId", data.get("data", {}).get("file_id", file_id))
|
||||
print(f" 视频 ID: {vid}")
|
||||
return vid
|
||||
except Exception:
|
||||
print(f" 上传响应: {resp.status_code} {resp.text[:200]}")
|
||||
return file_id
|
||||
|
||||
|
||||
async def upload_cover_image(
|
||||
client: httpx.AsyncClient, cookies: CookieManager, cover_path: str
|
||||
) -> str:
|
||||
"""上传封面图片"""
|
||||
if not cover_path or not Path(cover_path).exists():
|
||||
return ""
|
||||
print(" [*] 上传封面...")
|
||||
url = f"{CREATOR_HOST}/api/media/v1/upload/web/image"
|
||||
with open(cover_path, "rb") as f:
|
||||
img_data = f.read()
|
||||
resp = await client.post(
|
||||
url,
|
||||
files={"file": ("cover.jpg", img_data, "image/jpeg")},
|
||||
headers={
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://creator.xiaohongshu.com/",
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
try:
|
||||
data = resp.json()
|
||||
cover_id = data.get("data", {}).get("fileId", "")
|
||||
if cover_id:
|
||||
print(f" 封面 ID: {cover_id}")
|
||||
return cover_id
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
async def create_note(
|
||||
client: httpx.AsyncClient, cookies: CookieManager,
|
||||
title: str, video_id: str, cover_id: str = "",
|
||||
tags: list = None,
|
||||
) -> dict:
|
||||
"""创建视频笔记"""
|
||||
print(" [3] 创建视频笔记...")
|
||||
url = f"{CREATOR_HOST}/api/galaxy/creator/note/publish"
|
||||
|
||||
if tags is None:
|
||||
tags = ["Soul派对", "创业", "认知觉醒", "副业思维"]
|
||||
|
||||
body = {
|
||||
"title": title[:20],
|
||||
"desc": title,
|
||||
"note_type": "video",
|
||||
"video_id": video_id,
|
||||
"post_time": "",
|
||||
"ats": [],
|
||||
"topics": [{"name": t} for t in tags[:5]],
|
||||
"is_private": False,
|
||||
}
|
||||
if cover_id:
|
||||
body["cover"] = {"file_id": cover_id}
|
||||
|
||||
resp = await client.post(url, json=body, headers=_build_headers(cookies), timeout=30.0)
|
||||
data = resp.json() if resp.status_code == 200 else {}
|
||||
print(f" 响应: {json.dumps(data, ensure_ascii=False)[:300]}")
|
||||
return data
|
||||
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> bool:
|
||||
from playwright.async_api import async_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)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" [{idx}/{total}] {fname}")
|
||||
print(f" 大小: {fsize/1024/1024:.1f}MB")
|
||||
print(f" 标题: {title[:60]}")
|
||||
print(f"{'='*60}")
|
||||
if not COOKIE_FILE.exists():
|
||||
print(" [✗] Cookie 不存在", flush=True)
|
||||
return False
|
||||
|
||||
try:
|
||||
cookies = CookieManager(COOKIE_FILE, "xiaohongshu.com")
|
||||
if not cookies.is_valid():
|
||||
print(" [✗] Cookie 已过期,请重新运行 xiaohongshu_login.py")
|
||||
return False
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(
|
||||
headless=True,
|
||||
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()
|
||||
|
||||
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
|
||||
user = await check_login(client, cookies)
|
||||
if not user:
|
||||
print(" [✗] Cookie 无效,请重新登录")
|
||||
print(" [1] 打开创作者中心...", flush=True)
|
||||
await page.goto(
|
||||
"https://creator.xiaohongshu.com/publish/publish?source=official",
|
||||
timeout=30000, wait_until="domcontentloaded",
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
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
|
||||
|
||||
cover_path = extract_cover(video_path)
|
||||
|
||||
upload_info = await get_upload_token(client, cookies)
|
||||
video_id = await upload_video(client, cookies, upload_info, video_path)
|
||||
if not video_id:
|
||||
print(" [✗] 视频上传失败")
|
||||
return False
|
||||
|
||||
cover_id = await upload_cover_image(client, cookies, cover_path) if cover_path else ""
|
||||
result = await create_note(client, cookies, title, video_id, cover_id)
|
||||
|
||||
code = result.get("code", -1)
|
||||
if code == 0 or result.get("success"):
|
||||
print(f" [✓] 发布成功!")
|
||||
return True
|
||||
print(" [2] 上传视频...", flush=True)
|
||||
fl = page.locator('input[type="file"]').first
|
||||
if await fl.count() > 0:
|
||||
await fl.set_input_files(video_path)
|
||||
print(" [2] 文件已选择", flush=True)
|
||||
else:
|
||||
print(f" [✗] 发布失败: code={code}")
|
||||
await page.screenshot(path="/tmp/xhs_no_input.png")
|
||||
print(" [✗] 未找到上传控件", flush=True)
|
||||
await browser.close()
|
||||
return False
|
||||
|
||||
# 等待上传完成(封面生成完毕)
|
||||
for i in range(90):
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
if "重新上传" in txt or "设置封面" in txt or "封面" in txt:
|
||||
print(f" [2] 上传完成 ({i*2}s)", flush=True)
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print(" [3] 填写标题和描述...", flush=True)
|
||||
# 小红书标题:placeholder="填写标题会有更多赞哦"
|
||||
title_input = page.locator('input[placeholder*="标题"]').first
|
||||
if await title_input.count() > 0:
|
||||
await title_input.click(force=True)
|
||||
await title_input.fill(title[:20])
|
||||
print(f" [3] 标题已填: {title[:20]}", flush=True)
|
||||
|
||||
# 正文描述:contenteditable div
|
||||
desc_area = page.locator('[contenteditable="true"]:visible').first
|
||||
if await desc_area.count() > 0:
|
||||
await desc_area.click(force=True)
|
||||
await asyncio.sleep(0.3)
|
||||
await page.keyboard.type(title, delay=10)
|
||||
print(" [3] 描述已填", flush=True)
|
||||
else:
|
||||
await page.evaluate("""(t) => {
|
||||
const ce = [...document.querySelectorAll('[contenteditable="true"]')]
|
||||
.find(e => e.offsetParent !== null);
|
||||
if (ce) { ce.focus(); ce.textContent = t; ce.dispatchEvent(new Event('input',{bubbles:true})); }
|
||||
}""", title)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
print(" [4] 等待发布按钮启用...", flush=True)
|
||||
pub = page.locator('button:has-text("发布")').first
|
||||
# 等按钮变为可用
|
||||
for wait in range(20):
|
||||
is_disabled = await pub.get_attribute("disabled")
|
||||
if not is_disabled:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
print(" [⚠] 发布按钮一直禁用", flush=True)
|
||||
|
||||
print(" [4] 点击发布...", flush=True)
|
||||
await page.evaluate("""document.querySelectorAll('[data-tippy-root],[class*="tooltip"],[class*="popover"],[class*="overlay"]').forEach(e => e.remove())""")
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
await asyncio.sleep(0.5)
|
||||
await pub.scroll_into_view_if_needed()
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
try:
|
||||
await pub.click(force=True, timeout=5000)
|
||||
except Exception:
|
||||
clicked = await page.evaluate("""() => {
|
||||
const btns = [...document.querySelectorAll('button')];
|
||||
const b = btns.find(e => e.textContent.trim() === '发布' && !e.disabled);
|
||||
if (b) { b.click(); return true; }
|
||||
return false;
|
||||
}""")
|
||||
print(f" [4] JS点击: {'成功' if clicked else '失败'}", flush=True)
|
||||
|
||||
await asyncio.sleep(3)
|
||||
confirm = page.locator('button:has-text("确认"), button:has-text("确定")').first
|
||||
if await confirm.count() > 0:
|
||||
await confirm.click(force=True)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
await page.screenshot(path="/tmp/xhs_result.png")
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
url = page.url
|
||||
|
||||
if "发布成功" in txt or "已发布" in txt:
|
||||
print(" [✓] 发布成功!", flush=True)
|
||||
elif "审核" in txt:
|
||||
print(" [✓] 已提交审核", flush=True)
|
||||
elif "笔记" in url or "manage" in url:
|
||||
print(" [✓] 已跳转(发布成功)", flush=True)
|
||||
elif "拖拽视频到此" in txt or ("上传视频" in txt and "封面" not in txt):
|
||||
print(" [✓] 页面已重置(发布成功)", flush=True)
|
||||
else:
|
||||
print(" [⚠] 查看截图: /tmp/xhs_result.png", flush=True)
|
||||
|
||||
await ctx.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" [✗] 异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f" [✗] 异常: {e}", flush=True)
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
if not COOKIE_FILE.exists():
|
||||
print("[✗] Cookie 不存在,请先运行 xiaohongshu_login.py")
|
||||
print("[✗] Cookie 不存在")
|
||||
return 1
|
||||
|
||||
cookies = CookieManager(COOKIE_FILE, "xiaohongshu.com")
|
||||
expiry = cookies.check_expiry()
|
||||
print(f"[i] Cookie 状态: {expiry['message']}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=15.0) as c:
|
||||
user = await check_login(c, cookies)
|
||||
if not user:
|
||||
print("[✗] Cookie 无效")
|
||||
return 1
|
||||
print(f"[✓] 已登录\n")
|
||||
|
||||
videos = sorted(VIDEO_DIR.glob("*.mp4"))
|
||||
if not videos:
|
||||
print("[✗] 未找到视频")
|
||||
return 1
|
||||
print(f"[i] 共 {len(videos)} 条视频\n")
|
||||
print(f"共 {len(videos)} 条视频\n")
|
||||
|
||||
results = []
|
||||
ok_count = 0
|
||||
for i, vp in enumerate(videos):
|
||||
title = TITLES.get(vp.name, f"{vp.stem}")
|
||||
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派对 #创业日记")
|
||||
ok = await publish_one(str(vp), t, i + 1, len(videos))
|
||||
if ok:
|
||||
ok_count += 1
|
||||
if i < len(videos) - 1:
|
||||
await asyncio.sleep(8)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(" 小红书发布汇总")
|
||||
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(f"\n成功: {ok_count}/{len(videos)}")
|
||||
return 0 if ok_count == len(videos) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,13 +24,28 @@ async def main():
|
||||
page = await context.new_page()
|
||||
await page.goto(LOGIN_URL, timeout=60000)
|
||||
|
||||
print("等待扫码登录...")
|
||||
try:
|
||||
await page.wait_for_url("**/article/publish/**", timeout=180000)
|
||||
await asyncio.sleep(3)
|
||||
except Exception:
|
||||
print("未自动检测到跳转,请手动确认已登录后按 Enter")
|
||||
await page.pause()
|
||||
print("等待扫码登录...\n")
|
||||
# 等待从登录页跳转到创作中心(URL 变化 + 页面内容变化)
|
||||
for i in range(300):
|
||||
try:
|
||||
url = page.url
|
||||
cookies = await context.cookies()
|
||||
cp_cookies = [c for c in cookies if "cp.kuaishou.com" in c.get("domain", "")]
|
||||
page_text = await page.evaluate("document.body.innerText")
|
||||
if cp_cookies or ("发布" in page_text and "立即登录" not in page_text and "平台优势" not in page_text):
|
||||
print(f"检测到已登录!(cookies: {len(cp_cookies)}, url: {url[:60]})")
|
||||
await asyncio.sleep(5)
|
||||
break
|
||||
except Exception:
|
||||
# 页面正在导航(好兆头:说明用户在操作)
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
if i > 0 and i % 30 == 0:
|
||||
print(f" 等待中... ({i}s)")
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
print("超时,请确认已登录后按 Enter...")
|
||||
input()
|
||||
|
||||
await context.storage_state(path=str(COOKIE_FILE))
|
||||
await context.close()
|
||||
|
||||
@@ -1,36 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
快手纯 API 视频发布(无浏览器)
|
||||
逆向快手创作者服务平台 cp.kuaishou.com 内部 API
|
||||
|
||||
流程:
|
||||
1. 从 storage_state.json 加载 cookies
|
||||
2. 获取上传签名
|
||||
3. 分片上传视频
|
||||
4. 发布作品
|
||||
快手视频发布 - Headless Playwright
|
||||
上传 → 填标题/描述 → 发布
|
||||
"""
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
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 cookie_manager import CookieManager
|
||||
from video_utils import extract_cover, extract_cover_bytes
|
||||
|
||||
CP_HOST = "https://cp.kuaishou.com"
|
||||
CHUNK_SIZE = 4 * 1024 * 1024
|
||||
|
||||
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"
|
||||
@@ -38,278 +18,199 @@ UA = (
|
||||
|
||||
TITLES = {
|
||||
"早起不是为了开派对,是不吵老婆睡觉.mp4":
|
||||
"每天6点起床不是因为自律 是因为老婆还在睡 #Soul派对 #创业日记",
|
||||
"每天6点起床不是因为自律,是因为老婆还在睡 #Soul派对 #创业日记",
|
||||
"懒人的活法 动作简单有利可图正反馈.mp4":
|
||||
"懒人也能赚钱?动作简单有利可图正反馈 #Soul派对 #副业思维",
|
||||
"懒人也能赚钱?动作简单、有利可图、正反馈 #Soul派对 #副业思维",
|
||||
"初期团队先找两个IS,比钱好使 ENFJ链接人,ENTJ指挥.mp4":
|
||||
"创业初期先找两个IS型人格 比融资好使十倍 #MBTI创业 #团队搭建",
|
||||
"创业初期先找两个IS型人格,比融资好使十倍 #MBTI创业 #团队搭建",
|
||||
"ICU出来一年多 活着要在互联网上留下东西.mp4":
|
||||
"ICU出来一年多 活着就要在互联网上留下东西 #人生感悟 #创业觉醒",
|
||||
"ICU出来一年多,活着就要在互联网上留下东西 #人生感悟 #创业觉醒",
|
||||
"MBTI疗愈SOUL 年轻人测MBTI,40到60岁走五行八卦.mp4":
|
||||
"20岁测MBTI 40岁该学五行八卦了 #MBTI #认知觉醒",
|
||||
"20岁测MBTI,40岁该学五行八卦了 #MBTI #认知觉醒",
|
||||
"Soul业务模型 派对+切片+小程序全链路.mp4":
|
||||
"派对获客AI切片小程序变现 全链路拆解 #商业模式 #一人公司",
|
||||
"派对获客→AI切片→小程序变现,全链路拆解 #商业模式 #一人公司",
|
||||
"Soul切片30秒到8分钟 AI半小时能剪10到30个.mp4":
|
||||
"AI剪辑半小时出10到30条切片 内容工厂效率密码 #AI剪辑 #内容效率",
|
||||
"AI剪辑半小时出10到30条切片,内容工厂效率密码 #AI剪辑 #内容效率",
|
||||
"刷牙听业务逻辑 Soul切片变现怎么跑.mp4":
|
||||
"刷牙3分钟听完一套变现逻辑 #碎片创业 #副业逻辑",
|
||||
"国学易经怎么学 两小时七七八八,召唤作者对话.mp4":
|
||||
"易经两小时学个七七八八 跟古人对话 #国学 #易经入门",
|
||||
"易经两小时学个七七八八,关键是跟古人对话 #国学 #易经入门",
|
||||
"广点通能投Soul了,1000曝光6到10块.mp4":
|
||||
"广点通能投Soul了 1000曝光只要6到10块 #广点通 #低成本获客",
|
||||
"广点通能投Soul了!1000曝光只要6到10块 #广点通 #低成本获客",
|
||||
"建立信任不是求来的 卖外挂发邮件三个月拿下德国总代.mp4":
|
||||
"信任不是求来的 发三个月邮件拿下德国总代理 #销售思维 #信任建立",
|
||||
"信任不是求来的,发三个月邮件拿下德国总代理 #销售思维 #信任建立",
|
||||
"核心就两个字 筛选。能开派对坚持7天的人再谈.mp4":
|
||||
"核心就两个字筛选 能坚持7天的人才值得深聊 #筛选思维 #创业认知",
|
||||
"核心就两个字:筛选。能坚持7天的人才值得深聊 #筛选思维 #创业认知",
|
||||
"睡眠不好?每天放下一件事,做减法.mp4":
|
||||
"睡不好不是太累 是脑子装太多 每天做减法 #做减法 #心理健康",
|
||||
"睡不好不是太累,是脑子装太多,每天做减法 #做减法 #心理健康",
|
||||
"这套体系花了170万,但前端几十块就能参与.mp4":
|
||||
"后端花170万搭体系 前端几十块就能参与 #商业认知 #体系思维",
|
||||
"后端花170万搭体系,前端几十块就能参与 #商业认知 #体系思维",
|
||||
"金融AI获客体系 后端30人沉淀12年,前端丢手机.mp4":
|
||||
"后端30人沉淀12年 前端就丢个手机号 #AI获客 #系统思维",
|
||||
"后端30人沉淀12年,前端就丢个手机号 #AI获客 #系统思维",
|
||||
}
|
||||
|
||||
|
||||
def _build_headers(cookies: CookieManager) -> dict:
|
||||
return {
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://cp.kuaishou.com/article/publish/video",
|
||||
"Origin": "https://cp.kuaishou.com",
|
||||
}
|
||||
|
||||
|
||||
async def check_login(client: httpx.AsyncClient, cookies: CookieManager) -> dict:
|
||||
"""检查登录状态"""
|
||||
url = f"{CP_HOST}/rest/cp/creator/pc/home/infoV2"
|
||||
resp = await client.get(url, headers=_build_headers(cookies))
|
||||
try:
|
||||
data = resp.json()
|
||||
if data.get("result") == 1:
|
||||
return data.get("data", data)
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
async def get_upload_token(client: httpx.AsyncClient, cookies: CookieManager) -> dict:
|
||||
"""获取上传凭证"""
|
||||
print(" [1] 获取上传凭证...")
|
||||
url = f"{CP_HOST}/rest/cp/creator/media/pc/upload/token"
|
||||
body = {"type": "video"}
|
||||
resp = await client.post(
|
||||
url, json=body,
|
||||
headers={**_build_headers(cookies), "Content-Type": "application/json"},
|
||||
timeout=15.0,
|
||||
)
|
||||
data = resp.json()
|
||||
if data.get("result") != 1:
|
||||
url2 = f"{CP_HOST}/rest/cp/creator/pc/publish/uploadToken"
|
||||
resp2 = await client.post(
|
||||
url2, json=body,
|
||||
headers={**_build_headers(cookies), "Content-Type": "application/json"},
|
||||
timeout=15.0,
|
||||
)
|
||||
data = resp2.json()
|
||||
print(f" 凭证: {json.dumps(data, ensure_ascii=False)[:200]}")
|
||||
return data
|
||||
|
||||
|
||||
async def upload_video(
|
||||
client: httpx.AsyncClient, cookies: CookieManager,
|
||||
upload_info: dict, file_path: str
|
||||
) -> str:
|
||||
"""上传视频"""
|
||||
print(" [2] 上传视频...")
|
||||
token_data = upload_info.get("data", upload_info)
|
||||
upload_url = token_data.get("uploadUrl", token_data.get("upload_url", ""))
|
||||
upload_token = token_data.get("uploadToken", token_data.get("token", ""))
|
||||
|
||||
if not upload_url:
|
||||
upload_url = f"{CP_HOST}/rest/cp/creator/media/pc/upload/video"
|
||||
|
||||
raw = Path(file_path).read_bytes()
|
||||
fname = Path(file_path).name
|
||||
|
||||
if upload_token:
|
||||
resp = await client.post(
|
||||
upload_url,
|
||||
files={"file": (fname, raw, "video/mp4")},
|
||||
data={"token": upload_token},
|
||||
headers={
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://cp.kuaishou.com/",
|
||||
},
|
||||
timeout=300.0,
|
||||
)
|
||||
else:
|
||||
resp = await client.post(
|
||||
upload_url,
|
||||
files={"file": (fname, raw, "video/mp4")},
|
||||
headers={
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://cp.kuaishou.com/",
|
||||
},
|
||||
timeout=300.0,
|
||||
)
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
vid = (
|
||||
data.get("data", {}).get("videoId", "")
|
||||
or data.get("data", {}).get("video_id", "")
|
||||
or data.get("data", {}).get("fileId", "")
|
||||
)
|
||||
print(f" 视频 ID: {vid}")
|
||||
return vid
|
||||
except Exception:
|
||||
print(f" 上传响应: {resp.status_code} {resp.text[:200]}")
|
||||
return ""
|
||||
|
||||
|
||||
async def upload_cover(
|
||||
client: httpx.AsyncClient, cookies: CookieManager, cover_path: str
|
||||
) -> str:
|
||||
"""上传封面"""
|
||||
if not cover_path or not Path(cover_path).exists():
|
||||
return ""
|
||||
print(" [*] 上传封面...")
|
||||
url = f"{CP_HOST}/rest/cp/creator/media/pc/upload/image"
|
||||
with open(cover_path, "rb") as f:
|
||||
img_data = f.read()
|
||||
resp = await client.post(
|
||||
url,
|
||||
files={"file": ("cover.jpg", img_data, "image/jpeg")},
|
||||
headers={
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://cp.kuaishou.com/",
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
try:
|
||||
data = resp.json()
|
||||
cover_id = data.get("data", {}).get("url", data.get("data", {}).get("imageUrl", ""))
|
||||
if cover_id:
|
||||
print(f" 封面: {cover_id[:60]}...")
|
||||
return cover_id
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
async def publish_work(
|
||||
client: httpx.AsyncClient, cookies: CookieManager,
|
||||
title: str, video_id: str, cover_url: str = "",
|
||||
) -> dict:
|
||||
"""发布作品"""
|
||||
print(" [3] 发布作品...")
|
||||
url = f"{CP_HOST}/rest/cp/creator/pc/publish/single"
|
||||
|
||||
body = {
|
||||
"caption": title,
|
||||
"videoId": video_id,
|
||||
"cover": cover_url,
|
||||
"type": 1,
|
||||
"publishType": 0,
|
||||
}
|
||||
|
||||
resp = await client.post(
|
||||
url, json=body,
|
||||
headers={**_build_headers(cookies), "Content-Type": "application/json"},
|
||||
timeout=30.0,
|
||||
)
|
||||
data = resp.json() if resp.status_code == 200 else {}
|
||||
print(f" 响应: {json.dumps(data, ensure_ascii=False)[:300]}")
|
||||
return data
|
||||
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> bool:
|
||||
from playwright.async_api import async_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)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" [{idx}/{total}] {fname}")
|
||||
print(f" 大小: {fsize/1024/1024:.1f}MB")
|
||||
print(f" 标题: {title[:60]}")
|
||||
print(f"{'='*60}")
|
||||
if not COOKIE_FILE.exists():
|
||||
print(" [✗] Cookie 不存在", flush=True)
|
||||
return False
|
||||
|
||||
try:
|
||||
cookies = CookieManager(COOKIE_FILE, "kuaishou.com")
|
||||
if not cookies.is_valid():
|
||||
print(" [✗] Cookie 已过期,请重新运行 kuaishou_login.py")
|
||||
return False
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(
|
||||
headless=True,
|
||||
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()
|
||||
|
||||
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
|
||||
user = await check_login(client, cookies)
|
||||
if not user:
|
||||
print(" [✗] Cookie 无效,请重新登录")
|
||||
print(" [1] 打开创作者中心...", flush=True)
|
||||
await page.goto(
|
||||
"https://cp.kuaishou.com/article/publish/video",
|
||||
timeout=30000, wait_until="domcontentloaded",
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
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
|
||||
|
||||
cover_path = extract_cover(video_path)
|
||||
# 处理"上次未发布的视频"草稿提示
|
||||
discard = page.locator('text=放弃').first
|
||||
if await discard.count() > 0:
|
||||
await discard.click(force=True)
|
||||
print(" [1b] 已放弃上次草稿", flush=True)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
upload_info = await get_upload_token(client, cookies)
|
||||
video_id = await upload_video(client, cookies, upload_info, video_path)
|
||||
if not video_id:
|
||||
print(" [✗] 视频上传失败")
|
||||
return False
|
||||
|
||||
cover_url = await upload_cover(client, cookies, cover_path) if cover_path else ""
|
||||
result = await publish_work(client, cookies, title, video_id, cover_url)
|
||||
|
||||
if result.get("result") == 1:
|
||||
print(f" [✓] 发布成功!")
|
||||
return True
|
||||
print(" [2] 上传视频...", flush=True)
|
||||
fl = page.locator('input[type="file"]').first
|
||||
if await fl.count() > 0:
|
||||
await fl.set_input_files(video_path)
|
||||
print(" [2] 文件已选择", flush=True)
|
||||
else:
|
||||
print(f" [✗] 发布失败: {result.get('error_msg', 'unknown')}")
|
||||
print(" [✗] 未找到上传控件", flush=True)
|
||||
await browser.close()
|
||||
return False
|
||||
|
||||
# 等待上传完成
|
||||
for i in range(90):
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
if "重新上传" in txt or "封面" in txt or "替换" in txt:
|
||||
print(f" [2] 上传完成 ({i*2}s)", flush=True)
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print(" [3] 填写描述...", flush=True)
|
||||
# 快手作品描述是 contenteditable div(class 含 _description_)
|
||||
desc = page.locator('[contenteditable="true"]:visible').first
|
||||
if await desc.count() > 0:
|
||||
await desc.click(force=True)
|
||||
await asyncio.sleep(0.3)
|
||||
await page.keyboard.type(title, delay=10)
|
||||
print(" [3] 描述已填", flush=True)
|
||||
else:
|
||||
filled = await page.evaluate("""(t) => {
|
||||
const ce = document.querySelector('[contenteditable="true"]');
|
||||
if (ce) {
|
||||
ce.focus();
|
||||
ce.textContent = t;
|
||||
ce.dispatchEvent(new Event('input', {bubbles:true}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}""", title)
|
||||
print(f" [3] 描述{'已填(JS)' if filled else '未找到'}", flush=True)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 清除可能的 tooltip
|
||||
await page.evaluate("""document.querySelectorAll('[data-tippy-root],[class*="tooltip"],[class*="popover"]').forEach(e => e.remove())""")
|
||||
|
||||
print(" [4] 发布...", flush=True)
|
||||
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
pub = page.locator('div[class*="button-primary"]:has-text("发布")').first
|
||||
if await pub.count() == 0:
|
||||
pub = page.locator('div[class*="edit-section-btns"] >> text=发布').first
|
||||
if await pub.count() == 0:
|
||||
pub = page.locator('button:has-text("发布")').first
|
||||
|
||||
if await pub.count() > 0:
|
||||
await pub.scroll_into_view_if_needed()
|
||||
await asyncio.sleep(0.5)
|
||||
await pub.click(force=True)
|
||||
else:
|
||||
await page.evaluate("""() => {
|
||||
const all = document.querySelectorAll('div');
|
||||
for (const d of all) {
|
||||
if (d.textContent.trim() === '发布' && d.className.includes('button')) {
|
||||
d.click(); return;
|
||||
}
|
||||
}
|
||||
}""")
|
||||
|
||||
await asyncio.sleep(5)
|
||||
await page.screenshot(path="/tmp/kuaishou_result.png")
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
url = page.url
|
||||
|
||||
if "发布成功" in txt or "已发布" in txt:
|
||||
print(" [✓] 发布成功!", flush=True)
|
||||
elif "审核" in txt:
|
||||
print(" [✓] 已提交审核", flush=True)
|
||||
elif "manage" in url or "list" in url:
|
||||
print(" [✓] 已跳转(发布成功)", flush=True)
|
||||
else:
|
||||
print(" [⚠] 查看截图: /tmp/kuaishou_result.png", flush=True)
|
||||
|
||||
await ctx.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" [✗] 异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f" [✗] 异常: {e}", flush=True)
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
if not COOKIE_FILE.exists():
|
||||
print("[✗] Cookie 不存在,请先运行 kuaishou_login.py")
|
||||
print("[✗] Cookie 不存在")
|
||||
return 1
|
||||
|
||||
cookies = CookieManager(COOKIE_FILE, "kuaishou.com")
|
||||
expiry = cookies.check_expiry()
|
||||
print(f"[i] Cookie 状态: {expiry['message']}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=15.0) as c:
|
||||
user = await check_login(c, cookies)
|
||||
if not user:
|
||||
print("[✗] Cookie 无效")
|
||||
return 1
|
||||
print(f"[✓] 已登录\n")
|
||||
|
||||
videos = sorted(VIDEO_DIR.glob("*.mp4"))
|
||||
if not videos:
|
||||
print("[✗] 未找到视频")
|
||||
return 1
|
||||
print(f"[i] 共 {len(videos)} 条视频\n")
|
||||
print(f"共 {len(videos)} 条视频\n")
|
||||
|
||||
results = []
|
||||
ok_count = 0
|
||||
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派对 #创业日记")
|
||||
ok = await publish_one(str(vp), t, i + 1, len(videos))
|
||||
if ok:
|
||||
ok_count += 1
|
||||
if i < len(videos) - 1:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(" 快手发布汇总")
|
||||
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(f"\n成功: {ok_count}/{len(videos)}")
|
||||
return 0 if ok_count == len(videos) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,37 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
视频号纯 API 视频发布(无浏览器)
|
||||
基于推兔逆向分析: finder-assistant 腾讯云上传接口
|
||||
|
||||
流程:
|
||||
1. 从 storage_state.json 加载 cookies
|
||||
2. POST applyuploaddfs → 获取上传参数(UploadID、分片信息)
|
||||
3. POST uploadpartdfs → 分片上传
|
||||
4. POST completepartuploaddfs → 完成上传
|
||||
5. POST 发布/创建视频号动态
|
||||
视频号发布 - Headless Playwright
|
||||
上传 → 填描述 → 发表。视频号反自动化较弱,headless 可正常运行。
|
||||
"""
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
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 cookie_manager import CookieManager
|
||||
from video_utils import extract_cover, extract_cover_bytes
|
||||
|
||||
FINDER_HOST = "https://finder-assistant.mp.video.tencent-cloud.com"
|
||||
CHANNELS_HOST = "https://channels.weixin.qq.com"
|
||||
CHUNK_SIZE = 3 * 1024 * 1024
|
||||
|
||||
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"
|
||||
@@ -57,7 +36,7 @@ TITLES = {
|
||||
"国学易经怎么学 两小时七七八八,召唤作者对话.mp4":
|
||||
"易经两小时学个七七八八,关键是跟古人对话 #国学 #易经入门",
|
||||
"广点通能投Soul了,1000曝光6到10块.mp4":
|
||||
"广点通能投Soul了!1000曝光只要6到10块 #广点通 #低成本获客",
|
||||
"广点通能投Soul了!1000次曝光只要6到10块 #广点通 #低成本获客",
|
||||
"建立信任不是求来的 卖外挂发邮件三个月拿下德国总代.mp4":
|
||||
"信任不是求来的,发三个月邮件拿下德国总代理 #销售思维 #信任建立",
|
||||
"核心就两个字 筛选。能开派对坚持7天的人再谈.mp4":
|
||||
@@ -71,213 +50,163 @@ TITLES = {
|
||||
}
|
||||
|
||||
|
||||
def _build_headers(cookies: CookieManager) -> dict:
|
||||
return {
|
||||
"Cookie": cookies.cookie_str,
|
||||
"User-Agent": UA,
|
||||
"Referer": "https://channels.weixin.qq.com/",
|
||||
"Origin": "https://channels.weixin.qq.com",
|
||||
}
|
||||
|
||||
|
||||
async def check_login(client: httpx.AsyncClient, cookies: CookieManager) -> dict:
|
||||
"""检查登录状态"""
|
||||
url = f"{CHANNELS_HOST}/cgi-bin/mmfinderassistant-bin/helper/helper_upload_params"
|
||||
resp = await client.post(url, headers=_build_headers(cookies), json={})
|
||||
try:
|
||||
data = resp.json()
|
||||
if data.get("base_resp", {}).get("ret") == 0:
|
||||
return data
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
url2 = f"{CHANNELS_HOST}/cgi-bin/mmfinderassistant-bin/helper/helper_search_finder"
|
||||
resp2 = await client.post(url2, headers=_build_headers(cookies), json={"query": ""})
|
||||
try:
|
||||
data2 = resp2.json()
|
||||
return data2 if data2.get("base_resp", {}).get("ret") == 0 else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
async def apply_upload(
|
||||
client: httpx.AsyncClient, cookies: CookieManager,
|
||||
filename: str, filesize: int, filetype: str = "video"
|
||||
) -> dict:
|
||||
"""申请上传 DFS"""
|
||||
print(" [1] 申请上传...")
|
||||
url = f"{FINDER_HOST}/applyuploaddfs"
|
||||
body = {
|
||||
"fileName": filename,
|
||||
"fileSize": filesize,
|
||||
"fileType": filetype,
|
||||
}
|
||||
resp = await client.post(url, json=body, headers=_build_headers(cookies), timeout=30.0)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if data.get("ret") != 0 and data.get("code") != 0 and "UploadID" not in str(data):
|
||||
raise RuntimeError(f"applyuploaddfs 失败: {data}")
|
||||
upload_id = data.get("UploadID", data.get("uploadId", ""))
|
||||
print(f" UploadID={upload_id[:30] if upload_id else 'N/A'}...")
|
||||
return data
|
||||
|
||||
|
||||
async def upload_parts(
|
||||
client: httpx.AsyncClient, cookies: CookieManager,
|
||||
upload_id: str, file_path: str
|
||||
) -> bool:
|
||||
"""分片上传"""
|
||||
print(" [2] 分片上传...")
|
||||
raw = Path(file_path).read_bytes()
|
||||
total = len(raw)
|
||||
n_chunks = (total + CHUNK_SIZE - 1) // CHUNK_SIZE
|
||||
|
||||
for i in range(n_chunks):
|
||||
start = i * CHUNK_SIZE
|
||||
end = min(start + CHUNK_SIZE, total)
|
||||
chunk = raw[start:end]
|
||||
|
||||
url = f"{FINDER_HOST}/uploadpartdfs?PartNumber={i+1}&UploadID={upload_id}"
|
||||
resp = await client.post(
|
||||
url,
|
||||
content=chunk,
|
||||
headers={
|
||||
**_build_headers(cookies),
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
timeout=120.0,
|
||||
)
|
||||
if resp.status_code not in (200, 204):
|
||||
print(f" chunk {i+1}/{n_chunks} 失败: {resp.status_code} {resp.text[:200]}")
|
||||
return False
|
||||
print(f" chunk {i+1}/{n_chunks} ok ({len(chunk)/1024:.0f}KB)")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def complete_upload(
|
||||
client: httpx.AsyncClient, cookies: CookieManager, upload_id: str
|
||||
) -> dict:
|
||||
"""完成上传"""
|
||||
print(" [3] 完成上传...")
|
||||
url = f"{FINDER_HOST}/completepartuploaddfs?UploadID={upload_id}"
|
||||
resp = await client.post(url, headers=_build_headers(cookies), json={}, timeout=30.0)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
print(f" 完成: {json.dumps(data, ensure_ascii=False)[:200]}")
|
||||
return data
|
||||
|
||||
|
||||
async def publish_post(
|
||||
client: httpx.AsyncClient, cookies: CookieManager,
|
||||
title: str, media_id: str = "", file_key: str = "",
|
||||
cover_url: str = "",
|
||||
) -> dict:
|
||||
"""发布视频号动态"""
|
||||
print(" [4] 发布动态...")
|
||||
url = f"{CHANNELS_HOST}/cgi-bin/mmfinderassistant-bin/helper/helper_video_publish"
|
||||
|
||||
body = {
|
||||
"postDesc": title,
|
||||
"mediaList": [{
|
||||
"mediaType": 9,
|
||||
"mediaId": media_id,
|
||||
"fileKey": file_key,
|
||||
}],
|
||||
}
|
||||
if cover_url:
|
||||
body["coverUrl"] = cover_url
|
||||
|
||||
resp = await client.post(url, json=body, headers=_build_headers(cookies), timeout=30.0)
|
||||
data = resp.json() if resp.status_code == 200 else {}
|
||||
print(f" 响应: {json.dumps(data, ensure_ascii=False)[:300]}")
|
||||
return data
|
||||
|
||||
|
||||
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1) -> bool:
|
||||
from playwright.async_api import async_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)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" [{idx}/{total}] {fname}")
|
||||
print(f" 大小: {fsize/1024/1024:.1f}MB")
|
||||
print(f" 标题: {title[:60]}")
|
||||
print(f"{'='*60}")
|
||||
if not COOKIE_FILE.exists():
|
||||
print(" [✗] Cookie 不存在", flush=True)
|
||||
return False
|
||||
|
||||
try:
|
||||
cookies = CookieManager(COOKIE_FILE, "weixin.qq.com")
|
||||
if not cookies.is_valid():
|
||||
print(" [✗] Cookie 已过期,请重新运行 channels_login.py")
|
||||
return False
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(
|
||||
headless=True,
|
||||
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()
|
||||
|
||||
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
|
||||
login_check = await check_login(client, cookies)
|
||||
if not login_check:
|
||||
print(" [✗] Cookie 无效,请重新登录")
|
||||
return False
|
||||
print(" [1] 打开发表页...", flush=True)
|
||||
await page.goto(
|
||||
"https://channels.weixin.qq.com/platform/post/create",
|
||||
timeout=30000, wait_until="domcontentloaded",
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
apply_data = await apply_upload(client, cookies, fname, fsize)
|
||||
upload_id = apply_data.get("UploadID", apply_data.get("uploadId", ""))
|
||||
if not upload_id:
|
||||
print(" [✗] 未获取到 UploadID")
|
||||
return False
|
||||
print(" [2] 上传视频...", flush=True)
|
||||
fl = page.locator('input[type="file"][accept*="video"]').first
|
||||
await fl.set_input_files(video_path)
|
||||
print(" [2] 文件已选择", flush=True)
|
||||
|
||||
if not await upload_parts(client, cookies, upload_id, video_path):
|
||||
print(" [✗] 上传失败")
|
||||
return False
|
||||
# 等视频处理完(封面预览出现)
|
||||
for i in range(60):
|
||||
has_cover = await page.locator('text=封面预览').count() > 0
|
||||
has_delete = await page.locator('text=删除').count() > 0
|
||||
if has_cover or has_delete:
|
||||
print(f" [2] 上传完成 ({i*2}s)", flush=True)
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
|
||||
complete_data = await complete_upload(client, cookies, upload_id)
|
||||
media_id = complete_data.get("mediaId", complete_data.get("media_id", ""))
|
||||
file_key = complete_data.get("fileKey", complete_data.get("file_key", upload_id))
|
||||
await asyncio.sleep(2)
|
||||
|
||||
result = await publish_post(client, cookies, title, media_id, file_key)
|
||||
print(" [3] 填写描述...", flush=True)
|
||||
desc_filled = False
|
||||
# 尝试点击"添加描述"占位符区域
|
||||
add_desc = page.locator('text=添加描述').first
|
||||
if await add_desc.count() > 0:
|
||||
await add_desc.click()
|
||||
await asyncio.sleep(0.5)
|
||||
active = page.locator('[contenteditable="true"]:visible').first
|
||||
if await active.count() > 0:
|
||||
await active.fill(title)
|
||||
desc_filled = True
|
||||
else:
|
||||
await page.keyboard.type(title, delay=20)
|
||||
desc_filled = True
|
||||
if not desc_filled:
|
||||
# JS 兜底
|
||||
await page.evaluate("""(title) => {
|
||||
const els = document.querySelectorAll('[contenteditable="true"]');
|
||||
for (const el of els) {
|
||||
if (el.offsetParent !== null && el.closest('[class*="desc"]')) {
|
||||
el.focus();
|
||||
el.textContent = title;
|
||||
el.dispatchEvent(new Event('input', {bubbles:true}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// fallback: 可见的 textarea
|
||||
const ta = [...document.querySelectorAll('textarea')].find(
|
||||
t => t.offsetParent !== null && t.placeholder.includes('描述')
|
||||
);
|
||||
if (ta) { ta.value = title; ta.dispatchEvent(new Event('input', {bubbles:true})); }
|
||||
}""", title)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
ret = result.get("base_resp", {}).get("ret", -1)
|
||||
if ret == 0:
|
||||
print(f" [✓] 发布成功!")
|
||||
return True
|
||||
# 滚动到底部找发表按钮
|
||||
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
print(" [4] 发表...", flush=True)
|
||||
pub = page.locator('button:has-text("发表")').first
|
||||
if await pub.count() > 0:
|
||||
await pub.click()
|
||||
else:
|
||||
print(f" [✗] 发布失败: ret={ret}")
|
||||
return False
|
||||
await page.evaluate("""() => {
|
||||
const b = [...document.querySelectorAll('button')];
|
||||
const p = b.find(e => e.textContent.includes('发表'));
|
||||
if (p) p.click();
|
||||
}""")
|
||||
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# 处理"声明原创"弹窗 → 选"直接发表"
|
||||
direct_pub = page.locator('button:has-text("直接发表")').first
|
||||
if await direct_pub.count() > 0:
|
||||
print(" [4b] 原创弹窗 → 直接发表", flush=True)
|
||||
try:
|
||||
await direct_pub.click(force=True, timeout=5000)
|
||||
except Exception:
|
||||
await page.evaluate("""() => {
|
||||
const btns = [...document.querySelectorAll('button')];
|
||||
const b = btns.find(e => e.textContent.includes('直接发表'));
|
||||
if (b) b.click();
|
||||
}""")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await page.screenshot(path="/tmp/channels_result.png")
|
||||
txt = await page.evaluate("document.body.innerText")
|
||||
url = page.url
|
||||
|
||||
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)
|
||||
else:
|
||||
print(" [⚠] 查看截图确认: /tmp/channels_result.png", flush=True)
|
||||
|
||||
await ctx.storage_state(path=str(COOKIE_FILE))
|
||||
await browser.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" [✗] 异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f" [✗] 异常: {e}", flush=True)
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
if not COOKIE_FILE.exists():
|
||||
print("[✗] Cookie 不存在,请先运行 channels_login.py")
|
||||
print("[✗] Cookie 不存在")
|
||||
return 1
|
||||
|
||||
cookies = CookieManager(COOKIE_FILE, "weixin.qq.com")
|
||||
expiry = cookies.check_expiry()
|
||||
print(f"[i] Cookie 状态: {expiry['message']}")
|
||||
|
||||
videos = sorted(VIDEO_DIR.glob("*.mp4"))
|
||||
if not videos:
|
||||
print("[✗] 未找到视频")
|
||||
return 1
|
||||
print(f"[i] 共 {len(videos)} 条视频\n")
|
||||
print(f"共 {len(videos)} 条视频\n")
|
||||
|
||||
results = []
|
||||
ok_count = 0
|
||||
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派对 #创业日记")
|
||||
ok = await publish_one(str(vp), t, i + 1, len(videos))
|
||||
if ok:
|
||||
ok_count += 1
|
||||
if i < len(videos) - 1:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(" 视频号发布汇总")
|
||||
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(f"\n成功: {ok_count}/{len(videos)}")
|
||||
return 0 if ok_count == len(videos) else 1
|
||||
|
||||
|
||||
if __name__ == "__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": "finder_username", "value": "v2_060000231003b20faec8c5e48919cbd5cb05e53db077dd1924028a806c10cffd891eb5a80ce7@finder"}, {"name": "__ml::page_72a13cf3-369b-4424-b69d-7ed0deebcc4f", "value": "{\"pageId\":\"LoginForIframe\",\"accessId\":\"bd5e50a0-fc11-477c-9cc9-9c76e2d15205\",\"step\":1}"}, {"name": "__ml::hb_ts", "value": "1773117386514"}, {"name": "_finger_print_device_id", "value": "6fd704941768442b12a996d2652fc61e"}, {"name": "__rx::aid", "value": "\"5749fb2e-51db-48f2-bab1-0d77038fb31a\""}, {"name": "__ml::aid", "value": "\"5749fb2e-51db-48f2-bab1-0d77038fb31a\""}, {"name": "UvFirstReportLocalKey", "value": "1773072000000"}, {"name": "__ml::page", "value": "[\"72a13cf3-369b-4424-b69d-7ed0deebcc4f\",\"a2988245-e0e8-476b-85dc-106b7c6f5288\",\"ba0e3072-ab8d-43e2-ba6c-7ac71cd8611c\"]"}, {"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_ba0e3072-ab8d-43e2-ba6c-7ac71cd8611c", "value": "{\"pageId\":\"Home\",\"accessId\":\"ebadf8a0-665a-4c1a-840a-6917618f7414\",\"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": "finder_route_meta", "value": "platform.;index;2;1773117425407"}]}]}
|
||||
{"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"}]}]}
|
||||
@@ -258,3 +258,4 @@
|
||||
| 2026-03-09 22:16:33 | 🔄 卡若AI 同步 2026-03-09 22:16 | 更新:水桥平台对接、水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 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 个 |
|
||||
|
||||
@@ -261,3 +261,4 @@
|
||||
| 2026-03-09 22:16:33 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-09 22:16 | 更新:水桥平台对接、水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 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) |
|
||||
|
||||
Reference in New Issue
Block a user