chore: 清理历史遗留目录(Vue 前端/示例/测试/文档/赞助商资源),精简为 EveryPublish 核心结构

This commit is contained in:
Qiufeng
2026-08-21 15:27:11 +08:00
parent 80607c0411
commit 78bef8e2e1
117 changed files with 0 additions and 11212 deletions
-3
View File
@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: dreammis
-283
View File
@@ -1,283 +0,0 @@
# CLI 使用说明
项目现在提供一个统一的 CLI 入口 `sau`,当前主线已经接入:
- `douyin`
- `kuaishou`
- `xiaohongshu`
- `bilibili`
- `tencent`
- `baijiahao`
- `alipay`
- `weibo`
- `hupu`
- `youtube`
实现说明:
- `sau_cli.py` 是当前 CLI 的主入口和唯一主要实现文件
- `sau.exe` 是安装后在 Windows 虚拟环境里自动生成的命令入口,本质上还是调用 `sau_cli.py`
- 如果需要给 OpenClaw、Codex 等 agent 使用,可参考仓库内 skill:
- `skills/douyin-upload/`
- `skills/kuaishou-upload/`
- `skills/xiaohongshu-upload/`
- `skills/bilibili-upload/`
视频号、百家号和支付宝生活号目前只有 CLI 入口,暂未提供对应的 skill。
## 安装 CLI 入口
如果你希望直接使用 `sau` 命令,而不是手动执行 `python sau_cli.py`,先在项目根目录安装一次:
```bash
uv pip install -e .
```
安装后就可以直接使用:
```bash
sau douyin --help
sau kuaishou --help
sau xiaohongshu --help
sau bilibili --help
sau tencent --help
sau baijiahao --help
sau alipay --help
sau weibo --help
sau hupu --help
sau youtube --help
```
## 安装 patchright 浏览器
Windows 下推荐先指定镜像,再安装 Chromium:
```powershell
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright"; patchright install chromium
```
## 抖音 CLI 子命令
```bash
sau douyin login --account <account_name>
sau douyin login --account <account_name> --headless
sau douyin check --account <account_name>
sau douyin upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags 运动,训练
sau douyin upload-note --account <account_name> --images videos/1.png videos/2.png --title "图文标题" --note "图文示例" --tags 图文,测试
```
抖音短信验证码补充说明:
- 视频发布过程中如果触发短信二次验证,CLI 会优先读取项目根目录下的 `verify_code.txt`
- 如果未找到 `verify_code.txt`,并且当前命令是在交互式终端中手动运行,CLI 会直接在终端提示输入验证码
- 对 agent、自动任务、远程桥接这类场景,仍然可以继续用写入 `verify_code.txt` 的方式喂验证码
- 验证通过后,程序会自动清理 `verify_code.txt`
## 快手 CLI 子命令
```bash
sau kuaishou login --account <account_name>
sau kuaishou check --account <account_name>
sau kuaishou upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags 运动,训练
sau kuaishou upload-note --account <account_name> --images videos/1.png videos/2.png videos/3.png --title "图文标题" --note "图文示例" --tags 图文,测试
```
## 小红书 CLI 子命令
```bash
sau xiaohongshu login --account <account_name>
sau xiaohongshu check --account <account_name>
sau xiaohongshu upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags 小红书,视频
sau xiaohongshu upload-note --account <account_name> --images videos/1.png videos/2.png videos/3.png --title "图文标题" --note "图文示例" --tags 图文,测试
```
海外环境如果无法登录默认创作者后台,可以通过环境变量切换到 RedNote 域名。该设置同时作用于登录、cookie 校验、视频发布和图文发布:
```bash
SAU_XHS_CREATOR_BASE_URL=https://creator.rednote.com sau xiaohongshu login --account <account_name>
```
## Bilibili CLI 子命令
```bash
sau bilibili login --account <account_name>
sau bilibili check --account <account_name>
sau bilibili upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tid 249 --tags 足球,测试 --thumbnail covers/demo.png
```
补充说明:
- `creator` 之类的名字只是示例值,真正传的是用户自定义的 `account_name`
- 一个 `account_name` 对应一个账号文件,可以准备多个账号并发使用
- 浏览器平台统一元数据约定:
- 视频使用 `title + desc + tags`
- 图文使用 `title + note + tags`
- `sau bilibili ...` 会自动准备 `biliup`
- 如果本地没有 `biliup`,第一次运行会自动下载
- 如果上游 GitHub Release 有更新,运行时会先自动更新
- `sau bilibili login --account <name>` 建议由用户自己在本地真实终端里执行;如果终端里的二维码显示不完整,可直接打开当前目录下的 `qrcode.png` 扫码
## 视频号 CLI 子命令
```bash
sau tencent login --account <account_name>
sau tencent check --account <account_name>
sau tencent upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags 视频号,测试
```
视频号支持定时发布、草稿、合集和双比例封面:
```bash
sau tencent upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --schedule "2026-03-24 21:30" --thumbnail-landscape covers/landscape.png --thumbnail-portrait covers/portrait.png --collection "我的合集"
sau tencent upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --draft
```
视频号登录和上传依赖浏览器中的登录态。无头模式下如果需要扫码,CLI 会生成临时二维码;需要人工查看页面时可以加 `--headed`。
## 百家号 CLI 子命令
```bash
sau baijiahao login --account <account_name>
sau baijiahao check --account <account_name>
sau baijiahao upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags 百家号,测试
```
百家号当前支持登录、账号检查和视频上传;支持 `--thumbnail` 与 `--collection`,暂不支持 `--schedule`。上传前需要先完成百度账号登录并保存账号文件。
## 支付宝生活号 CLI 子命令
```bash
sau alipay login --account <account_name>
sau alipay check --account <account_name>
sau alipay upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags 生活号,测试
```
支付宝生活号当前支持登录、账号检查和视频上传;支持 `--thumbnail` 与 `--collection`,暂不支持图文上传和 `--schedule`。首次使用前需要在支付宝内容创作后台完成登录,并确认账号已开通生活号内容创作权限。
## YouTube CLI 子命令
```bash
sau youtube login --account <account_name>
sau youtube check --account <account_name>
sau youtube upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags tag1,tag2 --playlist "我的系列" --visibility public
```
YouTube 登录需要在浏览器中完成 Google 账号登录,不使用二维码。`--visibility` 可选 `public`、`unlisted` 或 `private`,`--playlist` 可选。
## 微博 CLI 子命令
```bash
sau weibo login --account <account_name>
sau weibo check --account <account_name>
sau weibo upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags 微博,测试 --thumbnail covers/demo.png
```
微博当前支持登录、账号检查和视频上传;标题最多 30 个字,封面图建议小于 5 MB,暂不支持图文上传和 `--schedule`。
## 虎扑 CLI 子命令
```bash
sau hupu login --account <account_name>
sau hupu check --account <account_name>
sau hupu upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags 虎扑,测试 --thumbnail covers/demo.png
```
虎扑当前支持登录、账号检查和视频上传;标题长度要求为 4–40 个字,暂不支持图文上传和 `--schedule`。虎扑登录可能需要在浏览器中完成 QQ 或手机号登录,需要人工查看页面时可以加 `--headed`。
## 登录二维码说明
- 抖音、快手、小红书、视频号、百家号、支付宝生活号、微博和虎扑登录过程中,CLI / uploader 可能会生成临时二维码图片
- 对普通用户来说,可以直接打开该图片扫码
- 对可操作本地文件的 agent 来说,不要只把图片路径告诉用户
- 这类二维码图片本身就是给用户扫码的,agent 应优先直接展示/发送本地图片给用户
- Bilibili 和 YouTube 当前不走这套本地二维码图片托管链路,登录按上面的平台说明处理即可
## 定时发布
抖音、快手、小红书、视频号的图文或视频上传,以及 Bilibili 的视频上传支持 `--schedule`。只要传了 `--schedule`,CLI 就会自动切换到对应平台的定时发布策略;不传则默认立即发布。百家号、支付宝生活号、微博和虎扑当前不支持 `--schedule`。
```bash
sau douyin upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --schedule "2026-03-24 21:30"
sau douyin upload-note --account <account_name> --images videos/1.png videos/2.png --title "图文标题" --note "图文示例" --schedule "2026-03-24 21:30"
sau kuaishou upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --schedule "2026-03-24 21:30"
sau kuaishou upload-note --account <account_name> --images videos/1.png videos/2.png videos/3.png --title "图文标题" --note "图文示例" --schedule "2026-03-24 21:30"
sau xiaohongshu upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --schedule "2026-03-24 21:30"
sau xiaohongshu upload-note --account <account_name> --images videos/1.png videos/2.png videos/3.png --title "图文标题" --note "图文示例" --schedule "2026-03-24 21:30"
sau bilibili upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tid 249 --schedule "2026-03-24 21:30"
sau tencent upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --schedule "2026-03-24 21:30"
```
## 运行时参数
CLI 将 `debug` 和 `headless` 拆成了两个独立维度:
```bash
--debug
--headless
--headed
```
- `--debug`: 打开调试行为,例如失败时保留更多调试信息
- `--headless`: 无头模式运行
- `--headed`: 有头模式运行
如果都不传,CLI 当前默认按 `headless=True` 运行。
补充:
- 抖音和快手的 CLI 默认都是无头模式
- 如果用户明确要求可见浏览器窗口,或确实需要人工看页面,再显式传 `--headed`
## 视频上传参数
```bash
--file videos/demo.mp4
--title "示例标题"
--desc "示例简介"
--tags 运动,训练
--thumbnail videos/demo.png
--thumbnail-landscape videos/cover-4x3.png
--thumbnail-portrait videos/cover-3x4.png
```
抖音和视频号支持同时设置两种比例的封面图:
- `--thumbnail-landscape`: 4:3 横版封面
- `--thumbnail-portrait`: 3:4 竖版封面
- `--thumbnail`: 兼容旧参数,等同于 3:4 竖版封面
视频号、百家号和支付宝生活号支持使用 `--collection` 指定已有合集;百家号和支付宝生活号还支持 `--thumbnail` 指定封面图。
抖音额外支持:
```bash
--product-link https://example.com/item
--product-title 示例商品
```
Bilibili 额外要求:
```bash
--tid 249
```
- `--tid` 第一版是必填
- `--tags` 会映射到 `biliup upload --tag`
- `--schedule` 会映射到 Bilibili 所需的时间戳参数
## 图文上传参数
```bash
--images videos/1.png videos/2.png videos/3.png
--title "图文标题"
--note "图文内容"
--tags 图文,测试
```
图文上传当前限制:
- 抖音:最多 35 张图片,不支持 GIF
- 快手:支持多张图片,建议传真实不同文件,不要把同一路径重复多次
- 小红书:支持多张图片,正文 `--note` 可选,但 `--title` 建议始终显式传入
后续维护 CLI 时,优先看 `sau_cli.py`、`uploader/` 和 `skills/`。
-148
View File
@@ -1,148 +0,0 @@
# Agent Bootstrap Prompt
这份文档是写给 `OpenClaw`、`Codex`、`Claude Code / cc` 这类 agent 客户端用户的。
目标不是让 agent 先通读整个仓库,而是先把 `social-auto-upload` 安装到可运行、可验证、可继续执行任务的状态。
当前主线已接入的平台:
- `bilibili`
- `douyin`
- `kuaishou`
- `xiaohongshu`
## 这份文档解决什么问题
现在仓库里已经有:
- 安装说明
- CLI 文档
- 平台 skill
但这些内容更偏向“agent 进入仓库之后怎么执行”。
这份文档补的是“用户第一次把仓库交给 agent 客户端时,应该怎么说”。
## 推荐使用方式
1. 把整个仓库给你的 agent 客户端。
2. 把下面这段启动提示词完整发给它。
3. 等 agent 完成安装和 CLI 验证后,再继续给它下达登录、上传、定时发布等任务。
## 通用启动提示词
复制下面整段,发给你的 agent:
```text
你现在在一个名为 `social-auto-upload` 的仓库中工作。
这是一个多平台社交媒体自动发布项目。当前主线已经接入:
- bilibili
- douyin
- kuaishou
- xiaohongshu
你的第一目标不是通读全部源码,也不是优先运行历史 examples,而是先把项目安装到“可运行、可验证、可继续执行任务”的状态。
请遵守以下规则:
1. 默认把仓库根目录视为当前工作目录。
2. 优先使用 `uv` 管理 Python 环境,不要默认回退到旧的 `requirements.txt`。
3. 优先使用当前主线 CLI:`sau`。
4. 优先参考这些文档:
- `docs/install.md`
- `docs/CLI.md`
- `docs/update.md`
5. 如果需要平台级操作,优先参考这些 skill:
- `skills/douyin-upload/`
- `skills/kuaishou-upload/`
- `skills/xiaohongshu-upload/`
- `skills/bilibili-upload/`
6. 不要默认走历史 `examples/` 和旧 Web 路径,除非当前 CLI 主线不可用。
7. 如果登录流程生成二维码图片,不要只返回图片路径;请直接展示图片,或者明确告诉我该打开哪个本地图片文件扫码。
8. 如果是 Bilibili 登录,不要在非交互环境里强行代跑;应改为指导我在本地真实终端执行。
9. 安装完成后,请优先验证以下命令:
- `sau --help`
- `sau douyin --help`
- `sau kuaishou --help`
- `sau xiaohongshu --help`
- `sau bilibili --help`
10. 完成后,请明确输出:
- 你实际执行了哪些命令
- 哪些验证通过了
- 当前项目是否已经进入“可继续登录/上传”的状态
- 推荐我下一步执行什么
如果过程中遇到错误,不要跳过,请先说明错误,再给出你准备采取的下一步动作。
```
## 安装完成后,你可以继续怎么说
下面这些是你可以继续发给 agent 的任务示例。
### 做一次平台登录
```text
请继续帮我登录小红书账号,使用有头模式,账号名用 `creator`。
```
```text
请继续帮我登录抖音账号,使用无头模式,账号名用 `creator`。
```
### 做一次 CLI 可用性检查
```text
请检查 bilibili、douyin、kuaishou、xiaohongshu 四个平台的 CLI 入口是否都可用,并告诉我缺什么依赖。
```
### 做一次真实上传
```text
请使用 xiaohongshu CLI,帮我上传一个图文草稿,使用定时发布,不要立即发布。
```
```text
请使用 douyin CLI,帮我上传一个视频,优先走当前主线,不要走历史 example。
```
## OpenClaw / Codex / Claude Code 使用建议
### OpenClaw
- 适合直接粘贴上面的完整启动提示词
- 如果支持把仓库作为工作目录挂载进去,优先先挂载仓库,再发提示词
- 如果支持本地文件展示,登录二维码应让 agent 直接展示图片
### Codex
- 建议先让它完成 bootstrap,再继续发平台任务
- 让它优先使用 `docs/install.md`、`docs/CLI.md` 和 `skills/`
- 不要让它一开始自由探索整个仓库,否则容易走到历史路径
### Claude Code / cc
- 建议先让仓库成为当前 workspace
- 再发完整启动提示词
- 后续按“安装 -> 验证 -> 登录 -> 上传”顺序继续给任务
## 为什么不按平台拆四套提示词
因为这个项目现在已经有统一的 CLI 主线。
用户第一次把仓库交给 agent 时,更需要的是:
- agent 知道主入口是什么
- agent 知道应该优先走哪条路径
- agent 知道哪些是历史路径
- agent 安装完成后先给出明确验收结果
等进入执行阶段,再让 agent 根据你的实际目标去选择:
- `bilibili`
- `douyin`
- `kuaishou`
- `xiaohongshu`
这样比给用户准备四套平台 prompt 更稳,也更容易维护。
-251
View File
@@ -1,251 +0,0 @@
# 安装说明
这个文档分成两部分:
- `For Humans`:给正常使用仓库的开发者、创作者、CLI 用户看
- `For AI Agents`:给 OpenClaw、Codex、Claude Code 一类 agent 看
如果你是“正在使用 agent 客户端的人”,想先给 agent 一段启动提示词,而不是直接阅读下面的执行细节,先看:
- [Agent Bootstrap Prompt](./agent-bootstrap.md)
## 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
sau kuaishou --help
sau xiaohongshu --help
sau bilibili --help
```
如果命令找不到,优先确认:
- 当前虚拟环境是否已激活
- 是否执行过 `uv pip install -e .`
### 7. 抖音主线示例
```bash
sau douyin login --account <account_name>
sau douyin check --account <account_name>
sau douyin upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介"
图文正文1:
$noteText = @"图文正文"@
sau douyin upload-note --account <account_name> --images videos/demo1.png videos/demo2.png --title "图文标题" --note $noteText --tags 'tag1,tag2'
图文正文2:
sau douyin upload-note --account <account_name> --images videos/demo1.png videos/demo2.png --title "图文标题" --notef '图文文件路径' --tags 'tag1,tag2'
添加 BGM(可选):
sau douyin upload-note --account <account_name> --images videos/demo1.png videos/demo2.png --title "图文标题" --note $noteText --tags 'tag1,tag2' --bgm '音乐名称'
```
抖音短信验证码补充说明:
- 视频发布过程中如果触发短信二次验证,程序会优先读取项目根目录下的 `verify_code.txt`
- 如果当前是你手动运行的交互式终端,没提供 `verify_code.txt` 时,CLI 会直接提示你在终端输入验证码
- 如果是 agent 或自动化桥接场景,仍然可以继续通过写入 `verify_code.txt` 来提供验证码
- 验证通过后,程序会自动删除 `verify_code.txt`
抖音卡login手动获取cookie:
- 目标服务器使用vnc
- 浏览器登录抖音创作者中心https://creator.douyin.com/
- 执行`bash export_douyin_cookie.sh --account <account_name>`
- 检查cookie可用性,执行`sau douyin check --account <account_name>`
### 8. 快手主线示例
```bash
sau kuaishou login --account <account_name>
sau kuaishou check --account <account_name>
sau kuaishou upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介"
sau kuaishou upload-note --account <account_name> --images videos/demo1.png videos/demo2.png videos/demo.png --title "图文标题" --note "图文正文"
```
### 9. 小红书主线示例
```bash
sau xiaohongshu login --account <account_name>
sau xiaohongshu check --account <account_name>
sau xiaohongshu upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介"
sau xiaohongshu upload-note --account <account_name> --images videos/demo1.png videos/demo2.png videos/demo.png --title "图文标题" --note "图文正文"
```
### 10. Bilibili 主线示例
```bash
sau bilibili login --account <account_name>
sau bilibili check --account <account_name>
sau bilibili upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tid 249
```
补充说明:
- `creator` 之类的名字只是示例值,真正传的是用户自定义的 `account_name`
- 一个 `account_name` 对应一个账号文件,可以准备多个账号并发使用
- 浏览器平台统一元数据约定:
- 视频使用 `title + desc + tags`
- 图文使用 `title + note + tags`
- 用户不需要手动安装 `biliup`
- 首次运行 Bilibili 相关命令时,程序会自动下载 `biliup`
- 后续运行会自动检查上游 release 并自动更新
- Bilibili 登录建议由用户自己在本地真实终端里执行;如果终端里的二维码显示不完整,可直接打开当前目录下的 `qrcode.png` 扫码
- 如果国内网络访问 GitHub Release 较慢,可先用 `https://gh-proxy.com/` 或 `https://gh-proxy.org/` 辅助访问对应 release 地址排障
- 示例:
- `https://gh-proxy.org/https://github.com/biliup/biliup/releases/download/v1.1.29/biliupR-v1.1.29-aarch64-linux.tar.xz`
## 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
sau kuaishou --help
sau xiaohongshu --help
sau bilibili --help
```
6. 如果用户的目标是抖音或快手的登录、cookie 校验、视频上传、图文上传,优先走 CLI:
```bash
sau douyin login
sau douyin check
sau douyin upload-video
sau douyin upload-note
sau kuaishou login
sau kuaishou check
sau kuaishou upload-video
sau kuaishou upload-note
sau xiaohongshu login
sau xiaohongshu check
sau xiaohongshu upload-video
sau xiaohongshu upload-note
sau bilibili login
sau bilibili check
sau bilibili upload-video
```
7. 如果用户明确在使用 skill 系统,再引导其阅读:
- `skills/douyin-upload/SKILL.md`
- `skills/douyin-upload/references/cli-contract.md`
- `skills/kuaishou-upload/SKILL.md`
- `skills/kuaishou-upload/references/cli-contract.md`
- `skills/xiaohongshu-upload/SKILL.md`
- `skills/xiaohongshu-upload/references/cli-contract.md`
- `skills/bilibili-upload/SKILL.md`
- `skills/bilibili-upload/references/cli-contract.md`
### 对 agent 的额外说明
- 当登录流程生成本地二维码图片时,不要只把图片路径发给用户
- 这类二维码图片本身就是给用户扫码的,agent 应优先直接展示/发送本地图片给用户扫码
- 如果环境支持查看本地图片,优先用查看图片能力把二维码展示出来;路径只作为补充信息
- Bilibili 登录当前不建议 agent 在非交互环境里直接代跑
- 正确做法是让用户自己在本地终端执行 `sau bilibili login --account <name>`;如果二维码显示不完整,再提示用户打开 `qrcode.png`
- `requirements.txt` 目前是历史兼容文件,不是主安装入口
- `uploader/` 是核心实现目录
- `sau_cli.py` 是当前 CLI 主入口
- `docs/legacy-web.md` 是历史 Web 版本说明,不保证当前可用
- Bilibili 首次运行时可能自动下载 `biliup`
-49
View File
@@ -1,49 +0,0 @@
# 历史 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`
-286
View File
@@ -1,286 +0,0 @@
# 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` 作为安装桥梁
@@ -1,458 +0,0 @@
# Bilibili CLI Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为 Bilibili 补齐和抖音、快手同层级的 `sau` CLI、自动更新 `biliup` 的运行时机制、对应 skill,以及完整文档与上游致谢说明。
**Architecture:** 保持轻量,不新建大而全框架。新增一个 Bilibili 运行时模块负责检查 GitHub Release、下载/更新 `biliup`、执行命令;`sau_cli.py` 仅补 `bilibili` 子命令和参数映射;skill、example、README/CLI/install/update 文档按现有 Douyin/Kuaishou 结构对齐。
**Tech Stack:** Python 3.10+, `requests`, `argparse`, `asyncio`, `subprocess`, `pathlib`, `unittest`, GitHub Releases, existing `biliup` integration
---
## File Structure
### New files
- `uploader/bilibili_uploader/runtime.py`
- Bilibili 运行时入口
- 负责 `biliup` 自动检查、自动下载、自动更新、执行命令
- `tests/__init__.py`
- 测试包初始化
- `tests/test_bilibili_runtime.py`
- 测试自动更新、下载、缓存复用、执行器行为
- `tests/test_sau_bilibili_cli.py`
- 测试 `sau bilibili` parser 和 dispatch 行为
- `skills/bilibili-upload/SKILL.md`
- Bilibili CLI skill 主说明
- `skills/bilibili-upload/references/runtime-requirements.md`
- 运行前提和自动下载说明
- `skills/bilibili-upload/references/cli-contract.md`
- `sau bilibili ...` 命令契约
- `skills/bilibili-upload/references/troubleshooting.md`
- 常见问题与排障
- `skills/bilibili-upload/scripts/examples/bilibili_commands.ps1`
- PowerShell 示例命令
- `skills/bilibili-upload/scripts/examples/bilibili_commands.sh`
- shell 示例命令
- `skills/bilibili-upload/scripts/examples/bilibili_cli_template.py`
- Python 调用模板
### Modified files
- `sau_cli.py`
- 补 `bilibili` 子命令
- 复用现有 `resolve_account_file()`、`parse_tags()`、`parse_schedule()`
- `examples/get_bilibili_cookie.py`
- 对齐新的 `sau bilibili login` 用法
- `examples/upload_video_to_bilibili.py`
- 对齐新的 CLI/账号文件约定
- `README.md`
- 补 Bilibili CLI 用法、自动下载说明、致谢说明
- `docs/CLI.md`
- 补 `sau bilibili login/check/upload-video`
- `docs/install.md`
- 补 Bilibili 自动下载/首次运行说明
- `docs/update.md`
- 补 Bilibili 自动更新行为说明
## Task 1: Bilibili 自动更新运行时
**Files:**
- Create: `uploader/bilibili_uploader/runtime.py`
- Create: `tests/__init__.py`
- Create: `tests/test_bilibili_runtime.py`
- [ ] **Step 1: 写 Bilibili 运行时测试**
使用 `unittest`,覆盖这些最小路径:
```python
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
from uploader.bilibili_uploader.runtime import (
build_biliup_runtime_path,
ensure_biliup_binary,
run_biliup_command,
)
class BiliupRuntimeTests(unittest.TestCase):
def test_build_biliup_runtime_path_returns_platform_path(self):
path = build_biliup_runtime_path("Windows")
self.assertTrue(str(path).endswith("biliup.exe"))
@patch("uploader.bilibili_uploader.runtime.fetch_latest_release")
def test_ensure_biliup_binary_downloads_when_missing(self, mock_release):
mock_release.return_value = {
"tag_name": "v1.0.0",
"asset_url": "https://example.invalid/biliup.exe",
"asset_name": "biliup.exe",
}
with patch("uploader.bilibili_uploader.runtime.download_biliup_asset") as mock_download:
ensure_biliup_binary(force_check=True)
mock_download.assert_called_once()
@patch("uploader.bilibili_uploader.runtime.fetch_latest_release")
def test_ensure_biliup_binary_reuses_local_when_up_to_date(self, mock_release):
mock_release.return_value = {
"tag_name": "v1.0.0",
"asset_url": "https://example.invalid/biliup.exe",
"asset_name": "biliup.exe",
}
with patch("uploader.bilibili_uploader.runtime.read_local_biliup_version", return_value="v1.0.0"):
with patch("uploader.bilibili_uploader.runtime.download_biliup_asset") as mock_download:
ensure_biliup_binary(force_check=True)
mock_download.assert_not_called()
@patch("uploader.bilibili_uploader.runtime.subprocess.run")
def test_run_biliup_command_returns_completed_process(self, mock_run):
mock_run.return_value = Mock(returncode=0, stdout="ok", stderr="")
result = run_biliup_command(["login"])
self.assertEqual(result.returncode, 0)
```
- [ ] **Step 2: 运行测试,确认先失败**
Run:
```powershell
.\.venv\Scripts\python.exe -m unittest tests.test_bilibili_runtime -v
```
Expected:
- 因为 `uploader.bilibili_uploader.runtime` 还不存在而失败
- [ ] **Step 3: 写最小运行时实现**
在 `uploader/bilibili_uploader/runtime.py` 里先补这些最小函数:
```python
def get_biliup_runtime_root() -> Path: ...
def build_biliup_runtime_path(system_name: str | None = None) -> Path: ...
def fetch_latest_release() -> dict: ...
def read_local_biliup_version() -> str | None: ...
def write_local_biliup_version(version: str) -> None: ...
def download_biliup_asset(release: dict, destination: Path) -> Path: ...
def ensure_biliup_binary(force_check: bool = True) -> Path: ...
def run_biliup_command(arguments: list[str]) -> subprocess.CompletedProcess[str]: ...
```
约束:
- 不引入复杂 manifest
- 直接面向 GitHub Release 最新版本
- 本地只保存当前版本字符串和二进制
- 保持简单的路径/网络/替换逻辑
- [ ] **Step 4: 再跑运行时测试,确认通过**
Run:
```powershell
.\.venv\Scripts\python.exe -m unittest tests.test_bilibili_runtime -v
```
Expected:
- 所有 `BiliupRuntimeTests` 通过
- [ ] **Step 5: 提交这一小步**
```powershell
git add uploader/bilibili_uploader/runtime.py tests/__init__.py tests/test_bilibili_runtime.py
git commit -m "feat: add biliup runtime bootstrap"
```
## Task 2: 接入 `sau bilibili` CLI
**Files:**
- Modify: `sau_cli.py`
- Create: `tests/test_sau_bilibili_cli.py`
- Reference: `uploader/bilibili_uploader/main.py`
- Reference: `utils/constant.py`
- [ ] **Step 1: 写 CLI parser 和 dispatch 测试**
在 `tests/test_sau_bilibili_cli.py` 中覆盖:
```python
import unittest
from argparse import Namespace
from pathlib import Path
from unittest.mock import AsyncMock, patch
import sau_cli
class BilibiliCliTests(unittest.TestCase):
def test_build_parser_accepts_bilibili_login(self):
parser = sau_cli.build_parser()
args = parser.parse_args(["bilibili", "login", "--account", "creator"])
self.assertEqual(args.platform, "bilibili")
self.assertEqual(args.action, "login")
def test_build_parser_requires_tid_for_upload_video(self):
parser = sau_cli.build_parser()
with self.assertRaises(SystemExit):
parser.parse_args([
"bilibili", "upload-video",
"--account", "creator",
"--file", "demo.mp4",
"--title", "hello",
"--desc", "hello",
])
def test_dispatch_bilibili_check_prints_valid(self):
args = Namespace(platform="bilibili", action="check", account="creator")
with patch("sau_cli.check_bilibili_account", new=AsyncMock(return_value=True)):
code = asyncio.run(sau_cli.dispatch(args))
self.assertEqual(code, 0)
```
- [ ] **Step 2: 运行测试,确认先失败**
Run:
```powershell
.\.venv\Scripts\python.exe -m unittest tests.test_sau_bilibili_cli -v
```
Expected:
- 因为 `sau_cli.py` 里还没有 `bilibili` parser/dispatch 分支而失败
- [ ] **Step 3: 在 `sau_cli.py` 中补 Bilibili 请求模型和命令**
只做最小接入,保持和 Douyin/Kuaishou 同风格:
```python
@dataclass(slots=True)
class BilibiliVideoUploadRequest:
account_name: str
video_file: Path
title: str
description: str
tid: int
tags: list[str]
publish_date: datetime | int
debug: bool = True
async def login_bilibili_account(account_name: str) -> dict: ...
async def check_bilibili_account(account_name: str) -> bool: ...
async def upload_bilibili_video(request: BilibiliVideoUploadRequest) -> Path: ...
```
Parser 最小要求:
- `sau bilibili login --account <name>`
- `sau bilibili check --account <name>`
- `sau bilibili upload-video --account ... --file ... --title ... --desc ... --tid ... [--tags] [--schedule]`
Dispatch 最小要求:
- 和其他平台一样输出 `valid` / `invalid`
- 上传成功后打印简洁摘要
- [ ] **Step 4: 复用现有 B 站参数语义**
在 Bilibili wrapper 中直接沿用现有工程概念:
- `tid` 必填
- `tags` 用现有 `parse_tags()`
- `schedule` 沿用现有 `parse_schedule()`
- `account` 仍通过 `resolve_account_file("bilibili", account_name)` 得到项目内账号路径
- [ ] **Step 5: 再跑 CLI 测试,确认通过**
Run:
```powershell
.\.venv\Scripts\python.exe -m unittest tests.test_sau_bilibili_cli -v
```
Expected:
- `BilibiliCliTests` 通过
- [ ] **Step 6: 做一次联测**
Run:
```powershell
.\.venv\Scripts\python.exe sau_cli.py bilibili --help
.\.venv\Scripts\python.exe sau_cli.py bilibili upload-video --help
```
Expected:
- 能看到 `login` / `check` / `upload-video`
- `upload-video` 中 `--tid` 显示为必填
- [ ] **Step 7: 提交这一小步**
```powershell
git add sau_cli.py tests/test_sau_bilibili_cli.py
git commit -m "feat: add bilibili cli commands"
```
## Task 3: 补 skill 和 example
**Files:**
- Create: `skills/bilibili-upload/SKILL.md`
- Create: `skills/bilibili-upload/references/runtime-requirements.md`
- Create: `skills/bilibili-upload/references/cli-contract.md`
- Create: `skills/bilibili-upload/references/troubleshooting.md`
- Create: `skills/bilibili-upload/scripts/examples/bilibili_commands.ps1`
- Create: `skills/bilibili-upload/scripts/examples/bilibili_commands.sh`
- Create: `skills/bilibili-upload/scripts/examples/bilibili_cli_template.py`
- Modify: `examples/get_bilibili_cookie.py`
- Modify: `examples/upload_video_to_bilibili.py`
- [ ] **Step 1: 参考 Douyin/Kuaishou skill 结构搭出 Bilibili skill**
要求:
- `SKILL.md` 风格和现有两个 skill 对齐
- 默认优先用 `sau bilibili ...`
- 明确写“程序会自动准备 `biliup`”
- [ ] **Step 2: 写示例命令文件**
示例命令至少包括:
```powershell
sau bilibili login --account creator
sau bilibili check --account creator
sau bilibili upload-video --account creator --file .\videos\demo.mp4 --title "demo" --desc "demo" --tid 249 --tags 足球,测试
```
- [ ] **Step 3: 修改本地 example**
让以下 example 明确转向新入口或新约定:
- `examples/get_bilibili_cookie.py`
- `examples/upload_video_to_bilibili.py`
要求:
- 不再让用户手动猜 `biliup.exe` 路径
- 明确说明现在推荐走 `sau bilibili ...`
- 继续保留 `VideoZoneTypes` 的使用示例
- [ ] **Step 4: 做一次文件级自检**
Run:
```powershell
Get-ChildItem skills\bilibili-upload -Recurse
Get-Content examples\get_bilibili_cookie.py
Get-Content examples\upload_video_to_bilibili.py
```
Expected:
- Bilibili skill 目录完整
- example 内容已切到新的 CLI/说明
- [ ] **Step 5: 提交这一小步**
```powershell
git add skills/bilibili-upload examples/get_bilibili_cookie.py examples/upload_video_to_bilibili.py
git commit -m "feat: add bilibili upload skill"
```
## Task 4: 补文档与上游致谢
**Files:**
- Modify: `README.md`
- Modify: `docs/CLI.md`
- Modify: `docs/install.md`
- Modify: `docs/update.md`
- [ ] **Step 1: 在 README 中补 Bilibili CLI 用法**
至少写清:
- `sau bilibili login`
- `sau bilibili check`
- `sau bilibili upload-video`
- 自动下载/自动更新 `biliup`
- [ ] **Step 2: 在 CLI 文档中补命令契约**
把 Bilibili 一节写成和 Douyin/Kuaishou 同风格:
- 参数表
- `tid` 必填
- `schedule` 的行为
- [ ] **Step 3: 在安装/更新文档中写清自动下载机制**
至少补这些说明:
- 用户不需要自己安装 `biliup`
- 第一次运行会自动下载
- 后续运行会自动检查更新
- [ ] **Step 4: 在文档中加入对上游项目的感谢与借用说明**
至少在 README 中补一段明确说明:
- Bilibili 能力基于 `biliup`
- 感谢/借用上游项目
- 给出项目地址
建议文案:
```markdown
## 致谢
本项目的 Bilibili 上传能力基于开源项目 `biliup` 的能力进行接入与封装。
感谢 `biliup` 项目及其贡献者提供的基础能力:
- https://github.com/biliup/biliup
```
- [ ] **Step 5: 做一次文档核对**
Run:
```powershell
Get-Content README.md | Select-String -Pattern "bilibili|biliup|致谢" -Context 1,2
Get-Content docs\CLI.md | Select-String -Pattern "bilibili" -Context 1,3
Get-Content docs\install.md | Select-String -Pattern "bilibili|biliup" -Context 1,2
Get-Content docs\update.md | Select-String -Pattern "bilibili|biliup" -Context 1,2
```
Expected:
- README、CLI、install、update 都出现 Bilibili 新内容
- README 里有明确的上游致谢
- [ ] **Step 6: 跑最终验证**
Run:
```powershell
.\.venv\Scripts\python.exe -m unittest tests.test_bilibili_runtime tests.test_sau_bilibili_cli -v
.\.venv\Scripts\python.exe sau_cli.py bilibili --help
.\.venv\Scripts\python.exe sau_cli.py bilibili upload-video --help
```
Expected:
- 单元测试通过
- Bilibili CLI 帮助可用
- `upload-video` 显示必填 `--tid`
- [ ] **Step 7: 提交收尾**
```powershell
git add README.md docs/CLI.md docs/install.md docs/update.md
git commit -m "docs: add bilibili cli guidance and attribution"
```
@@ -1,583 +0,0 @@
# Browser CLI Unification Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为抖音、快手、小红书三家浏览器平台统一 `sau` CLI 契约,补齐小红书 CLI/skill,并把视频正文统一为 `desc`、图文正文统一为 `note`。
**Architecture:** 保持现有 `sau_cli.py` 单文件主入口,不引入新的 CLI 框架。`sau_cli.py` 负责 parser、request dataclass、dispatch 和账号文件解析;各平台 uploader 只做最小字段接线;skill、README、CLI/install/update 文档、examples 同步切到统一契约,避免对外同时暴露多套命名。
**Tech Stack:** Python 3.10+, `argparse`, `asyncio`, `dataclasses`, `pathlib`, `unittest`, existing Patchright-based uploaders, repository markdown docs
---
## File Structure
### New files
- `tests/test_sau_browser_cli.py`
- 覆盖三家浏览器平台统一 CLI parser / dispatch / request 映射
- `skills/xiaohongshu-upload/SKILL.md`
- 小红书 CLI skill 主说明
- `skills/xiaohongshu-upload/references/runtime-requirements.md`
- 小红书运行前提
- `skills/xiaohongshu-upload/references/cli-contract.md`
- 小红书 CLI 契约
- `skills/xiaohongshu-upload/references/troubleshooting.md`
- 小红书排障文档
- `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_commands.ps1`
- PowerShell 示例命令
- `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_commands.sh`
- shell 示例命令
- `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_cli_template.py`
- Python 命令模板
### Modified files
- `sau_cli.py`
- 新增 `xiaohongshu` parser / dispatch
- 改造抖音、快手 request dataclass 和参数模型
- `uploader/douyin_uploader/main.py`
- 给视频接入 `desc`
- 给图文接入 `title + note`
- `uploader/ks_uploader/main.py`
- 给视频接入 `desc`
- 给图文接入 `title + note`
- `uploader/xiaohongshu_uploader/main.py`
- 只补 CLI 接线所需的最小字段适配
- `README.md`
- 补小红书 CLI / skill,并统一三家参数说明
- `docs/CLI.md`
- 统一三家浏览器平台命令契约
- `docs/install.md`
- 补小红书 CLI 示例和统一参数说明
- `docs/update.md`
- 补小红书检查项与 skill 路径
- `skills/douyin-upload/SKILL.md`
- `skills/douyin-upload/references/cli-contract.md`
- `skills/douyin-upload/scripts/examples/douyin_commands.ps1`
- `skills/douyin-upload/scripts/examples/douyin_commands.sh`
- `skills/douyin-upload/scripts/examples/douyin_cli_template.py`
- `skills/kuaishou-upload/SKILL.md`
- `skills/kuaishou-upload/references/cli-contract.md`
- `skills/kuaishou-upload/scripts/examples/kuaishou_commands.ps1`
- `skills/kuaishou-upload/scripts/examples/kuaishou_commands.sh`
- `skills/kuaishou-upload/scripts/examples/kuaishou_cli_template.py`
- `examples/get_xiaohongshu_cookie.py`
- `examples/upload_video_to_xiaohongshu.py`
## Task 1: 用测试锁定统一 CLI 契约
**Files:**
- Create: `tests/test_sau_browser_cli.py`
- Reference: `sau_cli.py`
- [ ] **Step 1: 写统一 CLI parser 测试**
在 `tests/test_sau_browser_cli.py` 里覆盖最小命令契约:
```python
import asyncio
import unittest
from argparse import Namespace
from pathlib import Path
from unittest.mock import AsyncMock, patch
import sau_cli
class BrowserCliParserTests(unittest.TestCase):
def test_build_parser_accepts_xiaohongshu_login(self):
parser = sau_cli.build_parser()
args = parser.parse_args(["xiaohongshu", "login", "--account", "creator"])
self.assertEqual(args.platform, "xiaohongshu")
self.assertEqual(args.action, "login")
def test_douyin_upload_video_accepts_desc(self):
parser = sau_cli.build_parser()
args = parser.parse_args([
"douyin", "upload-video",
"--account", "creator",
"--file", "demo.mp4",
"--title", "标题",
"--desc", "视频简介",
])
self.assertEqual(args.desc, "视频简介")
def test_kuaishou_upload_note_accepts_title_and_note(self):
parser = sau_cli.build_parser()
args = parser.parse_args([
"kuaishou", "upload-note",
"--account", "creator",
"--images", "1.png",
"--title", "图文标题",
"--note", "图文正文",
])
self.assertEqual(args.title, "图文标题")
self.assertEqual(args.note, "图文正文")
```
- [ ] **Step 2: 写 dispatch 路由测试**
继续在 `tests/test_sau_browser_cli.py` 中补最小 dispatch 验证:
```python
class BrowserCliDispatchTests(unittest.TestCase):
def test_dispatch_xiaohongshu_check_prints_valid(self):
args = Namespace(platform="xiaohongshu", action="check", account="creator")
with patch("sau_cli.check_xiaohongshu_account", new=AsyncMock(return_value=True)):
code = asyncio.run(sau_cli.dispatch(args))
self.assertEqual(code, 0)
def test_dispatch_douyin_upload_note_uses_new_request_fields(self):
args = Namespace(
platform="douyin",
action="upload-note",
account="creator",
images=[Path("1.png")],
title="图文标题",
note="图文正文",
tags="测试,图文",
schedule=0,
debug=False,
headless=True,
)
with patch("sau_cli.upload_note", new=AsyncMock()) as mock_upload:
asyncio.run(sau_cli.dispatch(args))
request = mock_upload.await_args.args[0]
self.assertEqual(request.title, "图文标题")
self.assertEqual(request.note, "图文正文")
```
- [ ] **Step 3: 先跑测试确认失败**
Run:
```powershell
py -3 -m unittest tests.test_sau_browser_cli -v
```
Expected:
- 因为 `sau_cli.py` 还没有 `xiaohongshu` 分支和统一字段而失败
- [ ] **Step 4: 提交测试脚手架**
```powershell
git add tests/test_sau_browser_cli.py
git commit -m "test: define browser cli unification contract"
```
## Task 2: 改 `sau_cli.py`,补齐统一参数与小红书路由
**Files:**
- Modify: `sau_cli.py`
- Reference: `uploader/xiaohongshu_uploader/main.py`
- Reference: `uploader/douyin_uploader/main.py`
- Reference: `uploader/ks_uploader/main.py`
- [ ] **Step 1: 增加小红书 request dataclass**
在 `sau_cli.py` 中新增:
```python
@dataclass(slots=True)
class XiaohongshuVideoUploadRequest:
account_name: str
video_file: Path
title: str
description: str
tags: list[str]
publish_date: datetime | int
thumbnail_file: Path | None = None
publish_strategy: str = XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
@dataclass(slots=True)
class XiaohongshuNoteUploadRequest:
account_name: str
image_files: list[Path]
title: str
note: str
tags: list[str]
publish_date: datetime | int
publish_strategy: str = XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
```
- [ ] **Step 2: 改造抖音、快手 request dataclass**
把字段统一到以下模型:
```python
class DouyinVideoUploadRequest:
title: str
description: str
class DouyinNoteUploadRequest:
title: str
note: str
class KuaishouVideoUploadRequest:
title: str
description: str
class KuaishouNoteUploadRequest:
title: str
note: str
```
要求:
- 视频用 `description`
- 图文用 `note`
- 不再把图文 request 设计成只有历史 `note` 语义但没有 `title`
- [ ] **Step 3: 改 parser**
在 `build_parser()` 里完成:
- `douyin upload-video` 增加 `--desc`
- `kuaishou upload-video` 增加 `--desc`
- `douyin upload-note` 改成 `--title --note`
- `kuaishou upload-note` 改成 `--title --note`
- 新增整套 `xiaohongshu`:
- `login`
- `check`
- `upload-video --title --desc --tags --thumbnail`
- `upload-note --images --title --note --tags`
- [ ] **Step 4: 补小红书账号操作函数**
在 `sau_cli.py` 中新增:
```python
async def login_xiaohongshu_account(account_name: str, headless: bool = True) -> dict: ...
async def check_xiaohongshu_account(account_name: str) -> bool: ...
async def upload_xiaohongshu_video(request: XiaohongshuVideoUploadRequest) -> Path: ...
async def upload_xiaohongshu_note(request: XiaohongshuNoteUploadRequest) -> Path: ...
```
要求:
- 账号路径继续走 `resolve_account_file("xiaohongshu", account_name)`
- 登录 / 检查分别复用 `xiaohongshu_setup` 与 `cookie_auth`
- 上传前先做 `setup(handle=False)` 校验
- [ ] **Step 5: 改 dispatch**
要求:
- 三家浏览器平台都统一输出:
- `login` 成功时打印账号文件路径
- `check` 输出 `valid` / `invalid`
- `upload-video` 打印简洁摘要
- `upload-note` 打印图片数量摘要
- request 构造统一映射:
- 视频:`title + description`
- 图文:`title + note`
- [ ] **Step 6: 跑 CLI 测试确认通过**
Run:
```powershell
py -3 -m unittest tests.test_sau_browser_cli -v
```
Expected:
- `BrowserCliParserTests`
- `BrowserCliDispatchTests`
全部通过
- [ ] **Step 7: 跑最小 help 自检**
Run:
```powershell
py -3 sau_cli.py douyin --help
py -3 sau_cli.py kuaishou --help
py -3 sau_cli.py xiaohongshu --help
py -3 sau_cli.py xiaohongshu upload-note --help
```
Expected:
- 小红书子命令存在
- 图文命令展示 `--title`、`--note`
- 视频命令展示 `--desc`
- [ ] **Step 8: 提交 CLI 主线**
```powershell
git add sau_cli.py tests/test_sau_browser_cli.py
git commit -m "feat: unify browser cli contracts"
```
## Task 3: 给 uploader 做最小字段接线
**Files:**
- Modify: `uploader/douyin_uploader/main.py`
- Modify: `uploader/ks_uploader/main.py`
- Modify: `uploader/xiaohongshu_uploader/main.py`
- Modify: `tests/test_xiaohongshu_uploader.py`
- [ ] **Step 1: 给抖音视频接入 `desc`**
在 `DouYinVideo.__init__()` 中补 `desc` 参数和 `self.desc`,并把发布页填写逻辑改成:
```python
await self.fill_title_and_description(page, self.title, self.desc or self.title, self.tags)
```
- [ ] **Step 2: 给抖音图文接入 `title + note`**
在 `DouYinNote.__init__()` 中补 `title` 参数,保留 `note` 作为图文正文,发布页填写改成:
```python
await self.fill_title_and_description(page, self.title, self.note, self.tags)
```
要求:
- `title` 必填
- `note` 可选但建议非空;如果现有逻辑要求非空,就继续保留校验
- [ ] **Step 3: 给快手视频接入 `desc`**
在 `KSVideo.__init__()` 中补 `desc` 参数和 `self.desc`,填写“描述”区域时改成:
```python
await page.keyboard.type(self.desc or self.title)
```
- [ ] **Step 4: 给快手图文接入 `title + note`**
在 `KSNote.__init__()` 中补 `title` 参数与 `self.title`,保留 `self.note` 作为正文。
要求:
- 图文上传校验里增加 `title` 必填
- 如果页面当前只有正文输入区,没有独立标题区,仍然要在 CLI / request / 构造函数层保持 `title` 字段,以便后续平台对齐
- [ ] **Step 5: 检查小红书 CLI 接线是否需要补充适配**
确认 `XiaoHongShuVideo` / `XiaoHongShuNote` 只需要以下映射即可:
```python
title=request.title
desc=request.description # 视频
desc=request.note # 图文
```
如果 `XiaoHongShuNote` 里还保留历史 `note` 兼容,不要删,只保持 CLI 层优先走 `title + note + tags`。
- [ ] **Step 6: 补一条小红书映射测试**
在 `tests/test_xiaohongshu_uploader.py` 中增加最小断言:
```python
def test_note_title_defaults_do_not_override_explicit_title(self):
app = xhs_main.XiaoHongShuNote(
image_paths=["a.png"],
note="正文",
tags=[],
publish_date=0,
account_file="account.json",
title="显式标题",
desc="图文正文",
)
self.assertEqual(app.title, "显式标题")
self.assertEqual(app.desc, "图文正文")
```
- [ ] **Step 7: 跑 uploader 相关测试**
Run:
```powershell
py -3 -m unittest tests.test_xiaohongshu_uploader -v
```
Expected:
- 小红书 uploader 相关单测通过
- [ ] **Step 8: 提交 uploader 接线**
```powershell
git add uploader/douyin_uploader/main.py uploader/ks_uploader/main.py uploader/xiaohongshu_uploader/main.py tests/test_xiaohongshu_uploader.py
git commit -m "feat: align browser uploader metadata fields"
```
## Task 4: 新增小红书 skill,并同步更新抖音、快手 skill
**Files:**
- Create: `skills/xiaohongshu-upload/SKILL.md`
- Create: `skills/xiaohongshu-upload/references/runtime-requirements.md`
- Create: `skills/xiaohongshu-upload/references/cli-contract.md`
- Create: `skills/xiaohongshu-upload/references/troubleshooting.md`
- Create: `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_commands.ps1`
- Create: `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_commands.sh`
- Create: `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_cli_template.py`
- Modify: `skills/douyin-upload/SKILL.md`
- Modify: `skills/douyin-upload/references/cli-contract.md`
- Modify: `skills/douyin-upload/scripts/examples/douyin_commands.ps1`
- Modify: `skills/douyin-upload/scripts/examples/douyin_commands.sh`
- Modify: `skills/douyin-upload/scripts/examples/douyin_cli_template.py`
- Modify: `skills/kuaishou-upload/SKILL.md`
- Modify: `skills/kuaishou-upload/references/cli-contract.md`
- Modify: `skills/kuaishou-upload/scripts/examples/kuaishou_commands.ps1`
- Modify: `skills/kuaishou-upload/scripts/examples/kuaishou_commands.sh`
- Modify: `skills/kuaishou-upload/scripts/examples/kuaishou_cli_template.py`
- [ ] **Step 1: 复制现有 skill 目录结构作为小红书骨架**
要求:
- 风格对齐 `skills/douyin-upload/`
- 默认优先走 `sau xiaohongshu ...`
- 明确小红书支持:
- `login`
- `check`
- `upload-video`
- `upload-note`
- [ ] **Step 2: 写小红书 CLI 契约**
至少写清:
```bash
sau xiaohongshu login --account <account>
sau xiaohongshu check --account <account>
sau xiaohongshu upload-video --account <account> --file <video> --title "<title>" [--desc "..."] [--tags ...]
sau xiaohongshu upload-note --account <account> --images <img...> --title "<title>" [--note "..."] [--tags ...]
```
- [ ] **Step 3: 更新抖音、快手 skill 契约**
要求:
- 视频示例命令补 `--desc`
- 图文示例命令改成 `--title --note`
- 所有模板文件同步更新,不要只改文档不改脚本
- [ ] **Step 4: 做 skill 文件级自检**
Run:
```powershell
Get-ChildItem skills\xiaohongshu-upload -Recurse
Get-Content skills\douyin-upload\references\cli-contract.md
Get-Content skills\kuaishou-upload\references\cli-contract.md
```
Expected:
- 小红书 skill 目录完整
- 抖音、快手 skill 契约都已经切到新参数模型
- [ ] **Step 5: 提交 skill 变更**
```powershell
git add skills/xiaohongshu-upload skills/douyin-upload skills/kuaishou-upload
git commit -m "feat: add xiaohongshu skill and align browser skill contracts"
```
## Task 5: 更新 examples 和 README / docs
**Files:**
- Modify: `examples/get_xiaohongshu_cookie.py`
- Modify: `examples/upload_video_to_xiaohongshu.py`
- Modify: `README.md`
- Modify: `docs/CLI.md`
- Modify: `docs/install.md`
- Modify: `docs/update.md`
- [ ] **Step 1: 改小红书 examples**
要求:
- `examples/get_xiaohongshu_cookie.py` 明确指向 `sau xiaohongshu login --account <account_name>` 这一主线
- `examples/upload_video_to_xiaohongshu.py` 说明当前主线优先走 CLI
- 如果继续保留 uploader 直连示例,注释里标明“调试入口 / 历史直连路径”
- [ ] **Step 2: 更新 README 平台表和快速开始**
README 至少要改这些位置:
- 平台能力表里把小红书改成:
- `CLI ✅`
- `Skill ✅`
- 快速开始里的浏览器平台命令改成统一契约
- 抖音、快手图文示例改成:
```bash
sau douyin upload-note --account <account_name> --images videos/1.png videos/2.png --title "图文标题" --note "图文正文"
sau kuaishou upload-note --account <account_name> --images videos/1.png videos/2.png --title "图文标题" --note "图文正文"
sau xiaohongshu upload-note --account <account_name> --images videos/1.png videos/2.png --title "图文标题" --note "图文正文"
```
- [ ] **Step 3: 更新 `docs/CLI.md`**
要求:
- 补 `xiaohongshu` 小节
- 把三家浏览器平台整理成一致说明:
- 视频:`title + desc + tags`
- 图文:`title + note + tags`
- 登录二维码说明补小红书
- [ ] **Step 4: 更新 `docs/install.md` 和 `docs/update.md`**
要求:
- 安装文档里补 `sau xiaohongshu --help`
- 更新文档里补小红书自检命令和 skill 路径
- 文档统一写 `account_name`
- [ ] **Step 5: 做文档与示例核对**
Run:
```powershell
Get-Content README.md | Select-String -Pattern "xiaohongshu|upload-note|--note|--desc" -Context 1,2
Get-Content docs\CLI.md | Select-String -Pattern "xiaohongshu|--note|--desc" -Context 1,2
Get-Content docs\install.md | Select-String -Pattern "xiaohongshu" -Context 1,2
Get-Content docs\update.md | Select-String -Pattern "xiaohongshu" -Context 1,2
```
Expected:
- README、CLI、install、update 都出现小红书 CLI
- 图文正文写成 `--note`
- 视频描述写成 `--desc`
- [ ] **Step 6: 跑最终最小验证**
Run:
```powershell
py -3 -m unittest tests.test_sau_browser_cli tests.test_xiaohongshu_uploader -v
py -3 sau_cli.py xiaohongshu --help
py -3 sau_cli.py xiaohongshu upload-video --help
py -3 sau_cli.py xiaohongshu upload-note --help
```
Expected:
- 单测通过
- 小红书 CLI 帮助存在
- 视频命令显示 `--desc`
- 图文命令显示 `--note`
- [ ] **Step 7: 提交收尾**
```powershell
git add examples/get_xiaohongshu_cookie.py examples/upload_video_to_xiaohongshu.py README.md docs/CLI.md docs/install.md docs/update.md
git commit -m "docs: align browser cli docs and examples"
```
@@ -1,47 +0,0 @@
# Xiaohongshu Shallow Alignment Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Align the browser-based Xiaohongshu uploader with the current Douyin/Kuaishou style for login QR flow, patchright usage, and formal video/note entry points without expanding into CLI or docs work.
**Architecture:** Keep the work inside `uploader/xiaohongshu_uploader/main.py` as a shallow refactor. Introduce a thin login flow with QR extraction, cookie validation, and result payloads, then add a shared base uploader plus separate video and note browser flows that reuse common setup.
**Tech Stack:** Python, patchright, async Playwright-compatible browser automation, unittest
---
### Task 1: Lock expected login/setup behavior
**Files:**
- Create: `tests/test_xiaohongshu_uploader.py`
- Modify: `uploader/xiaohongshu_uploader/main.py`
- [ ] Add tests for `xiaohongshu_setup(..., return_detail=True)` invalid-cookie behavior and uploader validation behavior.
- [ ] Run the targeted test file and confirm it fails for the expected missing behavior.
- [ ] Implement the minimal production changes to satisfy those tests.
### Task 2: Refactor Xiaohongshu uploader module
**Files:**
- Modify: `uploader/xiaohongshu_uploader/main.py`
- [ ] Switch the module from `playwright.async_api` to `patchright.async_api`.
- [ ] Replace `page.pause()` login with QR extraction, terminal printing, QR file saving, polling, and cookie persistence.
- [ ] Add a shared base uploader plus formal video and note entry points while keeping legacy public names callable.
### Task 3: Refresh local debug examples
**Files:**
- Modify: `examples/get_xiaohongshu_cookie.py`
- Modify: `examples/upload_video_to_xiaohongshu.py`
- [ ] Update the login example to use the aligned setup entry.
- [ ] Update the upload example to expose direct video/note debug functions.
### Task 4: Verify
**Files:**
- Test: `tests/test_xiaohongshu_uploader.py`
- [ ] Run the targeted unittest file.
- [ ] Run a quick Python syntax/import check for the modified uploader and examples.
@@ -1,279 +0,0 @@
# Bilibili CLI 设计
日期:2026-03-25
## 概要
这份设计要把 `bilibili` 挂到 `sau` 下面,用户侧体验尽量和现在的 `douyin`、`kuaishou` 保持一致。
核心约束只有两个:
- 用户不需要自己安装 `biliup`
- 外部统一走 `sau bilibili ...`
程序会把 `biliup` 当作内部运行时依赖来处理:
- `sau bilibili ...` 是唯一公开入口
- 本地没有 `biliup` 时自动下载
- 每次运行都检查 GitHub Release 最新版本
- 如果发现有更新,先自动更新,再继续执行当前命令
这份设计刻意保持轻量,不重新发明一套 B 站上传语义,而是直接复用仓库里现有的 B 站上传模型。
## 目标
- 让 `sau bilibili ...` 和 `sau douyin ...`、`sau kuaishou ...` 保持统一心智
- 隐藏 `biliup` 的安装细节,降低用户使用成本
- 复用项目里已经存在的账号文件、`VideoZoneTypes`、定时发布等能力
- 不做过度封装
## 非目标
- 第一版不把 `biliup` 二进制直接提交进仓库
- 第一版不维护本地 release manifest
- 第一版不做 B 站图文发布
- 第一版不重做现有的 B 站上传领域模型
## 当前项目基础
仓库里已经有 B 站上传能力:
- `uploader/bilibili_uploader/main.py` 目前直接封装了 `biliup.plugins.bili_webup`
- `examples/upload_video_to_bilibili.py` 已经在使用现有上传参数
- `utils/constant.py` 已经定义了完整的 `VideoZoneTypes`
也就是说,你现在项目里的 B 站上传语义已经很明确,核心就是:
- `file`
- `title`
- `desc`
- `tid`
- `tags`
- `dtime`
所以第一版 CLI 不需要重新造模型,直接沿用这套。
## 用户侧 CLI 设计
### 支持的命令
- `sau bilibili login`
- `sau bilibili check`
- `sau bilibili upload-video`
### 命令契约
#### `sau bilibili login`
作用:
- 自动准备 `biliup`
- 如果有更新则先升级
- 然后调用 `biliup` 完成登录
- 将账号数据按项目自己的账号文件规则保存下来
第一版行为:
- 本地没有 `biliup` 时自动下载最新 release
- 本地已有但上游有更新时自动升级
- 升级完成后继续执行登录流程
#### `sau bilibili check`
作用:
- 自动准备 `biliup`
- 检查当前账号是否可用
第一版行为:
- 结合本地账号文件存在性和 `biliup` 实际可用性来判断
- 输出风格和其他平台保持一致:
- `valid`
- `invalid`
#### `sau bilibili upload-video`
作用:
- 自动准备 `biliup`
- 走项目当前已有的 B 站上传参数体系完成视频上传
第一版参数:
- `--account` 必填
- `--file` 必填
- `--title` 必填
- `--desc` 必填
- `--tid` 必填
- `--tags` 选填
- `--schedule` 选填
明确决定:
- `tid` 在第一版里必须传
- 不给默认分区,避免猜测和隐式错误
## 运行时依赖策略
### 选定方案
`biliup` 不提交进仓库,也不要求用户手工安装。
`sau bilibili ...` 在运行时自动处理它:
1. 查找本地是否已有 `biliup`
2. 检查 GitHub Release 最新版本
3. 如果缺失或过期,则自动下载最新版本
4. 替换本地运行时副本
5. 继续执行本次命令
### 选择这个方案的原因
- 仓库体积更干净
- 用户不需要自己找 release、自己下载
- 对外仍然只有一个统一入口 `sau`
- 不需要使用 `git submodule`
### 接受的代价
这套方案明确接受一个现实:
- 每次运行都会检查上游 release
- 上游如果改 CLI 行为,可能会影响这层适配
所以这里的应对方式不是做重封装,而是保持 wrapper 很薄,减少被动维护成本。
## 存储与解析
`biliup` 应该存放在本地运行时缓存目录中,而不是源码目录中。
缓存目录只需要满足:
- 当前用户可写
- 可跨命令复用
- 不进入 git 管理
解析器的职责应当是:
- 识别当前操作系统
- 选择对应平台的 release asset
- 下载并替换可执行文件
- 返回最终可执行路径
## 轻量封装边界
为了避免过度封装,第一版只建议拆成 3 个很薄的部分。
### 1. Resolver
职责:
- 判断本地是否已有 `biliup`
- 检查 GitHub Release 最新版本
- 下载或更新可执行文件
- 返回最终可执行文件路径
### 2. Runner
职责:
- 调用解析出来的 `biliup`
- 收集退出码、标准输出、标准错误
- 对明显的进程级错误做一层项目内友好的报错转换
### 3. `sau_cli.py` 中的 bilibili 子命令
职责:
- 解析 `sau bilibili ...` 参数
- 把这些参数翻译成底层运行逻辑
- 让帮助信息风格和其他平台一致
第一版不需要更多层,也不需要再抽一套很重的统一框架。
## 与现有项目概念的映射
### 账号文件
B 站也继续沿用现在项目的账号别名机制:
- 用户传 `--account <name>`
- 程序解析成对应的账号文件路径
### 分区
`tid` 保持为一等参数。
`VideoZoneTypes` 继续保留并服务于:
- example
- 文档
- 后续可能的辅助工具
### 定时发布
`--schedule` 保持和当前 `sau` 其他平台一致的使用方式:
- 不传就是立即发布
- 传了就是定时发布
具体如何映射到底层 B 站执行逻辑,由 adapter 负责,不暴露给用户。
## 错误处理
第一版错误处理保持直接,不做花哨包装:
- 下载失败:明确告诉用户自动下载 `biliup` 失败
- 更新失败:明确告诉用户最新 release 准备失败
- 登录失败:保留 `biliup` 登录失败上下文
- 检查失败:输出 `invalid`
- 上传失败:返回非零退出码,并展示上游错误摘要
第一版不追求把所有 `biliup` 错误文本都重新翻译一遍。
## 文档影响范围
实现完成后,至少需要补齐这些地方:
- `README.md`
- `docs/CLI.md`
- 安装与更新文档
- 一套对应的 Bilibili skill
- Bilibili example 脚本
对外表达应当统一成:
- 用户使用的是 `sau bilibili ...`
- `biliup` 由程序自动准备
## 测试策略
第一版最少需要验证这些路径:
- `sau bilibili login --account <name>`
- `sau bilibili check --account <name>`
- `sau bilibili upload-video ...`
- 本地没有 `biliup` 时能自动下载
- 本地已有旧版本时能先升级再执行
- 本地已有最新版本时能直接复用
因为登录和上传涉及真实外部平台,第一版以手工验证为主是可以接受的。
## 推荐实现顺序
1. 在 `sau_cli.py` 中加入 `bilibili` 子命令
2. 增加一个最小可用的 `biliup` resolver
3. 增加一个最小可用的 `biliup` runner
4. 接上 `login / check / upload-video`
5. 补文档、example、skill
## 最终结论
- 对外入口固定为 `sau bilibili ...`
- 第一版支持 `login`、`check`、`upload-video`
- `tid` 必填
- `biliup` 不需要用户手动安装
- 每次运行都检查 GitHub Release
- 有新版本时先自动更新,再继续执行
- 整体实现保持轻量,不做过度封装
@@ -1,407 +0,0 @@
# 浏览器平台 CLI 统一与小红书 Skill 设计
日期:2026-03-25
## 概要
这份设计处理三个浏览器自动化平台的主线接口统一:
- 抖音
- 快手
- 小红书
目标不是重写 uploader,也不是再抽一层复杂框架,而是把当前已经存在但不一致的 CLI、skill、文档、示例收口成一套统一契约。
这次设计同时解决两个现实问题:
1. 小红书虽然已经有可用的浏览器 uploader,但还没有接入 `sau` CLI,也没有对应 skill。
2. 抖音、快手当前的 CLI 契约还带着历史字段,比如视频没有单独 `--desc`,而图文正文命名和视频描述语义混杂,不利于三家浏览器平台形成稳定统一的主线接口。
这次统一后的对外入口固定为:
- `sau douyin ...`
- `sau kuaishou ...`
- `sau xiaohongshu ...`
并让三家浏览器平台的视频、图文上传都遵循统一的主线参数模型:
- 视频:`title + desc + tags`
- 图文:`title + note + tags`
## 目标
- 给小红书补齐主线 CLI:
- `login`
- `check`
- `upload-video`
- `upload-note`
- 统一抖音、快手、小红书三家浏览器平台的 CLI 上传参数模型
- 补齐小红书 skill、示例脚本、README、CLI 文档、安装/更新文档
- 修正抖音、快手现有 CLI 契约里缺失的 `desc` 能力
- 保持现有 uploader 主体逻辑不大改,不做过度封装
## 非目标
- 这次不重构 uploader 架构
- 这次不改 Web 旧路径
- 这次不改 Bilibili 的上传契约
- 这次不做浏览器集成测试
- 这次不保留公开的 `--note` 主契约
## 当前项目基础
### 已有能力
- 抖音、快手已经接入 `sau_cli.py`
- Bilibili 已经接入 CLI 和 skill
- 小红书已经具备:
- 登录
- cookie 校验
- 视频上传
- 图文上传
- 定时发布
- 小红书 uploader 内部已经支持:
- 视频:`title + desc + tags`
- 图文:`title + desc + tags`
- 图文里的 `desc` 可选
### 当前不一致点
- 抖音视频 CLI:`--title`、`--tags`,没有 `--desc`
- 快手视频 CLI:`--title`、`--tags`,没有 `--desc`
- 抖音图文 CLI:`--note`、`--tags`
- 快手图文 CLI:`--note`、`--tags`
- 小红书还没有 CLI/skill 接口
也就是说,当前三家浏览器平台的主线能力并不统一,尤其是:
- 视频描述没有统一暴露为 `desc`
- 图文正文是否应该沿用 `note` 语义没有统一
## 统一后的 CLI 设计
### 支持的平台
- `douyin`
- `kuaishou`
- `xiaohongshu`
### 支持的动作
每个平台统一支持:
- `login`
- `check`
- `upload-video`
- `upload-note`
### 统一后的上传参数模型
#### 视频上传
```bash
sau <platform> upload-video \
--account <account_name> \
--file <video-path> \
--title "<title>" \
[--desc "<description>"] \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"] \
[平台特有参数...]
```
统一规则:
- `--title` 必填
- `--desc` 选填
- `--tags` 选填
- `--schedule` 选填
平台特有参数:
- 抖音:
- `--thumbnail`
- `--product-link`
- `--product-title`
- 快手:
- `--thumbnail`
- 小红书:
- `--thumbnail`
#### 图文上传
```bash
sau <platform> upload-note \
--account <account_name> \
--images <image-1> [image-2 ...] \
--title "<title>" \
[--note "<content>"] \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"]
```
统一规则:
- `--images` 必填
- `--title` 必填
- `--note` 选填
- `--tags` 选填
- `--schedule` 选填
明确决定:
- 图文主线正文统一叫 `note`
- 视频主线描述统一叫 `desc`
- 文档、skill、示例统一使用:
- 视频:`--title + --desc + --tags`
- 图文:`--title + --note + --tags`
## 数据模型设计
为了让 CLI 层和 uploader 层映射清晰,每个平台都保持各自的 request dataclass,但字段命名统一。
### 视频请求对象
统一字段:
- `account_name`
- `video_file`
- `title`
- `description`
- `tags`
- `publish_date`
- `publish_strategy`
- `debug`
- `headless`
平台特有字段保留:
- 抖音:
- `thumbnail_file`
- `product_link`
- `product_title`
- 快手:
- `thumbnail_file`
- 小红书:
- `thumbnail_file`
### 图文请求对象
统一字段:
- `account_name`
- `image_files`
- `title`
- `note`
- `tags`
- `publish_date`
- `publish_strategy`
- `debug`
- `headless`
这里明确保留 `note`,因为它更符合图文正文语义,不应强行复用视频里的 `description / desc` 命名。
## 与现有 uploader 的映射
### 抖音
- 视频上传继续复用 `DouYinVideo`
- 给抖音视频补齐 `desc` 输入映射
- 图文上传改为显式接收 `title + note + tags`
- `note` 在 CLI 层映射到抖音图文正文输入区
### 快手
- 视频上传继续复用 `KSVideo`
- 给快手视频补齐 `desc` 输入映射
- 图文上传改为显式接收 `title + note + tags`
### 小红书
- 登录、校验直接接 `xiaohongshu_setup` / `cookie_auth`
- 视频上传复用 `XiaoHongShuVideo`
- 图文上传复用 `XiaoHongShuNote`
- 因为小红书 uploader 已经支持 `title + desc + tags`,CLI 层把 `note` 稳定映射到图文正文即可
## 兼容与迁移策略
这次采用“直接统一,不保留旧的模糊公开契约”的策略。
具体表现:
- `README.md`
- `docs/CLI.md`
- `docs/install.md`
- `docs/update.md`
- `skills/douyin-upload/...`
- `skills/kuaishou-upload/...`
- 新增 `skills/xiaohongshu-upload/...`
- `scripts/examples/...`
都会在同一轮里切换到新契约,避免出现:
- 一部分文档把图文正文写成 `--note`
- 一部分文档把图文正文写成 `--desc`
这样做的代价是旧示例命令会失效,但换来的是主线契约彻底统一:
- 视频永远是 `desc`
- 图文永远是 `note`
## Skill 设计
新增:
- `skills/xiaohongshu-upload/SKILL.md`
- `skills/xiaohongshu-upload/references/cli-contract.md`
- `skills/xiaohongshu-upload/references/runtime-requirements.md`
- `skills/xiaohongshu-upload/references/troubleshooting.md`
- `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_commands.ps1`
- `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_commands.sh`
- `skills/xiaohongshu-upload/scripts/examples/xiaohongshu_cli_template.py`
并同步更新:
- `skills/douyin-upload/SKILL.md`
- `skills/douyin-upload/references/cli-contract.md`
- `skills/kuaishou-upload/SKILL.md`
- `skills/kuaishou-upload/references/cli-contract.md`
skill 原则继续保持:
- 优先走 `sau`
- agent 不要先读 uploader 源码
- CLI 失败时再看 troubleshooting
- 登录二维码图片优先直接展示给用户扫码
## 文档设计
至少更新:
- `README.md`
- `docs/CLI.md`
- `docs/install.md`
- `docs/update.md`
统一后的表达口径:
- 三家浏览器平台都已经接入 CLI
- 三家浏览器平台都已经接入 skill
- 视频上传统一字段:
- `title`
- `desc`
- `tags`
- 图文上传统一字段:
- `title`
- `note`
- `tags`
- `account_name` 是用户自定义账号名,不是固定只能叫 `creator`
- 一个 `account_name` 对应一个账号文件,可多账号隔离并发
## Example 设计
examples 保留两类路径:
1. CLI 主线示例
2. 历史直连 uploader 示例
小红书需要补到和其他平台同等级的主线表达里:
- `examples/get_xiaohongshu_cookie.py`
- `examples/upload_video_to_xiaohongshu.py`
- 如有必要,补一份更贴近 CLI 契约的调用示例
README 和 docs 中要明确说明:
- 推荐优先使用 `sau xiaohongshu ...`
- 历史直连 uploader 示例只是调试入口
## 错误处理
### 参数级错误
由 CLI parser 负责:
- 文件不存在
- 时间格式非法
- 缺少 `--title`
- 缺少 `--images`
### 业务级错误
由 uploader 和现有校验负责:
- cookie 不存在
- cookie 已失效
- 上传失败
- 页面结构异常
### 二维码口径
三家浏览器平台统一保留这条说明:
- 如果登录流程生成了本地二维码图片,agent 应优先直接展示/发送图片给用户扫码,而不是只返回路径
## 测试策略
这次只做最小但有价值的 CLI 级验证,不做浏览器集成测试。
至少补这些测试:
- parser 能识别 `xiaohongshu`
- 三个平台新契约能正确解析:
- `upload-video --title --desc --tags`
- `upload-note --images --title --note --tags`
- dispatch 能正确把参数转成对应 request
- 小红书 `login/check/upload-video/upload-note` 分支能被正确路由
保留现有:
- `tests/test_xiaohongshu_uploader.py`
## 文件影响范围
### 必改
- `sau_cli.py`
- `README.md`
- `docs/CLI.md`
- `docs/install.md`
- `docs/update.md`
### 新增
- `skills/xiaohongshu-upload/` 全套文件
- 对应 CLI 单测文件
### 同步修改
- `skills/douyin-upload/...`
- `skills/kuaishou-upload/...`
- `examples/get_xiaohongshu_cookie.py`
- `examples/upload_video_to_xiaohongshu.py`
## 推荐实现顺序
1. 先改 `sau_cli.py` 和 request 模型
2. 接上小红书 CLI 路由
3. 给抖音、快手补 `desc` / 图文新字段映射
4. 补 CLI 单测
5. 新增小红书 skill
6. 更新抖音、快手 skill 契约
7. 更新 README / CLI / install / update 文档
8. 更新 examples
## 最终结论
- 三家浏览器平台统一成同一套 CLI 动作:
- `login`
- `check`
- `upload-video`
- `upload-note`
- 三家浏览器平台统一成同一套主线元数据模型:
- 视频:`title + desc + tags`
- 图文:`title + note + tags`
- 小红书补齐 CLI 与 skill
- 抖音、快手补齐 `desc` 能力,并统一图文正文字段为 `note`
- 实现保持轻量,不做过度封装
-125
View File
@@ -1,125 +0,0 @@
# 更新说明
这个文档分成两部分:
- `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 kuaishou --help
sau xiaohongshu --help
sau bilibili --help
sau douyin check --account your_account
sau kuaishou check --account your_account
sau xiaohongshu check --account your_account
sau bilibili check --account your_account
```
如果你在使用 skill,也建议同步看一下:
- `skills/douyin-upload/SKILL.md`
- `skills/kuaishou-upload/SKILL.md`
- `skills/xiaohongshu-upload/SKILL.md`
- `skills/bilibili-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
sau kuaishou --help
sau xiaohongshu --help
sau bilibili --help
```
5. 如果任务是浏览器平台链路,再继续验证:
```bash
sau douyin check --account test
sau kuaishou check --account test
sau xiaohongshu check --account test
sau bilibili check --account test
```
6. 如果用户依赖 skill,再检查这些路径是否仍存在且契约没变:
- `skills/douyin-upload/SKILL.md`
- `skills/douyin-upload/references/cli-contract.md`
- `skills/douyin-upload/references/runtime-requirements.md`
- `skills/kuaishou-upload/SKILL.md`
- `skills/kuaishou-upload/references/cli-contract.md`
- `skills/kuaishou-upload/references/runtime-requirements.md`
- `skills/xiaohongshu-upload/SKILL.md`
- `skills/xiaohongshu-upload/references/cli-contract.md`
- `skills/xiaohongshu-upload/references/runtime-requirements.md`
- `skills/bilibili-upload/SKILL.md`
- `skills/bilibili-upload/references/cli-contract.md`
- `skills/bilibili-upload/references/runtime-requirements.md`
### 对 agent 的额外说明
- 优先相信 `pyproject.toml`,不要把 `requirements.txt` 视为当前主线真相
- 当前 README 只做总览,安装和更新以 `docs/install.md`、`docs/update.md` 为准
- Web 相关内容属于历史路径,见 `docs/legacy-web.md`
- 如果更新后的登录流程输出了本地二维码图片,agent 应直接把图片展示/发送给用户扫码,不要只回传图片路径
- Bilibili 命令运行时会自动检查并更新 `biliup`
- Bilibili 登录当前仍建议让用户自己在本地真实终端里执行;如果二维码显示不完整,可让用户直接打开 `qrcode.png` 扫码
View File
-18
View File
@@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
"""获取支付宝生活号 cookie:打开浏览器扫码登录后自动保存到 cookies/alipay_uploader/account.json"""
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from uploader.alipay_uploader.main import alipay_cookie_gen
async def main():
account_file = Path(__file__).resolve().parents[1] / "cookies" / "alipay_uploader" / "account.json"
await alipay_cookie_gen(str(account_file))
if __name__ == "__main__":
asyncio.run(main())
-10
View File
@@ -1,10 +0,0 @@
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.baijiahao_uploader.main import baijiahao_setup
if __name__ == '__main__':
account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json")
account_file.parent.mkdir(exist_ok=True)
cookie_setup = asyncio.run(baijiahao_setup(str(account_file), handle=True))
-111
View File
@@ -1,111 +0,0 @@
# -*- coding: utf-8 -*-
"""百家号(headless)扫码登录 → 保存 cookie。
说明:
- 无头浏览器打开百家号登录页并提取登录二维码,放大截图后保存为 png,
并用终端 ASCII 二维码展示;你手机/百度APP扫码登录。
- 扫码成功后自动把登录态写入 cookies/baijiahao_uploader/account.json。
用法:
python examples/get_baijiahao_cookie_headless.py
"""
from pathlib import Path
from playwright.async_api import async_playwright
from conf import BASE_DIR
from utils.login_qrcode import (
build_login_qrcode_path,
print_terminal_qrcode,
)
from uploader.baijiahao_uploader.main import cookie_auth, baijiahao_logger
QR_SELECTOR = 'img[src^="https://passport.baidu.com/v2/api/qrcode"]'
LOGIN_BTN_TEXT = "登录"
LOGIN_URL = "https://baijiahao.baidu.com/builder/theme/bjh/login"
async def _grab_qr(page, qrcode_path: Path) -> str:
qr = page.locator(QR_SELECTOR).first
await qr.wait_for(state="attached", timeout=60000)
src = await qr.get_attribute("src")
if src and src.startswith("https://"):
resp = await page.context.request.get(src)
qrcode_path.parent.mkdir(parents=True, exist_ok=True)
qrcode_path.write_bytes(await resp.body())
else:
await qr.screenshot(path=str(qrcode_path))
qrcode_content = ""
try:
import cv2
img = cv2.imread(str(qrcode_path))
if img is not None:
h, w = img.shape[:2]
up = cv2.resize(img, (w * 3, h * 3), interpolation=cv2.INTER_CUBIC)
qrcode_content = cv2.QRCodeDetector().detectAndDecode(up)[0] or ""
except Exception:
pass
return qrcode_content or ""
async def _wait_login(page, max_checks: int = 120, interval: int = 3) -> bool:
import asyncio
async def _logged_in():
if "login" in page.url:
return False
ctx = page.context
cookies = await ctx.cookies()
if any(c.get("name") in ("BDUSS", "STOKEN") for c in cookies):
return True
return False
for _ in range(max_checks):
if await _logged_in():
return True
await asyncio.sleep(interval)
return False
async def main():
account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json")
account_file.parent.mkdir(parents=True, exist_ok=True)
import os
if os.path.exists(account_file) and await cookie_auth(str(account_file)):
baijiahao_logger.success("[+] cookie 已有效,无需重新登录")
return
qrcode_path = build_login_qrcode_path(str(account_file))
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(
headless=True, args=["--no-sandbox", "--disable-blink-features=AutomationControlled"]
)
context = await browser.new_context()
page = await context.new_page()
await page.goto(LOGIN_URL, timeout=60000, wait_until="domcontentloaded")
await page.wait_for_timeout(4000)
# 进入登录弹窗
await page.get_by_text(LOGIN_BTN_TEXT, exact=True).first.click(timeout=10000)
await page.wait_for_timeout(4000)
qrcode_content = await _grab_qr(page, qrcode_path)
baijiahao_logger.info(f"🖼️ 二维码已保存: {qrcode_path}")
if qrcode_content:
print_terminal_qrcode(qrcode_content, qrcode_path, "百度APP/手机百度")
else:
print(f"未能解码二维码,请直接打开文件扫码:\n {qrcode_path}")
if await _wait_login(page):
baijiahao_logger.success("[+] 扫码登录成功,正在保存 cookie...")
await context.storage_state(path=str(account_file))
baijiahao_logger.success(f"[+] cookie 已保存: {account_file}")
else:
baijiahao_logger.error("[-] 等待扫码超时(约 6 分钟),未完成登录。")
await browser.close()
if __name__ == "__main__":
import asyncio
asyncio.run(main())
-24
View File
@@ -1,24 +0,0 @@
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from conf import BASE_DIR
if __name__ == "__main__":
# 建议直接在本地真实终端里运行这个脚本。
# 如果终端里的二维码显示不完整,可以打开当前目录下的 qrcode.png 扫码。
cli_path = Path(BASE_DIR) / "sau_cli.py"
subprocess.run(
[
sys.executable,
str(cli_path),
"bilibili",
"login",
"--account",
"creator",
],
check=True,
)
-10
View File
@@ -1,10 +0,0 @@
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.douyin_uploader.main import douyin_setup
if __name__ == '__main__':
account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json")
account_file.parent.mkdir(exist_ok=True)
cookie_setup = asyncio.run(douyin_setup(str(account_file), handle=True))
-21
View File
@@ -1,21 +0,0 @@
"""Legacy direct login helper for Kuaishou.
Current mainline usage prefers:
sau kuaishou login --account creator
"""
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.ks_uploader.main import ks_setup
def login_to_kuaishou():
account_file = Path(BASE_DIR / "cookies" / "kuaishou_creator.json")
account_file.parent.mkdir(exist_ok=True)
asyncio.run(ks_setup(str(account_file), handle=True))
if __name__ == '__main__':
login_to_kuaishou()
-11
View File
@@ -1,11 +0,0 @@
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.tencent_uploader.main import tencent_setup
if __name__ == "__main__":
account_file = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json")
account_file.parent.mkdir(exist_ok=True)
asyncio.run(tencent_setup(str(account_file), handle=True))
-10
View File
@@ -1,10 +0,0 @@
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.tk_uploader.main_chrome import tiktok_setup
if __name__ == '__main__':
account_file = Path(BASE_DIR / "cookies" / "tk_uploader" / "account.json")
account_file.parent.mkdir(exist_ok=True)
cookie_setup = asyncio.run(tiktok_setup(str(account_file), handle=True))
-24
View File
@@ -1,24 +0,0 @@
"""
当前主线优先使用 CLI:
sau xiaohongshu login --account <account_name>
这个脚本保留为小红书 uploader 的调试入口 / 历史直连路径。
"""
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.xiaohongshu_uploader.main import xiaohongshu_setup
if __name__ == '__main__':
account_file = Path(BASE_DIR / "cookies" / "xiaohongshu_uploader" / "account.json")
account_file.parent.mkdir(exist_ok=True)
result = asyncio.run(
xiaohongshu_setup(
str(account_file),
handle=True,
return_detail=True,
)
)
-32
View File
@@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
"""测试支付宝生活号上传:使用已保存的 cookie 上传并发布一个视频。"""
import asyncio
import sys
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from uploader.alipay_uploader.main import AlipayVideo
async def main():
base = Path(__file__).resolve().parents[1]
video_path = Path(r"D:\video-moving\assets\outro\outro_horizontal.mp4")
account_file = base / "cookies" / "alipay_uploader" / "account.json"
app = AlipayVideo(
title="测试发布标题",
file_path=str(video_path),
tags=["测试", "自动化"],
account_file=str(account_file),
desc="测试描述",
thumbnail_path=r"C:\Users\admin\Downloads\ScreenShot_2026-08-04_170159_630.png",
collection_name="",
headless=False,
)
await app.main()
if __name__ == "__main__":
asyncio.run(main())
-64
View File
@@ -1,64 +0,0 @@
"""Legacy direct-uploader example for Douyin.
Current mainline usage prefers:
sau douyin login --account creator
sau douyin upload-video ...
sau douyin upload-note ...
"""
import asyncio
from datetime import datetime
from pathlib import Path
from conf import BASE_DIR
from uploader.douyin_uploader.main import (
DOUYIN_PUBLISH_STRATEGY_IMMEDIATE,
DOUYIN_PUBLISH_STRATEGY_SCHEDULED,
DouYinNote,
DouYinVideo,
)
def upload_video_to_douyin():
account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json")
video_file_path = Path(BASE_DIR) / "videos/demo.mp4"
video_meta_title = "男子为了心爱之人每天坚守❤️‍🩹"
video_meta_hashtags = ["坚持不懈", "爱情执着", "奋斗使者", "短视频"]
thumbnail_landscape_path = Path(BASE_DIR) / "videos/demo.png"
thumbnail_portrait_path = Path(BASE_DIR) / "videos/demo.png"
video_meta_publish_time = datetime.strptime("2026-3-25 12:13", "%Y-%m-%d %H:%M")
app = DouYinVideo(
title=video_meta_title,
file_path=video_file_path,
tags=video_meta_hashtags,
publish_date=video_meta_publish_time,
thumbnail_landscape_path=thumbnail_landscape_path,
thumbnail_portrait_path=thumbnail_portrait_path,
account_file=account_file,
publish_strategy=DOUYIN_PUBLISH_STRATEGY_SCHEDULED,
)
asyncio.run(app.douyin_upload_video())
def upload_note_to_douyin():
account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json")
image_paths = [Path(BASE_DIR) / "videos/demo.png", Path(BASE_DIR) / "videos/demo.png", Path(BASE_DIR) / "videos/demo.png"]
note = "图文内容示例"
tags = ["图文", "示例", "抖音图文"]
video_meta_publish_time = datetime.strptime("2026-3-25 12:13", "%Y-%m-%d %H:%M")
app = DouYinNote(
image_paths=image_paths,
note=note,
tags=tags,
publish_date=0,
account_file=account_file,
publish_strategy=DOUYIN_PUBLISH_STRATEGY_IMMEDIATE,
)
asyncio.run(app.douyin_upload_note())
if __name__ == "__main__":
# upload_video_to_douyin()
upload_note_to_douyin()
-72
View File
@@ -1,72 +0,0 @@
"""Legacy direct-uploader example for Kuaishou.
Current mainline usage prefers:
sau kuaishou login --account creator
sau kuaishou upload-video ...
sau kuaishou upload-note ...
"""
import asyncio
from datetime import datetime
from pathlib import Path
from conf import BASE_DIR
from uploader.ks_uploader.main import (
KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE,
KUAISHOU_PUBLISH_STRATEGY_SCHEDULED,
KSNote,
KSVideo,
)
def upload_video_to_kuaishou():
account_file = Path(BASE_DIR / "cookies" / "kuaishou_creator.json")
account_file.parent.mkdir(exist_ok=True)
video_file_path = Path(BASE_DIR) / "videos/demo.mp4"
thumbnail_path = video_file_path.with_suffix(".png")
video_meta_title = "快手视频上传示例"
video_meta_hashtags = ["快手", "自动上传", "示例"]
video_meta_publish_time = datetime.strptime("2026-03-27 12:13", "%Y-%m-%d %H:%M")
publish_strategy = KUAISHOU_PUBLISH_STRATEGY_SCHEDULED
app = KSVideo(
title=video_meta_title,
file_path=video_file_path,
tags=video_meta_hashtags,
publish_date=video_meta_publish_time,
account_file=account_file,
thumbnail_path=thumbnail_path if thumbnail_path.exists() else None,
publish_strategy=publish_strategy,
)
asyncio.run(app.main())
def upload_note_to_kuaishou():
account_file = Path(BASE_DIR / "cookies" / "kuaishou_creator.json")
account_file.parent.mkdir(exist_ok=True)
image_paths = [
Path(BASE_DIR) / "videos/demo.png",
Path(BASE_DIR) / "videos/demo1.png",
Path(BASE_DIR) / "videos/demo2.png",
]
note = "快手图文内容示例"
tags = ["快手图文", "自动上传", "示例"]
note_meta_publish_time = datetime.strptime("2026-03-27 12:13", "%Y-%m-%d %H:%M")
publish_strategy = KUAISHOU_PUBLISH_STRATEGY_SCHEDULED
app = KSNote(
image_paths=image_paths,
note=note,
tags=tags,
publish_date=note_meta_publish_time,
account_file=account_file,
publish_strategy=publish_strategy,
)
asyncio.run(app.main())
if __name__ == '__main__':
upload_video_to_kuaishou()
# upload_note_to_kuaishou()
-27
View File
@@ -1,27 +0,0 @@
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.baijiahao_uploader.main import baijiahao_setup, BaiJiaHaoVideo
from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
if __name__ == '__main__':
filepath = Path(BASE_DIR) / "videos"
account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json")
# 获取视频目录
folder_path = Path(filepath)
# 获取文件夹中的所有文件
files = list(folder_path.glob("*.mp4"))
file_num = len(files)
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
cookie_setup = asyncio.run(baijiahao_setup(account_file, handle=False))
for index, file in enumerate(files):
title, tags = get_title_and_hashtags(str(file))
thumbnail_path = file.with_suffix('.png')
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
app = BaiJiaHaoVideo(title, file, tags, publish_datetimes[index], account_file)
asyncio.run(app.main(), debug=False)
-44
View File
@@ -1,44 +0,0 @@
import subprocess
import sys
from pathlib import Path
from conf import BASE_DIR
from utils.constant import VideoZoneTypes
from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
if __name__ == '__main__':
filepath = Path(BASE_DIR) / "videos"
folder_path = Path(filepath)
files = list(folder_path.glob("*.mp4"))
file_num = len(files)
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
cli_path = Path(BASE_DIR) / "sau_cli.py"
for index, file in enumerate(files):
title, tags = get_title_and_hashtags(str(file))
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
desc = title
schedule_text = publish_datetimes[index].strftime("%Y-%m-%d %H:%M")
command = [
sys.executable,
str(cli_path),
"bilibili",
"upload-video",
"--account",
"creator",
"--file",
str(file),
"--title",
title,
"--desc",
desc,
"--tid",
str(VideoZoneTypes.SPORTS_FOOTBALL.value),
"--tags",
",".join(tags),
"--schedule",
schedule_text,
]
subprocess.run(command, check=True)
-109
View File
@@ -1,109 +0,0 @@
"""
当前文件先保留为视频号 uploader 的调试入口 / 历史直连路径。
注意:
1. `uploader/tencent_uploader/main.py` 里的核心页面交互逻辑目前是骨架;
2. 你需要自己补视频里的 `fill_title_and_tags` / `wait_for_upload_complete` / `set_thumbnail` / `submit_publish` 等方法;
3. 你需要自己补图文里的 `switch_to_note_mode` / `upload_note_images` / `fill_note_title_and_tags` / `submit_publish` 等方法;
3. 补完后,这个 example 就可以直接作为本地调试入口继续使用。
"""
import asyncio
from datetime import datetime, timedelta
from pathlib import Path
from conf import BASE_DIR
from uploader.tencent_uploader.main import TENCENT_PUBLISH_STRATEGY_IMMEDIATE
from uploader.tencent_uploader.main import TENCENT_PUBLISH_STRATEGY_SCHEDULED
from uploader.tencent_uploader.main import TencentNote
from uploader.tencent_uploader.main import TencentVideo
ACCOUNT_FILE = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json")
def upload_video_to_tencent():
video_file = Path(BASE_DIR) / "videos" / "demo.mp4"
thumbnail_path = video_file.with_suffix(".png")
app = TencentVideo(
title="视频号视频示例",
file_path=str(video_file),
tags=["视频号", "自动上传", "调试入口"],
publish_strategy=TENCENT_PUBLISH_STRATEGY_IMMEDIATE,
publish_date=0,
account_file=str(ACCOUNT_FILE),
desc="这里是你后面准备填写的视频简介示例",
thumbnail_path=str(thumbnail_path) if thumbnail_path.exists() else None,
short_title="视频号示例",
category=None,
is_draft=False,
)
asyncio.run(app.tencent_upload_video())
def upload_video_to_tencent_scheduled():
video_file = Path(BASE_DIR) / "videos" / "demo.mp4"
thumbnail_path = video_file.with_suffix(".png")
publish_time = (datetime.now() + timedelta(hours=3)).replace(second=0, microsecond=0)
app = TencentVideo(
title="视频号定时发布示例",
file_path=str(video_file),
tags=["视频号", "定时发布", "调试入口"],
publish_strategy=TENCENT_PUBLISH_STRATEGY_SCHEDULED,
publish_date=publish_time,
account_file=str(ACCOUNT_FILE),
desc="这里是定时发布的视频简介示例",
thumbnail_path=str(thumbnail_path) if thumbnail_path.exists() else None,
short_title="定时发布示例",
category=None,
is_draft=False,
)
asyncio.run(app.tencent_upload_video())
def upload_note_to_tencent():
image_candidates = [
Path(BASE_DIR) / "videos" / "demo.png",
Path(BASE_DIR) / "videos" / "demo1.png",
Path(BASE_DIR) / "videos" / "demo2.png",
]
image_paths = [str(path) for path in image_candidates if path.exists()]
app = TencentNote(
image_paths=image_paths,
note="视频号图文内容示例 #图文调试",
tags=["视频号图文", "自动上传", "调试入口"],
publish_strategy=TENCENT_PUBLISH_STRATEGY_IMMEDIATE,
publish_date=0,
account_file=str(ACCOUNT_FILE),
title="视频号图文示例",
is_draft=False,
)
asyncio.run(app.tencent_upload_note())
def upload_note_to_tencent_scheduled():
image_candidates = [
Path(BASE_DIR) / "videos" / "demo.png",
Path(BASE_DIR) / "videos" / "demo1.png",
Path(BASE_DIR) / "videos" / "demo2.png",
]
image_paths = [str(path) for path in image_candidates if path.exists()]
publish_time = (datetime.now() + timedelta(hours=3)).replace(second=0, microsecond=0)
app = TencentNote(
image_paths=image_paths,
note="视频号图文定时发布示例 #图文调试",
tags=["视频号图文", "定时发布", "调试入口"],
publish_strategy=TENCENT_PUBLISH_STRATEGY_SCHEDULED,
publish_date=publish_time,
account_file=str(ACCOUNT_FILE),
title="视频号图文定时示例",
is_draft=False,
)
asyncio.run(app.tencent_upload_note())
if __name__ == "__main__":
upload_video_to_tencent()
# upload_video_to_tencent_scheduled()
# upload_note_to_tencent()
# upload_note_to_tencent_scheduled()
-30
View File
@@ -1,30 +0,0 @@
import asyncio
from pathlib import Path
from conf import BASE_DIR
# from tk_uploader.main import tiktok_setup, TiktokVideo
from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo
from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
if __name__ == '__main__':
filepath = Path(BASE_DIR) / "videos"
account_file = Path(BASE_DIR / "cookies" / "tk_uploader" / "account.json")
folder_path = Path(filepath)
# get video files from folder
files = list(folder_path.glob("*.mp4"))
file_num = len(files)
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
cookie_setup = asyncio.run(tiktok_setup(account_file, handle=True))
for index, file in enumerate(files):
title, tags = get_title_and_hashtags(str(file))
thumbnail_path = file.with_suffix('.png')
print(f"video_file_name:{file}")
print(f"video_title:{title}")
print(f"video_hashtag:{tags}")
if thumbnail_path.exists():
print(f"thumbnail_file_name:{thumbnail_path}")
app = TiktokVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path)
else:
app = TiktokVideo(title, file, tags, publish_datetimes[index], account_file)
asyncio.run(app.main(), debug=False)
-68
View File
@@ -1,68 +0,0 @@
import configparser
from pathlib import Path
from time import sleep
from xhs import XhsClient
from conf import BASE_DIR
from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
from uploader.xhs_uploader.main import sign_local, beauty_print
config = configparser.RawConfigParser()
config.read(Path(BASE_DIR / "uploader" / "xhs_uploader" / "accounts.ini"))
if __name__ == '__main__':
filepath = Path(BASE_DIR) / "videos"
# 获取视频目录
folder_path = Path(filepath)
# 获取文件夹中的所有文件
files = list(folder_path.glob("*.mp4"))
file_num = len(files)
cookies = config['account1']['cookies']
xhs_client = XhsClient(cookies, sign=sign_local, timeout=60)
# auth cookie
# 注意:该校验cookie方式可能并没那么准确
try:
xhs_client.get_video_first_frame_image_id("3214")
except:
print("cookie 失效")
exit()
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
for index, file in enumerate(files):
title, tags = get_title_and_hashtags(str(file))
# 加入到标题 补充标题(xhs 可以填1000字不写白不写)
tags_str = ' '.join(['#' + tag for tag in tags])
hash_tags_str = ''
hash_tags = []
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
topics = []
# 获取hashtag
for i in tags[:3]:
topic_official = xhs_client.get_suggest_topic(i)
if topic_official:
topic_official[0]['type'] = 'topic'
topic_one = topic_official[0]
hash_tag_name = topic_one['name']
hash_tags.append(hash_tag_name)
topics.append(topic_one)
hash_tags_str = ' ' + ' '.join(['#' + tag + '[话题]#' for tag in hash_tags])
note = xhs_client.create_video_note(title=title[:20], video_path=str(file),
desc=title + tags_str + hash_tags_str,
topics=topics,
is_private=False,
post_time=publish_datetimes[index].strftime("%Y-%m-%d %H:%M:%S"))
beauty_print(note)
# 强制休眠30s,避免风控(必要)
sleep(30)
-100
View File
@@ -1,100 +0,0 @@
"""
当前主线优先使用 CLI:
sau xiaohongshu login --account <account_name>
sau xiaohongshu upload-video --account <account_name> --file videos/demo.mp4 --title "示例标题" --desc "示例简介"
sau xiaohongshu upload-note --account <account_name> --images videos/1.png videos/2.png --title "图文标题" --note "图文正文"
这个脚本保留为小红书 uploader 的调试入口 / 历史直连路径。
"""
import asyncio
from datetime import datetime, timedelta
from pathlib import Path
from conf import BASE_DIR
from uploader.xiaohongshu_uploader.main import XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
from uploader.xiaohongshu_uploader.main import XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED
from uploader.xiaohongshu_uploader.main import XiaoHongShuNote
from uploader.xiaohongshu_uploader.main import XiaoHongShuVideo
ACCOUNT_FILE = Path(BASE_DIR / "cookies" / "xiaohongshu_uploader" / "account.json")
def upload_video_to_xiaohongshu():
video_file = Path(BASE_DIR) / "videos" / "demo.mp4"
thumbnail_path = video_file.with_suffix(".png")
app = XiaoHongShuVideo(
title="小红书视频示例",
file_path=str(video_file),
desc="你好",
tags=["小红书", "视频示例", "调试入口"],
publish_strategy=XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE,
publish_date=0,
account_file=str(ACCOUNT_FILE),
thumbnail_path=str(thumbnail_path) if thumbnail_path.exists() else None,
)
asyncio.run(app.xiaohongshu_upload_video())
def upload_video_to_xiaohongshu_scheduled():
video_file = Path(BASE_DIR) / "videos" / "demo.mp4"
thumbnail_path = video_file.with_suffix(".png")
publish_time = (datetime.now() + timedelta(hours=3)).replace(second=0, microsecond=0)
app = XiaoHongShuVideo(
title="小红书视频定时发布示例",
file_path=str(video_file),
desc="这是一条定时发布的小红书视频示例",
tags=["小红书", "定时发布", "调试入口"],
publish_strategy=XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED,
publish_date=publish_time,
account_file=str(ACCOUNT_FILE),
thumbnail_path=str(thumbnail_path) if thumbnail_path.exists() else None,
)
asyncio.run(app.xiaohongshu_upload_video())
def upload_note_to_xiaohongshu():
image_candidates = [
Path(BASE_DIR) / "videos" / "demo.png",
Path(BASE_DIR) / "videos" / "demo1.png",
Path(BASE_DIR) / "videos" / "demo2.png",
]
image_paths = [str(path) for path in image_candidates if path.exists()]
app = XiaoHongShuNote(
image_paths=image_paths,
note="小红书图文内容示例 #图文调试",
tags=["小红书图文", "自动上传", "调试入口"],
publish_strategy=XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE,
publish_date=0,
account_file=str(ACCOUNT_FILE),
title="小红书图文示例",
)
asyncio.run(app.xiaohongshu_upload_note())
def upload_note_to_xiaohongshu_scheduled():
image_candidates = [
Path(BASE_DIR) / "videos" / "demo.png",
Path(BASE_DIR) / "videos" / "demo1.png",
Path(BASE_DIR) / "videos" / "demo2.png",
]
image_paths = [str(path) for path in image_candidates if path.exists()]
publish_time = (datetime.now() + timedelta(hours=3)).replace(second=0, microsecond=0)
app = XiaoHongShuNote(
image_paths=image_paths,
note="小红书图文内容示例 #图文调试",
tags=["小红书图文", "自动上传", "调试入口"],
publish_strategy=XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED,
publish_date=publish_time,
account_file=str(ACCOUNT_FILE),
title="小红书图文示例",
)
asyncio.run(app.xiaohongshu_upload_note())
if __name__ == '__main__':
# upload_video_to_xiaohongshu()
# upload_video_to_xiaohongshu_scheduled()
# upload_note_to_xiaohongshu()
upload_note_to_xiaohongshu_scheduled()
-233
View File
@@ -1,233 +0,0 @@
#!/bin/bash
# export_douyin_cookie.sh
# 导出已登录 Chrome 的抖音 cookie 到 douyin_{8位随机字符}.json
# 使用 curl 直接调用 Chrome DevTools HTTP API
set -e
# 解析参数
USERNAME=""
while [ $# -gt 0 ]; do
case "$1" in
--account)
USERNAME="$2"
shift 2
;;
*)
echo "用法: $0 --account <用户名>"
exit 1
;;
esac
done
if [ -z "$USERNAME" ]; then
echo "未指定用户名,将使用随机文件名"
fi
DEBUG_PORT=9222
echo "=========================================="
echo " 抖音 Cookie 导出工具"
echo "=========================================="
echo ""
# 依赖: curl, python3, websocket-client
echo "检查 Chrome remote debugging..."
RESPONSE=$(curl -s "http://localhost:${DEBUG_PORT}/json" 2>/dev/null) || true
if ! echo "$RESPONSE" | grep -q "webSocketDebuggerUrl"; then
echo "Chrome remote debugging 未运行"
echo "请先启动 Chrome: chromium --remote-debugging-port=9222 ..."
exit 1
fi
echo "Chrome remote debugging 已运行"
echo ""
echo "获取抖音 Cookie..."
USERNAME="$USERNAME" python3 << 'PYEOF'
import json
import os
import uuid
import sys
# Shell 把 USERNAME 作为环境变量传过来, Python 需要显式取
USERNAME = os.environ.get('USERNAME', '')
DEBUG_PORT = 9222
if USERNAME:
COOKIE_FILE = f"cookies/douyin_{USERNAME}.json"
else:
COOKIE_FILE = f"cookies/douyin_{uuid.uuid4().hex[:8]}.json"
def get_douyin_cookies():
import urllib.request
import websocket
# 获取页面列表
url = f"http://localhost:{DEBUG_PORT}/json"
with urllib.request.urlopen(url, timeout=10) as response:
pages = json.loads(response.read().decode())
# 查找抖音创作者页面
douyin_page = None
for page in pages:
page_url = page.get("url", "")
if "creator.douyin.com" in page_url:
douyin_page = page
break
if not douyin_page:
print("未找到抖音创作者平台页面")
print("请先在 Chrome 中打开并登录 https://creator.douyin.com")
return False
page_url = douyin_page["url"]
ws_url = douyin_page.get("webSocketDebuggerUrl", "")
print(f"找到页面: {page_url}")
# 连接到目标页面的 WebSocket
print(f"连接页面 WebSocket...")
ws = websocket.create_connection(ws_url, timeout=30)
# 获取所有 Cookie
ws.send(json.dumps({"id": 1, "method": "Network.getAllCookies"}))
response = json.loads(ws.recv())
cookies = response.get("result", {}).get("cookies", [])
# 过滤抖音相关的 cookie
douyin_cookies = [
c for c in cookies
if "douyin.com" in c.get("domain", "") or ".douyin.com" in c.get("domain", "")
]
print(f"获取到 {len(douyin_cookies)} 个抖音 Cookie")
if len(douyin_cookies) == 0:
print("未获取到任何抖音 Cookie,可能未登录")
return False
# 获取 localStorage (origins)
print(f"获取 localStorage...")
local_storage_by_origin = {}
# 获取页面 frame 树
ws.send(json.dumps({"id": 2, "method": "Page.getResourceTree"}))
resp = json.loads(ws.recv())
frames = resp.get("result", {}).get("frameTree", {}).get("childFrames", [])
all_frames = [resp.get("result", {}).get("frameTree", {})] + frames
for frame in all_frames:
frame_url = frame.get("url", "")
frame_id = frame.get("id", "")
if "douyin.com" in frame_url or "bytedance.com" in frame_url:
# 执行 JavaScript 获取该 frame 的 localStorage
script = """
(function() {
var result = [];
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
result.push([key, localStorage.getItem(key)]);
}
return result;
})()
"""
ws.send(json.dumps({
"id": 3,
"method": "Runtime.evaluate",
"params": {"expression": script, "contextId": frame.get("id")}
}))
resp = json.loads(ws.recv())
result_eval = resp.get("result", {})
if result_eval.get("result", {}).get("type") == "array":
items = result_eval.get("result", {}).get("value", [])
if items:
origin = frame_url.rsplit("/", 2)[0] + "//" + frame_url.split("/")[2]
local_storage_by_origin[origin] = items
# 获取主文档的 localStorage
script_main = """
(function() {
var result = [];
try {
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
result.push([key, localStorage.getItem(key)]);
}
} catch(e) {}
return result;
})()
"""
ws.send(json.dumps({
"id": 4,
"method": "Runtime.evaluate",
"params": {"expression": script_main}
}))
resp = json.loads(ws.recv())
result_eval = resp.get("result", {})
if result_eval.get("result", {}).get("type") == "array":
items = result_eval.get("result", {}).get("value", [])
if items:
origin = page_url.rsplit("/", 2)[0] + "//" + page_url.split("/")[2]
local_storage_by_origin[origin] = items
ws.close()
# 构建 JSON
result = {
"cookies": [],
"origins": []
}
for c in douyin_cookies:
result["cookies"].append({
"name": c.get("name", ""),
"value": c.get("value", ""),
"domain": c.get("domain", ""),
"path": c.get("path", "/"),
"expires": c.get("expires", -1),
"httpOnly": c.get("httpOnly", False),
"secure": c.get("secure", True),
"sameSite": c.get("sameSite", "Lax")
})
# 添加 origins
for origin, items in local_storage_by_origin.items():
origin_entry = {
"origin": origin,
"localStorage": [{"name": name, "value": value} for name, value in items]
}
result["origins"].append(origin_entry)
# 保存
os.makedirs("cookies", exist_ok=True)
with open(COOKIE_FILE, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, ensure_ascii=False)
print(f"Cookie 已保存到: {COOKIE_FILE}")
print("")
# 打印关键 Cookie
key_cookies = ["sessionid", "uid_tt", "ssid", "ttwid"]
print("关键 Cookie:")
for c in douyin_cookies:
if c.get("name") in key_cookies:
val = c.get("value", "")
if len(val) > 30:
val = val[:30] + "..."
print(f" {c.get('name')}: {val}")
return True
if __name__ == "__main__":
if not get_douyin_cookies():
sys.exit(1)
PYEOF
echo ""
echo "=========================================="
echo " 完成!文件已导出为$COOKIE_FILE"
echo "=========================================="
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 901 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
View File
Binary file not shown.
-33
View File
@@ -1,33 +0,0 @@
## 启动项目:
python 版本:3.10
1. 安装依赖
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
2. 删除 db 目录下 database.db(如果没有直接运行createTable.py即可),运行 createTable.py 重新建库,避免出现脏数据
3. 修改 conf.py最下方 LOCAL_CHROME_PATH 为本地 chrome 浏览器地址
4. 运行根目录的 sau_backend.py
5. type字段(平台标识) 1 小红书 2 视频号 3 抖音 4 快手
## 接口说明
1. /upload post
上传接口,上传成功会返回文件的唯一id,后期靠这个发布视频
2. /login id参数 用户名 type参数 平台标识:登录流程,前端和后端建立sse连接,后端获取到图片base64编码后返回给前端,前端接受扫码后后端存库后返回200,前端主动断开连接,然后调取/getValidAccounts获取当前所有可用账号
3. /getValidAccounts 会获取当前所有可用cookie,时间较慢,会逐个校验cookie,status 1 有效 0 无效cookie
4. /postVideo 发布视频接口 post json传参
file_list /upload获取的文件唯一标识
account_list /getValidAccounts获取的filePath字段
type 类型字段(平台标识)
title 视频标题
tags 视频tag 列表,不带#
category 原作者说是原创表示,0表示不是原创其他表示为原创,但测试该字段没有效果
enableTimer 是否开启定时发布,默认关闭,开启传True,如果开启,下面三个必传,否则不传
videos_per_day 每天发布几个视频
daily_times 每天发布视频的时间,整形列表,与上面列表长度保持一致
start_days 开始天数,0 代表明天开始定时发布 1 代表明天的明天
以上三个字段是我的理解,不知道对不对,也不知道原作者为什么要这么设置
## 数据库说明
见当前目录下 db目录,py文件是创建脚本,db文件是sqlite数据库
## 文件说明
cookiesFile文件夹 存储cookie文件
myUtils文件夹 存储自己封装的python模块
videoFile文件夹 文件上传存放位置
web 文件夹 web路由目录
conf.py 全局配置,记得修改配置中 LOCAL_CHROME_PATH 为本机浏览器地址
-11
View File
@@ -1,11 +0,0 @@
# 开发环境配置
NODE_ENV=development
# API 基础地址
VITE_API_BASE_URL=/api
# 应用端口
VITE_PORT=5173
# 是否开启 mock
VITE_USE_MOCK=true
-8
View File
@@ -1,8 +0,0 @@
# 生产环境配置
NODE_ENV=production
# API 基础地址
VITE_API_BASE_URL=http://localhost:5409
# 是否开启 mock
VITE_USE_MOCK=false
-134
View File
@@ -1,134 +0,0 @@
# Vue3 + Vite 项目
一个基于 Vue3、Vite、Element Plus、Pinia、Vue Router 和 Axios 的现代化前端项目模板。
## 🚀 特性
- ⚡️ **Vite** - 极速的构建工具
- 🖖 **Vue 3** - 渐进式 JavaScript 框架
- 🎨 **Element Plus** - 基于 Vue 3 的组件库
- 🗂 **Vue Router** - 官方路由管理器(WebHash 模式)
- 📦 **Pinia** - 新一代状态管理
- 🔗 **Axios** - HTTP 请求库(已封装)
- 🎯 **Sass** - CSS 预处理器
- 📁 **规范化目录结构** - views 存放页面,components 存放组件
- 🔧 **完整配置** - 包含开发和生产环境配置
## 📦 安装
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build
# 预览生产构建
npm run preview
```
## 📁 项目结构
```
src/
├── api/ # API 接口
│ ├── index.js # API 统一导出
│ └── user.js # 用户相关 API
├── components/ # 公共组件
│ └── HelloWorld.vue # 示例组件
├── router/ # 路由配置
│ └── index.js # 路由主文件
├── stores/ # 状态管理
│ ├── index.js # Pinia 配置
│ └── user.js # 用户状态
├── styles/ # 样式文件
│ ├── index.scss # 主样式文件
│ ├── reset.scss # 重置样式
│ └── variables.scss # 样式变量
├── utils/ # 工具函数
│ └── request.js # HTTP 请求封装
├── views/ # 页面组件
│ ├── Home.vue # 首页
│ └── About.vue # 关于页面
├── App.vue # 根组件
└── main.js # 入口文件
```
## 🔧 配置说明
### 环境变量
- `.env` - 通用环境变量
- `.env.development` - 开发环境变量
- `.env.production` - 生产环境变量
### 路由配置
项目使用 Vue Router 4,配置为 WebHash 模式,路由文件位于 `src/router/index.js`。
### 状态管理
使用 Pinia 进行状态管理,store 文件位于 `src/stores/` 目录。
### HTTP 请求
Axios 已经过封装,包含:
- 请求/响应拦截器
- 错误处理
- Token 自动添加
- 统一的响应格式处理
使用方式:
```javascript
import { http } from '@/utils/request'
// GET 请求
const data = await http.get('/api/users')
// POST 请求
const result = await http.post('/api/users', { name: 'John' })
```
### 样式系统
- 使用 Sass 作为 CSS 预处理器
- 已删除所有浏览器默认样式
- 提供了完整的样式变量和工具类
- 支持 Element Plus 主题定制
## 🎨 组件库
项目集成了 Element Plus,所有组件都可以直接使用:
```vue
<template>
<el-button type="primary">按钮</el-button>
<el-input v-model="input" placeholder="请输入内容"></el-input>
</template>
```
## 📝 开发规范
1. **页面组件** 放在 `src/views/` 目录
2. **公共组件** 放在 `src/components/` 目录
3. **使用 setup 语法糖** 编写组件
4. **样式使用 Sass** 并遵循 BEM 命名规范
5. **API 请求** 统一放在 `src/api/` 目录
6. **状态管理** 按模块划分,放在 `src/stores/` 目录
## 🚀 部署
```bash
# 构建生产版本
npm run build
# 构建完成后,dist 目录包含所有静态文件
# 可以部署到任何静态文件服务器
```
## 📄 许可证
MIT License
-13
View File
@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SAU自媒体自动化运营系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
-25
View File
@@ -1,25 +0,0 @@
{
"name": "sau-admin",
"private": true,
"version": "0.0.0",
"author": "Edan.Lee",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.9.0",
"element-plus": "^2.9.11",
"pinia": "^3.0.2",
"sass": "^1.89.1",
"vue": "^3.5.13",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"vite": "^6.3.5"
}
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-198
View File
@@ -1,198 +0,0 @@
<template>
<div id="app">
<el-container>
<el-aside :width="isCollapse ? '64px' : '200px'">
<div class="sidebar">
<div class="logo">
<img v-show="isCollapse" src="/vite.svg" alt="Logo" class="logo-img">
<h2 v-show="!isCollapse">自媒体自动化运营系统</h2>
</div>
<el-menu
:router="true"
:default-active="activeMenu"
:collapse="isCollapse"
class="sidebar-menu"
background-color="#001529"
text-color="#fff"
active-text-color="#409EFF"
>
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/account-management">
<el-icon><User /></el-icon>
<span>账号管理</span>
</el-menu-item>
<el-menu-item index="/material-management">
<el-icon><Picture /></el-icon>
<span>素材管理</span>
</el-menu-item>
<el-menu-item index="/publish-center">
<el-icon><Upload /></el-icon>
<span>发布中心</span>
</el-menu-item>
<el-menu-item index="/about">
<el-icon><DataAnalysis /></el-icon>
<span>关于</span>
</el-menu-item>
</el-menu>
</div>
</el-aside>
<el-container>
<el-header>
<div class="header-content">
<div class="header-left">
<el-icon class="toggle-sidebar" @click="toggleSidebar"><Fold /></el-icon>
</div>
<div class="header-right">
<!-- 账号信息已移除 -->
</div>
</div>
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import {
HomeFilled, User, DataAnalysis,
Fold, Picture, Upload
} from '@element-plus/icons-vue'
const route = useRoute()
// 当前激活的菜单项
const activeMenu = computed(() => {
return route.path
})
// 侧边栏折叠状态
const isCollapse = ref(false)
// 切换侧边栏折叠状态
const toggleSidebar = () => {
isCollapse.value = !isCollapse.value
}
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
#app {
min-height: 100vh;
}
.el-container {
height: 100vh;
}
.el-aside {
background-color: #001529;
color: #fff;
height: 100vh;
overflow: hidden;
transition: width 0.3s;
.sidebar {
display: flex;
flex-direction: column;
height: 100%;
.logo {
height: 60px;
padding: 0 16px;
display: flex;
align-items: center;
background-color: #002140;
overflow: hidden;
.logo-img {
width: 32px;
height: 32px;
margin-right: 12px;
}
h2 {
color: #fff;
font-size: 16px;
font-weight: 600;
white-space: nowrap;
margin: 0;
}
}
.sidebar-menu {
border-right: none;
flex: 1;
.el-menu-item {
display: flex;
align-items: center;
.el-icon {
margin-right: 10px;
font-size: 18px;
}
}
}
}
}
.el-header {
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 0;
height: 60px;
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
padding: 0 16px;
.header-left {
.toggle-sidebar {
font-size: 20px;
cursor: pointer;
color: $text-regular;
&:hover {
color: $primary-color;
}
}
}
.header-right {
.user-dropdown {
display: flex;
align-items: center;
cursor: pointer;
.username {
margin: 0 8px;
color: $text-regular;
}
.el-icon {
font-size: 12px;
color: $text-secondary;
}
}
}
}
}
.el-main {
background-color: $bg-color-page;
padding: 20px;
overflow-y: auto;
}
</style>
-29
View File
@@ -1,29 +0,0 @@
import { http } from '@/utils/request'
// 账号管理相关API
export const accountApi = {
// 获取有效账号列表(带验证)
getValidAccounts() {
return http.get('/getValidAccounts')
},
// 获取账号列表(不带验证,快速加载)
getAccounts() {
return http.get('/getAccounts')
},
// 添加账号
addAccount(data) {
return http.post('/account', data)
},
// 更新账号
updateAccount(data) {
return http.post('/updateUserinfo', data)
},
// 删除账号
deleteAccount(id) {
return http.get(`/deleteAccount?id=${id}`)
}
}
-9
View File
@@ -1,9 +0,0 @@
// API 统一导出
export * from './user'
export * from './account'
export * from './material'
// 可以在这里添加其他API模块的导出
// export * from './product'
// export * from './order'
// export * from './common'
-30
View File
@@ -1,30 +0,0 @@
import { http } from '@/utils/request'
// 素材管理API
export const materialApi = {
// 获取所有素材
getAllMaterials: () => {
return http.get('/getFiles')
},
// 上传素材
uploadMaterial: (formData, onUploadProgress) => {
// 使用http.upload方法,它已经配置了正确的Content-Type
return http.upload('/uploadSave', formData, onUploadProgress)
},
// 删除素材
deleteMaterial: (id) => {
return http.get(`/deleteFile?id=${id}`)
},
// 下载素材
downloadMaterial: (filePath) => {
return `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'}/download/${filePath}`
},
// 获取素材预览URL
getMaterialPreviewUrl: (filename) => {
return `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'}/getFile?filename=${filename}`
}
}
-5
View File
@@ -1,5 +0,0 @@
import { http } from '@/utils/request'
// 用户相关API(预留)
// 注意:当前后端暂无用户认证接口,以下为预留定义
export const userApi = {}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

