diff --git a/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/README.md b/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/README.md index 2fc051ab..c2d8179d 100644 --- a/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/README.md +++ b/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/README.md @@ -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 ``` diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/参考资料/飞书日志_固定链接.md b/02_卡人(水)/水桥_平台对接/飞书管理/参考资料/飞书日志_固定链接.md index 8ba9af1d..b21a8477 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/参考资料/飞书日志_固定链接.md +++ b/02_卡人(水)/水桥_平台对接/飞书管理/参考资料/飞书日志_固定链接.md @@ -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>` 后重试**(先迁一个)。 diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/auto_log.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/auto_log.py index 7d0a50d6..ef2fad78 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/auto_log.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/auto_log.py @@ -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 diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_0321_custom.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_0321_custom.py index 06b2a018..3e75e4d5 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_0321_custom.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_0321_custom.py @@ -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) diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_three_focus.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_three_focus.py index 95d1aa3d..71d63252 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_three_focus.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_three_focus.py @@ -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) diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_with_summary.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_with_summary.py index 3fe667b8..000d9c36 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_with_summary.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_with_summary.py @@ -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) diff --git a/03_卡木(木)/木叶_视频内容/B站发布/脚本/bilibili_publish.py b/03_卡木(木)/木叶_视频内容/B站发布/脚本/bilibili_publish.py index ba806918..6c219d87 100644 --- a/03_卡木(木)/木叶_视频内容/B站发布/脚本/bilibili_publish.py +++ b/03_卡木(木)/木叶_视频内容/B站发布/脚本/bilibili_publish.py @@ -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站发布汇总") diff --git a/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_publish.py b/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_publish.py index f938e01b..d32b7d50 100644 --- a/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_publish.py +++ b/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_publish.py @@ -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__": diff --git a/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_storage_state.json b/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_storage_state.json index 99804982..606a8d32 100644 --- a/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_storage_state.json +++ b/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_storage_state.json @@ -1 +1 @@ -{"cookies": [{"name": "acw_tc", "value": "0a0d0ad817731175455962911e8b82f26f4e10bf59c739371c5956597af44d", "domain": "creator.xiaohongshu.com", "path": "/", "expires": 1773119345.544491, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "xsecappid", "value": "ugc", "domain": ".xiaohongshu.com", "path": "/", "expires": 1804653569, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "a1", "value": "19cd60a9fc67sq38y42r4sqx5kmpc1i6ahui3k5mm30000687072", "domain": ".xiaohongshu.com", "path": "/", "expires": 1804653546, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "webId", "value": "5a9ea25c0c3028732e8d594ed19a4a4e", "domain": ".xiaohongshu.com", "path": "/", "expires": 1804653546, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "acw_tc", "value": "0ad5896617731175467605301e072a8daa73426cd22c6f45abd8ab5a696b5d", "domain": "edith.xiaohongshu.com", "path": "/", "expires": 1773119346.709696, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "acw_tc", "value": "0a0d02c717731175468072164e9b868633fee8ce50830502ef086822d53e56", "domain": "customer.xiaohongshu.com", "path": "/", "expires": 1773119346.754063, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "websectiga", "value": "6169c1e84f393779a5f7fe7303038f3b47a78e47be716e7bec57ccce17d45f99", "domain": ".xiaohongshu.com", "path": "/", "expires": 1773376746, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "sec_poison_id", "value": "1801f7a8-8da3-4079-b7ec-98ed155ef987", "domain": ".xiaohongshu.com", "path": "/", "expires": 1773118151, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "gid", "value": "yjSfK800DW0KyjSfK80jixT2SKWMAqYv4JF4766MAC2T63q8USy3Kq888KYW8WJ8WqjSSYDi", "domain": ".xiaohongshu.com", "path": "/", "expires": 1807677572.150974, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "customer-sso-sid", "value": "68c5176154819623929937970htobts05deiiuxh", "domain": ".xiaohongshu.com", "path": "/", "expires": 1773722366.852965, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "x-user-id-creator.xiaohongshu.com", "value": "63b3cb6f000000002502c21d", "domain": ".xiaohongshu.com", "path": "/", "expires": 1807677567.852995, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "customerClientId", "value": "586477551116322", "domain": ".xiaohongshu.com", "path": "/", "expires": 1807677567.853006, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "access-token-creator.xiaohongshu.com", "value": "customer.creator.AT-68c517615481962393042947iutloo5r1fnkfqt3", "domain": ".xiaohongshu.com", "path": "/", "expires": 1775709566.853013, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "galaxy_creator_session_id", "value": "ksy02Mkehi3ye5IR60P01klUicMIL9V5Qscz", "domain": ".xiaohongshu.com", "path": "/", "expires": 1775709567.853097, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "galaxy.creator.beaker.session.id", "value": "1773117567623092062639", "domain": ".xiaohongshu.com", "path": "/", "expires": 1775709567.853107, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "loadts", "value": "1773117569286", "domain": ".xiaohongshu.com", "path": "/", "expires": 1804653569, "httpOnly": false, "secure": false, "sameSite": "Lax"}], "origins": [{"origin": "https://creator.xiaohongshu.com", "localStorage": [{"name": "b1b1", "value": "1"}, {"name": "USER_INFO_FOR_BIZ", "value": "{\"userId\":\"63b3cb6f000000002502c21d\",\"userName\":\"#\u5361\u82e5\ud83d\udd25(4:00\u8d77\u5e8a\u7684\u7537\u4eba)\",\"userAvatar\":\"https://sns-avatar-qc.xhscdn.com/avatar/65a68110d4b53385b6a72ed1.jpg?imageView2/2/w/80/format/jpg\",\"redId\":\"6244231151\",\"role\":\"creator\",\"permissions\":[\"creatorCollege\",\"creatorWiki\",\"noteInspiration\",\"creatorHome\",\"creatorData\",\"USER_GROUP\",\"creatorActivityCenter\",\"ORIGINAL_STATEMENT\"],\"zone\":\"86\",\"phone\":\"15880802661\",\"relatedUserId\":null,\"relatedUserName\":null,\"kolCoOrder\":false}"}, {"name": "p1", "value": "2"}, {"name": "last_tiga_update_time", "value": "1773117546764"}, {"name": "USER_INFO", "value": "{\"user\":{\"type\":\"User\",\"value\":{\"userId\":\"63b3cb6f000000002502c21d\",\"loginUserType\":\"creator\"}}}"}, {"name": "NEW_XHS_ABTEST_REPORT_KEY", "value": "{\"5a9ea25c0c3028732e8d594ed19a4a4e63b3cb6f000000002502c21d\":\"2026-03-10\"}"}, {"name": "sdt_source_storage_key", "value": "{\"signVersion\":\"1\",\"commonPatch\":[\"/fe_api/burdock/v2/note/post\",\"/api/sns/web/v1/comment/post\",\"/api/sns/web/v1/note/like\",\"/api/sns/web/v1/note/collect\",\"/api/sns/web/v1/user/follow\",\"/api/sns/web/v1/feed\",\"/api/sns/web/v1/login/activate\",\"/api/sns/web/v1/note/metrics_report\",\"/api/redcaptcha\",\"/api/store/jpd/main\",\"/phoenix/api/strategy/getAppStrategy\",\"/web_api/sns/v2/note\"],\"reportUrl\":\"/api/sec/v1/shield/webprofile\",\"desVersion\":\"2\",\"validate\":false,\"signUrl\":\"https://fe-static.xhscdn.com/as/v1/f218/a15/public/04b29480233f4def5c875875b6bdc3b1.js\",\"xhsTokenUrl\":\"https://fe-static.xhscdn.com/as/v1/3e44/public/bf7d4e32677698655a5cadc581fd09b3.js\",\"extraInfo\":{},\"url\":\"https://fe-static.xhscdn.com/as/v2/fp/962356ead351e7f2422eb57edff6982d.js\"}"}, {"name": "b1", "value": "I38rHdgsjopgIvesdVwgIC+oIELmBZ5e3VwXLgFTIxS3bqwErFeexd0ekncAzMFYnqthIhJeSnMDKutRI3KsYorWHPtGrbi0P9WfIi/eWc6eYqtyQApPI37ekmR6QL+5Ii6sdnoeSfqYHqwl2qt5B0DoIx+PGDi/sVtkIx0sxuwr4qtiIhuaIE3e3LV0I3VTIC7e0utl2ADmsLveDSKsSPw5IEvsiVtJOqw8BuwfPpdeTFWOIx4TIiu6ZPwrPut5IvlaLbgs3qtxIxes1VwHIkumIkIyejgsY/WTge7eSqte/D7sDcpipedeYrDtIC6eDVw2IENsSqtlnlSuNjVtIvoekqt3cZ7sVo4gIESyIhE2+9DUIvzy4I8OIic7ZPwAIviv4o/sDLds6PwVIC7eSd7ej9S4IEvs3IPMtVwUIids3s/sxZNeiVtbcUeeYVwEIvlzc0vefVwup9esSVwsIxltIxZSouwOgVwpsr4heU/e6LveDPwFIvgs1ros1DZiIi7sjbos3grFIE0sDqwHIvmZaVtfaVwBIE7sDqwxIiNs3uw5IkvsdqtlwuwLoVtUI3zXIvVr27lk2Ive1utCIEDtIkJeYut4bYRtn/0ejgI7Ih4s2uwfJPwSI35skqwWGD5s6WAs3phwIhos3fOs3utscPwaICJsWPw5IiJekeqLICKejd/sfPtUIx7sxuwD4BYaIhQgIv5s1M6e6gvs6aAeTqwdICmmIhFKJuw9IiLOI30eDakwLS0ekPw7IxVA4uw+b95eijes6Pt0yVtdIiZCICDHBVwqGuwbI36sDAgsiVwgmqtmIiM2eVt2I3FBbeZLIkAsdMNsD9/e3PwKmqtgIiPmIx5sVutWIEKejrpIICMnr0EaIv4wIC6sTqw9"}, {"name": "xhs_context_networkQuality", "value": "GOOD"}]}]} \ No newline at end of file +{"cookies": [{"name": "xsecappid", "value": "ugc", "domain": ".xiaohongshu.com", "path": "/", "expires": 1804656799, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "a1", "value": "19cd60a9fc67sq38y42r4sqx5kmpc1i6ahui3k5mm30000687072", "domain": ".xiaohongshu.com", "path": "/", "expires": 1804653546, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "webId", "value": "5a9ea25c0c3028732e8d594ed19a4a4e", "domain": ".xiaohongshu.com", "path": "/", "expires": 1804653546, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "gid", "value": "yjSfK800DW0KyjSfK80jixT2SKWMAqYv4JF4766MAC2T63q8USy3Kq888KYW8WJ8WqjSSYDi", "domain": ".xiaohongshu.com", "path": "/", "expires": 1807680802.945476, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "customer-sso-sid", "value": "68c5176154819623929937970htobts05deiiuxh", "domain": ".xiaohongshu.com", "path": "/", "expires": 1773722366.852965, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "x-user-id-creator.xiaohongshu.com", "value": "63b3cb6f000000002502c21d", "domain": ".xiaohongshu.com", "path": "/", "expires": 1807677567.852995, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "customerClientId", "value": "586477551116322", "domain": ".xiaohongshu.com", "path": "/", "expires": 1807677567.853006, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "access-token-creator.xiaohongshu.com", "value": "customer.creator.AT-68c517615481962393042947iutloo5r1fnkfqt3", "domain": ".xiaohongshu.com", "path": "/", "expires": 1775709566.853013, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "galaxy_creator_session_id", "value": "ksy02Mkehi3ye5IR60P01klUicMIL9V5Qscz", "domain": ".xiaohongshu.com", "path": "/", "expires": 1775709567.853097, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "galaxy.creator.beaker.session.id", "value": "1773117567623092062639", "domain": ".xiaohongshu.com", "path": "/", "expires": 1775709567.853107, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "acw_tc", "value": "0a0d09d017731197997238474e135e57ff5016e52597becbc8f5d1a3d7e1a8", "domain": "creator.xiaohongshu.com", "path": "/", "expires": 1773121599.669299, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "acw_tc", "value": "0a0bb2cf17731198005918942edd2600e54fb9cfbbed9b3ce86221de2a3b13", "domain": "edith.xiaohongshu.com", "path": "/", "expires": 1773121600.530442, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "websectiga", "value": "2845367ec3848418062e761c09db7caf4e8b79d132ccdd1a4f8e64a11d0cac0d", "domain": ".xiaohongshu.com", "path": "/", "expires": 1773379893, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "acw_tc", "value": "0a00dc2417731199288683944e18f5734aec37394ef84a3249c34a9b4a75c5", "domain": "www.xiaohongshu.com", "path": "/", "expires": 1773121728.815796, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "sec_poison_id", "value": "46fd0fc2-78a1-4681-8697-cac68e544787", "domain": ".xiaohongshu.com", "path": "/", "expires": 1773121298, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "loadts", "value": "1773120799816", "domain": ".xiaohongshu.com", "path": "/", "expires": 1804656799, "httpOnly": false, "secure": false, "sameSite": "Lax"}], "origins": [{"origin": "https://creator.xiaohongshu.com", "localStorage": [{"name": "USER_INFO_FOR_BIZ", "value": "{\"userId\":\"63b3cb6f000000002502c21d\",\"userName\":\"#\u5361\u82e5\ud83d\udd25(4:00\u8d77\u5e8a\u7684\u7537\u4eba)\",\"userAvatar\":\"https://sns-avatar-qc.xhscdn.com/avatar/65a68110d4b53385b6a72ed1.jpg?imageView2/2/w/80/format/jpg\",\"redId\":\"6244231151\",\"role\":\"creator\",\"permissions\":[\"creatorCollege\",\"creatorWiki\",\"noteInspiration\",\"creatorHome\",\"creatorData\",\"USER_GROUP\",\"creatorActivityCenter\",\"ORIGINAL_STATEMENT\"],\"zone\":\"86\",\"phone\":\"15880802661\",\"relatedUserId\":null,\"relatedUserName\":null,\"kolCoOrder\":false}"}, {"name": "uploader-permit-video-spectrum", "value": "{\"\u5e7f\u70b9\u901a\u80fd\u6295Soul\u4e86\uff0c1000\u66dd\u51496\u523010\u5757.mp4-3033345\":null,\"\u7761\u7720\u4e0d\u597d\uff1f\u6bcf\u5929\u653e\u4e0b\u4e00\u4ef6\u4e8b\uff0c\u505a\u51cf\u6cd5.mp4-7022452\":null,\"\u6838\u5fc3\u5c31\u4e24\u4e2a\u5b57 \u7b5b\u9009\u3002\u80fd\u5f00\u6d3e\u5bf9\u575a\u63017\u5929\u7684\u4eba\u518d\u8c08.mp4-8324133\":null}"}, {"name": "publish-uploader-history-upload-speed", "value": "[{\"speed\":[1.2095440585872903],\"domain\":\"ros-upload.xiaohongshu.com\",\"timestamp\":1773120805844},{\"speed\":[5.6512141280353205],\"domain\":\"ros-upload-d4.xhscdn.com\",\"timestamp\":1773120806751}]"}, {"name": "snsWebPublishCurrentUser", "value": "63b3cb6f000000002502c21d"}, {"name": "USER_INFO", "value": "{\"user\":{\"type\":\"User\",\"value\":{\"userId\":\"63b3cb6f000000002502c21d\",\"loginUserType\":\"creator\"}}}"}, {"name": "NEW_XHS_ABTEST_REPORT_KEY", "value": "{\"5a9ea25c0c3028732e8d594ed19a4a4e63b3cb6f000000002502c21d\":\"2026-03-10\"}"}, {"name": "b1b1", "value": "1"}, {"name": "score_display", "value": "1"}, {"name": "p1", "value": "6"}, {"name": "nps-userId", "value": "63b3cb6f000000002502c21d"}, {"name": "_speedList", "value": "[{\"ts\":1773119805768,\"speed\":21297121},{\"ts\":1773119805792,\"speed\":24177793.333333332},{\"ts\":1773119920874,\"speed\":210755388.66666666},{\"ts\":1773119920894,\"speed\":33181579},{\"ts\":1773120703041,\"speed\":201955666.66666666},{\"ts\":1773120703058,\"speed\":49044342.333333336},{\"ts\":1773120809464,\"speed\":90950888.66666667},{\"ts\":1773120809466,\"speed\":50470266.333333336}]"}, {"name": "score_timestamp", "value": "1773119928816"}, {"name": "last_tiga_update_time", "value": "1773120693283"}, {"name": "uploader-permit-image-spectrum", "value": "{\"cover.jpeg-291551\":null,\"cover.jpeg-298713\":null}"}, {"name": "sdt_source_storage_key", "value": "{\"url\":\"https://fe-static.xhscdn.com/as/v2/fp/962356ead351e7f2422eb57edff6982d.js\",\"desVersion\":\"2\",\"signVersion\":\"1\",\"xhsTokenUrl\":\"https://fe-static.xhscdn.com/as/v1/3e44/public/bf7d4e32677698655a5cadc581fd09b3.js\",\"reportUrl\":\"/api/sec/v1/shield/webprofile\",\"validate\":false,\"commonPatch\":[\"/fe_api/burdock/v2/note/post\",\"/api/sns/web/v1/comment/post\",\"/api/sns/web/v1/note/like\",\"/api/sns/web/v1/note/collect\",\"/api/sns/web/v1/user/follow\",\"/api/sns/web/v1/feed\",\"/api/sns/web/v1/login/activate\",\"/api/sns/web/v1/note/metrics_report\",\"/api/redcaptcha\",\"/api/store/jpd/main\",\"/phoenix/api/strategy/getAppStrategy\",\"/web_api/sns/v2/note\"],\"signUrl\":\"https://fe-static.xhscdn.com/as/v1/f218/a15/public/04b29480233f4def5c875875b6bdc3b1.js\",\"extraInfo\":{}}"}, {"name": "b1", "value": "I38rHdgsjopgIvesdVwgIC+oIELmBZ5e3VwXLgFTIxS3bqwErFeexd0ekncAzMFYnqthIhJeSnMDKutRI3KsYorWHPtGrbi0P9WfIi/eWc6eYqtyQApPI37ekmR6QLQ5Ii6sdneeSfqYHqwl2qt5B0DBIx+PGDV/sutkIx0sxuwr4qtiIhuaIE3e3LV0I3VTIC7e0Vtl2ADmsLveDSKsSPw5IEvsiVtJOqw8BuwfPpdeTFWOIx4TIiu6ZPwrPut5IvlaLbgs3qtxIxes1VwHIkumIkIyejgsY/WTge7eSqte/D7sDcpipedeYrDtIC6eDVw2IENsSqtlnlSuNjVtIvoekqt3cZ7sVo4gIESyIhE8+9DUIvzy4I8OIic7ZPwAIviv4o/sDLds6PwVIC7eSd7e0ez4IEvsxcZMtVwUIids3s/sxZNeiVtbcUeeYVwEIvlzc/deSuwCpfgsSPw+IxltIxZSouwOgVwpsr4heU/e6LveDPwFIvgs1ros1DZiIi7sjbos3grFIE0e3PtvIibROqwOOqthIxes1VwDIEgekVw5Ih3sjuw5NqwnoVwuICckI3HrN9iiJcAe1uwvIk4mIhdsDqwK2gTrb9OeWdpDIhDLJqtKaqwuIv6e6VtxQLge3l5siMmLIiAsx7esTutycPwOIvgeSPwvIigex0IeICdeS9Ke0Pt9Ix6sxuwU4eQNIEH+Iv7sxM6ex7vsYDosSPtzIkL1IhVGGuwlIkD+IxNe0AkyabdekPwuIxWvKPwsmoKexUNsTut9qVtRIkqNIvRV+VtTZPwJIiNsVe5eWqwxJutdICm6QVtqIkDCPYGhIxgsip0siFve6VwFBqtvI3WfIk3sSqteIC7e6oz/IhQUPUR9IERSIh7eYPwLquteoVt8IkPcICNsSl0eSVwN4oVnIEIMnut1QVtfIiqZIkTvI3S2Iv0edb5sj9kKsVwSIEI/yVwjQuwpIvNeDrWZIxqg2S/exutPNPtVeAIwe9qxIkRTLuw0IkOs1ZSmIxELBuwYIkTnwqt7eutKIC+AICZWI38mBVw6I3R+Ihde6uwkncbWIEZeICosYMJsiqw5IvE7IhGHalchIC4Y//FdIv6sYFde0PwJIhOsDutJIEq/IxgsWc=="}, {"name": "_renderInfo", "value": "angle (google, vulkan 1.3.0 (swiftshader device (llvm 10.0.0) (0x0000c0de)), swiftshader driver)"}, {"name": "xhs_context_networkQuality", "value": "WEAK"}]}]} \ No newline at end of file diff --git a/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_login.py b/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_login.py index 37ad6839..7ff4e957 100644 --- a/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_login.py +++ b/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_login.py @@ -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() diff --git a/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_publish.py b/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_publish.py index 5dfb832f..a01a5270 100644 --- a/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_publish.py +++ b/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_publish.py @@ -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__": diff --git a/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_storage_state.json b/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_storage_state.json index 72754093..8a1ac002 100644 --- a/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_storage_state.json +++ b/03_卡木(木)/木叶_视频内容/快手发布/脚本/kuaishou_storage_state.json @@ -1 +1 @@ -{"cookies": [{"name": "did", "value": "web_f5bd9b77b5e2c2779add3c1e8b4ce3b24d0a", "domain": ".kuaishou.com", "path": "/", "expires": 1807677582.285167, "httpOnly": false, "secure": true, "sameSite": "None"}, {"name": "kwpsecproductname", "value": "account-zt-pc", "domain": "passport.kuaishou.com", "path": "/", "expires": 1775709584, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "kwssectoken", "value": "MO6TUM078AraP3yFTJcGabWxxNNJr2rOxekltq2qCMj98CpxIgQjH4cXRRGaGkqF", "domain": "passport.kuaishou.com", "path": "/", "expires": 1773117944, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "kwscode", "value": "KlwJaW2s1DyB8GthUn/KsL0/9yUV+d0f9Cp8WEvbm4tjm/vYs1JCT0vtGbIFYZvr3Zj2PL20igk7LEPScD28VWoKT3Cd4YrNsNnhrVSrrpZe1JEAy3hYKhom6mFJovrQm/FyGxJsQodbMDhLDA7J40H/NSVPKijKGleezJHKpsCu3f02bOodk3+kQc+E5We3Uq71jZcARvW8eyXQOOUZzByQS==", "domain": "passport.kuaishou.com", "path": "/", "expires": 1773117944, "httpOnly": false, "secure": false, "sameSite": "Lax"}], "origins": [{"origin": "https://passport.kuaishou.com", "localStorage": [{"name": "WEBLOGGER_CHANNEL_SEQ_ID_NORMAL", "value": "1"}, {"name": "WEBLOGGER_V2_SEQ_ID_showEvent", "value": "1"}, {"name": "LOAD_DEVICE_INCREASE_ID", "value": "1"}, {"name": "OTHER_DEVICE_INCREASE_ID", "value": "1"}, {"name": "WEBLOGGER_CUSTOM_INCREAMENT_ID_KEY", "value": "1"}, {"name": "WEBLOGGER_INCREAMENT_ID_KEY", "value": "1"}, {"name": "kwfv1", "value": "KTUMBW0sDU+3rM5Fp+0CRL2Y9cegdaPANg+3XodzG6vOBmnBiHEV1+iA6Ut6XQEhja/slLMfxvcDqA1R2onBJVVrjwUBQcCXw9N/+7Pov0tf4NcBIAfqaiK/gcIr4h5gW/0hxhJ/qjCkhDf7yl3TON9svqo1uE9K6vXWZo0GNMoozxq9fVlxz+U38a1BGIK6O8coXuPuExSBg0CQXYutQEqQF=="}]}, {"origin": "https://cp.kuaishou.com", "localStorage": [{"name": "refresh_last_time_0x0810", "value": "1773117581804"}]}]} \ No newline at end of file +{"cookies": [{"name": "did", "value": "web_59afd87857628fadfacadff0c466f14cb3c9", "domain": ".kuaishou.com", "path": "/", "expires": 1807680283.271951, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "kwpsecproductname", "value": "account-zt-pc", "domain": "passport.kuaishou.com", "path": "/", "expires": 1775712260, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "kwpsecproductname", "value": "verification-captcha", "domain": "captcha.zt.kuaishou.com", "path": "/", "expires": 1775712266, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "kwfv1", "value": "PnGU+9+Y8008S+nH0U+0mjPf8fP08f+98f+nLlwnrIP9P78/DM+/cI8eWhPnHM+/GI8emj+BHF+nPEwnrMGfL980rhweGh+0pf+AY0+BH9P0rA+9zDG/L9+/mjP/YY8/WAwnP98nrM+0L9+/cA+eSS+/Gl+/PM+f8j8/mS8/cl+0ZA+APFG/LA+0GEG98f80cE8f+D+eQfGAbj+fQD8eqE+c==", "domain": ".kuaishou.com", "path": "/", "expires": 1775712266, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "didv", "value": "1773120283000", "domain": ".kuaishou.com", "path": "/", "expires": 1807680283.272001, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "bUserId", "value": "1000581252357", "domain": ".kuaishou.com", "path": "/", "expires": 1774416310.625656, "httpOnly": false, "secure": true, "sameSite": "None"}, {"name": "bUserId", "value": "1000581252357", "domain": "id.kuaishou.com", "path": "/", "expires": 1774934710.419152, "httpOnly": false, "secure": true, "sameSite": "None"}, {"name": "passToken", "value": "ChNwYXNzcG9ydC5wYXNzLXRva2VuEsABtfDxZXtPnjk15hJoTcYx1uyrt2hSh9FB2vixPMW8QbIw5tUZNIHAVEdpLCqdLrUQz21JzEsXkXkxcsydiVgBjqNRL_BX6WXjTbvGiVy9EgfMYI7dUwMzqXNxqpYs-6o1EqXYBw0oi42Zt9VJOjJep24Kfjemizr167KWJ89SJVrN7mt-M8d7m3MpG81IY9LTPLEF3Yaxo5talgc6DjHv7KsWzqAFIU9fwZ2TbHV7sKFWpdkOocwLhvVNSmtH1RjuGhI2r9gHVaFHkIbWYumijbjK4BsiIJTz8edocbgB9zeGNoP-jhNYGF5Y6ry53LhxjBIth1YKKAUwAQ", "domain": "id.kuaishou.com", "path": "/", "expires": 1774934710.41918, "httpOnly": true, "secure": true, "sameSite": "None"}, {"name": "userId", "value": "463733910", "domain": ".kuaishou.com", "path": "/", "expires": 1774416310.625615, "httpOnly": false, "secure": true, "sameSite": "None"}, {"name": "userId", "value": "463733910", "domain": "id.kuaishou.com", "path": "/", "expires": 1774934710.419174, "httpOnly": false, "secure": true, "sameSite": "None"}, {"name": "kuaishou.web.cp.api_st", "value": "ChZrdWFpc2hvdS53ZWIuY3AuYXBpLnN0ErABXsXP7DjhNAzC26oKr6CLwjNPwvTIgdy4TmxM-wbBIBFjGlDHlqSORXEHhjV73QzvdVuxCaFJ9_OZxTYj4_dvj2giUG6D-8LYqWSyMEipzPPGZEcCrwcx_RKAReJGY_Mlva_zuKKCcXjgqT0tkc7pU5qipNlyAti8ZViYb5iWFCIe2yzWLmIL__8WPB-2noA9FpZWR7fD4iwNI4eWAB6HSgkurwjRgcjYxuMl7pwb_s0aEjXDFoga_Fxpw8r2Z4xly4zAfyIgKVPmoCN5ZeImWYJ24gBwV6mLBDX9XrynVXg0KxvL6LIoBTAB", "domain": ".kuaishou.com", "path": "/", "expires": 1774416310.625689, "httpOnly": true, "secure": true, "sameSite": "None"}, {"name": "kuaishou.web.cp.api_ph", "value": "7f384b62e8e3f6e8e1afd657bd771846d551", "domain": ".kuaishou.com", "path": "/", "expires": 1774416310.62571, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "ks_onvideo_token", "value": "ZZOmMAIASPoaCz1VZqnGYg==", "domain": "onvideoapi.kuaishou.com", "path": "/", "expires": 1773206712.395114, "httpOnly": false, "secure": false, "sameSite": "Lax"}], "origins": [{"origin": "https://cp.kuaishou.com", "localStorage": [{"name": "OTHER_DEVICE_INCREASE_ID", "value": "1016"}, {"name": "refresh_last_time_0x0810", "value": "1773120256100"}, {"name": "ACTIVITY_SHOW_CLAIM_TOOLTIP", "value": "true"}, {"name": "PUBLISH_V2_TOUR", "value": "true"}, {"name": "LOAD_DEVICE_INCREASE_ID", "value": "15"}, {"name": "WEBLOGGER_CUSTOM_INCREAMENT_ID_KEY", "value": "67"}, {"name": "WEBLOGGER_INCREAMENT_ID_KEY", "value": "198"}]}, {"origin": "https://passport.kuaishou.com", "localStorage": [{"name": "WEBLOGGER_CHANNEL_SEQ_ID_NORMAL", "value": "13"}, {"name": "OTHER_DEVICE_INCREASE_ID", "value": "17"}, {"name": "kwfv1", "value": "PnGU+9+Y8008S+nH0U+0mjPf8fP08f+98f+nLlwnrIP9P78/DM+/cI8eWhPnHM+/GI8emj+BHF+nPEwnrMGfL980rIP/pfPeGlwemSPnp0G04fGAGU+eLE+fPlP0Gh+/DU80r78/ZA8ez08BQfPArI8nLMwncUwecl+0mS+eL9wnc="}, {"name": "WEBLOGGER_V2_SEQ_ID_showEvent", "value": "8"}, {"name": "LOAD_DEVICE_INCREASE_ID", "value": "3"}, {"name": "kwfcv1", "value": "1"}, {"name": "WEBLOGGER_CUSTOM_INCREAMENT_ID_KEY", "value": "19"}, {"name": "WEBLOGGER_V2_SEQ_ID_clickEvent", "value": "3"}, {"name": "WEBLOGGER_INCREAMENT_ID_KEY", "value": "13"}, {"name": "WEBLOGGER_V2_SEQ_ID_taskEvent", "value": "2"}]}, {"origin": "https://captcha.zt.kuaishou.com", "localStorage": [{"name": "WEBLOGGER_CHANNEL_SEQ_ID_NORMAL", "value": "6"}, {"name": "OTHER_DEVICE_INCREASE_ID", "value": "7"}, {"name": "kwfv1", "value": "PnGU+9+Y8008S+nH0U+0mjPf8fP08f+98f+nLlwnrIP9P78/DM+/cI8eWhPnHM+/GI8emj+BHF+nPEwnrMGfL980rhweGh+0pf+AY0+BH9P0rA+9zDG/L9+/mjP/YY8/WAwnP98nrM+0L9+/cA+eSS+/Gl+/PM+f8j8/mS8/cl+0ZA+APFG/LA+0GEG98f80cE8f+D+eQfGAbj+fQD8eqE+c=="}, {"name": "WEBLOGGER_V2_SEQ_ID_showEvent", "value": "2"}, {"name": "LOAD_DEVICE_INCREASE_ID", "value": "4"}, {"name": "kwfcv1", "value": "1"}, {"name": "WEBLOGGER_CUSTOM_INCREAMENT_ID_KEY", "value": "7"}, {"name": "WEBLOGGER_V2_SEQ_ID_clickEvent", "value": "1"}, {"name": "WEBLOGGER_INCREAMENT_ID_KEY", "value": "6"}, {"name": "WEBLOGGER_V2_SEQ_ID_taskEvent", "value": "3"}]}, {"origin": "https://app.m.kuaishou.com", "localStorage": [{"name": "CROSS_VERIFY_SESSION_d89a771b-87d8-4922-ba7b-ba9f09bb306f", "value": "{\"stateStack\":[],\"updateTime\":1773120283392}"}, {"name": "LOAD_DEVICE_INCREASE_ID", "value": "2"}, {"name": "OTHER_DEVICE_INCREASE_ID", "value": "7"}, {"name": "WEBLOGGER_CUSTOM_INCREAMENT_ID_KEY", "value": "6"}]}]} \ No newline at end of file diff --git a/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_publish.py b/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_publish.py index 46eebeef..bdb401d6 100644 --- a/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_publish.py +++ b/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_publish.py @@ -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__": diff --git a/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_storage_state.json b/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_storage_state.json index 36f22059..f25432ac 100644 --- a/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_storage_state.json +++ b/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_storage_state.json @@ -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"}]}]} \ No newline at end of file +{"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"}]}]} \ No newline at end of file diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 32382c1e..1f1f6a1b 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -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 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 1e73c219..2556a05f 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -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) |