重构douyin上传,增加douyin图文
修复douyin视频封面上传
This commit is contained in:
@@ -27,10 +27,11 @@ The project consists of a Python backend and a Vue.js frontend.
|
||||
|
||||
**Command-line Interface:**
|
||||
|
||||
The project also provides a command-line interface (CLI) for users who prefer to work from the terminal. The CLI supports two main actions:
|
||||
The project also provides a command-line interface (CLI) for users who prefer to work from the terminal. For new Douyin CLI work, prefer the `sau douyin ...` entrypoint over legacy example scripts.
|
||||
|
||||
* `login`: To log in to a social media platform.
|
||||
* `upload`: To upload a video to a social media platform, with an option to schedule the upload.
|
||||
* `login`: To log in to the Douyin uploader account.
|
||||
* `check`: To verify whether the saved Douyin cookie is still valid.
|
||||
* `upload`: To upload one video file with explicit metadata flags.
|
||||
|
||||
## Building and Running
|
||||
|
||||
@@ -82,13 +83,25 @@ To use the CLI, you can run the `cli_main.py` script with the appropriate argume
|
||||
**Login:**
|
||||
|
||||
```bash
|
||||
python cli_main.py <platform> <account_name> login
|
||||
sau douyin login --account <account_name>
|
||||
```
|
||||
|
||||
**Check:**
|
||||
|
||||
```bash
|
||||
sau douyin check --account <account_name>
|
||||
```
|
||||
|
||||
**Upload:**
|
||||
|
||||
```bash
|
||||
python cli_main.py <platform> <account_name> upload <video_file> [-pt {0,1}] [-t YYYY-MM-DD HH:MM]
|
||||
sau douyin upload --account <account_name> --file <video_file> --title <title> [--tags tag1,tag2] [--schedule YYYY-MM-DD HH:MM]
|
||||
```
|
||||
|
||||
**Install bundled skill:**
|
||||
|
||||
```bash
|
||||
sau skill install
|
||||
```
|
||||
|
||||
## Development Conventions
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
## 目录
|
||||
|
||||
- [💡 功能特性](#💡功能特性)
|
||||
- [🚀 支持的平台](#🚀支持的平台)
|
||||
- [🚀 当前主线](#🚀当前主线)
|
||||
- [📊 平台能力矩阵](#📊平台能力矩阵)
|
||||
- [💾 安装指南](#💾安装指南)
|
||||
- [🏁 快速开始](#🏁快速开始)
|
||||
- [🗂️ 重构计划](#🗂️重构计划)
|
||||
- [📣 近况说明](#📣近况说明)
|
||||
- [🐇 项目背景](#🐇项目背景)
|
||||
- [📃 详细文档](#📃详细文档)
|
||||
- [🐾 交流与支持](#🐾交流与支持)
|
||||
@@ -21,169 +24,131 @@
|
||||
|
||||
## 💡功能特性
|
||||
|
||||
### 已支持平台
|
||||
这个项目不是在和 agent 抢活,而是在补 AI 自媒体最后一公里里最容易翻车的那一段。
|
||||
|
||||
- **国内平台**:
|
||||
- [x] 抖音
|
||||
- [x] 视频号
|
||||
- [x] Bilibili
|
||||
- [x] 小红书
|
||||
- [x] 快手
|
||||
- [x] 百家号
|
||||
- **国外平台**:
|
||||
- [x] TikTok
|
||||
- `social-auto-upload` 更偏向“经过大量验证的脚本化、程序化执行”
|
||||
- agent 更擅长“理解任务、编排流程、生成内容、调用工具”
|
||||
- 两者结合,通常比单纯依赖 agent 直接操作浏览器更稳
|
||||
|
||||
### 核心功能
|
||||
为什么 agent 已经能操作浏览器了,还是需要这个项目?
|
||||
|
||||
- [x] 定时上传 (Cron Job / Scheduled Upload)
|
||||
- [ ] Cookie 管理 (部分实现,持续优化中)
|
||||
- [ ] 国外平台 Proxy 设置 (部分实现)
|
||||
- agent 直接操作浏览器,很多时候要反复解析网页、读 DOM、截图理解、再决定下一步动作
|
||||
- 这类流程每次执行路径都可能不太一样,稳定性依赖当下页面状态、模型判断和上下文质量
|
||||
- 对上传、登录、定时发布这类高重复动作来说,这会额外消耗大量 token、算力和执行时间
|
||||
- 一旦平台页面有轻微波动,agent 还可能重新理解、重新试错,成本会继续上升
|
||||
|
||||
### 计划支持与开发中
|
||||
这个项目的价值就在这里:
|
||||
|
||||
- **平台扩展**:
|
||||
- [ ] YouTube
|
||||
- **功能增强**:
|
||||
- [x] 更易用的版本 (GUI / CLI 交互优化)
|
||||
- [x] API 封装
|
||||
- [x] Docker 部署
|
||||
- [ ] 自动化上传 (更智能的调度策略)
|
||||
- [ ] 多线程/异步上传优化
|
||||
- [ ] Slack/消息推送通知
|
||||
- 把高频、重复、已经跑通过很多次的平台动作,沉淀成稳定的 uploader / CLI / skill
|
||||
- 把“理解网页并临场决策”的不确定性,尽量收敛成“直接调用一个被验证过的能力”
|
||||
- 让 agent 少走弯路,少烧 token,把算力留给更适合 AI 的环节,比如选题、写文案、排流程、调度任务
|
||||
- 让整条链路更接近真正可持续的生产化,而不是每次都从零开始看页面、猜按钮、试流程
|
||||
|
||||
### 2025.10.30目前现状
|
||||
该项目本人很长一段时间没维护了,有比较大的问题也是能简单快速修复就修复掉
|
||||
## 📊平台能力矩阵
|
||||
|
||||
因为我自己也在创业,每天时间都用不完
|
||||
下表描述的是当前仓库内“实际已有实现”的能力,不代表都已经完成 CLI 化或 skill 化。
|
||||
|
||||
目前问题主要集中在
|
||||
1. 小红书部分,这部分是直接适用xhs这个库来实现的
|
||||
2. web 端(vue版本),这个版本是群友LeeDebug他帮忙做的(再次感谢他)
|
||||
| 平台 | 登录/账号准备 | 视频上传 | 图文上传 | 定时发布 | CLI | Skill | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 抖音 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 当前主线重构最完整 |
|
||||
| Bilibili | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 依赖 `biliup` |
|
||||
| 小红书(浏览器版) | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 当前仓库有浏览器 uploader |
|
||||
| 快手 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 浏览器自动化 |
|
||||
| 视频号 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 对应 `tencent_uploader` |
|
||||
| 百家号 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 浏览器自动化 |
|
||||
| TikTok | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 当前示例走 Chrome 版实现 |
|
||||
|
||||
因为我日常也在用,我用的不是web端,而是最初`uploader`文件夹里的版本,也就是文档里提到的部分https://sap-doc.nasdaddy.com/
|
||||
所以这里一般遇到的问题,我都会尝试去解决,一并推送到该仓库
|
||||
|
||||
目前能遇到的问题,基本上都比较小,可能是元素变化导致的
|
||||
在初期设计的时候,其实我已经参考了某些不可变元素去选择,极大的避免了后期因为平台页面修改导致的元素变化
|
||||
## 🚀当前主线
|
||||
|
||||
该项目不仅仅是技术人员,有不少是非技术的从业人员,他们是没能力修复一个简单弱小的bug
|
||||
为了能帮助更多的人,所以呼吁**技术小伙伴**
|
||||
当前仓库这轮重构的主线很明确:
|
||||
|
||||
如果大家
|
||||
- 修复了一些bug
|
||||
- 增加一些对大家有帮助的功能
|
||||
- 先把抖音链路打磨完整,作为 uploader / CLI / skill 的样板
|
||||
- 逐步把其他平台往统一结构上收敛
|
||||
- 默认围绕 `uv`、`patchright`、无头模式、CLI 化、Skill 化推进
|
||||
- Web 相关代码目前保留为历史版本,不是当前主要维护方向
|
||||
|
||||
请积极的提出pr,我会想尽可能的确认后合并的,在此感谢大家对于开源项目的支持,帮助更多的人
|
||||
如果你是第一次使用这个项目:
|
||||
|
||||
我自己也会尽100%的力量,在自己项目稳定后,修bug,加更多的平台,开发出gradio版本(更易部署),大家谅解
|
||||
- 安装看:[安装说明](./docs/install.md)
|
||||
- 更新看:[更新说明](./docs/update.md)
|
||||
- CLI 看:[CLI 使用说明](./docs/CLI.md)
|
||||
- agent / skill 看:[Douyin Upload Skill](./skills/douyin-upload/SKILL.md)
|
||||
- 历史 Web 说明看:[历史 Web 版本说明](./docs/legacy-web.md)
|
||||
|
||||
---
|
||||
|
||||
## 🚀支持的平台
|
||||
## 📣近况说明
|
||||
|
||||
本项目通过各平台对应的 `uploader` 模块实现视频上传功能。您可以在 `examples` 目录下找到各个平台的使用示例脚本。
|
||||
`2026.03.24`
|
||||
|
||||
每个示例脚本展示了如何配置和调用相应的 uploader。
|
||||
最近我的重心一直都在创业上,而且手里还有一些项目没完全跑通,所以这个仓库前面有很长一段时间,我确实没有办法投入特别多精力去持续维护。
|
||||
|
||||
这个项目不知不觉已经 `9k+ star` 了,社群里也已经有 `2000+` 小伙伴了。看到它真的在持续帮到大家,我心里还是挺开心的,也是真的很感谢大家一直以来的支持、反馈。
|
||||
|
||||
所以我想,决定先停一下,抽一段时间出来,把这个项目好好重构和优化一轮。
|
||||
|
||||
接下来这段时间,这个仓库应该会进入一个相对密集更新的阶段。我现在最想先做的事情主要有这几件:
|
||||
|
||||
1. 使用更隐蔽、更稳定的自动化方案,尽量降低平台检测风险
|
||||
2. 补齐一些常用平台的图文能力,并逐步完成 CLI 化、Skill 化
|
||||
3. 陆续测试并上架到更多 skill 平台,让大家的龙虾、螃蟹、毛毛虫都能打通 AI 自媒体的最后一道关
|
||||
|
||||
所以如果你之前觉得这个项目更新有点慢,哈哈哈,后面大概率会快很多。也欢迎大家继续关注,最近应该会是一段持续修、持续更、持续重构的阶段。
|
||||
|
||||
## 🗂️重构计划
|
||||
|
||||
项目正在进行一轮整体重构,当前重构重点是:
|
||||
|
||||
- 各平台 uploader 的结构收敛
|
||||
- CLI 统一接入
|
||||
- 面向 OpenClaw、Codex、 Claude Code 等工具的 skill 化
|
||||
- 更换为 `patchright` 驱动,提升兼容性与隐蔽性
|
||||
- 主线优先围绕无头模式推进
|
||||
|
||||
“无头模式(headless)”,指的是浏览器在后台运行,不弹出可见窗口,但自动化流程仍然会照常执行。这样更适合 CLI、服务端、自动任务和 agent 场景。
|
||||
|
||||
Web 端相关代码仍然保留,但已经不是当前主线,不保证可直接运行,也不保证与当前 uploader/CLI 完全同步。
|
||||
|
||||
## 💾安装指南
|
||||
|
||||
1. **克隆项目**:
|
||||
```bash
|
||||
git clone https://github.com/dreammis/social-auto-upload.git
|
||||
cd social-auto-upload
|
||||
```
|
||||
安装、更新、环境准备不再在首页重复展开,统一收敛到文档里:
|
||||
|
||||
2. **安装依赖**:
|
||||
建议在虚拟环境中安装依赖。
|
||||
```bash
|
||||
conda create -n social-auto-upload python=3.10
|
||||
conda activate social-auto-upload
|
||||
# 挂载清华镜像 or 命令行代理
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
- 人类用户优先看:[安装说明](./docs/install.md)
|
||||
- 需要更新仓库时看:[更新说明](./docs/update.md)
|
||||
- 如果你是 CLI 用户,再配合看:[CLI 使用说明](./docs/CLI.md)
|
||||
|
||||
3. **安装 Playwright 浏览器驱动**:
|
||||
```bash
|
||||
playwright install chromium firefox
|
||||
```
|
||||
根据您的需求,至少需要安装 `chromium`。`firefox` 主要用于 TikTok 上传(旧版)。
|
||||
当前主线默认使用:
|
||||
|
||||
4. **修改配置文件**:
|
||||
复制 `conf.example.py` 并重命名为 `conf.py`。
|
||||
在 `conf.py` 中,您需要配置以下内容:
|
||||
- `LOCAL_CHROME_PATH`: 本地 Chrome 浏览器的路径,比如 `C:\Program Files\Google\Chrome\Application\chrome.exe` 保存。
|
||||
|
||||
**临时解决方案**
|
||||
|
||||
需要在根目录创建 `cookiesFile` 和 `videoFile` 两个文件夹,分别是 存储cookie文件 和 存储上传文件 的文件夹
|
||||
|
||||
5. **配置数据库**:
|
||||
如果 db/database.db 文件不存在,您可以运行以下命令来初始化数据库:
|
||||
```bash
|
||||
cd db
|
||||
python createTable.py
|
||||
```
|
||||
此命令将初始化 SQLite 数据库。
|
||||
|
||||
6. **启动后端项目**:
|
||||
```bash
|
||||
python sau_backend.py
|
||||
```
|
||||
后端项目将在 `http://localhost:5409` 启动。
|
||||
|
||||
7. **启动前端项目**:
|
||||
```bash
|
||||
cd sau_frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
前端项目将在 `http://localhost:5173` 启动,在浏览器中打开此链接即可访问。
|
||||
|
||||
|
||||
> 非程序员用户可以参考:[新手级教程](https://juejin.cn/post/7372114027840208911)
|
||||
- `uv` 管理环境
|
||||
- `pyproject.toml` 管理依赖
|
||||
- `patchright` 作为浏览器驱动
|
||||
- `sau` 作为 CLI 入口
|
||||
|
||||
`requirements.txt` 目前主要用于历史兼容路径,普通用户不需要优先使用它。
|
||||
|
||||
## 🏁快速开始
|
||||
|
||||
1. **准备 Cookie**:
|
||||
大多数平台需要登录后的 Cookie 信息才能进行操作。请参照 examples 目录下各 `get_xxx_cookie.py` 脚本(例如 get_douyin_cookie.py, get_ks_cookie.py)的说明,运行脚本以生成并保存 Cookie 文件(通常在 `cookies/[PLATFORM]_uploader/account.json`)。
|
||||
### 方式 1:使用 CLI
|
||||
|
||||
2. **准备视频文件**:
|
||||
将需要上传的视频文件(通常为 `.mp4` 格式)放置在 videos 目录下。
|
||||
部分平台支持视频封面,可以将封面图片(例如 `.png` 格式,与视频同名)也放在此目录。
|
||||
如果需要上传标题及标签,请在视频文件旁边创建一个同名的 `.txt` 文件,内容为标题和标签,以换行分隔。
|
||||
当前只有抖音已经完成 CLI 化:
|
||||
|
||||
3. **修改并运行示例脚本**:
|
||||
打开 examples 目录中您想使用的平台的上传脚本(例如 upload_video_to_douyin.py)。
|
||||
- 根据脚本内的注释和说明,确认 Cookie 文件路径、视频文件路径等配置是否正确。
|
||||
- 您可以修改脚本以适应您的具体需求,例如批量上传、自定义标题、标签等。
|
||||
```bash
|
||||
sau douyin login --account creator
|
||||
sau douyin check --account creator
|
||||
sau douyin upload-video --account creator --file videos/demo.mp4 --title "示例标题"
|
||||
```
|
||||
|
||||
4. **执行上传**:
|
||||
运行修改后的示例脚本,例如:
|
||||
```bash
|
||||
python examples/upload_video_to_douyin.py
|
||||
```
|
||||
### 方式 2:使用 examples
|
||||
|
||||
## Docker 环境
|
||||
### 自己构建镜像
|
||||
1. **构建Docker镜像**:
|
||||
```
|
||||
docker build -t social-auto-upload:latest .
|
||||
```
|
||||
2. **运行Docker容器**:
|
||||
```
|
||||
docker run -d -it -p 5409:5409 social-auto-upload:latest
|
||||
```
|
||||
### 使用预构建镜像
|
||||
1. **拉取镜像**:
|
||||
```
|
||||
docker pull gzxy/social-auto-upload:latest
|
||||
```
|
||||
2. **运行Docker容器**:
|
||||
```
|
||||
docker run -d -it -p 5409:5409 gzxy/social-auto-upload:latest
|
||||
```
|
||||
启动容器后访问:[http://localhost:5409](http://localhost:5409)
|
||||
其他平台当前仍以 `examples/` 下的示例脚本为主,例如:
|
||||
|
||||
- `examples/upload_to_douyin.py`
|
||||
- `examples/upload_video_to_bilibili.py`
|
||||
- `examples/upload_video_to_kuaishou.py`
|
||||
- `examples/upload_video_to_tencent.py`
|
||||
- `examples/upload_video_to_baijiahao.py`
|
||||
- `examples/upload_video_to_tiktok.py`
|
||||
- `examples/upload_video_to_xiaohongshu.py`
|
||||
|
||||
## 🐇项目背景
|
||||
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).parent.resolve()
|
||||
XHS_SERVER = "http://127.0.0.1:11901"
|
||||
LOCAL_CHROME_PATH = "" # change me necessary! for example C:/Program Files/Google/Chrome/Application/chrome.exe
|
||||
LOCAL_CHROME_HEADLESS = False
|
||||
XHS_SERVER = "http://127.0.0.1:11901" # only used by xhs-related flows
|
||||
LOCAL_CHROME_PATH = "" # optional, e.g. C:/Program Files/Google/Chrome/Application/chrome.exe
|
||||
LOCAL_CHROME_HEADLESS = True # default headless behavior for uploader/examples
|
||||
DEBUG_MODE = True # default debug behavior
|
||||
|
||||
+26
-1
@@ -5,11 +5,36 @@
|
||||
实现说明:
|
||||
|
||||
- `sau_cli.py` 是当前 CLI 的主入口和唯一主要实现文件
|
||||
- `sau.exe` 是安装后在 Windows 虚拟环境里自动生成的命令入口,本质上还是调用 `sau_cli.py`
|
||||
- 如果需要给 OpenClaw、Codex 等 agent 使用,可参考仓库内 skill:`skills/douyin-upload/`
|
||||
|
||||
## 安装 CLI 入口
|
||||
|
||||
如果你希望直接使用 `sau` 命令,而不是手动执行 `python sau_cli.py`,先在项目根目录安装一次:
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
安装后就可以直接使用:
|
||||
|
||||
```bash
|
||||
sau douyin --help
|
||||
```
|
||||
|
||||
## 安装 patchright 浏览器
|
||||
|
||||
Windows 下推荐先指定镜像,再安装 Chromium:
|
||||
|
||||
```powershell
|
||||
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright"; patchright install chromium
|
||||
```
|
||||
|
||||
## 抖音 CLI 子命令
|
||||
|
||||
```bash
|
||||
sau douyin login --account creator
|
||||
sau douyin login --account creator --headless
|
||||
sau douyin check --account creator
|
||||
sau douyin upload-video --account creator --file videos/demo.mp4 --title "示例标题" --tags 运动,训练
|
||||
sau douyin upload-note --account creator --images videos/1.png videos/2.png --note "图文示例" --tags 图文,测试
|
||||
@@ -64,4 +89,4 @@ CLI 将 `debug` 和 `headless` 拆成了两个独立维度:
|
||||
- 最多 35 张图片
|
||||
- 不支持 GIF
|
||||
|
||||
仓库内的轻量 skill 源位于 `skills/douyin-cli/`,后续维护 CLI 时应优先以这里和 `social_auto_upload/` 下的新入口为准。
|
||||
后续维护 CLI 时,优先看 `sau_cli.py` 和 `uploader/`。
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
# 安装说明
|
||||
|
||||
这个文档分成两部分:
|
||||
|
||||
- `For Humans`:给正常使用仓库的开发者、创作者、CLI 用户看
|
||||
- `For AI Agents`:给 OpenClaw、Codex、Claude Code 一类 agent 看
|
||||
|
||||
## For Humans
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dreammis/social-auto-upload.git
|
||||
cd social-auto-upload
|
||||
```
|
||||
|
||||
### 2. 创建虚拟环境
|
||||
|
||||
推荐使用 `uv`:
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
uv venv
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
Linux / macOS:
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
### 3. 安装主线依赖
|
||||
|
||||
当前主线依赖已经放到 `pyproject.toml`,推荐直接执行:
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
安装完成后,会注册 `sau` 命令。
|
||||
|
||||
### 4. 安装 patchright Chromium
|
||||
|
||||
当前主线使用 `patchright` 驱动浏览器。
|
||||
|
||||
国内用户推荐先指定镜像,再安装 Chromium。
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright"; patchright install chromium
|
||||
```
|
||||
|
||||
Linux / macOS:
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright" patchright install chromium
|
||||
```
|
||||
|
||||
### 5. 配置 conf.py
|
||||
|
||||
复制一份配置:
|
||||
|
||||
```bash
|
||||
cp conf.example.py conf.py
|
||||
```
|
||||
|
||||
Windows 也可以直接手动复制并重命名。
|
||||
|
||||
当前通常还会用到这些配置项:
|
||||
|
||||
- `LOCAL_CHROME_PATH`
|
||||
- `LOCAL_CHROME_HEADLESS`
|
||||
- `DEBUG_MODE`
|
||||
|
||||
`XHS_SERVER` 目前只和小红书旧流程相关。
|
||||
|
||||
### 6. 验证 CLI 是否可用
|
||||
|
||||
```bash
|
||||
sau --help
|
||||
sau douyin --help
|
||||
```
|
||||
|
||||
如果命令找不到,优先确认:
|
||||
|
||||
- 当前虚拟环境是否已激活
|
||||
- 是否执行过 `uv pip install -e .`
|
||||
|
||||
### 7. 抖音主线示例
|
||||
|
||||
```bash
|
||||
sau douyin login --account creator
|
||||
sau douyin check --account creator
|
||||
sau douyin upload-video --account creator --file videos/demo.mp4 --title "示例标题"
|
||||
```
|
||||
|
||||
## For AI Agents
|
||||
|
||||
如果你是一个可执行命令的 agent,请优先按下面顺序处理:
|
||||
|
||||
1. 先假设仓库根目录就是当前工作目录
|
||||
2. 优先使用 `uv` 管理环境,不要默认回退到旧的 `requirements.txt`
|
||||
3. 安装命令优先使用:
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
4. 如需浏览器驱动,优先使用:
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright"; patchright install chromium
|
||||
```
|
||||
|
||||
Linux / macOS:
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright" patchright install chromium
|
||||
```
|
||||
|
||||
5. 安装完成后,优先检查:
|
||||
|
||||
```bash
|
||||
sau --help
|
||||
sau douyin --help
|
||||
```
|
||||
|
||||
6. 如果用户的目标是抖音登录、cookie 校验、视频上传、图文上传,优先走 CLI:
|
||||
|
||||
```bash
|
||||
sau douyin login
|
||||
sau douyin check
|
||||
sau douyin upload-video
|
||||
sau douyin upload-note
|
||||
```
|
||||
|
||||
7. 如果用户明确在使用 skill 系统,再引导其阅读:
|
||||
|
||||
- `skills/douyin-upload/SKILL.md`
|
||||
- `skills/douyin-upload/references/cli-contract.md`
|
||||
|
||||
### 对 agent 的额外说明
|
||||
|
||||
- `requirements.txt` 目前是历史兼容文件,不是主安装入口
|
||||
- `uploader/` 是核心实现目录
|
||||
- `sau_cli.py` 是当前 CLI 主入口
|
||||
- `docs/legacy-web.md` 是历史 Web 版本说明,不保证当前可用
|
||||
@@ -0,0 +1,49 @@
|
||||
# 历史 Web 版本说明
|
||||
|
||||
这套 Web 相关代码主要包括:
|
||||
|
||||
- `sau_backend.py`
|
||||
- `sau_backend/`
|
||||
- `sau_frontend/`
|
||||
|
||||
它们属于项目过去阶段的实现,当前已经不是主线维护方向。
|
||||
|
||||
## 当前定位
|
||||
|
||||
- 作为历史版本保留
|
||||
- 作为过去 API / Web 封装思路的参考
|
||||
- 不承诺当前一定可直接运行
|
||||
- 不承诺和当前 `uploader/`、`sau_cli.py` 的最新实现完全同步
|
||||
|
||||
## 为什么单独拆出来说明
|
||||
|
||||
当前工程正在整体重构,主线已经切到:
|
||||
|
||||
- `uploader/`:核心平台实现
|
||||
- `sau_cli.py`:CLI 主入口
|
||||
- `skills/`:面向 agent 的 skill
|
||||
|
||||
所以 README 不再把 Web 版本当成主入口来介绍,避免让新用户误以为这是当前最稳定的使用方式。
|
||||
|
||||
## 如果你仍然想研究这套历史 Web 版本
|
||||
|
||||
可以参考这些文件:
|
||||
|
||||
- `sau_backend/README.md`
|
||||
- `sau_frontend/README.md`
|
||||
- `sau_backend.py`
|
||||
|
||||
但请预期:
|
||||
|
||||
- 接口契约可能与当前主线不一致
|
||||
- 平台能力覆盖可能落后于当前 `uploader/`
|
||||
- 依赖和运行方式可能需要自行排障
|
||||
|
||||
## 当前推荐入口
|
||||
|
||||
如果你要使用当前主线能力,优先看:
|
||||
|
||||
- `uploader/`
|
||||
- `sau_cli.py`
|
||||
- `docs/CLI.md`
|
||||
- `skills/douyin-upload/SKILL.md`
|
||||
@@ -0,0 +1,286 @@
|
||||
# Skill 分发与发布说明
|
||||
|
||||
这份文档是给 `social-auto-upload` 后续做独立 skill 分发时用的。
|
||||
|
||||
当前仓库已经具备两层能力:
|
||||
|
||||
- 一个可安装的 CLI:`sau`
|
||||
- 一个可被安装到 Codex 的内置 skill:`douyin-cli`
|
||||
|
||||
后续当主流程和 bug 修复完成后,可以再继续做 PyPI 发布、安装优化、以及更多平台的独立 skill。
|
||||
|
||||
## 先说结论
|
||||
|
||||
`skill` 不一定必须是一个 Python 包。
|
||||
|
||||
它可以是:
|
||||
|
||||
- 一个 skill 目录
|
||||
- 一个独立仓库
|
||||
- 一个安装器脚本
|
||||
- 一个 Docker 镜像
|
||||
- 一个包管理器可安装的分发物
|
||||
|
||||
但从“别人最快安装和使用”的角度看,最常见、最省事的仍然是:
|
||||
|
||||
1. 用一个安装包分发真正的运行能力
|
||||
2. 用一个 skill 安装动作把 skill 放到 AI 工具的技能目录
|
||||
|
||||
对这个项目来说,最推荐的形式是:
|
||||
|
||||
- Python 包负责提供 `sau` 命令
|
||||
- `sau skill install` 负责把 skill 安装到 `~/.codex/skills/`
|
||||
|
||||
也就是:
|
||||
|
||||
```bash
|
||||
pip install social-auto-upload
|
||||
sau skill install
|
||||
```
|
||||
|
||||
## skill 一定要是包吗
|
||||
|
||||
不是。
|
||||
|
||||
### 1. skill 只是一个目录
|
||||
|
||||
这是最原始也最常见的形式。
|
||||
|
||||
通常内容是:
|
||||
|
||||
- `SKILL.md`
|
||||
- `agents/openai.yaml`
|
||||
- `references/`
|
||||
- `scripts/`
|
||||
|
||||
这种形式本身已经是一个可用 skill 了,不一定需要打包。
|
||||
|
||||
问题在于:
|
||||
|
||||
- 用户要知道把它复制到哪里
|
||||
- 用户要手动安装
|
||||
- skill 如果依赖额外脚本或运行时,安装体验会比较差
|
||||
|
||||
适合:
|
||||
|
||||
- 内部团队
|
||||
- 仓库内开发规范
|
||||
- 还在快速迭代的 skill
|
||||
|
||||
### 2. skill 是一个独立仓库
|
||||
|
||||
这也完全成立。
|
||||
|
||||
例如:
|
||||
|
||||
- 一个仓库专门放 `SKILL.md`
|
||||
- 附带 `scripts/install.py`
|
||||
- 或者 README 教用户复制到 `~/.codex/skills/`
|
||||
|
||||
这种模式的优点是:
|
||||
|
||||
- skill 自己独立版本管理
|
||||
- 不依赖主业务仓库
|
||||
- 可以公开发布
|
||||
|
||||
缺点是:
|
||||
|
||||
- 用户还是可能要 clone
|
||||
- 或者还需要执行安装脚本
|
||||
|
||||
适合:
|
||||
|
||||
- 想把 skill 当产品独立维护
|
||||
- skill 和业务代码已经明显拆开
|
||||
|
||||
### 3. skill 跟随一个包分发
|
||||
|
||||
这是当前这个项目最适合的方向。
|
||||
|
||||
思路是:
|
||||
|
||||
- Python 包里内置一份 skill 资源
|
||||
- 安装包后即可执行 `sau skill install`
|
||||
- CLI 和 skill 一起发版
|
||||
|
||||
优点是:
|
||||
|
||||
- 用户体验最好
|
||||
- skill 和实际命令保持一致
|
||||
- 版本对应关系清晰
|
||||
- 不需要用户 clone 仓库
|
||||
|
||||
适合:
|
||||
|
||||
- skill 背后有真实 CLI/SDK/工具
|
||||
- 用户最终是要“使用能力”而不只是“阅读说明”
|
||||
|
||||
### 4. skill 用 Docker 交付
|
||||
|
||||
也可以。
|
||||
|
||||
常见方式是:
|
||||
|
||||
- Docker 里装好运行环境
|
||||
- skill 告诉 AI 通过 `docker run ...` 去执行命令
|
||||
|
||||
优点是:
|
||||
|
||||
- 环境一致性很好
|
||||
- 本地依赖复杂时特别有用
|
||||
|
||||
缺点是:
|
||||
|
||||
- 用户必须先装 Docker
|
||||
- 浏览器自动化、桌面登录、cookie、本地文件挂载都会更复杂
|
||||
- 对抖音这种需要本地浏览器交互的流程不一定更友好
|
||||
|
||||
对当前项目来说,Docker 更适合:
|
||||
|
||||
- 后端服务
|
||||
- 批处理任务
|
||||
- 服务器环境
|
||||
|
||||
不太适合作为“普通用户首次使用抖音登录 skill”的唯一交付方式。
|
||||
|
||||
## AI 安装环境、启动脚本、仓库,这些算不算 skill
|
||||
|
||||
算,但要区分“skill 本体”和“skill 的安装/运行载体”。
|
||||
|
||||
可以这样理解:
|
||||
|
||||
- `SKILL.md` 是 skill 本体
|
||||
- 仓库、包、Docker、安装脚本,是 skill 的分发和运行载体
|
||||
|
||||
所以:
|
||||
|
||||
- skill 可以住在仓库里
|
||||
- skill 可以被包一起带出去
|
||||
- skill 也可以借助 Docker 运行它依赖的环境
|
||||
|
||||
只要最终用户能:
|
||||
|
||||
1. 安装它
|
||||
2. 让 AI 发现它
|
||||
3. 真正调用它依赖的能力
|
||||
|
||||
那它就是成立的。
|
||||
|
||||
## 对这个项目最合适的方案
|
||||
|
||||
### 当前推荐方案
|
||||
|
||||
第一阶段:
|
||||
|
||||
- 继续在这个仓库里修主流程和 bug
|
||||
- 保持 `sau` 命令稳定
|
||||
- 保持包内 skill 与 CLI 契约一致
|
||||
|
||||
第二阶段:
|
||||
|
||||
- 打包并发布到 PyPI
|
||||
- 用户通过 `pip install social-auto-upload` 安装
|
||||
- 用户执行 `sau skill install`
|
||||
|
||||
第三阶段:
|
||||
|
||||
- 根据需要把更多平台拆成独立 skill
|
||||
- 例如 `douyin-cli`、`tencent-cli`、`tiktok-cli`
|
||||
|
||||
### 为什么现在不优先做“独立 skill 仓库”
|
||||
|
||||
因为当前最核心的问题还不是“skill 放哪”,而是:
|
||||
|
||||
- 上传流程是否稳定
|
||||
- CLI 契约是否稳定
|
||||
- 实际用户安装后能不能跑通
|
||||
|
||||
在这些都还在收敛的阶段,先让 skill 随包分发是最稳妥的。
|
||||
|
||||
## 未来可选的三种正式发布路线
|
||||
|
||||
### 路线 A:PyPI 包 + 包内 skill
|
||||
|
||||
用户安装:
|
||||
|
||||
```bash
|
||||
pip install social-auto-upload
|
||||
sau skill install
|
||||
```
|
||||
|
||||
优点:
|
||||
|
||||
- 最容易传播
|
||||
- 安装简单
|
||||
- 版本管理清晰
|
||||
|
||||
这是当前首选路线。
|
||||
|
||||
### 路线 B:独立 skill 仓库 + PyPI 包
|
||||
|
||||
用户安装能力:
|
||||
|
||||
```bash
|
||||
pip install social-auto-upload
|
||||
```
|
||||
|
||||
用户安装 skill:
|
||||
|
||||
- clone skill 仓库
|
||||
- 或跑 skill 仓库提供的安装脚本
|
||||
|
||||
优点:
|
||||
|
||||
- skill 可以单独演进
|
||||
- 可以给不同 AI 工具维护不同 metadata
|
||||
|
||||
缺点:
|
||||
|
||||
- 安装链路更长
|
||||
|
||||
### 路线 C:Docker + skill
|
||||
|
||||
用户:
|
||||
|
||||
- 安装 Docker
|
||||
- 拉镜像
|
||||
- 安装 skill
|
||||
- skill 内部调用 docker 命令
|
||||
|
||||
优点:
|
||||
|
||||
- 依赖环境最稳定
|
||||
|
||||
缺点:
|
||||
|
||||
- 对本地浏览器自动化和交互式登录不够友好
|
||||
|
||||
更适合服务端任务,不是当前首选。
|
||||
|
||||
## 当前项目的发布建议
|
||||
|
||||
当主流程稳定后,建议按这个顺序走:
|
||||
|
||||
1. 先保证 `sau douyin login/check/upload` 真机可用
|
||||
2. 验证 `sau skill install` 安装后的 skill 可以被 Codex 正常识别
|
||||
3. 本地打 wheel 做一次冷启动安装测试
|
||||
4. 再发布 PyPI
|
||||
|
||||
建议的最终用户路径是:
|
||||
|
||||
```bash
|
||||
pip install social-auto-upload
|
||||
playwright install chromium
|
||||
sau skill install
|
||||
sau douyin login --account my-account
|
||||
```
|
||||
|
||||
## 一句话回答
|
||||
|
||||
`skill` 不是必须做成包,但如果你想让别人“最快安装、最少理解成本、最少手工操作”,那就最好让“运行能力”走包分发,让 `skill` 跟着包一起被安装。
|
||||
|
||||
对这个项目来说,最佳落地方案不是“只发一个 skill 仓库”,而是:
|
||||
|
||||
- `social-auto-upload` 作为可安装包
|
||||
- `douyin-cli` 作为包内 skill
|
||||
- `sau skill install` 作为安装桥梁
|
||||
@@ -0,0 +1,98 @@
|
||||
# 更新说明
|
||||
|
||||
这个文档分成两部分:
|
||||
|
||||
- `For Humans`:给正常使用仓库的人看
|
||||
- `For AI Agents`:给能执行命令和自动排查的 agent 看
|
||||
|
||||
## For Humans
|
||||
|
||||
### 1. 拉取最新代码
|
||||
|
||||
```bash
|
||||
git pull
|
||||
```
|
||||
|
||||
如果你平时是切分支开发,请先确认当前所在分支。
|
||||
|
||||
### 2. 更新本地可编辑安装
|
||||
|
||||
如果仓库最近改了 CLI、入口脚本、依赖配置,建议重新执行:
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
### 3. 如有需要,更新浏览器依赖
|
||||
|
||||
当前主线使用 `patchright`。
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright"; patchright install chromium
|
||||
```
|
||||
|
||||
Linux / macOS:
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright" patchright install chromium
|
||||
```
|
||||
|
||||
### 4. 更新后自检
|
||||
|
||||
建议至少检查:
|
||||
|
||||
```bash
|
||||
sau --help
|
||||
sau douyin --help
|
||||
sau douyin check --account your_account
|
||||
```
|
||||
|
||||
如果你在使用 skill,也建议同步看一下:
|
||||
|
||||
- `skills/douyin-upload/SKILL.md`
|
||||
- `docs/CLI.md`
|
||||
|
||||
## For AI Agents
|
||||
|
||||
如果你是 agent,更新仓库后建议按下面顺序做最小验证:
|
||||
|
||||
1. 拉取最新代码:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
```
|
||||
|
||||
2. 重新同步本地安装:
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
3. 如需浏览器驱动,更新 `patchright` Chromium
|
||||
|
||||
4. 重新验证 CLI:
|
||||
|
||||
```bash
|
||||
sau --help
|
||||
sau douyin --help
|
||||
```
|
||||
|
||||
5. 如果任务是抖音链路,再继续验证:
|
||||
|
||||
```bash
|
||||
sau douyin check --account test
|
||||
```
|
||||
|
||||
6. 如果用户依赖 skill,再检查这些路径是否仍存在且契约没变:
|
||||
|
||||
- `skills/douyin-upload/SKILL.md`
|
||||
- `skills/douyin-upload/references/cli-contract.md`
|
||||
- `skills/douyin-upload/references/runtime-requirements.md`
|
||||
|
||||
### 对 agent 的额外说明
|
||||
|
||||
- 优先相信 `pyproject.toml`,不要把 `requirements.txt` 视为当前主线真相
|
||||
- 当前 README 只做总览,安装和更新以 `docs/install.md`、`docs/update.md` 为准
|
||||
- Web 相关内容属于历史路径,见 `docs/legacy-web.md`
|
||||
+8
-5
@@ -80,10 +80,9 @@ def parse_schedule(raw_schedule: str | None) -> datetime | int:
|
||||
return datetime.strptime(raw_schedule, SCHEDULE_FORMAT)
|
||||
|
||||
|
||||
async def login_account(account_name: str) -> Path:
|
||||
async def login_account(account_name: str, headless: bool = True) -> dict:
|
||||
account_file = resolve_account_file(account_name)
|
||||
await douyin_setup(str(account_file), handle=True)
|
||||
return account_file
|
||||
return await douyin_setup(str(account_file), handle=True, return_detail=True, headless=headless)
|
||||
|
||||
|
||||
async def check_account(account_name: str) -> bool:
|
||||
@@ -178,6 +177,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
for action_name in ("login", "check"):
|
||||
action_parser = douyin_actions.add_parser(action_name, help=f"Douyin {action_name}")
|
||||
action_parser.add_argument("--account", required=True, help="Douyin account alias")
|
||||
if action_name == "login":
|
||||
add_runtime_flags(action_parser)
|
||||
|
||||
upload_video_parser = douyin_actions.add_parser("upload-video", help="Upload one video to Douyin")
|
||||
upload_video_parser.add_argument("--account", required=True, help="Douyin account alias")
|
||||
@@ -205,8 +206,10 @@ async def dispatch(args: argparse.Namespace) -> int:
|
||||
raise RuntimeError(f"Unsupported platform: {args.platform}")
|
||||
|
||||
if args.action == "login":
|
||||
account_file = await login_account(args.account)
|
||||
print(f"Douyin login flow completed: {account_file}")
|
||||
result = await login_account(args.account, headless=args.headless)
|
||||
if not result["success"]:
|
||||
raise RuntimeError(result["message"])
|
||||
print(f"Douyin login flow completed: {result['account_file']}")
|
||||
return 0
|
||||
|
||||
if args.action == "check":
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: douyin-upload
|
||||
description: 当 agent 需要通过已安装的 `sau` CLI 完成抖音登录、cookie 校验、视频上传或图文发布时使用这个 skill。该 skill 适用于已经安装 `social-auto-upload` 且可调用 `sau` 命令的环境。优先使用这个 skill 进行稳定的命令式抖音工作流,而不是一开始就阅读 uploader 源码。
|
||||
---
|
||||
|
||||
# 抖音上传 Skill
|
||||
|
||||
优先把 `sau` 作为主接口。
|
||||
|
||||
不要假设当前环境一定能读取仓库源码。
|
||||
不要一开始就去读 `uploader/`。
|
||||
只有在命令不可用或 CLI 执行失败时,才回退到故障排查说明。
|
||||
|
||||
## 功能概览
|
||||
|
||||
| 功能 | 命令入口 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 抖音登录 | `sau douyin login --account <name>` | 生成或刷新指定账号的 cookie |
|
||||
| cookie 校验 | `sau douyin check --account <name>` | 检查指定账号 cookie 是否有效 |
|
||||
| 视频上传 | `sau douyin upload-video ...` | 上传并发布抖音视频 |
|
||||
| 图文上传 | `sau douyin upload-note ...` | 上传并发布抖音图文 |
|
||||
|
||||
## 默认工作流
|
||||
|
||||
1. 先确认 `references/runtime-requirements.md` 里的运行前提。
|
||||
2. 再确认 `references/cli-contract.md` 里的命令契约。
|
||||
3. 执行匹配的 `sau douyin ...` 命令。
|
||||
4. 如果命令失败,再看 `references/troubleshooting.md`。
|
||||
|
||||
## 支持动作
|
||||
|
||||
- 使用 `sau douyin login --account <name>` 登录抖音
|
||||
- 使用 `sau douyin check --account <name>` 校验 cookie 是否有效
|
||||
- 使用 `sau douyin upload-video ...` 上传抖音视频
|
||||
- 使用 `sau douyin upload-note ...` 上传抖音图文
|
||||
|
||||
## 命令选择建议
|
||||
|
||||
- 当用户需要新的 cookie,或现有 cookie 已失效时,使用 `login`
|
||||
- 当用户只需要确认 cookie 状态时,使用 `check`
|
||||
- 当用户要发布视频时,使用 `upload-video`
|
||||
- 当用户要发布图文时,使用 `upload-note`
|
||||
|
||||
## 执行前检查
|
||||
|
||||
- 先确认当前 shell 里是否可以调用 `sau`
|
||||
- 如果 `sau` 不可用,按 `references/runtime-requirements.md` 里的回退方式处理
|
||||
- 当用户明确指定无头或有头模式时,显式传 `--headless` 或 `--headed`
|
||||
- 只有用户明确要求定时发布时,才使用 `--schedule`
|
||||
|
||||
## 模板文件
|
||||
|
||||
当你需要稳定的命令模板时,使用 `scripts/examples/` 下的文件:
|
||||
|
||||
- `douyin_commands.ps1`
|
||||
- `douyin_commands.sh`
|
||||
- `douyin_cli_template.py`
|
||||
|
||||
## 参考文档
|
||||
|
||||
- 运行前提:`references/runtime-requirements.md`
|
||||
- CLI 契约:`references/cli-contract.md`
|
||||
- 故障排查:`references/troubleshooting.md`
|
||||
@@ -0,0 +1,99 @@
|
||||
# 抖音 CLI 契约
|
||||
|
||||
这个 skill 默认假设当前环境已经安装并可调用 `sau` 命令。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### 登录
|
||||
|
||||
```bash
|
||||
sau douyin login --account <account>
|
||||
```
|
||||
|
||||
- 必填参数:
|
||||
- `--account`
|
||||
- 作用:
|
||||
- 启动抖音登录流程,为指定账号生成或刷新 cookie 文件
|
||||
|
||||
### 校验 cookie
|
||||
|
||||
```bash
|
||||
sau douyin check --account <account>
|
||||
```
|
||||
|
||||
- 必填参数:
|
||||
- `--account`
|
||||
- 预期输出:
|
||||
- `valid`:cookie 可用
|
||||
- `invalid`:cookie 缺失或已失效
|
||||
|
||||
### 上传视频
|
||||
|
||||
```bash
|
||||
sau douyin upload-video \
|
||||
--account <account> \
|
||||
--file <video-path> \
|
||||
--title "<title>" \
|
||||
[--tags tag1,tag2] \
|
||||
[--schedule "YYYY-MM-DD HH:MM"] \
|
||||
[--thumbnail <image-path>] \
|
||||
[--product-link <url>] \
|
||||
[--product-title "<title>"] \
|
||||
[--debug] \
|
||||
[--headless | --headed]
|
||||
```
|
||||
|
||||
- 必填参数:
|
||||
- `--account`
|
||||
- `--file`
|
||||
- `--title`
|
||||
- 可选参数:
|
||||
- `--tags`
|
||||
- `--schedule`
|
||||
- `--thumbnail`
|
||||
- `--product-link`
|
||||
- `--product-title`
|
||||
- `--debug`
|
||||
- `--headless`
|
||||
- `--headed`
|
||||
|
||||
### 上传图文
|
||||
|
||||
```bash
|
||||
sau douyin upload-note \
|
||||
--account <account> \
|
||||
--images <image-1> [image-2 ...] \
|
||||
--note "<content>" \
|
||||
[--tags tag1,tag2] \
|
||||
[--schedule "YYYY-MM-DD HH:MM"] \
|
||||
[--debug] \
|
||||
[--headless | --headed]
|
||||
```
|
||||
|
||||
- 必填参数:
|
||||
- `--account`
|
||||
- `--images`
|
||||
- `--note`
|
||||
- 可选参数:
|
||||
- `--tags`
|
||||
- `--schedule`
|
||||
- `--debug`
|
||||
- `--headless`
|
||||
- `--headed`
|
||||
|
||||
## 发布策略
|
||||
|
||||
- 如果不传 `--schedule`,CLI 使用立即发布
|
||||
- 如果传了 `--schedule`,CLI 自动切换为定时发布
|
||||
- 时间格式为:
|
||||
|
||||
```text
|
||||
YYYY-MM-DD HH:MM
|
||||
```
|
||||
|
||||
## 额外说明
|
||||
|
||||
- `upload-video` 每次命令只支持一个视频文件
|
||||
- `upload-note` 每次命令支持多张图片
|
||||
- `upload-note` 当前不支持 GIF
|
||||
- `upload-note` 当前最多支持 35 张图片
|
||||
@@ -0,0 +1,66 @@
|
||||
# 运行前提
|
||||
|
||||
这个 skill 默认假设当前环境已经具备:
|
||||
|
||||
- 已安装 `social-auto-upload`
|
||||
- 可以调用 `sau` 命令,或至少有等效调用方式
|
||||
- 已为 `patchright` 安装 Chromium
|
||||
|
||||
## 推荐安装方式
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
## 安装 patchright 浏览器
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright"; patchright install chromium
|
||||
```
|
||||
|
||||
Linux / macOS(bash / zsh):
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright" patchright install chromium
|
||||
```
|
||||
|
||||
## 常见调用方式
|
||||
|
||||
### 如果 `sau` 已经在 PATH 中
|
||||
|
||||
```bash
|
||||
sau douyin --help
|
||||
```
|
||||
|
||||
### 如果虚拟环境存在,但还没有激活
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
sau douyin --help
|
||||
```
|
||||
|
||||
### 如果你想直接调用可执行文件
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\sau.exe douyin --help
|
||||
```
|
||||
|
||||
### 如果你更倾向于使用 uv
|
||||
|
||||
```bash
|
||||
uv run sau douyin --help
|
||||
```
|
||||
|
||||
## 无头和有头模式
|
||||
|
||||
- 使用 `--headless` 表示无头模式
|
||||
- 使用 `--headed` 表示有头模式
|
||||
- 如果用户明确要求无头登录,也要预期 CLI 会通过控制台输出或临时图片路径提供二维码相关提示
|
||||
@@ -0,0 +1,84 @@
|
||||
# 故障排查
|
||||
|
||||
## 找不到 `sau` 命令
|
||||
|
||||
可以尝试以下方式:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
sau douyin --help
|
||||
```
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\sau.exe douyin --help
|
||||
```
|
||||
|
||||
```bash
|
||||
uv run sau douyin --help
|
||||
```
|
||||
|
||||
如果当前环境还没有安装项目:
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
## cookie 无效或已过期
|
||||
|
||||
先检查 cookie 状态:
|
||||
|
||||
```bash
|
||||
sau douyin check --account <account>
|
||||
```
|
||||
|
||||
如果无效,就重新登录:
|
||||
|
||||
```bash
|
||||
sau douyin login --account <account>
|
||||
```
|
||||
|
||||
## 无头登录二维码处理
|
||||
|
||||
如果用户无法使用终端二维码输出:
|
||||
|
||||
- 查找 CLI 打印出来的临时二维码图片路径
|
||||
- 让用户直接用抖音 APP 扫描该图片
|
||||
|
||||
如果终端二维码显示不正常,优先使用保存下来的图片路径,而不是反复尝试随机的终端设置。
|
||||
|
||||
## 上传参数缺失
|
||||
|
||||
### 视频上传
|
||||
|
||||
最少需要:
|
||||
|
||||
- `--account`
|
||||
- `--file`
|
||||
- `--title`
|
||||
|
||||
### 图文上传
|
||||
|
||||
最少需要:
|
||||
|
||||
- `--account`
|
||||
- `--images`
|
||||
- `--note`
|
||||
|
||||
## 图片限制
|
||||
|
||||
对 `upload-note` 来说:
|
||||
|
||||
- 不支持 GIF
|
||||
- 最多 35 张图片
|
||||
|
||||
如果超出这些限制,先减少图片数量或替换文件格式,再重试。
|
||||
|
||||
## 定时发布
|
||||
|
||||
时间格式使用:
|
||||
|
||||
```text
|
||||
YYYY-MM-DD HH:MM
|
||||
```
|
||||
|
||||
如果不需要定时发布,去掉 `--schedule` 即可改为立即发布。
|
||||
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
|
||||
def run_command(command: list[str]) -> None:
|
||||
print("Running:", " ".join(shlex.quote(part) for part in command))
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
account = "creator"
|
||||
|
||||
commands = [
|
||||
["sau", "douyin", "login", "--account", account, "--headless"],
|
||||
["sau", "douyin", "check", "--account", account],
|
||||
[
|
||||
"sau",
|
||||
"douyin",
|
||||
"upload-video",
|
||||
"--account",
|
||||
account,
|
||||
"--file",
|
||||
"videos/demo.mp4",
|
||||
"--title",
|
||||
"Douyin video from Python",
|
||||
"--tags",
|
||||
"cli,video",
|
||||
"--thumbnail",
|
||||
"videos/demo.png",
|
||||
"--headless",
|
||||
],
|
||||
[
|
||||
"sau",
|
||||
"douyin",
|
||||
"upload-note",
|
||||
"--account",
|
||||
account,
|
||||
"--images",
|
||||
"videos/1.png",
|
||||
"videos/2.png",
|
||||
"--note",
|
||||
"Douyin note from Python",
|
||||
"--tags",
|
||||
"cli,note",
|
||||
"--headless",
|
||||
],
|
||||
]
|
||||
|
||||
for command in commands:
|
||||
run_command(command)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
# PowerShell examples for the installed sau CLI.
|
||||
|
||||
$account = "creator"
|
||||
$video = "videos/demo.mp4"
|
||||
$thumbnail = "videos/demo.png"
|
||||
$noteImages = @("videos/1.png", "videos/2.png")
|
||||
|
||||
sau douyin login --account $account --headless
|
||||
sau douyin check --account $account
|
||||
|
||||
sau douyin upload-video `
|
||||
--account $account `
|
||||
--file $video `
|
||||
--title "Douyin video from PowerShell" `
|
||||
--tags "cli,video" `
|
||||
--thumbnail $thumbnail `
|
||||
--headless
|
||||
|
||||
sau douyin upload-note `
|
||||
--account $account `
|
||||
--images $noteImages `
|
||||
--note "Douyin note from PowerShell" `
|
||||
--tags "cli,note" `
|
||||
--headless
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
account="creator"
|
||||
video="videos/demo.mp4"
|
||||
thumbnail="videos/demo.png"
|
||||
|
||||
sau douyin login --account "$account" --headless
|
||||
sau douyin check --account "$account"
|
||||
|
||||
sau douyin upload-video \
|
||||
--account "$account" \
|
||||
--file "$video" \
|
||||
--title "Douyin video from bash" \
|
||||
--tags "cli,video" \
|
||||
--thumbnail "$thumbnail" \
|
||||
--headless
|
||||
|
||||
sau douyin upload-note \
|
||||
--account "$account" \
|
||||
--images "videos/1.png" "videos/2.png" \
|
||||
--note "Douyin note from bash" \
|
||||
--tags "cli,note" \
|
||||
--headless
|
||||
@@ -24,6 +24,10 @@ DOUYIN_PUBLISH_STRATEGY_IMMEDIATE = "immediate"
|
||||
DOUYIN_PUBLISH_STRATEGY_SCHEDULED = "scheduled"
|
||||
|
||||
|
||||
def _msg(emoji: str, text: str) -> str:
|
||||
return f"{emoji} {text}"
|
||||
|
||||
|
||||
async def _emit_qrcode_callback(qrcode_callback, payload: dict):
|
||||
if not qrcode_callback:
|
||||
return
|
||||
@@ -47,33 +51,31 @@ def _build_login_result(success: bool, status: str, message: str, account_file:
|
||||
async def cookie_auth(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.chromium.launch(headless=True, channel="chrome")
|
||||
context = await browser.new_context(storage_state=account_file)
|
||||
context = await set_init_script(context)
|
||||
page = await context.new_page()
|
||||
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
|
||||
try:
|
||||
await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000)
|
||||
except Exception:
|
||||
print("[+] 等待5秒 cookie 失效")
|
||||
await context.close()
|
||||
context = await browser.new_context(storage_state=account_file)
|
||||
context = await set_init_script(context)
|
||||
page = await context.new_page()
|
||||
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
|
||||
try:
|
||||
await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if await page.get_by_text("手机号登录").count() or await page.get_by_text("扫码登录").count():
|
||||
return False
|
||||
|
||||
return True
|
||||
finally:
|
||||
await browser.close()
|
||||
return False
|
||||
|
||||
if await page.get_by_text("手机号登录").count() or await page.get_by_text("扫码登录").count():
|
||||
print("[+] 等待5秒 cookie 失效")
|
||||
return False
|
||||
|
||||
print("[+] cookie 有效")
|
||||
return True
|
||||
|
||||
|
||||
async def douyin_setup(account_file, handle=False, return_detail=False, qrcode_callback=None):
|
||||
async def douyin_setup(account_file, handle=False, return_detail=False, qrcode_callback=None, headless: bool = LOCAL_CHROME_HEADLESS):
|
||||
if not os.path.exists(account_file) or not await cookie_auth(account_file):
|
||||
if not handle:
|
||||
result = _build_login_result(False, "cookie_invalid", "cookie文件不存在或已失效", account_file)
|
||||
return result if return_detail else False
|
||||
douyin_logger.info("[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件")
|
||||
result = await douyin_cookie_gen(account_file, qrcode_callback=qrcode_callback)
|
||||
douyin_logger.info(_msg("🥹", "cookie 失效了,准备打开浏览器重新登录"))
|
||||
result = await douyin_cookie_gen(account_file, qrcode_callback=qrcode_callback, headless=headless)
|
||||
return result if return_detail else result["success"]
|
||||
|
||||
result = _build_login_result(True, "cookie_valid", "cookie有效", account_file)
|
||||
@@ -108,13 +110,13 @@ async def _save_douyin_qrcode(page: Page, account_file: str, previous_qrcode_pat
|
||||
qrcode_path = save_data_url_image(qrcode_src, build_login_qrcode_path(account_file))
|
||||
if previous_qrcode_path and previous_qrcode_path != qrcode_path:
|
||||
if remove_qrcode_file(previous_qrcode_path):
|
||||
douyin_logger.info(f"[+] 已删除临时二维码文件: {previous_qrcode_path}")
|
||||
douyin_logger.info(f"[+] 已提取抖音登录二维码并保存到: {qrcode_path}")
|
||||
douyin_logger.info(_msg("🧹", f"临时二维码文件已清理: {previous_qrcode_path}"))
|
||||
douyin_logger.info(_msg("🖼️", f"二维码已经准备好啦,已保存到: {qrcode_path}"))
|
||||
qrcode_content = decode_qrcode_from_path(qrcode_path)
|
||||
if qrcode_content:
|
||||
print_terminal_qrcode(qrcode_content, qrcode_path, "抖音APP")
|
||||
else:
|
||||
douyin_logger.warning(f"[!] 无法将二维码打印到控制台,请打开 {qrcode_path} 扫码")
|
||||
douyin_logger.warning(_msg("😵", f"终端没法完整显示二维码,请打开 {qrcode_path} 扫码"))
|
||||
qrcode_info = {
|
||||
"image_path": str(qrcode_path),
|
||||
"image_data_url": qrcode_src,
|
||||
@@ -123,16 +125,39 @@ async def _save_douyin_qrcode(page: Page, account_file: str, previous_qrcode_pat
|
||||
return qrcode_info
|
||||
|
||||
|
||||
async def _is_douyin_login_completed(page: Page) -> bool:
|
||||
if not page.url.startswith("https://creator.douyin.com/creator-micro/home"):
|
||||
return False
|
||||
|
||||
login_markers = [
|
||||
page.get_by_text("扫码登录", exact=True).first,
|
||||
page.get_by_text("手机号登录", exact=True).first,
|
||||
page.get_by_text("二维码失效", exact=True).first,
|
||||
page.get_by_role("img", name="二维码").first,
|
||||
]
|
||||
|
||||
for marker in login_markers:
|
||||
if not await marker.count():
|
||||
continue
|
||||
try:
|
||||
if await marker.is_visible():
|
||||
return False
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _wait_for_douyin_login(page: Page, account_file: str, qrcode_info: dict, qrcode_callback=None, poll_interval: int = 3, max_checks: int = 100) -> dict:
|
||||
qrcode_path = Path(qrcode_info["image_path"])
|
||||
for _ in range(max_checks):
|
||||
if page.url.startswith("https://creator.douyin.com/creator-micro/home"):
|
||||
douyin_logger.info(f"[+] 检测到已跳转到登录后页面: {page.url}")
|
||||
if await _is_douyin_login_completed(page):
|
||||
douyin_logger.info(_msg("🥳", f"扫码成功,已经跳转到登录后页面: {page.url}"))
|
||||
return _build_login_result(True, "success", "抖音扫码登录成功", account_file, qrcode_info, page.url)
|
||||
|
||||
expired_box = page.get_by_text("二维码失效", exact=True).locator("..").first
|
||||
if await expired_box.count() and await expired_box.is_visible():
|
||||
douyin_logger.warning("[!] 二维码失效,正在刷新二维码...")
|
||||
douyin_logger.warning(_msg("😵", "二维码失效了,小人马上去刷新"))
|
||||
await expired_box.click()
|
||||
await asyncio.sleep(1)
|
||||
qrcode_info = await _save_douyin_qrcode(page, account_file, qrcode_path, qrcode_callback=qrcode_callback)
|
||||
@@ -143,9 +168,15 @@ async def _wait_for_douyin_login(page: Page, account_file: str, qrcode_info: dic
|
||||
return _build_login_result(False, "timeout", "等待抖音扫码登录超时", account_file, qrcode_info, page.url)
|
||||
|
||||
|
||||
async def douyin_cookie_gen(account_file, qrcode_callback=None, poll_interval: int = 3, max_checks: int = 100):
|
||||
async def douyin_cookie_gen(
|
||||
account_file,
|
||||
qrcode_callback=None,
|
||||
poll_interval: int = 3,
|
||||
max_checks: int = 100,
|
||||
headless: bool = LOCAL_CHROME_HEADLESS,
|
||||
):
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.chromium.launch(headless=True, channel="chrome")
|
||||
browser = await playwright.chromium.launch(headless=headless, channel="chrome")
|
||||
context = await browser.new_context()
|
||||
context = await set_init_script(context)
|
||||
qrcode_path = None
|
||||
@@ -155,7 +186,7 @@ async def douyin_cookie_gen(account_file, qrcode_callback=None, poll_interval: i
|
||||
await page.goto("https://creator.douyin.com/")
|
||||
qrcode_info = await _save_douyin_qrcode(page, account_file, qrcode_callback=qrcode_callback)
|
||||
qrcode_path = Path(qrcode_info["image_path"])
|
||||
douyin_logger.info("[+] 等待扫码登录完成...")
|
||||
douyin_logger.info(_msg("🧍", "请扫码,小人正在耐心等待登录完成"))
|
||||
result = await _wait_for_douyin_login(
|
||||
page,
|
||||
account_file,
|
||||
@@ -167,11 +198,22 @@ async def douyin_cookie_gen(account_file, qrcode_callback=None, poll_interval: i
|
||||
if result["success"]:
|
||||
await asyncio.sleep(2)
|
||||
await context.storage_state(path=account_file)
|
||||
if not await cookie_auth(account_file):
|
||||
result = _build_login_result(
|
||||
False,
|
||||
"cookie_invalid",
|
||||
"抖音扫码流程结束,但 cookie 校验失败",
|
||||
account_file,
|
||||
qrcode_info,
|
||||
page.url,
|
||||
)
|
||||
except Exception as exc:
|
||||
result = _build_login_result(False, "failed", str(exc), account_file, current_url=page.url if "page" in locals() else "")
|
||||
finally:
|
||||
if remove_qrcode_file(qrcode_path):
|
||||
douyin_logger.info(f"[+] 已删除临时二维码文件: {qrcode_path}")
|
||||
douyin_logger.info(_msg("🧹", f"临时二维码文件已清理: {qrcode_path}"))
|
||||
if not result["success"]:
|
||||
douyin_logger.error(_msg("😢", f"登录失败: {result['message']}"))
|
||||
await context.close()
|
||||
await browser.close()
|
||||
return result
|
||||
@@ -257,7 +299,7 @@ class DouYinBaseUploader(BaseVideoUploader):
|
||||
await page.wait_for_selector('input[placeholder="请输入商品短标题"]', timeout=10000)
|
||||
short_title_input = page.locator('input[placeholder="请输入商品短标题"]')
|
||||
if not await short_title_input.count():
|
||||
douyin_logger.error("[-] 未找到商品短标题输入框")
|
||||
douyin_logger.error(_msg("😵", "没找到商品短标题输入框"))
|
||||
return False
|
||||
|
||||
product_title = product_title[:10]
|
||||
@@ -267,11 +309,11 @@ class DouYinBaseUploader(BaseVideoUploader):
|
||||
finish_button = page.locator('button:has-text("完成编辑")')
|
||||
if "disabled" not in await finish_button.get_attribute("class"):
|
||||
await finish_button.click()
|
||||
douyin_logger.debug("[+] 成功点击'完成编辑'按钮")
|
||||
douyin_logger.debug(_msg("🥳", "已点击“完成编辑”按钮"))
|
||||
await page.wait_for_selector(".semi-modal-content", state="hidden", timeout=5000)
|
||||
return True
|
||||
|
||||
douyin_logger.error("[-] '完成编辑'按钮处于禁用状态,尝试直接关闭对话框")
|
||||
douyin_logger.error(_msg("😵", "“完成编辑”按钮是灰的,小人先把弹窗关掉"))
|
||||
cancel_button = page.locator('button:has-text("取消")')
|
||||
if await cancel_button.count():
|
||||
await cancel_button.click()
|
||||
@@ -287,42 +329,42 @@ class DouYinBaseUploader(BaseVideoUploader):
|
||||
await page.wait_for_selector("text=添加标签", timeout=10000)
|
||||
dropdown = page.get_by_text("添加标签").locator("..").locator("..").locator("..").locator(".semi-select").first
|
||||
if not await dropdown.count():
|
||||
douyin_logger.error("[-] 未找到标签下拉框")
|
||||
douyin_logger.error(_msg("😵", "没找到标签下拉框"))
|
||||
return False
|
||||
douyin_logger.debug("[-] 找到标签下拉框,准备选择'购物车'")
|
||||
douyin_logger.debug(_msg("🧍", "找到标签下拉框,小人准备选择“购物车”"))
|
||||
await dropdown.click()
|
||||
await page.wait_for_selector('[role="listbox"]', timeout=5000)
|
||||
await page.locator('[role="option"]:has-text("购物车")').click()
|
||||
douyin_logger.debug("[+] 成功选择'购物车'")
|
||||
douyin_logger.debug(_msg("🥳", "已经选中“购物车”"))
|
||||
|
||||
await page.wait_for_selector('input[placeholder="粘贴商品链接"]', timeout=5000)
|
||||
input_field = page.locator('input[placeholder="粘贴商品链接"]')
|
||||
await input_field.fill(product_link)
|
||||
douyin_logger.debug(f"[+] 已输入商品链接: {product_link}")
|
||||
douyin_logger.debug(_msg("🔗", f"商品链接已经填好了: {product_link}"))
|
||||
|
||||
add_button = page.locator('span:has-text("添加链接")')
|
||||
button_class = await add_button.get_attribute("class")
|
||||
if "disable" in button_class:
|
||||
douyin_logger.error("[-] '添加链接'按钮不可用")
|
||||
douyin_logger.error(_msg("😵", "“添加链接”按钮现在点不了"))
|
||||
return False
|
||||
await add_button.click()
|
||||
douyin_logger.debug("[+] 成功点击'添加链接'按钮")
|
||||
douyin_logger.debug(_msg("🥳", "已点击“添加链接”按钮"))
|
||||
|
||||
await page.wait_for_timeout(2000)
|
||||
error_modal = page.locator("text=未搜索到对应商品")
|
||||
if await error_modal.count():
|
||||
confirm_button = page.locator('button:has-text("确定")')
|
||||
await confirm_button.click()
|
||||
douyin_logger.error("[-] 商品链接无效")
|
||||
douyin_logger.error(_msg("😢", "这个商品链接无效"))
|
||||
return False
|
||||
|
||||
if not await self.handle_product_dialog(page, product_title):
|
||||
return False
|
||||
|
||||
douyin_logger.debug("[+] 成功设置商品链接")
|
||||
douyin_logger.debug(_msg("🥳", "商品链接设置好了"))
|
||||
return True
|
||||
except Exception as e:
|
||||
douyin_logger.error(f"[-] 设置商品链接时出错: {str(e)}")
|
||||
douyin_logger.error(_msg("😢", f"设置商品链接时出错: {str(e)}"))
|
||||
return False
|
||||
|
||||
|
||||
@@ -369,35 +411,35 @@ class DouYinVideo(DouYinBaseUploader):
|
||||
self.thumbnail_portrait_path = str(self.validate_image_file(self.thumbnail_portrait_path))
|
||||
|
||||
async def handle_upload_error(self, page):
|
||||
douyin_logger.info("视频出错了,重新上传中")
|
||||
douyin_logger.warning(_msg("😵", "视频上传摔了一跤,小人马上重新上传"))
|
||||
await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path)
|
||||
|
||||
async def handle_auto_video_cover(self, page):
|
||||
if await page.get_by_text("请设置封面后再发布").first.is_visible():
|
||||
print(" [-] 检测到需要设置封面提示...")
|
||||
douyin_logger.info(_msg("🧍", "发布前还得先把封面弄好"))
|
||||
recommend_cover = page.locator('[class^="recommendCover-"]').first
|
||||
if await recommend_cover.count():
|
||||
print(" [-] 正在选择第一个推荐封面...")
|
||||
douyin_logger.info(_msg("🏃", "小人去选第一个推荐封面"))
|
||||
try:
|
||||
await recommend_cover.click()
|
||||
await asyncio.sleep(1)
|
||||
confirm_text = "是否确认应用此封面?"
|
||||
if await page.get_by_text(confirm_text).first.is_visible():
|
||||
print(f" [-] 检测到确认弹窗: {confirm_text}")
|
||||
douyin_logger.info(_msg("🪟", f"弹出确认框了: {confirm_text}"))
|
||||
await page.get_by_role("button", name="确定").click()
|
||||
print(" [-] 已点击确认应用封面")
|
||||
douyin_logger.info(_msg("🥳", "推荐封面已经应用"))
|
||||
await asyncio.sleep(1)
|
||||
print(" [-] 已完成封面选择流程")
|
||||
douyin_logger.info(_msg("🥳", "封面选择流程完成"))
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" [-] 选择封面失败: {e}")
|
||||
douyin_logger.warning(_msg("😵", f"推荐封面没选成功: {e}"))
|
||||
return False
|
||||
|
||||
async def set_thumbnail(self, page: Page):
|
||||
if not self.thumbnail_landscape_path and not self.thumbnail_portrait_path:
|
||||
return
|
||||
|
||||
douyin_logger.info(" [-] 正在设置视频封面...")
|
||||
douyin_logger.info(_msg("🏃", "小人正在设置视频封面"))
|
||||
await page.click('text="选择封面"')
|
||||
cover_locator_str = 'div[id*="creator-content-modal"]'
|
||||
cover_locator = page.locator(cover_locator_str)
|
||||
@@ -409,23 +451,23 @@ class DouYinVideo(DouYinBaseUploader):
|
||||
await page.wait_for_timeout(1000)
|
||||
await upload_input.set_input_files(self.thumbnail_landscape_path)
|
||||
await page.wait_for_timeout(2000)
|
||||
douyin_logger.info(" [-] 横版封面上传完成")
|
||||
douyin_logger.info(_msg("🖼️", "横版封面上传完成"))
|
||||
|
||||
if self.thumbnail_portrait_path:
|
||||
await cover_locator.locator("div[class*='steps'] div").nth(1).click()
|
||||
await page.wait_for_timeout(1000)
|
||||
await upload_input.set_input_files(self.thumbnail_portrait_path)
|
||||
await page.wait_for_timeout(2000)
|
||||
douyin_logger.info(" [-] 竖版封面上传完成")
|
||||
douyin_logger.info(_msg("🖼️", "竖版封面上传完成"))
|
||||
|
||||
await cover_locator.locator('button:visible:has-text("完成")').click()
|
||||
douyin_logger.info(" [+] 视频封面设置完成!")
|
||||
douyin_logger.info(_msg("🥳", "视频封面设置完成"))
|
||||
await page.wait_for_selector("div.extractFooter", state="detached")
|
||||
|
||||
async def upload(self, playwright: Playwright) -> None:
|
||||
douyin_logger.info("[-] 正在校验 cookie、视频文件、封面和发布时间...")
|
||||
douyin_logger.info(_msg("🧍", "小人先检查 cookie、视频文件、封面和发布时间"))
|
||||
await self.validate_upload_args()
|
||||
douyin_logger.info("[+] 上传前校验通过")
|
||||
douyin_logger.info(_msg("🥳", "上传前检查通过"))
|
||||
|
||||
browser = await playwright.chromium.launch(headless=self.headless, channel="chrome")
|
||||
context = await browser.new_context(
|
||||
@@ -436,8 +478,8 @@ class DouYinVideo(DouYinBaseUploader):
|
||||
|
||||
page = await context.new_page()
|
||||
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
|
||||
douyin_logger.info(f"[+]正在上传-------{self.title}.mp4")
|
||||
douyin_logger.info("[-] 正在打开主页...")
|
||||
douyin_logger.info(_msg("🏃", f"小人开始搬运视频: {self.title}.mp4"))
|
||||
douyin_logger.info(_msg("🧭", "小人正在赶往上传主页"))
|
||||
await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload")
|
||||
await page.locator("div[class^='container'] input").set_input_files(self.file_path)
|
||||
|
||||
@@ -447,7 +489,7 @@ class DouYinVideo(DouYinBaseUploader):
|
||||
"https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page",
|
||||
timeout=3000,
|
||||
)
|
||||
douyin_logger.info("[+] 成功进入version_1发布页面!")
|
||||
douyin_logger.info(_msg("🥳", "已经进入 version_1 发布页面"))
|
||||
break
|
||||
except Exception:
|
||||
try:
|
||||
@@ -455,36 +497,36 @@ class DouYinVideo(DouYinBaseUploader):
|
||||
"https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page",
|
||||
timeout=3000,
|
||||
)
|
||||
douyin_logger.info("[+] 成功进入version_2发布页面!")
|
||||
douyin_logger.info(_msg("🥳", "已经进入 version_2 发布页面"))
|
||||
break
|
||||
except Exception:
|
||||
print(" [-] 超时未进入视频发布页面,重新尝试...")
|
||||
douyin_logger.debug(_msg("🧍", "还没进到视频发布页面,小人继续等一会"))
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
douyin_logger.info(" [-] 正在填充标题、描述和话题...")
|
||||
douyin_logger.info(_msg("✍️", "小人开始填标题、描述和话题"))
|
||||
await self.fill_title_and_description(page, self.title, self.title, self.tags)
|
||||
douyin_logger.info(f"总共添加{len(self.tags)}个话题")
|
||||
douyin_logger.info(_msg("🏷️", f"小人一共贴了 {len(self.tags)} 个话题"))
|
||||
|
||||
while True:
|
||||
try:
|
||||
number = await page.locator('[class^="long-card"] div:has-text("重新上传")').count()
|
||||
if number > 0:
|
||||
douyin_logger.success(" [-]视频上传完毕")
|
||||
douyin_logger.success(_msg("🥳", "视频已经传完啦"))
|
||||
break
|
||||
douyin_logger.info(" [-] 正在上传视频中...")
|
||||
douyin_logger.info(_msg("🏃", "小人正在努力上传视频"))
|
||||
await asyncio.sleep(2)
|
||||
if await page.locator('div.progress-div > div:has-text("上传失败")').count():
|
||||
douyin_logger.error(" [-] 发现上传出错了... 准备重试")
|
||||
douyin_logger.error(_msg("😵", "检测到上传失败,小人准备重试"))
|
||||
await self.handle_upload_error(page)
|
||||
except Exception:
|
||||
douyin_logger.info(" [-] 正在上传视频中...")
|
||||
douyin_logger.debug(_msg("🧍", "小人还在等视频上传完成"))
|
||||
await asyncio.sleep(2)
|
||||
|
||||
if self.productLink and self.productTitle:
|
||||
douyin_logger.info(" [-] 正在设置商品链接...")
|
||||
douyin_logger.info(_msg("🛒", "小人正在设置商品链接"))
|
||||
await self.set_product_link(page, self.productLink, self.productTitle)
|
||||
douyin_logger.info(" [+] 完成设置商品链接...")
|
||||
douyin_logger.info(_msg("🥳", "商品链接设置完成"))
|
||||
|
||||
await self.set_thumbnail(page)
|
||||
|
||||
@@ -505,17 +547,17 @@ class DouYinVideo(DouYinBaseUploader):
|
||||
"https://creator.douyin.com/creator-micro/content/manage**",
|
||||
timeout=3000,
|
||||
)
|
||||
douyin_logger.success(" [-]视频发布成功")
|
||||
douyin_logger.success(_msg("🥳", "视频发布成功,小人开心收工"))
|
||||
break
|
||||
except Exception:
|
||||
await self.handle_auto_video_cover(page)
|
||||
douyin_logger.info(" [-] 视频正在发布中...")
|
||||
douyin_logger.info(_msg("🏃", "小人正在冲刺发布视频"))
|
||||
if self.debug:
|
||||
await page.screenshot(full_page=True)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
await context.storage_state(path=self.account_file)
|
||||
douyin_logger.success(" [-]cookie更新完毕!")
|
||||
douyin_logger.success(_msg("🥳", "cookie 更新完毕"))
|
||||
await asyncio.sleep(2)
|
||||
await context.close()
|
||||
await browser.close()
|
||||
@@ -570,12 +612,12 @@ class DouYinNote(DouYinBaseUploader):
|
||||
self.image_paths = normalized_image_paths
|
||||
|
||||
async def upload_note_content(self, page: Page) -> None:
|
||||
douyin_logger.info(f"[+]正在上传图文,共 {len(self.image_paths)} 张图片")
|
||||
douyin_logger.info("[-] 正在切换到图文发布...")
|
||||
douyin_logger.info(_msg("🏃", f"小人开始搬运图文,共 {len(self.image_paths)} 张图片"))
|
||||
douyin_logger.info(_msg("🔀", "小人正在切换到图文发布"))
|
||||
await page.get_by_text("发布图文", exact=True).click()
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
douyin_logger.info("[-] 正在上传图片...")
|
||||
douyin_logger.info(_msg("📤", "小人正在上传图片"))
|
||||
await page.locator("div[class^='container'] input[accept*='image']").set_input_files(self.image_paths)
|
||||
|
||||
while True:
|
||||
@@ -584,16 +626,16 @@ class DouYinNote(DouYinBaseUploader):
|
||||
"**/creator-micro/content/post/image?**",
|
||||
timeout=3000,
|
||||
)
|
||||
douyin_logger.info("[+] 成功进入图文发布页面!")
|
||||
douyin_logger.info(_msg("🥳", "已经进入图文发布页面"))
|
||||
break
|
||||
except Exception:
|
||||
douyin_logger.info(" [-] 正在上传图片中...")
|
||||
douyin_logger.debug(_msg("🧍", "小人还在等图片上传完成"))
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
douyin_logger.info(" [-] 正在填充标题、描述和话题...")
|
||||
douyin_logger.info(_msg("✍️", "小人开始填标题、描述和话题"))
|
||||
await self.fill_title_and_description(page, self.note, self.note, self.tags)
|
||||
douyin_logger.info(f"总共添加{len(self.tags)}个话题")
|
||||
douyin_logger.info(_msg("🏷️", f"小人一共贴了 {len(self.tags)} 个话题"))
|
||||
|
||||
if self.publish_strategy == DOUYIN_PUBLISH_STRATEGY_SCHEDULED and self.publish_date != 0:
|
||||
await self.set_schedule_time_douyin(page, self.publish_date)
|
||||
@@ -607,16 +649,16 @@ class DouYinNote(DouYinBaseUploader):
|
||||
"**/creator-micro/content/manage?enter_from=publish**",
|
||||
timeout=3000,
|
||||
)
|
||||
douyin_logger.success(" [-]图文发布成功")
|
||||
douyin_logger.success(_msg("🥳", "图文发布成功,小人开心收工"))
|
||||
break
|
||||
except Exception:
|
||||
douyin_logger.info(" [-] 图文正在发布中...")
|
||||
douyin_logger.info(_msg("🏃", "小人正在冲刺发布图文"))
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def upload(self, playwright: Playwright) -> None:
|
||||
douyin_logger.info("[-] 正在校验 cookie、图片和发布时间...")
|
||||
douyin_logger.info(_msg("🧍", "小人先检查 cookie、图片和发布时间"))
|
||||
await self.validate_upload_args()
|
||||
douyin_logger.info("[+] 图文上传前校验通过")
|
||||
douyin_logger.info(_msg("🥳", "图文上传前检查通过"))
|
||||
|
||||
browser = await playwright.chromium.launch(headless=self.headless, channel="chrome")
|
||||
context = await browser.new_context(
|
||||
@@ -629,7 +671,7 @@ class DouYinNote(DouYinBaseUploader):
|
||||
try:
|
||||
page = await context.new_page()
|
||||
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
|
||||
douyin_logger.info("[-] 正在打开图文发布页...")
|
||||
douyin_logger.info(_msg("🧭", "小人正在赶往图文发布页"))
|
||||
await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload")
|
||||
|
||||
await self.upload_note_content(page)
|
||||
@@ -637,7 +679,7 @@ class DouYinNote(DouYinBaseUploader):
|
||||
finally:
|
||||
if upload_success:
|
||||
await context.storage_state(path=self.account_file)
|
||||
douyin_logger.success(" [-]cookie更新完毕!")
|
||||
douyin_logger.success(_msg("🥳", "cookie 更新完毕"))
|
||||
await asyncio.sleep(2)
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
+22
-1
@@ -2,6 +2,7 @@
|
||||
from datetime import datetime
|
||||
import base64
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import segno
|
||||
@@ -43,10 +44,30 @@ def decode_qrcode_from_path(qrcode_path: Path) -> str | None:
|
||||
return qrcode_content or None
|
||||
|
||||
|
||||
def _print_ascii_qrcode(qrcode) -> None:
|
||||
border = 1
|
||||
rows = list(qrcode.matrix)
|
||||
empty_line = " " * (len(rows[0]) + border * 2)
|
||||
print(empty_line)
|
||||
for row in rows:
|
||||
line = [" "] * border
|
||||
line.extend("##" if cell else " " for cell in row)
|
||||
line.extend([" "] * border)
|
||||
print("".join(line))
|
||||
print(empty_line)
|
||||
|
||||
|
||||
def print_terminal_qrcode(qrcode_content: str, qrcode_path: Path, app_name: str) -> None:
|
||||
print()
|
||||
print(f"请使用{app_name}扫描下方二维码登录:")
|
||||
segno.make(qrcode_content, error='L', boost_error=False).terminal(compact=True, border=0)
|
||||
qrcode = segno.make(qrcode_content, error="L", boost_error=False)
|
||||
try:
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
qrcode.terminal(compact=True, border=0)
|
||||
except (UnicodeEncodeError, OSError):
|
||||
print("当前终端不支持 Unicode 二维码字符,已切换为 ASCII 打印:")
|
||||
_print_ascii_qrcode(qrcode)
|
||||
print("在 Windows 下建议使用 Windows Terminal(支持 UTF-8,可完整显示二维码)")
|
||||
print(f"否则请打开 {qrcode_path} 扫码")
|
||||
print()
|
||||
|
||||
Reference in New Issue
Block a user