-20
View File
@@ -1,20 +0,0 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './styles/index.scss'
const app = createApp(App)
// 注册 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(pinia)
app.use(ElementPlus)
app.mount('#app')
-41
View File
@@ -1,41 +0,0 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
import AccountManagement from '../views/AccountManagement.vue'
import MaterialManagement from '../views/MaterialManagement.vue'
import PublishCenter from '../views/PublishCenter.vue'
import About from '../views/About.vue'
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard
},
{
path: '/account-management',
name: 'AccountManagement',
component: AccountManagement
},
{
path: '/material-management',
name: 'MaterialManagement',
component: MaterialManagement
},
{
path: '/publish-center',
name: 'PublishCenter',
component: PublishCenter
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
-62
View File
@@ -1,62 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAccountStore = defineStore('account', () => {
// 存储所有账号信息
const accounts = ref([])
// 平台类型映射
const platformTypes = {
1: '小红书',
2: '视频号',
3: '抖音',
4: '快手'
}
// 设置账号列表
const setAccounts = (accountsData) => {
// 转换后端返回的数据格式为前端使用的格式
accounts.value = accountsData.map(item => {
return {
id: item[0],
type: item[1],
filePath: item[2],
name: item[3],
status: item[4] === -1 ? '验证中' : (item[4] === 1 ? '正常' : '异常'),
platform: platformTypes[item[1]] || '未知'
}
})
}
// 添加账号
const addAccount = (account) => {
accounts.value.push(account)
}
// 更新账号
const updateAccount = (id, updatedAccount) => {
const index = accounts.value.findIndex(acc => acc.id === id)
if (index !== -1) {
accounts.value[index] = { ...accounts.value[index], ...updatedAccount }
}
}
// 删除账号
const deleteAccount = (id) => {
accounts.value = accounts.value.filter(acc => acc.id !== id)
}
// 根据平台获取账号
const getAccountsByPlatform = (platform) => {
return accounts.value.filter(acc => acc.platform === platform)
}
return {
accounts,
setAccounts,
addAccount,
updateAccount,
deleteAccount,
getAccountsByPlatform
}
})
-69
View File
@@ -1,69 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAppStore = defineStore('app', () => {
// 是否是第一次进入账号管理页面
const isFirstTimeAccountManagement = ref(true)
// 是否是第一次进入素材管理页面
const isFirstTimeMaterialManagement = ref(true)
// 账号管理页面刷新状态
const isAccountRefreshing = ref(false)
// 素材列表数据
const materials = ref([])
// 设置账号管理页面已访问
const setAccountManagementVisited = () => {
isFirstTimeAccountManagement.value = false
}
// 设置素材管理页面已访问
const setMaterialManagementVisited = () => {
isFirstTimeMaterialManagement.value = false
}
// 重置所有访问状态(用于重新登录或刷新应用时)
const resetVisitStatus = () => {
isFirstTimeAccountManagement.value = true
isFirstTimeMaterialManagement.value = true
}
// 更新素材列表
const setMaterials = (materialList) => {
materials.value = materialList
}
// 添加新素材
const addMaterial = (material) => {
materials.value.push(material)
}
// 删除素材
const removeMaterial = (materialId) => {
const index = materials.value.findIndex(m => m.id === materialId)
if (index > -1) {
materials.value.splice(index, 1)
}
}
// 设置账号管理页面刷新状态
const setAccountRefreshing = (status) => {
isAccountRefreshing.value = status
}
return {
isFirstTimeAccountManagement,
isFirstTimeMaterialManagement,
isAccountRefreshing,
materials,
setAccountManagementVisited,
setMaterialManagementVisited,
resetVisitStatus,
setMaterials,
addMaterial,
removeMaterial,
setAccountRefreshing
}
})
-9
View File
@@ -1,9 +0,0 @@
import { createPinia } from 'pinia'
import { useUserStore } from './user'
import { useAccountStore } from './account'
import { useAppStore } from './app'
const pinia = createPinia()
export default pinia
export { useUserStore, useAccountStore, useAppStore }
-31
View File
@@ -1,31 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const userInfo = ref({
name: '',
email: ''
})
const isLoggedIn = ref(false)
const setUserInfo = (info) => {
userInfo.value = info
isLoggedIn.value = true
}
const logout = () => {
userInfo.value = {
name: '',
email: ''
}
isLoggedIn.value = false
}
return {
userInfo,
isLoggedIn,
setUserInfo,
logout
}
})
-121
View File
@@ -1,121 +0,0 @@
// 导入重置样式
@use './reset.scss';
// 导入变量
@use './variables.scss' as *;
// 全局样式
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
color: $text-primary;
background-color: $bg-color-page;
}
#app {
min-height: 100vh;
}
// 通用工具类
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.flex {
display: flex;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex-column {
display: flex;
flex-direction: column;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
// 间距工具类
.m-0 { margin: 0; }
.mt-0 { margin-top: 0; }
.mr-0 { margin-right: 0; }
.mb-0 { margin-bottom: 0; }
.ml-0 { margin-left: 0; }
.m-1 { margin: $spacing-xs; }
.mt-1 { margin-top: $spacing-xs; }
.mr-1 { margin-right: $spacing-xs; }
.mb-1 { margin-bottom: $spacing-xs; }
.ml-1 { margin-left: $spacing-xs; }
.m-2 { margin: $spacing-sm; }
.mt-2 { margin-top: $spacing-sm; }
.mr-2 { margin-right: $spacing-sm; }
.mb-2 { margin-bottom: $spacing-sm; }
.ml-2 { margin-left: $spacing-sm; }
.m-3 { margin: $spacing-md; }
.mt-3 { margin-top: $spacing-md; }
.mr-3 { margin-right: $spacing-md; }
.mb-3 { margin-bottom: $spacing-md; }
.ml-3 { margin-left: $spacing-md; }
.m-4 { margin: $spacing-lg; }
.mt-4 { margin-top: $spacing-lg; }
.mr-4 { margin-right: $spacing-lg; }
.mb-4 { margin-bottom: $spacing-lg; }
.ml-4 { margin-left: $spacing-lg; }
.p-0 { padding: 0; }
.pt-0 { padding-top: 0; }
.pr-0 { padding-right: 0; }
.pb-0 { padding-bottom: 0; }
.pl-0 { padding-left: 0; }
.p-1 { padding: $spacing-xs; }
.pt-1 { padding-top: $spacing-xs; }
.pr-1 { padding-right: $spacing-xs; }
.pb-1 { padding-bottom: $spacing-xs; }
.pl-1 { padding-left: $spacing-xs; }
.p-2 { padding: $spacing-sm; }
.pt-2 { padding-top: $spacing-sm; }
.pr-2 { padding-right: $spacing-sm; }
.pb-2 { padding-bottom: $spacing-sm; }
.pl-2 { padding-left: $spacing-sm; }
.p-3 { padding: $spacing-md; }
.pt-3 { padding-top: $spacing-md; }
.pr-3 { padding-right: $spacing-md; }
.pb-3 { padding-bottom: $spacing-md; }
.pl-3 { padding-left: $spacing-md; }
.p-4 { padding: $spacing-lg; }
.pt-4 { padding-top: $spacing-lg; }
.pr-4 { padding-right: $spacing-lg; }
.pb-4 { padding-bottom: $spacing-lg; }
.pl-4 { padding-left: $spacing-lg; }
-119
View File
@@ -1,119 +0,0 @@
/* CSS Reset - 删除浏览器默认样式 */
/* 1. Use a more-intuitive box-sizing model */
*, *::before, *::after {
box-sizing: border-box;
}
/* 2. Remove default margin and padding */
* {
margin: 0;
padding: 0;
}
/* 3. Allow percentage-based heights in the application */
html, body {
height: 100%;
}
/* 4. Add accessible line-height and improve text rendering */
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
}
/* 5. Improve media defaults */
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
/* 6. Remove built-in form typography styles */
input, button, textarea, select {
font: inherit;
}
/* 7. Avoid text overflows */
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/* 8. Create a root stacking context */
#root, #__next, #app {
isolation: isolate;
}
/* 9. Remove list styles */
ul, ol {
list-style: none;
}
/* 10. Remove default button styles */
button {
background: none;
border: none;
cursor: pointer;
}
/* 11. Remove default link styles */
a {
text-decoration: none;
color: inherit;
}
/* 12. Remove default table styles */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* 13. Remove default fieldset styles */
fieldset {
border: none;
}
/* 14. Remove default legend styles */
legend {
display: table;
}
/* 15. Remove default details/summary styles */
details {
display: block;
}
summary {
display: list-item;
}
/* 16. Remove default hr styles */
hr {
border: none;
height: 1px;
background: #ccc;
}
/* 17. Remove default blockquote styles */
blockquote {
quotes: none;
}
blockquote:before,
blockquote:after {
content: '';
content: none;
}
/* 18. Remove default cite styles */
cite {
font-style: normal;
}
/* 19. Remove default address styles */
address {
font-style: normal;
}
-54
View File
@@ -1,54 +0,0 @@
// 颜色变量
$primary-color: #409eff;
$success-color: #67c23a;
$warning-color: #e6a23c;
$danger-color: #f56c6c;
$info-color: #909399;
// 文字颜色
$text-primary: #303133;
$text-regular: #606266;
$text-secondary: #909399;
$text-placeholder: #c0c4cc;
// 边框颜色
$border-base: #dcdfe6;
$border-light: #e4e7ed;
$border-lighter: #ebeef5;
$border-extra-light: #f2f6fc;
// 背景颜色
$bg-color: #ffffff;
$bg-color-page: #f2f3f5;
$bg-color-overlay: #ffffff;
// 字体大小
$font-size-extra-large: 20px;
$font-size-large: 18px;
$font-size-medium: 16px;
$font-size-base: 14px;
$font-size-small: 13px;
$font-size-extra-small: 12px;
// 间距
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;
// 圆角
$border-radius-base: 4px;
$border-radius-small: 2px;
$border-radius-round: 20px;
$border-radius-circle: 50%;
// 阴影
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
$box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
// 层级
$z-index-normal: 1;
$z-index-top: 1000;
$z-index-popper: 2000;
-100
View File
@@ -1,100 +0,0 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409',
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 可以在这里添加token等认证信息
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
const { data } = response
// 根据后端接口规范处理响应
if (data.code === 200 || data.success) {
return data
} else {
ElMessage.error(data.msg || data.message || '请求失败')
return Promise.reject(new Error(data.msg || data.message || '请求失败'))
}
},
(error) => {
console.error('响应错误:', error)
// 处理HTTP错误状态码
if (error.response) {
const { status } = error.response
switch (status) {
case 401:
ElMessage.error('未授权,请重新登录')
// 可以在这里处理登录跳转
break
case 403:
ElMessage.error('拒绝访问')
break
case 404:
ElMessage.error('请求地址不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
default:
ElMessage.error('网络错误')
}
} else {
ElMessage.error('网络连接失败')
}
return Promise.reject(error)
}
)
// 封装常用的请求方法
export const http = {
get(url, params) {
return request.get(url, { params })
},
post(url, data, config = {}) {
return request.post(url, data, config)
},
put(url, data, config = {}) {
return request.put(url, data, config)
},
delete(url, params) {
return request.delete(url, { params })
},
upload(url, formData, onUploadProgress) {
return request.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress
})
}
}
export default request
-114
View File
@@ -1,114 +0,0 @@
<template>
<div class="about">
<el-card class="about-card">
<div class="about-header">
<h1>自媒体自动化运营系统</h1>
<p class="version">social-auto-upload</p>
</div>
<el-divider />
<div class="about-section">
<h3>系统简介</h3>
<p>
本系统是一款强大的自动化工具,帮助内容创作者和运营人员一键将视频内容高效发布到多个国内外主流社交媒体平台。
支持视频上传、定时发布等功能。
</p>
</div>
<div class="about-section">
<h3>支持平台</h3>
<div class="platform-tags">
<el-tag type="danger">抖音</el-tag>
<el-tag type="success">快手</el-tag>
<el-tag type="warning">视频号</el-tag>
<el-tag type="info">小红书</el-tag>
</div>
</div>
<div class="about-section">
<h3>核心功能</h3>
<ul class="feature-list">
<li>多平台账号管理与登录状态维护</li>
<li>视频素材上传与管理</li>
<li>一键多平台发布</li>
<li>定时发布与批量发布</li>
<li>Cookie 导入导出</li>
</ul>
</div>
<div class="about-section">
<h3>技术栈</h3>
<div class="tech-tags">
<el-tag effect="plain">Vue 3</el-tag>
<el-tag effect="plain">Element Plus</el-tag>
<el-tag effect="plain">Pinia</el-tag>
<el-tag effect="plain">Flask</el-tag>
<el-tag effect="plain">Playwright</el-tag>
<el-tag effect="plain">SQLite</el-tag>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
// 关于页面组件
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
.about {
max-width: 700px;
margin: 0 auto;
.about-card {
.about-header {
text-align: center;
h1 {
color: $text-primary;
margin: 0 0 8px 0;
font-size: 24px;
}
.version {
color: $text-secondary;
font-size: 14px;
margin: 0;
}
}
.about-section {
margin-bottom: 24px;
h3 {
font-size: 16px;
color: $text-primary;
margin: 0 0 12px 0;
}
p {
color: $text-secondary;
line-height: 1.8;
margin: 0;
}
.platform-tags,
.tech-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.feature-list {
margin: 0;
padding-left: 20px;
color: $text-secondary;
line-height: 2;
}
}
}
}
</style>
File diff suppressed because it is too large Load Diff
-431
View File
@@ -1,431 +0,0 @@
<template>
<div class="dashboard">
<div class="page-header">
<h1>自媒体自动化运营系统</h1>
</div>
<div class="dashboard-content">
<el-row :gutter="20">
<!-- 账号统计卡片 -->
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-card-content">
<div class="stat-icon">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ accountStats.total }}</div>
<div class="stat-label">账号总数</div>
</div>
</div>
<div class="stat-footer">
<div class="stat-detail">
<span>正常: {{ accountStats.normal }}</span>
<span>异常: {{ accountStats.abnormal }}</span>
</div>
</div>
</el-card>
</el-col>
<!-- 平台统计卡片 -->
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-card-content">
<div class="stat-icon platform-icon">
<el-icon><Platform /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ platformStats.total }}</div>
<div class="stat-label">已接入平台</div>
</div>
</div>
<div class="stat-footer">
<div class="stat-detail">
<el-tooltip content="快手账号" placement="top">
<el-tag size="small" type="success">{{ platformStats.kuaishou }}</el-tag>
</el-tooltip>
<el-tooltip content="抖音账号" placement="top">
<el-tag size="small" type="danger">{{ platformStats.douyin }}</el-tag>
</el-tooltip>
<el-tooltip content="视频号账号" placement="top">
<el-tag size="small" type="warning">{{ platformStats.channels }}</el-tag>
</el-tooltip>
<el-tooltip content="小红书账号" placement="top">
<el-tag size="small" type="info">{{ platformStats.xiaohongshu }}</el-tag>
</el-tooltip>
</div>
</div>
</el-card>
</el-col>
<!-- 素材统计卡片 -->
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-card-content">
<div class="stat-icon content-icon">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ contentStats.total }}</div>
<div class="stat-label">素材总数</div>
</div>
</div>
<div class="stat-footer">
<div class="stat-detail">
<span>视频: {{ contentStats.videos }}</span>
<span>图片: {{ contentStats.images }}</span>
<span>其他: {{ contentStats.others }}</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 快捷操作区域 -->
<div class="quick-actions">
<h2>快捷操作</h2>
<el-row :gutter="20">
<el-col :span="6">
<el-card class="action-card" @click="navigateTo('/account-management')">
<div class="action-icon">
<el-icon><UserFilled /></el-icon>
</div>
<div class="action-title">账号管理</div>
<div class="action-desc">管理所有平台账号</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="action-card" @click="navigateTo('/material-management')">
<div class="action-icon">
<el-icon><Upload /></el-icon>
</div>
<div class="action-title">素材管理</div>
<div class="action-desc">上传和管理视频素材</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="action-card" @click="navigateTo('/publish-center')">
<div class="action-icon">
<el-icon><Timer /></el-icon>
</div>
<div class="action-title">发布中心</div>
<div class="action-desc">发布内容到各平台</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="action-card" @click="navigateTo('/about')">
<div class="action-icon">
<el-icon><DataAnalysis /></el-icon>
</div>
<div class="action-title">关于系统</div>
<div class="action-desc">查看系统信息</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 素材列表 -->
<div class="recent-tasks">
<div class="section-header">
<h2>最近上传素材</h2>
<el-button text @click="navigateTo('/material-management')">查看全部</el-button>
</div>
<el-table :data="recentMaterials" style="width: 100%" v-loading="loading">
<el-table-column prop="filename" label="文件名" width="300" />
<el-table-column prop="filesize" label="文件大小" width="120">
<template #default="scope">
{{ scope.row.filesize }} MB
</template>
</el-table-column>
<el-table-column prop="upload_time" label="上传时间" width="200" />
<el-table-column label="类型" width="100">
<template #default="scope">
<el-tag
:type="getFileTypeTag(scope.row.filename)"
effect="plain"
size="small"
>
{{ getFileType(scope.row.filename) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && recentMaterials.length === 0" description="暂无素材数据" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {
User, UserFilled, Platform, Document,
Upload, Timer, DataAnalysis
} from '@element-plus/icons-vue'
import { accountApi } from '@/api/account'
import { materialApi } from '@/api/material'
import { useAccountStore } from '@/stores/account'
import { useAppStore } from '@/stores/app'
const router = useRouter()
const accountStore = useAccountStore()
const appStore = useAppStore()
const loading = ref(false)
// 账号统计数据 - 从真实数据计算
const accountStats = computed(() => {
const accounts = accountStore.accounts
const normal = accounts.filter(a => a.status === '正常').length
const abnormal = accounts.filter(a => a.status !== '正常' && a.status !== '验证中').length
return {
total: accounts.length,
normal,
abnormal
}
})
// 平台统计数据 - 从真实数据计算
const platformStats = computed(() => {
const accounts = accountStore.accounts
const kuaishou = accounts.filter(a => a.platform === '快手').length
const douyin = accounts.filter(a => a.platform === '抖音').length
const channels = accounts.filter(a => a.platform === '视频号').length
const xiaohongshu = accounts.filter(a => a.platform === '小红书').length
// 统计有账号的平台数量
const total = [kuaishou, douyin, channels, xiaohongshu].filter(n => n > 0).length
return { total, kuaishou, douyin, channels, xiaohongshu }
})
// 素材统计数据 - 从真实数据计算
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const contentStats = computed(() => {
const materials = appStore.materials
const videos = materials.filter(m => videoExtensions.some(ext => m.filename.toLowerCase().endsWith(ext))).length
const images = materials.filter(m => imageExtensions.some(ext => m.filename.toLowerCase().endsWith(ext))).length
return {
total: materials.length,
videos,
images,
others: materials.length - videos - images
}
})
// 最近上传的素材(最多显示5条)
const recentMaterials = computed(() => {
return [...appStore.materials]
.sort((a, b) => new Date(b.upload_time) - new Date(a.upload_time))
.slice(0, 5)
})
// 获取文件类型
const getFileType = (filename) => {
if (videoExtensions.some(ext => filename.toLowerCase().endsWith(ext))) return '视频'
if (imageExtensions.some(ext => filename.toLowerCase().endsWith(ext))) return '图片'
return '其他'
}
// 获取文件类型标签颜色
const getFileTypeTag = (filename) => {
const type = getFileType(filename)
return { '视频': 'success', '图片': 'warning', '其他': 'info' }[type] || 'info'
}
// 导航到指定路由
const navigateTo = (path) => {
router.push(path)
}
// 加载数据
const fetchDashboardData = async () => {
loading.value = true
try {
// 并行获取账号和素材数据
const [accountRes, materialRes] = await Promise.allSettled([
accountApi.getAccounts(),
materialApi.getAllMaterials()
])
if (accountRes.status === 'fulfilled' && accountRes.value.code === 200) {
accountStore.setAccounts(accountRes.value.data)
}
if (materialRes.status === 'fulfilled' && materialRes.value.code === 200) {
appStore.setMaterials(materialRes.value.data)
}
} catch (error) {
console.error('获取仪表盘数据失败:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDashboardData()
})
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
.dashboard {
.page-header {
margin-bottom: 20px;
h1 {
font-size: 24px;
color: $text-primary;
margin: 0;
}
}
.dashboard-content {
.stat-card {
height: 140px;
margin-bottom: 20px;
.stat-card-content {
display: flex;
align-items: center;
margin-bottom: 15px;
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: rgba($primary-color, 0.1);
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
.el-icon {
font-size: 30px;
color: $primary-color;
}
&.platform-icon {
background-color: rgba($success-color, 0.1);
.el-icon {
color: $success-color;
}
}
&.content-icon {
background-color: rgba($info-color, 0.1);
.el-icon {
color: $info-color;
}
}
}
.stat-info {
.stat-value {
font-size: 24px;
font-weight: bold;
color: $text-primary;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: $text-secondary;
}
}
}
.stat-footer {
border-top: 1px solid $border-lighter;
padding-top: 10px;
.stat-detail {
display: flex;
justify-content: space-between;
color: $text-secondary;
font-size: 13px;
.el-tag {
margin-right: 5px;
}
}
}
}
.quick-actions {
margin: 20px 0 30px;
h2 {
font-size: 18px;
margin-bottom: 15px;
color: $text-primary;
}
.action-card {
height: 160px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.action-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: rgba($primary-color, 0.1);
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
.el-icon {
font-size: 24px;
color: $primary-color;
}
}
.action-title {
font-size: 16px;
font-weight: bold;
color: $text-primary;
margin-bottom: 5px;
}
.action-desc {
font-size: 13px;
color: $text-secondary;
text-align: center;
}
}
}
.recent-tasks {
margin-top: 30px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
h2 {
font-size: 18px;
color: $text-primary;
margin: 0;
}
}
}
}
}
</style>
@@ -1,557 +0,0 @@
<template>
<div class="material-management">
<div class="page-header">
<h1>素材管理</h1>
</div>
<div class="material-list-container">
<div class="material-search">
<el-input
v-model="searchKeyword"
placeholder="输入文件名搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleUploadMaterial">上传素材</el-button>
<el-button type="info" @click="fetchMaterials" :loading="false">
<el-icon :class="{ 'is-loading': isRefreshing }"><Refresh /></el-icon>
<span v-if="isRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredMaterials.length > 0" class="material-list">
<el-table :data="filteredMaterials" style="width: 100%">
<el-table-column prop="uuid" label="UUID" width="180" />
<el-table-column prop="filename" label="文件名" width="300" />
<el-table-column prop="filesize" label="文件大小" width="120">
<template #default="scope">
{{ scope.row.filesize }} MB
</template>
</el-table-column>
<el-table-column prop="upload_time" label="上传时间" width="180" />
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handlePreview(scope.row)">预览</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="empty-data">
<el-empty description="暂无素材数据" />
</div>
</div>
<!-- 上传对话框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传素材"
width="40%"
@close="handleUploadDialogClose"
>
<div class="upload-form">
<el-form label-width="80px">
<el-form-item label="文件名称:">
<el-input
v-model="customFilename"
placeholder="选填 (仅单个文件时生效)"
:disabled="customFilenameDisabled"
clearable
/>
</el-form-item>
<el-form-item label="选择文件">
<el-upload
class="upload-demo"
drag
multiple
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:file-list="fileList"
>
<el-icon class="el-icon--upload"><Upload /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持视频、图片等格式文件,可一次选择多个文件
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="上传列表" v-if="fileList.length > 0">
<div class="upload-file-list">
<div v-for="file in fileList" :key="file.uid" class="upload-file-item">
<span class="file-name">{{ file.name }}</span>
<el-progress
:percentage="uploadProgress[file.uid]?.percentage || 0"
:text-inside="true"
:stroke-width="20"
style="width: 100%; margin-top: 5px;"
>
<span>{{ uploadProgress[file.uid]?.speed || '' }}</span>
</el-progress>
</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpload" :loading="isUploading">
{{ isUploading ? '上传中' : '确认上传' }}
</el-button>
</div>
</template>
</el-dialog>
<!-- 预览对话框 -->
<el-dialog
v-model="previewDialogVisible"
title="素材预览"
width="50%"
:top="'10vh'"
>
<div class="preview-container" v-if="currentMaterial">
<div v-if="isVideoFile(currentMaterial.filename)" class="video-preview">
<video controls style="max-width: 100%; max-height: 60vh;">
<source :src="getPreviewUrl(currentMaterial.file_path)" type="video/mp4">
您的浏览器不支持视频播放
</video>
</div>
<div v-else-if="isImageFile(currentMaterial.filename)" class="image-preview">
<img :src="getPreviewUrl(currentMaterial.file_path)" style="max-width: 100%; max-height: 60vh;" />
</div>
<div v-else class="file-info">
<p>文件名: {{ currentMaterial.filename }}</p>
<p>文件大小: {{ currentMaterial.filesize }} MB</p>
<p>上传时间: {{ currentMaterial.upload_time }}</p>
<el-button type="primary" @click="downloadFile(currentMaterial)">下载文件</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { Refresh, Upload } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { materialApi } from '@/api/material'
import { useAppStore } from '@/stores/app'
// 获取应用状态管理
const appStore = useAppStore()
// 搜索和状态控制
const searchKeyword = ref('')
const isRefreshing = ref(false)
const isUploading = ref(false)
// 对话框控制
const uploadDialogVisible = ref(false)
const previewDialogVisible = ref(false)
const currentMaterial = ref(null)
// 文件上传
const fileList = ref([])
const customFilename = ref('')
const customFilenameDisabled = computed(() => fileList.value.length > 1)
const uploadProgress = ref({}); // { [uid]: { percentage: 0, speed: '' } }
watch(fileList, (newList) => {
if (newList.length <= 1) {
// If you want to clear the custom name when going back to single file, uncomment below
// customFilename.value = ''
}
});
// 获取素材列表
const fetchMaterials = async () => {
isRefreshing.value = true
try {
const response = await materialApi.getAllMaterials()
if (response.code === 200) {
appStore.setMaterials(response.data)
ElMessage.success('刷新成功')
} else {
ElMessage.error('获取素材列表失败')
}
} catch (error) {
console.error('获取素材列表出错:', error)
ElMessage.error('获取素材列表失败')
} finally {
isRefreshing.value = false
}
}
// 过滤素材
const filteredMaterials = computed(() => {
if (!searchKeyword.value) return appStore.materials
const keyword = searchKeyword.value.toLowerCase()
return appStore.materials.filter(material =>
material.filename.toLowerCase().includes(keyword)
)
})
// 搜索处理
const handleSearch = () => {
// 搜索逻辑已通过计算属性实现
}
// 上传素材
const handleUploadMaterial = () => {
// 清空变量
fileList.value = []
customFilename.value = ''
uploadProgress.value = {};
uploadDialogVisible.value = true
}
// 关闭上传对话框时清空变量
const handleUploadDialogClose = () => {
fileList.value = []
customFilename.value = ''
uploadProgress.value = {};
}
// 文件选择变更
const handleFileChange = (file, uploadFileList) => {
fileList.value = uploadFileList;
const newProgress = {};
for (const f of uploadFileList) {
newProgress[f.uid] = { percentage: 0, speed: '' };
}
uploadProgress.value = newProgress;
}
const handleFileRemove = (file, uploadFileList) => {
fileList.value = uploadFileList;
const newProgress = { ...uploadProgress.value };
delete newProgress[file.uid];
uploadProgress.value = newProgress;
}
// 提交上传
const submitUpload = async () => {
if (fileList.value.length === 0) {
ElMessage.warning('请选择要上传的文件')
return
}
isUploading.value = true
for (const file of fileList.value) {
try {
// 确保文件对象存在
if (!file || !file.raw) {
ElMessage.warning(`文件 ${file.name} 对象无效,已跳过`)
continue
}
const formData = new FormData()
formData.append('file', file.raw)
// 只有当只有一个文件时,自定义文件名才生效
if (fileList.value.length === 1 && customFilename.value.trim()) {
formData.append('filename', customFilename.value.trim())
}
let lastLoaded = 0;
let lastTime = Date.now();
const response = await materialApi.uploadMaterial(formData, (progressEvent) => {
const progressData = uploadProgress.value[file.uid];
if (!progressData) return;
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
progressData.percentage = progress;
const currentTime = Date.now();
const timeDiff = (currentTime - lastTime) / 1000; // in seconds
const loadedDiff = progressEvent.loaded - lastLoaded;
if (timeDiff > 0.5) { // Update speed every 0.5 seconds
const speed = loadedDiff / timeDiff; // bytes per second
if (speed > 1024 * 1024) {
progressData.speed = (speed / (1024 * 1024)).toFixed(2) + ' MB/s';
} else {
progressData.speed = (speed / 1024).toFixed(2) + ' KB/s';
}
lastLoaded = progressEvent.loaded;
lastTime = currentTime;
}
})
if (response.code === 200) {
ElMessage.success(`文件 ${file.name} 上传成功`)
const progressData = uploadProgress.value[file.uid];
if(progressData) progressData.speed = '完成';
} else {
ElMessage.error(`文件 ${file.name} 上传失败: ${response.msg || '未知错误'}`)
}
} catch (error) {
console.error(`上传文件 ${file.name} 出错:`, error)
ElMessage.error(`文件 ${file.name} 上传失败: ${error.message || '未知错误'}`)
}
}
isUploading.value = false
// Keep dialog open to show results
// uploadDialogVisible.value = false
await fetchMaterials()
}
// 预览素材
const handlePreview = async (material) => {
currentMaterial.value = null
previewDialogVisible.value = true
ElMessage.info('加载中...')
try {
// 等待一小段时间以确保对话框已打开
await new Promise(resolve => setTimeout(resolve, 100))
currentMaterial.value = material
} catch (error) {
console.error('预览素材出错:', error)
ElMessage.error('预览加载失败')
previewDialogVisible.value = false
}
}
// 删除素材
const handleDelete = (material) => {
ElMessageBox.confirm(
`确定要删除素材 ${material.filename} 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
const response = await materialApi.deleteMaterial(material.id)
if (response.code === 200) {
appStore.removeMaterial(material.id)
ElMessage.success('删除成功')
} else {
ElMessage.error(response.msg || '删除失败')
}
} catch (error) {
console.error('删除素材出错:', error)
ElMessage.error('删除失败')
}
})
.catch(() => {
// 取消删除
})
}
// 获取预览URL
const getPreviewUrl = (filePath) => {
const filename = filePath.split('/').pop()
return materialApi.getMaterialPreviewUrl(filename)
}
// 下载文件
const downloadFile = (material) => {
const url = materialApi.downloadMaterial(material.file_path)
window.open(url, '_blank')
}
// 判断文件类型
const isVideoFile = (filename) => {
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
return videoExtensions.some(ext => filename.toLowerCase().endsWith(ext))
}
const isImageFile = (filename) => {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
return imageExtensions.some(ext => filename.toLowerCase().endsWith(ext))
}
// 组件挂载时获取素材列表
onMounted(() => {
// 只有store中没有数据时才获取
if (appStore.materials.length === 0) {
fetchMaterials()
}
})
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.material-management {
.page-header {
margin-bottom: 20px;
h1 {
font-size: 24px;
font-weight: 500;
color: $text-primary;
margin: 0;
}
}
.material-list-container {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
.material-search {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.el-input {
width: 300px;
}
.action-buttons {
display: flex;
gap: 10px;
.is-loading {
animation: rotate 1s linear infinite;
}
}
}
.material-list {
margin-top: 20px;
}
.empty-data {
padding: 40px 0;
}
}
.material-upload {
width: 100%;
}
.preview-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 0 20px;
.file-info {
text-align: center;
margin-top: 20px;
}
}
}
.upload-form {
padding: 0 20px;
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.upload-demo {
width: 100%;
}
}
.dialog-footer {
padding: 0 20px;
display: flex;
justify-content: flex-end;
}
.upload-file-list {
width: 100%;
}
.upload-file-item {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.upload-file-item .file-name {
font-size: 14px;
color: #606266;
margin-bottom: 5px;
display: block;
}
/* 覆盖Element Plus对话框样式 */
:deep(.el-dialog__body) {
padding: 20px 0;
}
:deep(.el-dialog__header) {
padding-left: 20px;
padding-right: 20px;
margin-right: 0;
}
:deep(.el-dialog__footer) {
padding-top: 10px;
padding-bottom: 15px;
}
/* 修改上传进度条样式 */
:deep(.el-progress__text) {
color: #303133 !important; /* 深灰色字体,确保在各种背景上都可见 */
font-size: 12px;
}
:deep(.el-progress--line) {
margin-bottom: 10px;
}
.upload-file-item {
border: 1px solid #dcdfe6;
border-radius: 6px; /* 增加圆角 */
padding: 12px; /* 增加内边距 */
margin-bottom: 12px; /* 增加外边距 */
background-color: #fafafa; /* 轻微背景色 */
transition: box-shadow 0.3s; /* 添加过渡效果 */
}
.upload-file-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 悬停效果 */
}
.upload-file-item .file-name {
font-size: 14px;
color: #303133; /* 深灰色字体 */
margin-bottom: 8px; /* 增加底部间距 */
display: block;
font-weight: 500;
}
</style>
File diff suppressed because it is too large Load Diff
-45
View File
@@ -1,45 +0,0 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
css: {
preprocessorOptions: {
scss: {
// 移除自动导入,改用@use语法
}
}
},
server: {
port: 5173,
open: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:5409',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
sourcemap: false,
chunkSizeWarningLimit: 1600,
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
elementPlus: ['element-plus'],
utils: ['axios']
}
}
}
}
})
-54
View File
@@ -1,54 +0,0 @@
---
name: bilibili-upload
description: 当 agent 需要通过已安装的 `sau` CLI 完成 Bilibili 登录、账号校验或视频上传时使用这个 skill。优先使用 `sau bilibili ...`,程序会自动准备 `biliup`,不要求用户手动安装。
---
# Bilibili 上传 Skill
优先把 `sau` 作为主接口。
不要一开始就让用户自己找 `biliup` 或手动下载 release。
程序会在运行时自动检查、自动下载、自动更新 `biliup`。
## 功能概览
| 功能 | 命令入口 | 说明 |
| --- | --- | --- |
| 登录 | `sau bilibili login --account <name>` | 需要用户自己在本地真实终端里执行,用于生成或刷新登录信息 |
| 校验 | `sau bilibili check --account <name>` | 检查指定账号当前是否有效 |
| 视频上传 | `sau bilibili upload-video ...` | 上传一条 Bilibili 视频 |
## 默认工作流
1. 先确认 `references/runtime-requirements.md`
2. 再确认 `references/cli-contract.md`
3. 执行匹配的 `sau bilibili ...` 命令
4. 如果命令失败,再看 `references/troubleshooting.md`
## 命令选择建议
- 用户没有登录信息,先让用户自己在本地终端执行 `login`
- 用户只想确认账号状态,先用 `check`
- 用户要发视频,用 `upload-video`
## 执行前检查
- 优先确认当前环境能运行 `sau`
- 如果 `sau` 不在 PATH 中,可以用仓库里的 `sau_cli.py`
- 不要要求用户手动下载 `biliup`
- 第一次运行 Bilibili 命令时,程序可能会自动联网准备 `biliup`
- 对 agent 来说,不要在非交互环境里硬跑 `sau bilibili login`
- 正确做法是让用户自己在本地终端执行 `sau bilibili login --account <name>`
- 如果终端里的二维码显示不完整,提醒用户直接打开当前目录下的 `qrcode.png` 扫码
## 模板文件
- `scripts/examples/bilibili_commands.ps1`
- `scripts/examples/bilibili_commands.sh`
- `scripts/examples/bilibili_cli_template.py`
## 参考文档
- 运行前提:`references/runtime-requirements.md`
- CLI 契约:`references/cli-contract.md`
- 故障排查:`references/troubleshooting.md`
@@ -1,66 +0,0 @@
# Bilibili CLI 契约
这个 skill 默认假设当前环境已经安装并可调用 `sau` 命令。
## 命令列表
### 登录
```bash
sau bilibili login --account <account>
```
- 必填参数:
- `--account`
- 作用:
- 自动准备 `biliup`
- 触发 Bilibili 登录流程
- 账号说明:
- `--account` 传的是用户自定义的 `account_name`,不是固定只能叫 `creator`
- 一个 `account_name` 对应一个账号文件,可用于多账号隔离和并发任务
- 登录方式说明:
- 这个命令应该由用户自己在本地真实终端里运行
- 如果终端二维码显示不完整,可直接打开当前目录下的 `qrcode.png` 扫码
- agent 不应该在非交互环境里硬跑这个命令
### 校验账号
```bash
sau bilibili check --account <account>
```
- 必填参数:
- `--account`
- 预期输出:
- `valid`
- `invalid`
### 上传视频
```bash
sau bilibili upload-video \
--account <account> \
--file <video-path> \
--title "<title>" \
--desc "<desc>" \
--tid <category-id> \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"]
```
- 必填参数:
- `--account`
- `--file`
- `--title`
- `--desc`
- `--tid`
- 可选参数:
- `--tags`
- `--schedule`
## 额外说明
- `--tid` 第一版必须传
- `--tags` 使用逗号分隔
- `--schedule` 走 `sau` 统一时间格式
- 程序会自动准备和更新 `biliup`
@@ -1,49 +0,0 @@
# 运行前提
这个 skill 默认假设当前环境已经具备:
- 已安装 `social-auto-upload`
- 可以调用 `sau`,或者至少可以执行 `python sau_cli.py`
- 当前机器首次运行时可以联网访问 GitHub Release
## 关键说明
用户不需要自己安装 `biliup`。
当你执行 `sau bilibili ...` 时:
- 如果本地没有 `biliup`,程序会自动下载
- 如果上游 GitHub Release 有更新,程序会自动更新后再继续执行
## 推荐调用方式
### `sau` 已在 PATH 中
```bash
sau bilibili --help
```
### 仓库内直接调用
PowerShell:
```powershell
.\.venv\Scripts\python.exe sau_cli.py bilibili --help
```
bash / zsh:
```bash
python sau_cli.py bilibili --help
```
## 首次运行注意事项
- 首次运行可能比其他平台更慢,因为要自动准备 `biliup`
- 如果网络无法访问 GitHub Release,Bilibili 命令会失败
- 一旦本地已经准备好 `biliup`,后续命令会直接复用
- `sau bilibili login --account <name>` 需要用户自己在本地真实终端里执行
- 如果终端二维码显示不完整,通常可以直接打开当前目录下的 `qrcode.png` 扫码
- 如果国内网络访问 GitHub Release 较慢,可先用 `https://gh-proxy.com/` 或 `https://gh-proxy.org/` 辅助访问对应 release 地址排障
- 示例:
- `https://gh-proxy.org/https://github.com/biliup/biliup/releases/download/v1.1.29/biliupR-v1.1.29-aarch64-linux.tar.xz`
@@ -1,69 +0,0 @@
# Bilibili 常见问题
## 1. 首次运行很慢
这是正常情况。
原因通常是程序正在自动下载 `biliup`。
## 2. 自动下载失败
先检查:
- 当前网络是否能访问 GitHub
- GitHub Release 是否可访问
- 本地目录是否有写权限
- 国内网络较慢时,可先用 `https://gh-proxy.com/` 或 `https://gh-proxy.org/` 辅助访问对应 release 地址排障
- 示例:
- `https://gh-proxy.org/https://github.com/biliup/biliup/releases/download/v1.1.29/biliupR-v1.1.29-aarch64-linux.tar.xz`
## 3. `check` 返回 `invalid`
常见原因:
- 账号文件不存在
- 登录信息已失效
- `biliup renew` 失败
建议:
```bash
sau bilibili login --account <account>
```
## 4. 登录时报 `not a terminal`
常见原因:
- 你是在非交互环境里触发了 `sau bilibili login`
- 例如 agent 的命令执行器、管道环境、被接管标准输出的进程
建议:
- 改成由用户自己在本地真实终端里执行:
```bash
sau bilibili login --account <account>
```
- 如果终端里的二维码显示不完整,直接打开当前目录下的 `qrcode.png` 扫码
## 5. 上传失败
优先检查:
- `--tid` 是否正确
- 视频文件是否真实存在
- 标题、简介、标签是否符合平台要求
- 当前登录信息是否仍然有效
## 6. 上游更新后行为变化
当前 Bilibili 集成会自动跟随上游 `biliup` 最新 release。
如果上游命令行为变化,可能会影响本项目的 Bilibili CLI。
排障时请同时确认:
- 当前下载到的 `biliup` 版本
- 上游 release 是否有最近变更
@@ -1,40 +0,0 @@
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from conf import BASE_DIR
from utils.constant import VideoZoneTypes
def main() -> None:
account = "account_a"
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
cli_path = Path(BASE_DIR) / "sau_cli.py"
command = [
sys.executable,
str(cli_path),
"bilibili",
"upload-video",
"--account",
account,
"--file",
str(Path(BASE_DIR) / "videos" / "demo.mp4"),
"--title",
"Bilibili CLI Demo",
"--desc",
"Bilibili CLI Demo",
"--tid",
str(VideoZoneTypes.SPORTS_FOOTBALL.value),
"--tags",
"足球,测试",
"--schedule",
"2026-03-26 16:00",
]
subprocess.run(command, check=True)
if __name__ == "__main__":
main()
@@ -1,21 +0,0 @@
# 登录
# 建议由用户自己在本地真实终端里执行。
# 如果终端二维码显示不完整,可以打开当前目录下的 qrcode.png 扫码。
$account = "account_a"
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
sau bilibili login --account $account
# 校验
sau bilibili check --account $account
# 上传视频
sau bilibili upload-video `
--account $account `
--file .\videos\demo.mp4 `
--title "Bilibili CLI Demo" `
--desc "Bilibili CLI Demo" `
--tid 249 `
--tags 足球,测试 `
--schedule "2026-03-26 16:00"
@@ -1,21 +0,0 @@
# login
# Run this in a local interactive terminal.
# If the terminal QR code is incomplete, open ./qrcode.png and scan that image.
account="account_a"
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
sau bilibili login --account "$account"
# check
sau bilibili check --account "$account"
# upload video
sau bilibili upload-video \
--account "$account" \
--file ./videos/demo.mp4 \
--title "Bilibili CLI Demo" \
--desc "Bilibili CLI Demo" \
--tid 249 \
--tags 足球,测试 \
--schedule "2026-03-26 16:00"
-70
View File
@@ -1,70 +0,0 @@
---
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 ...` | 上传并发布抖音图文 |
元数据约定:
- 视频使用 `title + desc + tags`
- 图文使用 `title + note + tags`
## 默认工作流
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`
@@ -1,109 +0,0 @@
# 抖音 CLI 契约
这个 skill 默认假设当前环境已经安装并可调用 `sau` 命令。
## 命令列表
### 登录
```bash
sau douyin login --account <account>
```
- 必填参数:
- `--account`
- 作用:
- 启动抖音登录流程,为指定账号生成或刷新 cookie 文件
- 如果登录过程中生成本地二维码图片,agent 应优先直接把图片展示/发送给用户扫码,而不是只回传路径
- 账号说明:
- `--account` 传的是用户自定义的 `account_name`,不是固定只能叫 `creator`
- 一个 `account_name` 对应一个账号文件,可用于多账号隔离和并发任务
### 校验 cookie
```bash
sau douyin check --account <account>
```
- 必填参数:
- `--account`
- 预期输出:
- `valid`:cookie 可用
- `invalid`:cookie 缺失或已失效
### 上传视频
```bash
sau douyin upload-video \
--account <account> \
--file <video-path> \
--title "<title>" \
[--desc "<description>"] \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"] \
[--thumbnail <image-path>] \
[--product-link <url>] \
[--product-title "<title>"] \
[--debug] \
[--headless | --headed]
```
- 必填参数:
- `--account`
- `--file`
- `--title`
- 可选参数:
- `--desc`
- `--tags`
- `--schedule`
- `--thumbnail`
- `--product-link`
- `--product-title`
- `--debug`
- `--headless`
- `--headed`
### 上传图文
```bash
sau douyin upload-note \
--account <account> \
--images <image-1> [image-2 ...] \
--title "<title>" \
[--note "<content>"] \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"] \
[--debug] \
[--headless | --headed]
```
- 必填参数:
- `--account`
- `--images`
- `--title`
- 可选参数:
- `--note`
- `--tags`
- `--schedule`
- `--debug`
- `--headless`
- `--headed`
## 发布策略
- 如果不传 `--schedule`,CLI 使用立即发布
- 如果传了 `--schedule`,CLI 自动切换为定时发布
- 时间格式为:
```text
YYYY-MM-DD HH:MM
```
## 额外说明
- `upload-video` 每次命令只支持一个视频文件
- `upload-note` 每次命令支持多张图片
- 视频描述字段统一使用 `--desc`
- 图文正文统一使用 `--note`
- `upload-note` 当前不支持 GIF
- `upload-note` 当前最多支持 35 张图片
@@ -1,67 +0,0 @@
# 运行前提
这个 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 会通过控制台输出或临时图片路径提供二维码相关提示
- 如果登录过程中已经生成了本地二维码图片,agent 应优先直接把图片展示/发送给用户扫码,不要只告诉用户图片路径
@@ -1,87 +0,0 @@
# 故障排查
## 找不到 `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 打印出来的临时二维码图片
- agent 不要只把图片路径回给用户
- agent 应优先直接把本地二维码图片展示/发送给用户扫码
如果终端二维码显示不正常,优先使用保存下来的图片路径,而不是反复尝试随机的终端设置。
## 上传参数缺失
### 视频上传
最少需要:
- `--account`
- `--file`
- `--title`
### 图文上传
最少需要:
- `--account`
- `--images`
- `--title`
`--note` 当前是可选图文正文。
## 图片限制
对 `upload-note` 来说:
- 不支持 GIF
- 最多 35 张图片
如果超出这些限制,先减少图片数量或替换文件格式,再重试。
## 定时发布
时间格式使用:
```text
YYYY-MM-DD HH:MM
```
如果不需要定时发布,去掉 `--schedule` 即可改为立即发布。
@@ -1,62 +0,0 @@
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 = "account_a"
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
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",
"--desc",
"Douyin video description from Python",
"--tags",
"cli,video",
"--thumbnail",
"videos/demo.png",
"--headless",
],
[
"sau",
"douyin",
"upload-note",
"--account",
account,
"--images",
"videos/1.png",
"videos/2.png",
"--title",
"Douyin note title from Python",
"--note",
"Douyin note from Python",
"--tags",
"cli,note",
"--headless",
],
]
for command in commands:
run_command(command)
if __name__ == "__main__":
main()
@@ -1,28 +0,0 @@
# PowerShell examples for the installed sau CLI.
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
$account = "account_a"
$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" `
--desc "Douyin video description from PowerShell" `
--tags "cli,video" `
--thumbnail $thumbnail `
--headless
sau douyin upload-note `
--account $account `
--images $noteImages `
--title "Douyin note title from PowerShell" `
--note "Douyin note from PowerShell" `
--tags "cli,note" `
--headless
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
account="account_a"
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" \
--desc "Douyin video description from bash" \
--tags "cli,video" \
--thumbnail "$thumbnail" \
--headless
sau douyin upload-note \
--account "$account" \
--images "videos/1.png" "videos/2.png" \
--title "Douyin note title from bash" \
--note "Douyin note from bash" \
--tags "cli,note" \
--headless
-70
View File
@@ -1,70 +0,0 @@
---
name: kuaishou-upload
description: 当 agent 需要通过已安装的 `sau` CLI 完成快手登录、cookie 校验、视频上传或图文发布时使用这个 skill。该 skill 适用于已经安装 `social-auto-upload` 且可调用 `sau` 命令的环境。优先使用这个 skill 进行稳定的命令式快手工作流,而不是一开始就阅读 uploader 源码。
---
# 快手上传 Skill
优先把 `sau` 作为主接口。
不要假设当前环境一定能读取仓库源码。
不要一开始就去读 `uploader/`。
只有在命令不可用或 CLI 执行失败时,才回退到故障排查说明。
## 功能概览
| 功能 | 命令入口 | 说明 |
| --- | --- | --- |
| 快手登录 | `sau kuaishou login --account <name>` | 生成或刷新指定账号的 cookie |
| cookie 校验 | `sau kuaishou check --account <name>` | 检查指定账号 cookie 是否有效 |
| 视频上传 | `sau kuaishou upload-video ...` | 上传并发布快手视频 |
| 图文上传 | `sau kuaishou upload-note ...` | 上传并发布快手图文 |
元数据约定:
- 视频使用 `title + desc + tags`
- 图文使用 `title + note + tags`
## 默认工作流
1. 先确认 `references/runtime-requirements.md` 里的运行前提。
2. 再确认 `references/cli-contract.md` 里的命令契约。
3. 执行匹配的 `sau kuaishou ...` 命令。
4. 如果命令失败,再看 `references/troubleshooting.md`。
## 支持动作
- 使用 `sau kuaishou login --account <name>` 登录快手
- 使用 `sau kuaishou check --account <name>` 校验 cookie 是否有效
- 使用 `sau kuaishou upload-video ...` 上传快手视频
- 使用 `sau kuaishou upload-note ...` 上传快手图文
## 命令选择建议
- 当用户需要新的 cookie,或现有 cookie 已失效时,使用 `login`
- 当用户只需要确认 cookie 状态时,使用 `check`
- 当用户要发布视频时,使用 `upload-video`
- 当用户要发布图文时,使用 `upload-note`
## 执行前检查
- 先确认当前 shell 里是否可以调用 `sau`
- 如果 `sau` 不可用,按 `references/runtime-requirements.md` 里的回退方式处理
- 当用户明确指定无头或有头模式时,显式传 `--headless` 或 `--headed`
- 只有用户明确要求定时发布时,才使用 `--schedule`
- 如果登录流程生成了本地二维码图片,不要只把图片路径告诉用户
- 二维码图片本身就是给用户扫码的,优先直接把本地图片展示/发送给用户
## 模板文件
当你需要稳定的命令模板时,使用 `scripts/examples/` 下的文件:
- `kuaishou_commands.ps1`
- `kuaishou_commands.sh`
- `kuaishou_cli_template.py`
## 参考文档
- 运行前提:`references/runtime-requirements.md`
- CLI 契约:`references/cli-contract.md`
- 故障排查:`references/troubleshooting.md`
@@ -1,104 +0,0 @@
# 快手 CLI 契约
这个 skill 默认假设当前环境已经安装并可调用 `sau` 命令。
## 命令列表
### 登录
```bash
sau kuaishou login --account <account>
```
- 必填参数:
- `--account`
- 作用:
- 启动快手登录流程,为指定账号生成或刷新 cookie 文件
- 如果登录过程中生成本地二维码图片,agent 应优先直接把图片展示/发送给用户扫码,而不是只回传路径
- 账号说明:
- `--account` 传的是用户自定义的 `account_name`,不是固定只能叫 `creator`
- 一个 `account_name` 对应一个账号文件,可用于多账号隔离和并发任务
### 校验 cookie
```bash
sau kuaishou check --account <account>
```
- 必填参数:
- `--account`
- 预期输出:
- `valid`:cookie 可用
- `invalid`:cookie 缺失或已失效
### 上传视频
```bash
sau kuaishou upload-video \
--account <account> \
--file <video-path> \
--title "<title>" \
[--desc "<description>"] \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"] \
[--thumbnail <image-path>] \
[--debug] \
[--headless | --headed]
```
- 必填参数:
- `--account`
- `--file`
- `--title`
- 可选参数:
- `--desc`
- `--tags`
- `--schedule`
- `--thumbnail`
- `--debug`
- `--headless`
- `--headed`
### 上传图文
```bash
sau kuaishou upload-note \
--account <account> \
--images <image-1> [image-2 ...] \
--title "<title>" \
[--note "<content>"] \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"] \
[--debug] \
[--headless | --headed]
```
- 必填参数:
- `--account`
- `--images`
- `--title`
- 可选参数:
- `--note`
- `--tags`
- `--schedule`
- `--debug`
- `--headless`
- `--headed`
## 发布策略
- 如果不传 `--schedule`,CLI 使用立即发布
- 如果传了 `--schedule`,CLI 自动切换为定时发布
- 时间格式为:
```text
YYYY-MM-DD HH:MM
```
## 额外说明
- `upload-video` 每次命令只支持一个视频文件
- `upload-note` 每次命令支持多张图片
- 视频描述字段统一使用 `--desc`
- 图文正文统一使用 `--note`
- `upload-note` 当前要求传入真实的多张图片文件,而不是同一路径重复多次
@@ -1,68 +0,0 @@
# 运行前提
这个 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 kuaishou --help
```
### 如果虚拟环境存在,但还没有激活
PowerShell:
```powershell
.\.venv\Scripts\Activate.ps1
sau kuaishou --help
```
### 如果你想直接调用可执行文件
PowerShell:
```powershell
.\.venv\Scripts\sau.exe kuaishou --help
```
### 如果你更倾向于使用 uv
```bash
uv run sau kuaishou --help
```
## 无头和有头模式
- 使用 `--headless` 表示无头模式
- 使用 `--headed` 表示有头模式
- 快手 CLI 默认按无头模式运行
- 如果用户明确要求可见浏览器窗口,或二维码展示确实有问题,再切到 `--headed`
- 如果登录过程中已经生成了本地二维码图片,agent 应优先直接把图片展示/发送给用户扫码,不要只告诉用户图片路径
@@ -1,85 +0,0 @@
# 故障排查
## 找不到 `sau` 命令
可以尝试以下方式:
```powershell
.\.venv\Scripts\Activate.ps1
sau kuaishou --help
```
```powershell
.\.venv\Scripts\sau.exe kuaishou --help
```
```bash
uv run sau kuaishou --help
```
如果当前环境还没有安装项目:
```bash
uv pip install -e .
```
## cookie 无效或已过期
先检查 cookie 状态:
```bash
sau kuaishou check --account <account>
```
如果无效,就重新登录:
```bash
sau kuaishou login --account <account>
```
## 登录二维码问题
如果用户反馈二维码在终端里不好扫:
- 先优先使用 CLI / uploader 生成的本地二维码图片
- 只有用户明确需要可见浏览器窗口,或图片方案仍然不方便时,再切到 `--headed`
- 如果 CLI / uploader 已经生成临时二维码图片,agent 不要只回图片路径
- agent 应优先直接把本地二维码图片展示/发送给用户扫码
- 图片路径只作为补充信息
## 上传参数缺失
### 视频上传
最少需要:
- `--account`
- `--file`
- `--title`
### 图文上传
最少需要:
- `--account`
- `--images`
- `--title`
`--note` 当前是可选图文正文。
## 图文上传只有一张生效
如果用户一次传了多张图片,但页面只识别到一张,先确认:
- `--images` 里传的是不是真正不同的文件
- 不是同一路径重复多次
## 定时发布
时间格式使用:
```text
YYYY-MM-DD HH:MM
```
如果不需要定时发布,去掉 `--schedule` 即可改为立即发布。
@@ -1,63 +0,0 @@
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 = "account_a"
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
commands = [
["sau", "kuaishou", "login", "--account", account],
["sau", "kuaishou", "check", "--account", account],
[
"sau",
"kuaishou",
"upload-video",
"--account",
account,
"--file",
"videos/demo.mp4",
"--title",
"Kuaishou video from Python",
"--desc",
"Kuaishou video description from Python",
"--tags",
"cli,video",
"--thumbnail",
"videos/demo.png",
"--headless",
],
[
"sau",
"kuaishou",
"upload-note",
"--account",
account,
"--images",
"videos/1.png",
"videos/2.png",
"videos/3.png",
"--title",
"Kuaishou note title from Python",
"--note",
"Kuaishou note from Python",
"--tags",
"cli,note",
"--headless",
],
]
for command in commands:
run_command(command)
if __name__ == "__main__":
main()
@@ -1,28 +0,0 @@
# PowerShell examples for the installed sau CLI.
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
$account = "account_a"
$video = "videos/demo.mp4"
$thumbnail = "videos/demo.png"
$noteImages = @("videos/1.png", "videos/2.png", "videos/3.png")
sau kuaishou login --account $account
sau kuaishou check --account $account
sau kuaishou upload-video `
--account $account `
--file $video `
--title "Kuaishou video from PowerShell" `
--desc "Kuaishou video description from PowerShell" `
--tags "cli,video" `
--thumbnail $thumbnail `
--headless
sau kuaishou upload-note `
--account $account `
--images $noteImages `
--title "Kuaishou note title from PowerShell" `
--note "Kuaishou note from PowerShell" `
--tags "cli,note" `
--headless
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
account="account_a"
video="videos/demo.mp4"
thumbnail="videos/demo.png"
sau kuaishou login --account "$account"
sau kuaishou check --account "$account"
sau kuaishou upload-video \
--account "$account" \
--file "$video" \
--title "Kuaishou video from bash" \
--desc "Kuaishou video description from bash" \
--tags "cli,video" \
--thumbnail "$thumbnail" \
--headless
sau kuaishou upload-note \
--account "$account" \
--images "videos/1.png" "videos/2.png" "videos/3.png" \
--title "Kuaishou note title from bash" \
--note "Kuaishou note from bash" \
--tags "cli,note" \
--headless
-70
View File
@@ -1,70 +0,0 @@
---
name: xiaohongshu-upload
description: 当 agent 需要通过已安装的 `sau` CLI 完成小红书登录、cookie 校验、视频上传或图文发布时使用这个 skill。该 skill 适用于已经安装 `social-auto-upload` 且可调用 `sau` 命令的环境。优先使用这个 skill 进行稳定的命令式小红书工作流,而不是一开始就阅读 uploader 源码。
---
# 小红书上传 Skill
优先把 `sau` 作为主接口。
不要假设当前环境一定能读取仓库源码。
不要一开始就去读 `uploader/`。
只有在命令不可用或 CLI 执行失败时,才回退到故障排查说明。
## 功能概览
| 功能 | 命令入口 | 说明 |
| --- | --- | --- |
| 小红书登录 | `sau xiaohongshu login --account <name>` | 生成或刷新指定账号的 cookie |
| cookie 校验 | `sau xiaohongshu check --account <name>` | 检查指定账号 cookie 是否有效 |
| 视频上传 | `sau xiaohongshu upload-video ...` | 上传并发布小红书视频 |
| 图文上传 | `sau xiaohongshu upload-note ...` | 上传并发布小红书图文 |
元数据约定:
- 视频使用 `title + desc + tags`
- 图文使用 `title + note + tags`
## 默认工作流
1. 先确认 `references/runtime-requirements.md` 里的运行前提。
2. 再确认 `references/cli-contract.md` 里的命令契约。
3. 执行匹配的 `sau xiaohongshu ...` 命令。
4. 如果命令失败,再看 `references/troubleshooting.md`。
## 支持动作
- 使用 `sau xiaohongshu login --account <name>` 登录小红书
- 使用 `sau xiaohongshu check --account <name>` 校验 cookie 是否有效
- 使用 `sau xiaohongshu upload-video ...` 上传小红书视频
- 使用 `sau xiaohongshu upload-note ...` 上传小红书图文
## 命令选择建议
- 当用户需要新的 cookie,或现有 cookie 已失效时,使用 `login`
- 当用户只需要确认 cookie 状态时,使用 `check`
- 当用户要发布视频时,使用 `upload-video`
- 当用户要发布图文时,使用 `upload-note`
## 执行前检查
- 先确认当前 shell 里是否可以调用 `sau`
- 如果 `sau` 不可用,按 `references/runtime-requirements.md` 里的回退方式处理
- 当用户明确指定无头或有头模式时,显式传 `--headless` 或 `--headed`
- 只有用户明确要求定时发布时,才使用 `--schedule`
- 如果登录流程生成了本地二维码图片,不要只把图片路径告诉用户
- 二维码图片本身就是给用户扫码的,优先直接把本地图片展示/发送给用户
## 模板文件
当你需要稳定的命令模板时,使用 `scripts/examples/` 下的文件:
- `xiaohongshu_commands.ps1`
- `xiaohongshu_commands.sh`
- `xiaohongshu_cli_template.py`
## 参考文档
- 运行前提:`references/runtime-requirements.md`
- CLI 契约:`references/cli-contract.md`
- 故障排查:`references/troubleshooting.md`
@@ -1,103 +0,0 @@
# 小红书 CLI 契约
这个 skill 默认假设当前环境已经安装并可调用 `sau` 命令。
## 命令列表
### 登录
```bash
sau xiaohongshu login --account <account>
```
- 必填参数:
- `--account`
- 作用:
- 启动小红书登录流程,为指定账号生成或刷新 cookie 文件
- 如果登录过程中生成本地二维码图片,agent 应优先直接把图片展示/发送给用户扫码,而不是只回传路径
- 账号说明:
- `--account` 传的是用户自定义的 `account_name`,不是固定只能叫 `creator`
- 一个 `account_name` 对应一个账号文件,可用于多账号隔离和并发任务
### 校验 cookie
```bash
sau xiaohongshu check --account <account>
```
- 必填参数:
- `--account`
- 预期输出:
- `valid`:cookie 可用
- `invalid`:cookie 缺失或已失效
### 上传视频
```bash
sau xiaohongshu upload-video \
--account <account> \
--file <video-path> \
--title "<title>" \
[--desc "<description>"] \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"] \
[--thumbnail <image-path>] \
[--debug] \
[--headless | --headed]
```
- 必填参数:
- `--account`
- `--file`
- `--title`
- 可选参数:
- `--desc`
- `--tags`
- `--schedule`
- `--thumbnail`
- `--debug`
- `--headless`
- `--headed`
### 上传图文
```bash
sau xiaohongshu upload-note \
--account <account> \
--images <image-1> [image-2 ...] \
--title "<title>" \
[--note "<content>"] \
[--tags tag1,tag2] \
[--schedule "YYYY-MM-DD HH:MM"] \
[--debug] \
[--headless | --headed]
```
- 必填参数:
- `--account`
- `--images`
- `--title`
- 可选参数:
- `--note`
- `--tags`
- `--schedule`
- `--debug`
- `--headless`
- `--headed`
## 发布策略
- 如果不传 `--schedule`,CLI 使用立即发布
- 如果传了 `--schedule`,CLI 自动切换为定时发布
- 时间格式为:
```text
YYYY-MM-DD HH:MM
```
## 额外说明
- `upload-video` 每次命令只支持一个视频文件
- `upload-note` 每次命令支持多张图片
- 视频描述字段统一使用 `--desc`
- 图文正文统一使用 `--note`
@@ -1,67 +0,0 @@
# 运行前提
这个 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 xiaohongshu --help
```
### 如果虚拟环境存在,但还没有激活
PowerShell:
```powershell
.\.venv\Scripts\Activate.ps1
sau xiaohongshu --help
```
### 如果你想直接调用可执行文件
PowerShell:
```powershell
.\.venv\Scripts\sau.exe xiaohongshu --help
```
### 如果你更倾向于使用 uv
```bash
uv run sau xiaohongshu --help
```
## 无头和有头模式
- 使用 `--headless` 表示无头模式
- 使用 `--headed` 表示有头模式
- 如果用户明确要求无头登录,也要预期 CLI 会通过控制台输出或临时图片路径提供二维码相关提示
- 如果登录过程中已经生成了本地二维码图片,agent 应优先直接把图片展示/发送给用户扫码,不要只告诉用户图片路径
@@ -1,78 +0,0 @@
# 故障排查
## 找不到 `sau` 命令
可以尝试以下方式:
```powershell
.\.venv\Scripts\Activate.ps1
sau xiaohongshu --help
```
```powershell
.\.venv\Scripts\sau.exe xiaohongshu --help
```
```bash
uv run sau xiaohongshu --help
```
如果当前环境还没有安装项目:
```bash
uv pip install -e .
```
## cookie 无效或已过期
先检查 cookie 状态:
```bash
sau xiaohongshu check --account <account>
```
如果无效,就重新登录:
```bash
sau xiaohongshu login --account <account>
```
## 无头登录二维码处理
如果用户无法使用终端二维码输出:
- 查找 CLI 打印出来的临时二维码图片
- agent 不要只把图片路径回给用户
- agent 应优先直接把本地二维码图片展示/发送给用户扫码
如果终端二维码显示不正常,优先使用保存下来的图片路径,而不是反复尝试随机的终端设置。
## 上传参数缺失
### 视频上传
最少需要:
- `--account`
- `--file`
- `--title`
### 图文上传
最少需要:
- `--account`
- `--images`
- `--title`
`--note` 当前是可选图文正文。
## 定时发布
时间格式使用:
```text
YYYY-MM-DD HH:MM
```
如果不需要定时发布,去掉 `--schedule` 即可改为立即发布。
@@ -1,62 +0,0 @@
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 = "account_a"
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
commands = [
["sau", "xiaohongshu", "login", "--account", account, "--headless"],
["sau", "xiaohongshu", "check", "--account", account],
[
"sau",
"xiaohongshu",
"upload-video",
"--account",
account,
"--file",
"videos/demo.mp4",
"--title",
"Xiaohongshu video from Python",
"--desc",
"Xiaohongshu video description from Python",
"--tags",
"cli,video",
"--thumbnail",
"videos/demo.png",
"--headless",
],
[
"sau",
"xiaohongshu",
"upload-note",
"--account",
account,
"--images",
"videos/1.png",
"videos/2.png",
"--title",
"Xiaohongshu note title from Python",
"--note",
"Xiaohongshu note from Python",
"--tags",
"cli,note",
"--headless",
],
]
for command in commands:
run_command(command)
if __name__ == "__main__":
main()
@@ -1,28 +0,0 @@
# PowerShell examples for the installed sau CLI.
# account_name is user-defined. One account_name maps to one account file.
# You can prepare multiple account names and run them in parallel.
$account = "account_a"
$video = "videos/demo.mp4"
$thumbnail = "videos/demo.png"
$noteImages = @("videos/1.png", "videos/2.png")
sau xiaohongshu login --account $account --headless
sau xiaohongshu check --account $account
sau xiaohongshu upload-video `
--account $account `
--file $video `
--title "Xiaohongshu video from PowerShell" `
--desc "Xiaohongshu video description from PowerShell" `
--tags "cli,video" `
--thumbnail $thumbnail `
--headless
sau xiaohongshu upload-note `
--account $account `
--images $noteImages `
--title "Xiaohongshu note title from PowerShell" `
--note "Xiaohongshu note from PowerShell" `
--tags "cli,note" `
--headless

Some files were not shown because too many files have changed in this diff Show More