Showing preview only (335K chars total). Download the full file or copy to clipboard to get everything.
Repository: dreammis/social-auto-upload
Branch: main
Commit: b8b3107881f2
Files: 86
Total size: 313.5 KB
Directory structure:
gitextract_x6olz_e2/
├── .dockerignore
├── .gitignore
├── CLAUDE.md
├── Dockerfile
├── README.md
├── __init__.py
├── cli_main.py
├── conf.example.py
├── db/
│ └── createTable.py
├── examples/
│ ├── __init__.py
│ ├── get_baijiahao_cookie.py
│ ├── get_bilibili_cookie.py
│ ├── get_douyin_cookie.py
│ ├── get_kuaishou_cookie.py
│ ├── get_tencent_cookie.py
│ ├── get_tk_cookie.py
│ ├── get_xiaohongshu_cookie.py
│ ├── upload_video_to_baijiahao.py
│ ├── upload_video_to_bilibili.py
│ ├── upload_video_to_douyin.py
│ ├── upload_video_to_kuaishou.py
│ ├── upload_video_to_tencent.py
│ ├── upload_video_to_tiktok.py
│ ├── upload_video_to_xhs.py
│ └── upload_video_to_xiaohongshu.py
├── myUtils/
│ ├── __init__.py
│ ├── auth.py
│ ├── login.py
│ └── postVideo.py
├── requirements.txt
├── sau_backend/
│ └── README.md
├── sau_backend.py
├── sau_frontend/
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── App.vue
│ │ ├── api/
│ │ │ ├── account.js
│ │ │ ├── index.js
│ │ │ ├── material.js
│ │ │ └── user.js
│ │ ├── main.js
│ │ ├── router/
│ │ │ └── index.js
│ │ ├── stores/
│ │ │ ├── account.js
│ │ │ ├── app.js
│ │ │ ├── index.js
│ │ │ └── user.js
│ │ ├── styles/
│ │ │ ├── index.scss
│ │ │ ├── reset.scss
│ │ │ └── variables.scss
│ │ ├── utils/
│ │ │ └── request.js
│ │ └── views/
│ │ ├── About.vue
│ │ ├── AccountManagement.vue
│ │ ├── Dashboard.vue
│ │ ├── MaterialManagement.vue
│ │ └── PublishCenter.vue
│ └── vite.config.js
├── start-win.bat
├── uploader/
│ ├── __init__.py
│ ├── baijiahao_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── bilibili_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── douyin_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── ks_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── tencent_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── tk_uploader/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── main_chrome.py
│ │ └── tk_config.py
│ ├── xhs_uploader/
│ │ ├── __init__.py
│ │ ├── accounts.ini
│ │ ├── main.py
│ │ └── xhs_login_qrcode.py
│ └── xiaohongshu_uploader/
│ ├── __init__.py
│ └── main.py
├── utils/
│ ├── __init__.py
│ ├── base_social_media.py
│ ├── browser_hook.py
│ ├── constant.py
│ ├── files_times.py
│ ├── log.py
│ └── network.py
└── videos/
└── demo.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.venv
.git
.idea
cookies
conf.py
sau_frontend/dist
sau_frontend/node_modules
sau_frontend/package-lock.json
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.DS_Store
# ignore cookie file
tencent_uploader/*.json
youtube_uploader/*.json
douyin_uploader/*.json
bilibili_uploader/*.json
tk_uploader/*.json
# 配置文件
conf.py
# 账号文件
cookies
# Frontend
.vite/
dist/
node_modules/
package-lock.json
# database
db/database.db
# 临时文件夹
cookiesFile
uploadFile
videoFile
================================================
FILE: CLAUDE.md
================================================
## Project Overview
This project, `social-auto-upload`, is a powerful automation tool designed to help content creators and operators efficiently publish video content to multiple domestic and international mainstream social media platforms in one click. The project implements video upload, scheduled release and other functions for platforms such as `Douyin`, `Bilibili`, `Xiaohongshu`, `Kuaishou`, `WeChat Channel`, `Baijiahao` and `TikTok`.
The project consists of a Python backend and a Vue.js frontend.
**Backend:**
* Framework: Flask
* Core Functionality:
* Handles file uploads and management.
* Interacts with a SQLite database to store information about files and user accounts.
* Uses `playwright` for browser automation to interact with social media platforms.
* Provides a RESTful API for the frontend to consume.
* Uses Server-Sent Events (SSE) for real-time communication with the frontend during the login process.
**Frontend:**
* Framework: Vue.js
* Build Tool: Vite
* UI Library: Element Plus
* State Management: Pinia
* Routing: Vue Router
* Core Functionality:
* Provides a web interface for managing social media accounts, video files, and publishing videos.
* Communicates with the backend via a RESTful API.
**Command-line Interface:**
The project also provides a command-line interface (CLI) for users who prefer to work from the terminal. The CLI supports two main actions:
* `login`: To log in to a social media platform.
* `upload`: To upload a video to a social media platform, with an option to schedule the upload.
## Building and Running
### Backend
1. **Install dependencies:**
```bash
pip install -r requirements.txt
```
2. **Install Playwright browser drivers:**
```bash
playwright install chromium
```
3. **Initialize the database:**
```bash
python db/createTable.py
```
4. **Run the backend server:**
```bash
python sau_backend.py
```
The backend server will start on `http://localhost:5409`.
### Frontend
1. **Navigate to the frontend directory:**
```bash
cd sau_frontend
```
2. **Install dependencies:**
```bash
npm install
```
3. **Run the development server:**
```bash
npm run dev
```
The frontend development server will start on `http://localhost:5173`.
### Command-line Interface
To use the CLI, you can run the `cli_main.py` script with the appropriate arguments.
**Login:**
```bash
python cli_main.py <platform> <account_name> login
```
**Upload:**
```bash
python cli_main.py <platform> <account_name> upload <video_file> [-pt {0,1}] [-t YYYY-MM-DD HH:MM]
```
## Development Conventions
* The backend code is located in the root directory and the `myUtils` and `uploader` directories.
* The frontend code is located in the `sau_frontend` directory.
* The project uses a SQLite database for data storage. The database file is located at `db/database.db`.
* The `conf.example.py` file should be copied to `conf.py` and configured with the appropriate settings.
* The `requirements.txt` file lists the Python dependencies.
* The `package.json` file in the `sau_frontend` directory lists the frontend dependencies.
================================================
FILE: Dockerfile
================================================
FROM node:22.21.1 AS builder
WORKDIR /app
RUN npm config set registry https://registry.npmmirror.com
COPY sau_frontend .
RUN npm install
ENV NODE_ENV=production
ENV PATH=/app/node_modules/.bin:$PATH
# 替换前端中的地址
RUN sed -i 's#\${baseUrl}##g' /app/src/views/AccountManagement.vue
RUN sed -i "s#\${import\.meta\.env\.VITE_API_BASE_URL || 'http:\/\/localhost:5409'}##g" /app/src/api/material.js
RUN sed -i 's#localhost:5409##g' /app/.env.production
RUN npm run build
FROM python:3.10.19
WORKDIR /app
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
RUN apt-get update && apt-get install -y --no-install-recommends libnss3 \
libnspr4 \
libdbus-1-3 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatspi2.0-0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libxkbcommon0 \
libasound2 && rm -rf /var/lib/apt/lists/*
RUN pip config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN playwright install chromium-headless-shell
COPY . .
COPY --from=builder /app/dist/index.html /app
COPY --from=builder /app/dist/assets /app/assets
COPY --from=builder /app/dist/vite.svg /app/assets
RUN cp conf.example.py conf.py
RUN mkdir -p /app/videoFile
RUN mkdir -p /app/cookiesFile
EXPOSE 5409
CMD ["python", "sau_backend.py"]
================================================
FILE: README.md
================================================
# social-auto-upload
`social-auto-upload` 是一个强大的自动化工具,旨在帮助内容创作者和运营者高效地将视频内容一键发布到多个国内外主流社交媒体平台。
项目实现了对 `抖音`、`Bilibili`、`小红书`、`快手`、`视频号`、`百家号` 以及 `TikTok` 等平台的视频上传、定时发布等功能。
结合各平台 `uploader` 模块,您可以轻松配置和扩展支持的平台,并通过示例脚本快速上手。
<img src="media/show/tkupload.gif" alt="tiktok show" width="800"/>
## 目录
- [💡 功能特性](#💡功能特性)
- [🚀 支持的平台](#🚀支持的平台)
- [💾 安装指南](#💾安装指南)
- [🏁 快速开始](#🏁快速开始)
- [🐇 项目背景](#🐇项目背景)
- [📃 详细文档](#📃详细文档)
- [🐾 交流与支持](#🐾交流与支持)
- [🤝 贡献指南](#🤝贡献指南)
- [📜 许可证](#📜许可证)
- [⭐ Star History](#⭐Star-History)
## 💡功能特性
### 已支持平台
- **国内平台**:
- [x] 抖音
- [x] 视频号
- [x] Bilibili
- [x] 小红书
- [x] 快手
- [x] 百家号
- **国外平台**:
- [x] TikTok
### 核心功能
- [x] 定时上传 (Cron Job / Scheduled Upload)
- [ ] Cookie 管理 (部分实现,持续优化中)
- [ ] 国外平台 Proxy 设置 (部分实现)
### 计划支持与开发中
- **平台扩展**:
- [ ] YouTube
- **功能增强**:
- [x] 更易用的版本 (GUI / CLI 交互优化)
- [x] API 封装
- [x] Docker 部署
- [ ] 自动化上传 (更智能的调度策略)
- [ ] 多线程/异步上传优化
- [ ] Slack/消息推送通知
### 2025.10.30目前现状
该项目本人很长一段时间没维护了,有比较大的问题也是能简单快速修复就修复掉
因为我自己也在创业,每天时间都用不完
目前问题主要集中在
1. 小红书部分,这部分是直接适用xhs这个库来实现的
2. web 端(vue版本),这个版本是群友LeeDebug他帮忙做的(再次感谢他)
因为我日常也在用,我用的不是web端,而是最初`uploader`文件夹里的版本,也就是文档里提到的部分https://sap-doc.nasdaddy.com/
所以这里一般遇到的问题,我都会尝试去解决,一并推送到该仓库
目前能遇到的问题,基本上都比较小,可能是元素变化导致的
在初期设计的时候,其实我已经参考了某些不可变元素去选择,极大的避免了后期因为平台页面修改导致的元素变化
该项目不仅仅是技术人员,有不少是非技术的从业人员,他们是没能力修复一个简单弱小的bug
为了能帮助更多的人,所以呼吁**技术小伙伴**
如果大家
- 修复了一些bug
- 增加一些对大家有帮助的功能
请积极的提出pr,我会想尽可能的确认后合并的,在此感谢大家对于开源项目的支持,帮助更多的人
我自己也会尽100%的力量,在自己项目稳定后,修bug,加更多的平台,开发出gradio版本(更易部署),大家谅解
---
## 🚀支持的平台
本项目通过各平台对应的 `uploader` 模块实现视频上传功能。您可以在 `examples` 目录下找到各个平台的使用示例脚本。
每个示例脚本展示了如何配置和调用相应的 uploader。
## 💾安装指南
1. **克隆项目**:
```bash
git clone https://github.com/dreammis/social-auto-upload.git
cd social-auto-upload
```
2. **安装依赖**:
建议在虚拟环境中安装依赖。
```bash
conda create -n social-auto-upload python=3.10
conda activate social-auto-upload
# 挂载清华镜像 or 命令行代理
pip install -r requirements.txt
```
3. **安装 Playwright 浏览器驱动**:
```bash
playwright install chromium firefox
```
根据您的需求,至少需要安装 `chromium`。`firefox` 主要用于 TikTok 上传(旧版)。
4. **修改配置文件**:
复制 `conf.example.py` 并重命名为 `conf.py`。
在 `conf.py` 中,您需要配置以下内容:
- `LOCAL_CHROME_PATH`: 本地 Chrome 浏览器的路径,比如 `C:\Program Files\Google\Chrome\Application\chrome.exe` 保存。
**临时解决方案**
需要在根目录创建 `cookiesFile` 和 `videoFile` 两个文件夹,分别是 存储cookie文件 和 存储上传文件 的文件夹
5. **配置数据库**:
如果 db/database.db 文件不存在,您可以运行以下命令来初始化数据库:
```bash
cd db
python createTable.py
```
此命令将初始化 SQLite 数据库。
6. **启动后端项目**:
```bash
python sau_backend.py
```
后端项目将在 `http://localhost:5409` 启动。
7. **启动前端项目**:
```bash
cd sau_frontend
npm install
npm run dev
```
前端项目将在 `http://localhost:5173` 启动,在浏览器中打开此链接即可访问。
> 非程序员用户可以参考:[新手级教程](https://juejin.cn/post/7372114027840208911)
## 🏁快速开始
1. **准备 Cookie**:
大多数平台需要登录后的 Cookie 信息才能进行操作。请参照 examples 目录下各 `get_xxx_cookie.py` 脚本(例如 get_douyin_cookie.py, get_ks_cookie.py)的说明,运行脚本以生成并保存 Cookie 文件(通常在 `cookies/[PLATFORM]_uploader/account.json`)。
2. **准备视频文件**:
将需要上传的视频文件(通常为 `.mp4` 格式)放置在 videos 目录下。
部分平台支持视频封面,可以将封面图片(例如 `.png` 格式,与视频同名)也放在此目录。
如果需要上传标题及标签,请在视频文件旁边创建一个同名的 `.txt` 文件,内容为标题和标签,以换行分隔。
3. **修改并运行示例脚本**:
打开 examples 目录中您想使用的平台的上传脚本(例如 upload_video_to_douyin.py)。
- 根据脚本内的注释和说明,确认 Cookie 文件路径、视频文件路径等配置是否正确。
- 您可以修改脚本以适应您的具体需求,例如批量上传、自定义标题、标签等。
4. **执行上传**:
运行修改后的示例脚本,例如:
```bash
python examples/upload_video_to_douyin.py
```
## Docker 环境
### 自己构建镜像
1. **构建Docker镜像**:
```
docker build -t social-auto-upload:latest .
```
2. **运行Docker容器**:
```
docker run -d -it -p 5409:5409 social-auto-upload:latest
```
### 使用预构建镜像
1. **拉取镜像**:
```
docker pull gzxy/social-auto-upload:latest
```
2. **运行Docker容器**:
```
docker run -d -it -p 5409:5409 gzxy/social-auto-upload:latest
```
启动容器后访问:[http://localhost:5409](http://localhost:5409)
## 🐇项目背景
该项目最初是我个人用于自动化管理社交媒体视频发布的工具。我的主要发布策略是提前一天设置定时发布,因此项目中很多定时发布相关的逻辑是基于“第二天”的时间进行计算的。
如果您需要立即发布或其他定制化的发布策略,欢迎研究源码或在社区提问。
## 📃详细文档
更详细的文档和说明,请查看:[social-auto-upload 官方文档](https://sap-doc.nasdaddy.com/)
## 🐾交流与支持
[☕ Donate as u like](https://www.buymeacoffee.com/hysn2001m) - 如果您觉得这个项目对您有帮助,可以考虑赞助。
如果您也是独立开发者、技术爱好者,对 #技术变现 #AI创业 #跨境电商 #自动化工具 #视频创作 等话题感兴趣,欢迎加入社群交流。
### Creator
<table>
<td align="center">
<a href="https://sap-doc.nasdaddy.com/">
<img src="media/mp.jpg" width="200px" alt="NasDaddy公众号"/>
<br />
<sub><b>微信公众号</b></sub>
</a>
<br />
<a href="https://github.com/dreammis/social-auto-upload/commits?author=dreammis" title="Code">💻</a>
<br />
关注公众号,后台回复 `上传` 获取加群方式
</td>
<td align="center">
<a href="https://sap-doc.nasdaddy.com/">
<img src="media/QR.png" width="200px" alt="赞赏码/入群引导"/>
<br />
<sub><b>交流群 (通过公众号获取)</b></sub>
</a>
<br />
<a href="https://sap-doc.nasdaddy.com/" title="Documentation">📖</a>
<br />
如果您觉得项目有用,可以考虑打赏支持一下
</td>
</table>
### Active Core Team
<table>
<td align="center">
<a href="https://leedebug.github.io/">
<img src="media/edan-qrcode.png" width="200px" alt="Edan Lee"/>
<br />
<sub><b>Edan Lee</b></sub>
</a>
<br />
<a href="https://github.com/dreammis/social-auto-upload/commits?author=LeeDebug" title="Code">💻</a>
<a href="https://leedebug.github.io/" title="Documentation">📖</a>
<br />
封装了 api 接口和 web 前端管理界面
<br />
(请注明来意:进群、学习、企业咨询等)
</td>
</table>
## 🤝贡献指南
欢迎各种形式的贡献,包括但不限于:
- 提交 Bug报告 和 Feature请求。
- 改进代码、文档。
- 分享使用经验和教程。
如果您希望贡献代码,请遵循以下步骤:
1. Fork 本仓库。
2. 创建一个新的分支 (`git checkout -b feature/YourFeature` 或 `bugfix/YourBugfix`)。
3. 提交您的更改 (`git commit -m 'Add some feature'`)。
4. Push到您的分支 (`git push origin feature/YourFeature`)。
5. 创建一个 Pull Request。
## 📜许可证
本项目暂时采用 [MIT License](LICENSE) 开源许可证。
## ⭐Star-History
> 如果这个项目对您有帮助,请给一个 ⭐ Star 以表示支持!
[](https://star-history.com/#dreammis/social-auto-upload&Date)
================================================
FILE: __init__.py
================================================
================================================
FILE: cli_main.py
================================================
import argparse
import asyncio
from datetime import datetime
from os.path import exists
from pathlib import Path
from conf import BASE_DIR
from uploader.douyin_uploader.main import douyin_setup, DouYinVideo
from uploader.ks_uploader.main import ks_setup, KSVideo
from uploader.tencent_uploader.main import weixin_setup, TencentVideo
from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo
from utils.base_social_media import get_supported_social_media, get_cli_action, SOCIAL_MEDIA_DOUYIN, \
SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU
from utils.constant import TencentZoneTypes
from utils.files_times import get_title_and_hashtags
def parse_schedule(schedule_raw):
if schedule_raw:
schedule = datetime.strptime(schedule_raw, '%Y-%m-%d %H:%M')
else:
schedule = None
return schedule
async def main():
# 主解析器
parser = argparse.ArgumentParser(description="Upload video to multiple social-media.")
parser.add_argument("platform", metavar='platform', choices=get_supported_social_media(), help="Choose social-media platform: douyin tencent tiktok kuaishou")
parser.add_argument("account_name", type=str, help="Account name for the platform: xiaoA")
subparsers = parser.add_subparsers(dest="action", metavar='action', help="Choose action", required=True)
actions = get_cli_action()
for action in actions:
action_parser = subparsers.add_parser(action, help=f'{action} operation')
if action == 'login':
# Login 不需要额外参数
continue
elif action == 'upload':
action_parser.add_argument("video_file", help="Path to the Video file")
action_parser.add_argument("-pt", "--publish_type", type=int, choices=[0, 1],
help="0 for immediate, 1 for scheduled", default=0)
action_parser.add_argument('-t', '--schedule', help='Schedule UTC time in %Y-%m-%d %H:%M format')
# 解析命令行参数
args = parser.parse_args()
# 参数校验
if args.action == 'upload':
if not exists(args.video_file):
raise FileNotFoundError(f'Could not find the video file at {args["video_file"]}')
if args.publish_type == 1 and not args.schedule:
parser.error("The schedule must must be specified for scheduled publishing.")
account_file = Path(BASE_DIR / "cookies" / f"{args.platform}_{args.account_name}.json")
account_file.parent.mkdir(exist_ok=True)
# 根据 action 处理不同的逻辑
if args.action == 'login':
print(f"Logging in with account {args.account_name} on platform {args.platform}")
if args.platform == SOCIAL_MEDIA_DOUYIN:
await douyin_setup(str(account_file), handle=True)
elif args.platform == SOCIAL_MEDIA_TIKTOK:
await tiktok_setup(str(account_file), handle=True)
elif args.platform == SOCIAL_MEDIA_TENCENT:
await weixin_setup(str(account_file), handle=True)
elif args.platform == SOCIAL_MEDIA_KUAISHOU:
await ks_setup(str(account_file), handle=True)
elif args.action == 'upload':
title, tags = get_title_and_hashtags(args.video_file)
video_file = args.video_file
if args.publish_type == 0:
print("Uploading immediately...")
publish_date = 0
else:
print("Scheduling videos...")
publish_date = parse_schedule(args.schedule)
if args.platform == SOCIAL_MEDIA_DOUYIN:
await douyin_setup(account_file, handle=False)
app = DouYinVideo(title, video_file, tags, publish_date, account_file)
elif args.platform == SOCIAL_MEDIA_TIKTOK:
await tiktok_setup(account_file, handle=True)
app = TiktokVideo(title, video_file, tags, publish_date, account_file)
elif args.platform == SOCIAL_MEDIA_TENCENT:
await weixin_setup(account_file, handle=True)
category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传
app = TencentVideo(title, video_file, tags, publish_date, account_file, category)
elif args.platform == SOCIAL_MEDIA_KUAISHOU:
await ks_setup(account_file, handle=True)
app = KSVideo(title, video_file, tags, publish_date, account_file)
else:
print("Wrong platform, please check your input")
exit()
await app.main()
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: conf.example.py
================================================
from pathlib import Path
BASE_DIR = Path(__file__).parent.resolve()
XHS_SERVER = "http://127.0.0.1:11901"
LOCAL_CHROME_PATH = "" # change me necessary! for example C:/Program Files/Google/Chrome/Application/chrome.exe
LOCAL_CHROME_HEADLESS = False
================================================
FILE: db/createTable.py
================================================
import sqlite3
import json
import os
# 数据库文件路径(如果不存在会自动创建)
db_file = './database.db'
# 如果数据库已存在,则删除旧的表(可选)
# if os.path.exists(db_file):
# os.remove(db_file)
# 连接到SQLite数据库(如果文件不存在则会自动创建)
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
# 创建账号记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type INTEGER NOT NULL,
filePath TEXT NOT NULL, -- 存储文件路径
userName TEXT NOT NULL,
status INTEGER DEFAULT 0
)
''')
# 创建文件记录表
cursor.execute('''CREATE TABLE IF NOT EXISTS file_records (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 唯一标识每条记录
filename TEXT NOT NULL, -- 文件名
filesize REAL, -- 文件大小(单位:MB)
upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 上传时间,默认当前时间
file_path TEXT -- 文件路径
)
''')
# 提交更改
conn.commit()
print("✅ 表创建成功")
# 关闭连接
conn.close()
================================================
FILE: examples/__init__.py
================================================
================================================
FILE: examples/get_baijiahao_cookie.py
================================================
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))
================================================
FILE: examples/get_bilibili_cookie.py
================================================
# cd uploader/bilibili_uploader
# biliup.exe -u account.json login
================================================
FILE: examples/get_douyin_cookie.py
================================================
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))
================================================
FILE: examples/get_kuaishou_cookie.py
================================================
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.ks_uploader.main import ks_setup
if __name__ == '__main__':
account_file = Path(BASE_DIR / "cookies" / "ks_uploader" / "account.json")
account_file.parent.mkdir(exist_ok=True)
cookie_setup = asyncio.run(ks_setup(str(account_file), handle=True))
================================================
FILE: examples/get_tencent_cookie.py
================================================
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.tencent_uploader.main import weixin_setup
if __name__ == '__main__':
account_file = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json")
account_file.parent.mkdir(exist_ok=True)
cookie_setup = asyncio.run(weixin_setup(str(account_file), handle=True))
================================================
FILE: examples/get_tk_cookie.py
================================================
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))
================================================
FILE: examples/get_xiaohongshu_cookie.py
================================================
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)
cookie_setup = asyncio.run(xiaohongshu_setup(str(account_file), handle=True))
================================================
FILE: examples/upload_video_to_baijiahao.py
================================================
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)
================================================
FILE: examples/upload_video_to_bilibili.py
================================================
import time
from pathlib import Path
from uploader.bilibili_uploader.main import read_cookie_json_file, extract_keys_from_json, random_emoji, BilibiliUploader
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"
# how to get cookie, see the file of get_bilibili_cookie.py.
account_file = Path(BASE_DIR / "cookies" / "bilibili_uploader" / "account.json")
if not account_file.exists():
print(f"{account_file.name} 配置文件不存在")
exit()
cookie_data = read_cookie_json_file(account_file)
cookie_data = extract_keys_from_json(cookie_data)
tid = VideoZoneTypes.SPORTS_FOOTBALL.value # 设置分区id
# 获取视频目录
folder_path = Path(filepath)
# 获取文件夹中的所有文件
files = list(folder_path.glob("*.mp4"))
file_num = len(files)
timestamps = generate_schedule_time_next_day(file_num, 1, daily_times=[16], timestamps=True)
for index, file in enumerate(files):
title, tags = get_title_and_hashtags(str(file))
# just avoid error, bilibili don't allow same title of video.
title += random_emoji()
tags_str = ','.join([tag for tag in tags])
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
# I set desc same as title, do what u like.
desc = title
bili_uploader = BilibiliUploader(cookie_data, file, title, desc, tid, tags, timestamps[index])
bili_uploader.upload()
# life is beautiful don't so rush. be kind be patience
time.sleep(30)
================================================
FILE: examples/upload_video_to_douyin.py
================================================
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.douyin_uploader.main import douyin_setup, DouYinVideo
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" / "douyin_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(douyin_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}")
# 暂时没有时间修复封面上传,故先隐藏掉该功能
# if thumbnail_path.exists():
# app = DouYinVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path=thumbnail_path)
# else:
app = DouYinVideo(title, file, tags, publish_datetimes[index], account_file)
asyncio.run(app.main(), debug=False)
================================================
FILE: examples/upload_video_to_kuaishou.py
================================================
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.ks_uploader.main import ks_setup, KSVideo
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" / "ks_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(ks_setup(account_file, handle=False))
for index, file in enumerate(files):
title, tags = get_title_and_hashtags(str(file))
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
app = KSVideo(title, file, tags, publish_datetimes[index], account_file)
asyncio.run(app.main(), debug=False)
================================================
FILE: examples/upload_video_to_tencent.py
================================================
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.tencent_uploader.main import weixin_setup, TencentVideo
from utils.constant import TencentZoneTypes
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" / "tencent_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(weixin_setup(account_file, handle=True))
category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传
for index, file in enumerate(files):
title, tags = get_title_and_hashtags(str(file))
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
app = TencentVideo(title, file, tags, publish_datetimes[index], account_file, category)
asyncio.run(app.main(), debug=False)
================================================
FILE: examples/upload_video_to_tiktok.py
================================================
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)
================================================
FILE: examples/upload_video_to_xhs.py
================================================
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)
================================================
FILE: examples/upload_video_to_xiaohongshu.py
================================================
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.xiaohongshu_uploader.main import xiaohongshu_setup, XiaoHongShuVideo
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" / "xiaohongshu_uploader" / "58a391ba-4082-11f0-a321-44e51723d63c.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(xiaohongshu_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}")
# 暂时没有时间修复封面上传,故先隐藏掉该功能
# if thumbnail_path.exists():
# app = XiaoHongShuVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path=thumbnail_path)
# else:
app = XiaoHongShuVideo(title, file, tags, 0, account_file)
asyncio.run(app.main(), debug=False)
================================================
FILE: myUtils/__init__.py
================================================
================================================
FILE: myUtils/auth.py
================================================
import asyncio
import configparser
import os
from playwright.async_api import async_playwright
from xhs import XhsClient
from conf import BASE_DIR, LOCAL_CHROME_HEADLESS
from utils.base_social_media import set_init_script
from utils.log import tencent_logger, kuaishou_logger, douyin_logger
from pathlib import Path
from uploader.xhs_uploader.main import sign_local
async def cookie_auth_douyin(account_file):
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS)
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
# 创建一个新的页面
page = await context.new_page()
# 访问指定的 URL
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
try:
await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000)
# 2024.06.17 抖音创作者中心改版
# 判断
# 等待“扫码登录”元素出现,超时 5 秒(如果 5 秒没出现,说明 cookie 有效)
try:
await page.get_by_text("扫码登录").wait_for(timeout=5000)
douyin_logger.error("[+] cookie 失效,需要扫码登录")
return False
except:
douyin_logger.success("[+] cookie 有效")
return True
except:
douyin_logger.error("[+] 等待5秒 cookie 失效")
await context.close()
await browser.close()
return False
async def cookie_auth_tencent(account_file):
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS)
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
# 创建一个新的页面
page = await context.new_page()
# 访问指定的 URL
await page.goto("https://channels.weixin.qq.com/platform/post/create")
try:
await page.wait_for_selector('div.title-name:has-text("微信小店")', timeout=5000) # 等待5秒
tencent_logger.error("[+] 等待5秒 cookie 失效")
return False
except:
tencent_logger.success("[+] cookie 有效")
return True
async def cookie_auth_ks(account_file):
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS)
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
# 创建一个新的页面
page = await context.new_page()
# 访问指定的 URL
await page.goto("https://cp.kuaishou.com/article/publish/video")
try:
await page.wait_for_selector("div.names div.container div.name:text('机构服务')", timeout=5000) # 等待5秒
kuaishou_logger.info("[+] 等待5秒 cookie 失效")
return False
except:
kuaishou_logger.success("[+] cookie 有效")
return True
async def cookie_auth_xhs(account_file):
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS)
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
# 创建一个新的页面
page = await context.new_page()
# 访问指定的 URL
await page.goto("https://creator.xiaohongshu.com/creator-micro/content/upload")
try:
await page.wait_for_url("https://creator.xiaohongshu.com/creator-micro/content/upload", timeout=5000)
except:
print("[+] 等待5秒 cookie 失效")
await context.close()
await browser.close()
return False
# 2024.06.17 抖音创作者中心改版
if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count():
print("[+] 等待5秒 cookie 失效")
return False
else:
print("[+] cookie 有效")
return True
async def check_cookie(type, file_path):
match type:
# 小红书
case 1:
return await cookie_auth_xhs(Path(BASE_DIR / "cookiesFile" / file_path))
# 视频号
case 2:
return await cookie_auth_tencent(Path(BASE_DIR / "cookiesFile" / file_path))
# 抖音
case 3:
return await cookie_auth_douyin(Path(BASE_DIR / "cookiesFile" / file_path))
# 快手
case 4:
return await cookie_auth_ks(Path(BASE_DIR / "cookiesFile" / file_path))
case _:
return False
# a = asyncio.run(check_cookie(1,"3a6cfdc0-3d51-11f0-8507-44e51723d63c.json"))
# print(a)
================================================
FILE: myUtils/login.py
================================================
import asyncio
import sqlite3
from playwright.async_api import async_playwright
from myUtils.auth import check_cookie
from utils.base_social_media import set_init_script
import uuid
from pathlib import Path
from conf import BASE_DIR, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH
# 统一获取浏览器启动配置(防风控+引入本地浏览器)
def get_browser_options():
options = {
'headless': LOCAL_CHROME_HEADLESS,
'args': [
'--disable-blink-features=AutomationControlled', # 核心防爬屏蔽:去掉 window.navigator.webdriver 标签
'--lang=zh-CN',
'--disable-infobars',
'--start-maximized'
]
}
# 如果用户在 conf.py 里配置了本地 Chrome,就用本地的,这样成功率极高
if LOCAL_CHROME_PATH:
options['executable_path'] = LOCAL_CHROME_PATH
return options
# 抖音登录
async def douyin_cookie_gen(id,status_queue):
url_changed_event = asyncio.Event()
async def on_url_change():
# 检查是否是主框架的变化
if page.url != original_url:
url_changed_event.set()
async with async_playwright() as playwright:
options = get_browser_options()
# Make sure to run headed.
browser = await playwright.chromium.launch(**options)
# Setup context however you like.
context = await browser.new_context() # Pass any options
context = await set_init_script(context)
# Pause the page, and start recording manually.
page = await context.new_page()
await page.goto("https://creator.douyin.com/")
original_url = page.url
img_locator = page.get_by_role("img", name="二维码")
# 获取 src 属性值
src = await img_locator.get_attribute("src")
print("✅ 图片地址:", src)
status_queue.put(src)
# 监听页面的 'framenavigated' 事件,只关注主框架的变化
page.on('framenavigated',
lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None)
try:
# 等待 URL 变化或超时
await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒
print("监听页面跳转成功")
except asyncio.TimeoutError:
print("监听页面跳转超时")
await page.close()
await context.close()
await browser.close()
status_queue.put("500")
return None
uuid_v1 = uuid.uuid1()
print(f"UUID v1: {uuid_v1}")
# 确保cookiesFile目录存在
cookies_dir = Path(BASE_DIR / "cookiesFile")
cookies_dir.mkdir(exist_ok=True)
await context.storage_state(path=cookies_dir / f"{uuid_v1}.json")
result = await check_cookie(3, f"{uuid_v1}.json")
if not result:
status_queue.put("500")
await page.close()
await context.close()
await browser.close()
return None
await page.close()
await context.close()
await browser.close()
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_info (type, filePath, userName, status)
VALUES (?, ?, ?, ?)
''', (3, f"{uuid_v1}.json", id, 1))
conn.commit()
print("✅ 用户状态已记录")
status_queue.put("200")
# 视频号登录
async def get_tencent_cookie(id,status_queue):
url_changed_event = asyncio.Event()
async def on_url_change():
# 检查是否是主框架的变化
if page.url != original_url:
url_changed_event.set()
async with async_playwright() as playwright:
options = {
'args': [
'--lang en-GB'
],
'headless': LOCAL_CHROME_HEADLESS, # Set headless option here
}
# Make sure to run headed.
browser = await playwright.chromium.launch(**options)
# Setup context however you like.
context = await browser.new_context() # Pass any options
# Pause the page, and start recording manually.
context = await set_init_script(context)
page = await context.new_page()
await page.goto("https://channels.weixin.qq.com")
original_url = page.url
# 监听页面的 'framenavigated' 事件,只关注主框架的变化
page.on('framenavigated',
lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None)
# 等待 iframe 出现(最多等 60 秒)
iframe_locator = page.frame_locator("iframe").first
# 获取 iframe 中的第一个 img 元素
img_locator = iframe_locator.get_by_role("img").first
# 获取 src 属性值
src = await img_locator.get_attribute("src")
print("✅ 图片地址:", src)
status_queue.put(src)
try:
# 等待 URL 变化或超时
await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒
print("监听页面跳转成功")
except asyncio.TimeoutError:
status_queue.put("500")
print("监听页面跳转超时")
await page.close()
await context.close()
await browser.close()
return None
uuid_v1 = uuid.uuid1()
print(f"UUID v1: {uuid_v1}")
# 确保cookiesFile目录存在
cookies_dir = Path(BASE_DIR / "cookiesFile")
cookies_dir.mkdir(exist_ok=True)
await context.storage_state(path=cookies_dir / f"{uuid_v1}.json")
result = await check_cookie(2,f"{uuid_v1}.json")
if not result:
status_queue.put("500")
await page.close()
await context.close()
await browser.close()
return None
await page.close()
await context.close()
await browser.close()
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_info (type, filePath, userName, status)
VALUES (?, ?, ?, ?)
''', (2, f"{uuid_v1}.json", id, 1))
conn.commit()
print("✅ 用户状态已记录")
status_queue.put("200")
# 快手登录
async def get_ks_cookie(id,status_queue):
url_changed_event = asyncio.Event()
async def on_url_change():
# 检查是否是主框架的变化
if page.url != original_url:
url_changed_event.set()
async with async_playwright() as playwright:
options = {
'args': [
'--lang en-GB'
],
'headless': LOCAL_CHROME_HEADLESS, # Set headless option here
}
# Make sure to run headed.
browser = await playwright.chromium.launch(**options)
# Setup context however you like.
context = await browser.new_context() # Pass any options
context = await set_init_script(context)
# Pause the page, and start recording manually.
page = await context.new_page()
await page.goto("https://cp.kuaishou.com")
# 定位并点击“立即登录”按钮(类型为 link)
await page.get_by_role("link", name="立即登录").click()
await page.get_by_text("扫码登录").click()
img_locator = page.get_by_role("img", name="qrcode")
# 获取 src 属性值
src = await img_locator.get_attribute("src")
original_url = page.url
print("✅ 图片地址:", src)
status_queue.put(src)
# 监听页面的 'framenavigated' 事件,只关注主框架的变化
page.on('framenavigated',
lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None)
try:
# 等待 URL 变化或超时
await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒
print("监听页面跳转成功")
except asyncio.TimeoutError:
status_queue.put("500")
print("监听页面跳转超时")
await page.close()
await context.close()
await browser.close()
return None
uuid_v1 = uuid.uuid1()
print(f"UUID v1: {uuid_v1}")
# 确保cookiesFile目录存在
cookies_dir = Path(BASE_DIR / "cookiesFile")
cookies_dir.mkdir(exist_ok=True)
await context.storage_state(path=cookies_dir / f"{uuid_v1}.json")
result = await check_cookie(4, f"{uuid_v1}.json")
if not result:
status_queue.put("500")
await page.close()
await context.close()
await browser.close()
return None
await page.close()
await context.close()
await browser.close()
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_info (type, filePath, userName, status)
VALUES (?, ?, ?, ?)
''', (4, f"{uuid_v1}.json", id, 1))
conn.commit()
print("✅ 用户状态已记录")
status_queue.put("200")
# 小红书登录
async def xiaohongshu_cookie_gen(id,status_queue):
url_changed_event = asyncio.Event()
async def on_url_change():
# 检查是否是主框架的变化
if page.url != original_url:
url_changed_event.set()
async with async_playwright() as playwright:
options = {
'args': [
'--lang en-GB'
],
'headless': LOCAL_CHROME_HEADLESS, # Set headless option here
}
# Make sure to run headed.
browser = await playwright.chromium.launch(**options)
# Setup context however you like.
context = await browser.new_context() # Pass any options
context = await set_init_script(context)
# Pause the page, and start recording manually.
page = await context.new_page()
await page.goto("https://creator.xiaohongshu.com/")
await page.locator('img.css-wemwzq').click()
img_locator = page.get_by_role("img").nth(2)
# 获取 src 属性值
src = await img_locator.get_attribute("src")
original_url = page.url
print("✅ 图片地址:", src)
status_queue.put(src)
# 监听页面的 'framenavigated' 事件,只关注主框架的变化
page.on('framenavigated',
lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None)
try:
# 等待 URL 变化或超时
await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒
print("监听页面跳转成功")
except asyncio.TimeoutError:
status_queue.put("500")
print("监听页面跳转超时")
await page.close()
await context.close()
await browser.close()
return None
uuid_v1 = uuid.uuid1()
print(f"UUID v1: {uuid_v1}")
# 确保cookiesFile目录存在
cookies_dir = Path(BASE_DIR / "cookiesFile")
cookies_dir.mkdir(exist_ok=True)
await context.storage_state(path=cookies_dir / f"{uuid_v1}.json")
result = await check_cookie(1, f"{uuid_v1}.json")
if not result:
status_queue.put("500")
await page.close()
await context.close()
await browser.close()
return None
await page.close()
await context.close()
await browser.close()
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_info (type, filePath, userName, status)
VALUES (?, ?, ?, ?)
''', (1, f"{uuid_v1}.json", id, 1))
conn.commit()
print("✅ 用户状态已记录")
status_queue.put("200")
# a = asyncio.run(xiaohongshu_cookie_gen(4,None))
# print(a)
================================================
FILE: myUtils/postVideo.py
================================================
import asyncio
from pathlib import Path
from conf import BASE_DIR
from uploader.douyin_uploader.main import DouYinVideo
from uploader.ks_uploader.main import KSVideo
from uploader.tencent_uploader.main import TencentVideo
from uploader.xiaohongshu_uploader.main import XiaoHongShuVideo
from utils.constant import TencentZoneTypes
from utils.files_times import generate_schedule_time_next_day
def post_video_tencent(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0, is_draft=False):
# 生成文件的完整路径
account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
files = [Path(BASE_DIR / "videoFile" / file) for file in files]
if enableTimer:
publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days)
else:
publish_datetimes = [0 for i in range(len(files))]
for index, file in enumerate(files):
for cookie in account_file:
print(f"文件路径{str(file)}")
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
app = TencentVideo(title, str(file), tags, publish_datetimes[index], cookie, category, is_draft)
asyncio.run(app.main(), debug=False)
def post_video_DouYin(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0,
thumbnail_path = '',
productLink = '', productTitle = ''):
# 生成文件的完整路径
account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
files = [Path(BASE_DIR / "videoFile" / file) for file in files]
if enableTimer:
publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days)
else:
publish_datetimes = [0 for i in range(len(files))]
for index, file in enumerate(files):
for cookie in account_file:
print(f"文件路径{str(file)}")
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
app = DouYinVideo(title, str(file), tags, publish_datetimes[index], cookie, thumbnail_path, productLink, productTitle)
asyncio.run(app.main(), debug=False)
def post_video_ks(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0):
# 生成文件的完整路径
account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
files = [Path(BASE_DIR / "videoFile" / file) for file in files]
if enableTimer:
publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days)
else:
publish_datetimes = [0 for i in range(len(files))]
for index, file in enumerate(files):
for cookie in account_file:
print(f"文件路径{str(file)}")
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
app = KSVideo(title, str(file), tags, publish_datetimes[index], cookie)
asyncio.run(app.main(), debug=False)
def post_video_xhs(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0):
# 生成文件的完整路径
account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
files = [Path(BASE_DIR / "videoFile" / file) for file in files]
file_num = len(files)
if enableTimer:
publish_datetimes = generate_schedule_time_next_day(file_num, videos_per_day, daily_times,start_days)
else:
publish_datetimes = 0
for index, file in enumerate(files):
for cookie in account_file:
# 打印视频文件名、标题和 hashtag
print(f"视频文件名:{file}")
print(f"标题:{title}")
print(f"Hashtag:{tags}")
app = XiaoHongShuVideo(title, file, tags, publish_datetimes, cookie)
asyncio.run(app.main(), debug=False)
# post_video("333",["demo.mp4"],"d","d")
# post_video_DouYin("333",["demo.mp4"],"d","d")
================================================
FILE: sau_backend/README.md
================================================
## 启动项目:
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 为本机浏览器地址
================================================
FILE: sau_backend.py
================================================
import asyncio
import os
import sqlite3
import threading
import time
import uuid
from pathlib import Path
from queue import Queue
from flask_cors import CORS
from myUtils.auth import check_cookie
from flask import Flask, request, jsonify, Response, render_template, send_from_directory
from conf import BASE_DIR
from myUtils.login import get_tencent_cookie, douyin_cookie_gen, get_ks_cookie, xiaohongshu_cookie_gen
from myUtils.postVideo import post_video_tencent, post_video_DouYin, post_video_ks, post_video_xhs
active_queues = {}
app = Flask(__name__)
#允许所有来源跨域访问
CORS(app)
# 限制上传文件大小为160MB
app.config['MAX_CONTENT_LENGTH'] = 160 * 1024 * 1024
# 获取当前目录(假设 index.html 和 assets 在这里)
current_dir = os.path.dirname(os.path.abspath(__file__))
# 处理所有静态资源请求(未来打包用)
@app.route('/assets/<filename>')
def custom_static(filename):
return send_from_directory(os.path.join(current_dir, 'assets'), filename)
# 处理 favicon.ico 静态资源(未来打包用)
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(current_dir, 'assets'), 'vite.svg')
@app.route('/vite.svg')
def vite_svg():
return send_from_directory(os.path.join(current_dir, 'assets'), 'vite.svg')
# (未来打包用)
@app.route('/')
def index(): # put application's code here
return send_from_directory(current_dir, 'index.html')
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({
"code": 400,
"data": None,
"msg": "No file part in the request"
}), 400
file = request.files['file']
if file.filename == '':
return jsonify({
"code": 400,
"data": None,
"msg": "No selected file"
}), 400
try:
# 保存文件到指定位置
uuid_v1 = uuid.uuid1()
print(f"UUID v1: {uuid_v1}")
filepath = Path(BASE_DIR / "videoFile" / f"{uuid_v1}_{file.filename}")
file.save(filepath)
return jsonify({"code":200,"msg": "File uploaded successfully", "data": f"{uuid_v1}_{file.filename}"}), 200
except Exception as e:
return jsonify({"code":500,"msg": str(e),"data":None}), 500
@app.route('/getFile', methods=['GET'])
def get_file():
# 获取 filename 参数
filename = request.args.get('filename')
if not filename:
return jsonify({"code": 400, "msg": "filename is required", "data": None}), 400
# 防止路径穿越攻击
if '..' in filename or filename.startswith('/'):
return jsonify({"code": 400, "msg": "Invalid filename", "data": None}), 400
# 拼接完整路径
file_path = str(Path(BASE_DIR / "videoFile"))
# 返回文件
return send_from_directory(file_path,filename)
@app.route('/uploadSave', methods=['POST'])
def upload_save():
if 'file' not in request.files:
return jsonify({
"code": 400,
"data": None,
"msg": "No file part in the request"
}), 400
file = request.files['file']
if file.filename == '':
return jsonify({
"code": 400,
"data": None,
"msg": "No selected file"
}), 400
# 获取表单中的自定义文件名(可选)
custom_filename = request.form.get('filename', None)
if custom_filename:
filename = custom_filename + "." + file.filename.split('.')[-1]
else:
filename = file.filename
try:
# 生成 UUID v1
uuid_v1 = uuid.uuid1()
print(f"UUID v1: {uuid_v1}")
# 构造文件名和路径
final_filename = f"{uuid_v1}_{filename}"
filepath = Path(BASE_DIR / "videoFile" / f"{uuid_v1}_{filename}")
# 保存文件
file.save(filepath)
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO file_records (filename, filesize, file_path)
VALUES (?, ?, ?)
''', (filename, round(float(os.path.getsize(filepath)) / (1024 * 1024),2), final_filename))
conn.commit()
print("✅ 上传文件已记录")
return jsonify({
"code": 200,
"msg": "File uploaded and saved successfully",
"data": {
"filename": filename,
"filepath": final_filename
}
}), 200
except Exception as e:
print(f"Upload failed: {e}")
return jsonify({
"code": 500,
"msg": f"upload failed: {e}",
"data": None
}), 500
@app.route('/getFiles', methods=['GET'])
def get_all_files():
try:
# 使用 with 自动管理数据库连接
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
conn.row_factory = sqlite3.Row # 允许通过列名访问结果
cursor = conn.cursor()
# 查询所有记录
cursor.execute("SELECT * FROM file_records")
rows = cursor.fetchall()
# 将结果转为字典列表,并提取UUID
data = []
for row in rows:
row_dict = dict(row)
# 从 file_path 中提取 UUID (文件名的第一部分,下划线前)
if row_dict.get('file_path'):
file_path_parts = row_dict['file_path'].split('_', 1) # 只分割第一个下划线
if len(file_path_parts) > 0:
row_dict['uuid'] = file_path_parts[0] # UUID 部分
else:
row_dict['uuid'] = ''
else:
row_dict['uuid'] = ''
data.append(row_dict)
return jsonify({
"code": 200,
"msg": "success",
"data": data
}), 200
except Exception as e:
return jsonify({
"code": 500,
"msg": str("get file failed!"),
"data": None
}), 500
@app.route("/getAccounts", methods=['GET'])
def getAccounts():
"""快速获取所有账号信息,不进行cookie验证"""
try:
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM user_info''')
rows = cursor.fetchall()
rows_list = [list(row) for row in rows]
print("\n📋 当前数据表内容(快速获取):")
for row in rows:
print(row)
return jsonify(
{
"code": 200,
"msg": None,
"data": rows_list
}), 200
except Exception as e:
print(f"获取账号列表时出错: {str(e)}")
return jsonify({
"code": 500,
"msg": f"获取账号列表失败: {str(e)}",
"data": None
}), 500
@app.route("/getValidAccounts",methods=['GET'])
async def getValidAccounts():
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM user_info''')
rows = cursor.fetchall()
rows_list = [list(row) for row in rows]
print("\n📋 当前数据表内容:")
for row in rows:
print(row)
for row in rows_list:
flag = await check_cookie(row[1],row[2])
if not flag:
row[4] = 0
cursor.execute('''
UPDATE user_info
SET status = ?
WHERE id = ?
''', (0,row[0]))
conn.commit()
print("✅ 用户状态已更新")
for row in rows:
print(row)
return jsonify(
{
"code": 200,
"msg": None,
"data": rows_list
}),200
@app.route('/deleteFile', methods=['GET'])
def delete_file():
file_id = request.args.get('id')
if not file_id or not file_id.isdigit():
return jsonify({
"code": 400,
"msg": "Invalid or missing file ID",
"data": None
}), 400
try:
# 获取数据库连接
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 查询要删除的记录
cursor.execute("SELECT * FROM file_records WHERE id = ?", (file_id,))
record = cursor.fetchone()
if not record:
return jsonify({
"code": 404,
"msg": "File not found",
"data": None
}), 404
record = dict(record)
# 获取文件路径并删除实际文件
file_path = Path(BASE_DIR / "videoFile" / record['file_path'])
if file_path.exists():
try:
file_path.unlink() # 删除文件
print(f"✅ 实际文件已删除: {file_path}")
except Exception as e:
print(f"⚠️ 删除实际文件失败: {e}")
# 即使删除文件失败,也要继续删除数据库记录,避免数据不一致
else:
print(f"⚠️ 实际文件不存在: {file_path}")
# 删除数据库记录
cursor.execute("DELETE FROM file_records WHERE id = ?", (file_id,))
conn.commit()
return jsonify({
"code": 200,
"msg": "File deleted successfully",
"data": {
"id": record['id'],
"filename": record['filename']
}
}), 200
except Exception as e:
return jsonify({
"code": 500,
"msg": str("delete failed!"),
"data": None
}), 500
@app.route('/deleteAccount', methods=['GET'])
def delete_account():
account_id = request.args.get('id')
if not account_id or not account_id.isdigit():
return jsonify({
"code": 400,
"msg": "Invalid or missing account ID",
"data": None
}), 400
account_id = int(account_id)
try:
# 获取数据库连接
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 查询要删除的记录
cursor.execute("SELECT * FROM user_info WHERE id = ?", (account_id,))
record = cursor.fetchone()
if not record:
return jsonify({
"code": 404,
"msg": "account not found",
"data": None
}), 404
record = dict(record)
# 删除关联的cookie文件
if record.get('filePath'):
cookie_file_path = Path(BASE_DIR / "cookiesFile" / record['filePath'])
if cookie_file_path.exists():
try:
cookie_file_path.unlink()
print(f"✅ Cookie文件已删除: {cookie_file_path}")
except Exception as e:
print(f"⚠️ 删除Cookie文件失败: {e}")
# 删除数据库记录
cursor.execute("DELETE FROM user_info WHERE id = ?", (account_id,))
conn.commit()
return jsonify({
"code": 200,
"msg": "account deleted successfully",
"data": None
}), 200
except Exception as e:
return jsonify({
"code": 500,
"msg": f"delete failed: {str(e)}",
"data": None
}), 500
# SSE 登录接口
@app.route('/login')
def login():
# 1 小红书 2 视频号 3 抖音 4 快手
type = request.args.get('type')
# 账号名
id = request.args.get('id')
# 模拟一个用于异步通信的队列
status_queue = Queue()
active_queues[id] = status_queue
def on_close():
print(f"清理队列: {id}")
del active_queues[id]
# 启动异步任务线程
thread = threading.Thread(target=run_async_function, args=(type,id,status_queue), daemon=True)
thread.start()
response = Response(sse_stream(status_queue,), mimetype='text/event-stream')
response.headers['Cache-Control'] = 'no-cache'
response.headers['X-Accel-Buffering'] = 'no' # 关键:禁用 Nginx 缓冲
response.headers['Content-Type'] = 'text/event-stream'
response.headers['Connection'] = 'keep-alive'
return response
@app.route('/postVideo', methods=['POST'])
def postVideo():
# 获取JSON数据
data = request.get_json()
if not data:
return jsonify({"code": 400, "msg": "请求数据不能为空", "data": None}), 400
# 从JSON数据中提取fileList和accountList
file_list = data.get('fileList', [])
account_list = data.get('accountList', [])
type = data.get('type')
title = data.get('title')
tags = data.get('tags')
category = data.get('category')
enableTimer = data.get('enableTimer')
if category == 0:
category = None
productLink = data.get('productLink', '')
productTitle = data.get('productTitle', '')
thumbnail_path = data.get('thumbnail', '')
is_draft = data.get('isDraft', False) # 新增参数:是否保存为草稿
videos_per_day = data.get('videosPerDay')
daily_times = data.get('dailyTimes')
start_days = data.get('startDays')
# 参数校验
if not file_list:
return jsonify({"code": 400, "msg": "文件列表不能为空", "data": None}), 400
if not account_list:
return jsonify({"code": 400, "msg": "账号列表不能为空", "data": None}), 400
if not type:
return jsonify({"code": 400, "msg": "平台类型不能为空", "data": None}), 400
if not title:
return jsonify({"code": 400, "msg": "标题不能为空", "data": None}), 400
# 打印获取到的数据(仅作为示例)
print("File List:", file_list)
print("Account List:", account_list)
try:
match type:
case 1:
post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
case 2:
post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, is_draft)
case 3:
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, thumbnail_path, productLink, productTitle)
case 4:
post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
case _:
return jsonify({"code": 400, "msg": f"不支持的平台类型: {type}", "data": None}), 400
# 返回响应给客户端
return jsonify(
{
"code": 200,
"msg": "发布任务已提交",
"data": None
}), 200
except Exception as e:
print(f"发布视频时出错: {str(e)}")
return jsonify({
"code": 500,
"msg": f"发布失败: {str(e)}",
"data": None
}), 500
@app.route('/updateUserinfo', methods=['POST'])
def updateUserinfo():
# 获取JSON数据
data = request.get_json()
# 从JSON数据中提取 type 和 userName
user_id = data.get('id')
type = data.get('type')
userName = data.get('userName')
try:
# 获取数据库连接
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 更新数据库记录
cursor.execute('''
UPDATE user_info
SET type = ?,
userName = ?
WHERE id = ?;
''', (type, userName, user_id))
conn.commit()
return jsonify({
"code": 200,
"msg": "account update successfully",
"data": None
}), 200
except Exception as e:
return jsonify({
"code": 500,
"msg": str("update failed!"),
"data": None
}), 500
@app.route('/postVideoBatch', methods=['POST'])
def postVideoBatch():
data_list = request.get_json()
if not isinstance(data_list, list):
return jsonify({"code": 400, "msg": "Expected a JSON array", "data": None}), 400
for data in data_list:
# 从JSON数据中提取fileList和accountList
file_list = data.get('fileList', [])
account_list = data.get('accountList', [])
type = data.get('type')
title = data.get('title')
tags = data.get('tags')
category = data.get('category')
enableTimer = data.get('enableTimer')
if category == 0:
category = None
productLink = data.get('productLink', '')
productTitle = data.get('productTitle', '')
is_draft = data.get('isDraft', False)
videos_per_day = data.get('videosPerDay')
daily_times = data.get('dailyTimes')
start_days = data.get('startDays')
# 打印获取到的数据(仅作为示例)
print("File List:", file_list)
print("Account List:", account_list)
match type:
case 1:
post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
case 2:
post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, is_draft)
case 3:
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days, productLink, productTitle)
case 4:
post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days)
# 返回响应给客户端
return jsonify(
{
"code": 200,
"msg": None,
"data": None
}), 200
# Cookie文件上传API
@app.route('/uploadCookie', methods=['POST'])
def upload_cookie():
try:
if 'file' not in request.files:
return jsonify({
"code": 400,
"msg": "没有找到Cookie文件",
"data": None
}), 400
file = request.files['file']
if file.filename == '':
return jsonify({
"code": 400,
"msg": "Cookie文件名不能为空",
"data": None
}), 400
if not file.filename.endswith('.json'):
return jsonify({
"code": 400,
"msg": "Cookie文件必须是JSON格式",
"data": None
}), 400
# 获取账号信息
account_id = request.form.get('id')
platform = request.form.get('platform')
if not account_id or not platform:
return jsonify({
"code": 400,
"msg": "缺少账号ID或平台信息",
"data": None
}), 400
# 从数据库获取账号的文件路径
with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT filePath FROM user_info WHERE id = ?', (account_id,))
result = cursor.fetchone()
if not result:
return jsonify({
"code": 500,
"msg": "账号不存在",
"data": None
}), 404
# 保存上传的Cookie文件到对应路径
cookie_file_path = Path(BASE_DIR / "cookiesFile" / result['filePath'])
cookie_file_path.parent.mkdir(parents=True, exist_ok=True)
file.save(str(cookie_file_path))
# 更新数据库中的账号信息(可选,比如更新更新时间)
# 这里可以根据需要添加额外的处理逻辑
return jsonify({
"code": 200,
"msg": "Cookie文件上传成功",
"data": None
}), 200
except Exception as e:
print(f"上传Cookie文件时出错: {str(e)}")
return jsonify({
"code": 500,
"msg": f"上传Cookie文件失败: {str(e)}",
"data": None
}), 500
# Cookie文件下载API
@app.route('/downloadCookie', methods=['GET'])
def download_cookie():
try:
file_path = request.args.get('filePath')
if not file_path:
return jsonify({
"code": 500,
"msg": "缺少文件路径参数",
"data": None
}), 400
# 验证文件路径的安全性,防止路径遍历攻击
cookie_file_path = Path(BASE_DIR / "cookiesFile" / file_path).resolve()
base_path = Path(BASE_DIR / "cookiesFile").resolve()
if not cookie_file_path.is_relative_to(base_path):
return jsonify({
"code": 500,
"msg": "非法文件路径",
"data": None
}), 400
if not cookie_file_path.exists():
return jsonify({
"code": 500,
"msg": "Cookie文件不存在",
"data": None
}), 404
# 返回文件
return send_from_directory(
directory=str(cookie_file_path.parent),
path=cookie_file_path.name,
as_attachment=True
)
except Exception as e:
print(f"下载Cookie文件时出错: {str(e)}")
return jsonify({
"code": 500,
"msg": f"下载Cookie文件失败: {str(e)}",
"data": None
}), 500
# 包装函数:在线程中运行异步函数
def run_async_function(type,id,status_queue):
match type:
case '1':
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(xiaohongshu_cookie_gen(id, status_queue))
loop.close()
case '2':
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(get_tencent_cookie(id,status_queue))
loop.close()
case '3':
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(douyin_cookie_gen(id,status_queue))
loop.close()
case '4':
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(get_ks_cookie(id,status_queue))
loop.close()
# SSE 流生成器函数
def sse_stream(status_queue):
while True:
if not status_queue.empty():
msg = status_queue.get()
yield f"data: {msg}\n\n"
else:
# 避免 CPU 占满
time.sleep(0.1)
if __name__ == '__main__':
app.run(host='0.0.0.0' ,port=5409)
================================================
FILE: sau_frontend/README.md
================================================
# 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
================================================
FILE: sau_frontend/index.html
================================================
<!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>
================================================
FILE: sau_frontend/package.json
================================================
{
"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"
}
}
================================================
FILE: sau_frontend/src/App.vue
================================================
<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>
================================================
FILE: sau_frontend/src/api/account.js
================================================
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}`)
}
}
================================================
FILE: sau_frontend/src/api/index.js
================================================
// API 统一导出
export * from './user'
export * from './account'
export * from './material'
// 可以在这里添加其他API模块的导出
// export * from './product'
// export * from './order'
// export * from './common'
================================================
FILE: sau_frontend/src/api/material.js
================================================
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}`
}
}
================================================
FILE: sau_frontend/src/api/user.js
================================================
import { http } from '@/utils/request'
// 用户相关API(预留)
// 注意:当前后端暂无用户认证接口,以下为预留定义
export const userApi = {}
================================================
FILE: sau_frontend/src/main.js
================================================
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')
================================================
FILE: sau_frontend/src/router/index.js
================================================
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
================================================
FILE: sau_frontend/src/stores/account.js
================================================
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
}
})
================================================
FILE: sau_frontend/src/stores/app.js
================================================
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
}
})
================================================
FILE: sau_frontend/src/stores/index.js
================================================
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 }
================================================
FILE: sau_frontend/src/stores/user.js
================================================
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
}
})
================================================
FILE: sau_frontend/src/styles/index.scss
================================================
// 导入重置样式
@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; }
================================================
FILE: sau_frontend/src/styles/reset.scss
================================================
/* 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;
}
================================================
FILE: sau_frontend/src/styles/variables.scss
================================================
// 颜色变量
$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;
================================================
FILE: sau_frontend/src/utils/request.js
================================================
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
================================================
FILE: sau_frontend/src/views/About.vue
================================================
<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: sau_frontend/src/views/AccountManagement.vue
================================================
<template>
<div class="account-management">
<div class="page-header">
<h1>账号管理</h1>
</div>
<div class="account-tabs">
<el-tabs v-model="activeTab" class="account-tabs-nav">
<el-tab-pane label="全部" name="all">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredAccounts.length > 0" class="account-list">
<el-table :data="filteredAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.status)"
effect="plain"
:class="{'clickable-status': isStatusClickable(scope.row.status)}"
@click="handleStatusClick(scope.row)"
>
<el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
<Loading />
</el-icon>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
<el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</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-tab-pane>
<el-tab-pane label="快手" name="kuaishou">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredKuaishouAccounts.length > 0" class="account-list">
<el-table :data="filteredKuaishouAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.status)"
effect="plain"
:class="{'clickable-status': isStatusClickable(scope.row.status)}"
@click="handleStatusClick(scope.row)"
>
<el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
<Loading />
</el-icon>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
<el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</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-tab-pane>
<el-tab-pane label="抖音" name="douyin">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredDouyinAccounts.length > 0" class="account-list">
<el-table :data="filteredDouyinAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.status)"
effect="plain"
:class="{'clickable-status': isStatusClickable(scope.row.status)}"
@click="handleStatusClick(scope.row)"
>
<el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
<Loading />
</el-icon>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
<el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</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-tab-pane>
<el-tab-pane label="视频号" name="channels">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredChannelsAccounts.length > 0" class="account-list">
<el-table :data="filteredChannelsAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.status)"
effect="plain"
:class="{'clickable-status': isStatusClickable(scope.row.status)}"
@click="handleStatusClick(scope.row)"
>
<el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
<Loading />
</el-icon>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
<el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</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-tab-pane>
<el-tab-pane label="小红书" name="xiaohongshu">
<div class="account-list-container">
<div class="account-search">
<el-input
v-model="searchKeyword"
placeholder="输入名称或账号搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@input="handleSearch"
/>
<div class="action-buttons">
<el-button type="primary" @click="handleAddAccount">添加账号</el-button>
<el-button type="info" @click="fetchAccounts" :loading="false">
<el-icon :class="{ 'is-loading': appStore.isAccountRefreshing }"><Refresh /></el-icon>
<span v-if="appStore.isAccountRefreshing">刷新中</span>
</el-button>
</div>
</div>
<div v-if="filteredXiaohongshuAccounts.length > 0" class="account-list">
<el-table :data="filteredXiaohongshuAccounts" style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :src="getDefaultAvatar(scope.row.name)" :size="40" />
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="platform" label="平台">
<template #default="scope">
<el-tag
:type="getPlatformTagType(scope.row.platform)"
effect="plain"
>
{{ scope.row.platform }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.status)"
effect="plain"
:class="{'clickable-status': isStatusClickable(scope.row.status)}"
@click="handleStatusClick(scope.row)"
>
<el-icon :class="scope.row.status === '验证中' ? 'is-loading' : ''" v-if="scope.row.status === '验证中'">
<Loading />
</el-icon>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="primary" :icon="Download" @click="handleDownloadCookie(scope.row)">下载Cookie</el-button>
<el-button size="small" type="info" :icon="Upload" @click="handleUploadCookie(scope.row)">上传Cookie</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-tab-pane>
</el-tabs>
</div>
<!-- 添加/编辑账号对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="!sseConnecting"
:show-close="!sseConnecting"
>
<el-form :model="accountForm" label-width="80px" :rules="rules" ref="accountFormRef">
<el-form-item label="平台" prop="platform">
<el-select
v-model="accountForm.platform"
placeholder="请选择平台"
style="width: 100%"
:disabled="dialogType === 'edit' || sseConnecting"
>
<el-option label="快手" value="快手" />
<el-option label="抖音" value="抖音" />
<el-option label="视频号" value="视频号" />
<el-option label="小红书" value="小红书" />
</el-select>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input
v-model="accountForm.name"
placeholder="请输入账号名称"
:disabled="sseConnecting"
/>
</el-form-item>
<!-- 二维码显示区域 -->
<div v-if="sseConnecting" class="qrcode-container">
<div v-if="qrCodeData && !loginStatus" class="qrcode-wrapper">
<p class="qrcode-tip">请使用对应平台APP扫描二维码登录</p>
<img :src="qrCodeData" alt="登录二维码" class="qrcode-image" />
</div>
<div v-else-if="!qrCodeData && !loginStatus" class="loading-wrapper">
<el-icon class="is-loading"><Refresh /></el-icon>
<span>请求中...</span>
</div>
<div v-else-if="loginStatus === '200'" class="success-wrapper">
<el-icon><CircleCheckFilled /></el-icon>
<span>添加成功</span>
</div>
<div v-else-if="loginStatus === '500'" class="error-wrapper">
<el-icon><CircleCloseFilled /></el-icon>
<span>添加失败,请稍后再试</span>
</div>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="submitAccountForm"
:loading="sseConnecting"
:disabled="sseConnecting"
>
{{ sseConnecting ? '请求中' : '确认' }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { Refresh, CircleCheckFilled, CircleCloseFilled, Download, Upload, Loading } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { accountApi } from '@/api/account'
import { useAccountStore } from '@/stores/account'
import { useAppStore } from '@/stores/app'
import { http } from '@/utils/request'
// 获取账号状态管理
const accountStore = useAccountStore()
// 获取应用状态管理
const appStore = useAppStore()
// 当前激活的标签页
const activeTab = ref('all')
// 搜索关键词
const searchKeyword = ref('')
// 获取账号数据(快速,不验证)
const fetchAccountsQuick = async () => {
try {
const res = await accountApi.getAccounts()
if (res.code === 200 && res.data) {
// 将所有账号的状态暂时设为"验证中"
const accountsWithPendingStatus = res.data.map(account => {
const updatedAccount = [...account];
updatedAccount[4] = -1; // -1 表示验证中的临时状态
return updatedAccount;
});
accountStore.setAccounts(accountsWithPendingStatus);
}
} catch (error) {
console.error('快速获取账号数据失败:', error)
}
}
// 获取账号数据(带验证)
const fetchAccounts = async () => {
if (appStore.isAccountRefreshing) return
appStore.setAccountRefreshing(true)
try {
const res = await accountApi.getValidAccounts()
if (res.code === 200 && res.data) {
accountStore.setAccounts(res.data)
ElMessage.success('账号数据获取成功')
// 标记为已访问
if (appStore.isFirstTimeAccountManagement) {
appStore.setAccountManagementVisited()
}
} else {
ElMessage.error('获取账号数据失败')
}
} catch (error) {
console.error('获取账号数据失败:', error)
ElMessage.error('获取账号数据失败')
} finally {
appStore.setAccountRefreshing(false)
}
}
// 后台验证所有账号(优化版本,使用setTimeout避免阻塞UI)
const validateAllAccountsInBackground = async () => {
// 使用setTimeout将验证过程放在下一个事件循环,避免阻塞UI
setTimeout(async () => {
try {
const res = await accountApi.getValidAccounts()
if (res.code === 200 && res.data) {
accountStore.setAccounts(res.data)
}
} catch (error) {
console.error('后台验证账号失败:', error)
}
}, 0)
}
// 页面加载时获取账号数据
onMounted(() => {
// 快速获取账号列表(不验证),立即显示
fetchAccountsQuick()
// 在后台验证所有账号
setTimeout(() => {
validateAllAccountsInBackground()
}, 100) // 稍微延迟一下,让用户看到快速加载的效果
})
// 获取平台标签类型
const getPlatformTagType = (platform) => {
const typeMap = {
'快手': 'success',
'抖音': 'danger',
'视频号': 'warning',
'小红书': 'info'
}
return typeMap[platform] || 'info'
}
// 判断状态是否可点击(异常状态可点击)
const isStatusClickable = (status) => {
return status === '异常'; // 只有异常状态可点击,验证中不可点击
}
// 获取状态标签类型
const getStatusTagType = (status) => {
if (status === '验证中') {
return 'info'; // 验证中使用灰色
} else if (status === '正常') {
return 'success'; // 正常使用绿色
} else {
return 'danger'; // 无效使用红色
}
}
// 处理状态点击事件
const handleStatusClick = (row) => {
if (isStatusClickable(row.status)) {
// 触发重新登录流程
handleReLogin(row)
}
}
// 过滤后的账号列表
const filteredAccounts = computed(() => {
if (!searchKeyword.value) return accountStore.accounts
return accountStore.accounts.filter(account =>
account.name.includes(searchKeyword.value)
)
})
// 按平台过滤的账号列表
const filteredKuaishouAccounts = computed(() => {
return filteredAccounts.value.filter(account => account.platform === '快手')
})
const filteredDouyinAccounts = computed(() => {
return filteredAccounts.value.filter(account => account.platform === '抖音')
})
const filteredChannelsAccounts = computed(() => {
return filteredAccounts.value.filter(account => account.platform === '视频号')
})
const filteredXiaohongshuAccounts = computed(() => {
return filteredAccounts.value.filter(account => account.platform === '小红书')
})
// 搜索处理
const handleSearch = () => {
// 搜索逻辑已通过计算属性实现
}
// 对话框相关
const dialogVisible = ref(false)
const dialogType = ref('add') // 'add' 或 'edit'
const accountFormRef = ref(null)
// 账号表单
const accountForm = reactive({
id: null,
name: '',
platform: '',
status: '正常'
})
// 表单验证规则
const rules = {
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
name: [{ required: true, message: '请输入账号名称', trigger: 'blur' }]
}
// SSE连接状态
const sseConnecting = ref(false)
const qrCodeData = ref('')
const loginStatus = ref('')
// 添加账号
const handleAddAccount = () => {
dialogType.value = 'add'
Object.assign(accountForm, {
id: null,
name: '',
platform: '',
status: '正常'
})
// 重置SSE状态
sseConnecting.value = false
qrCodeData.value = ''
loginStatus.value = ''
dialogVisible.value = true
}
// 编辑账号
const handleEdit = (row) => {
dialogType.value = 'edit'
Object.assign(accountForm, {
id: row.id,
name: row.name,
platform: row.platform,
status: row.status
})
dialogVisible.value = true
}
// 删除账号
const handleDelete = (row) => {
ElMessageBox.confirm(
`确定要删除账号 ${row.name} 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
// 调用API删除账号
const response = await accountApi.deleteAccount(row.id)
if (response.code === 200) {
// 从状态管理中删除账号
accountStore.deleteAccount(row.id)
ElMessage({
type: 'success',
message: '删除成功',
})
} else {
ElMessage.error(response.msg || '删除失败')
}
} catch (error) {
console.error('删除账号失败:', error)
ElMessage.error('删除账号失败')
}
})
.catch(() => {
// 取消删除
})
}
// 下载Cookie文件
const handleDownloadCookie = (row) => {
// 从后端获取Cookie文件
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
const downloadUrl = `${baseUrl}/downloadCookie?filePath=${encodeURIComponent(row.filePath)}`
// 创建一个隐藏的链接来触发下载
const link = document.createElement('a')
link.href = downloadUrl
link.download = `${row.name}_cookie.json`
link.target = '_blank'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
// 上传Cookie文件
const handleUploadCookie = (row) => {
// 创建一个隐藏的文件输入框
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.style.display = 'none'
document.body.appendChild(input)
input.onchange = async (event) => {
const file = event.target.files[0]
if (!file) return
// 检查文件类型
if (!file.name.endsWith('.json')) {
ElMessage.error('请选择JSON格式的Cookie文件')
document.body.removeChild(input)
return
}
try {
// 创建FormData对象
const formData = new FormData()
formData.append('file', file)
formData.append('id', row.id)
formData.append('platform', row.platform)
// 使用统一的http封装发送上传请求
const result = await http.upload('/uploadCookie', formData)
ElMessage.success('Cookie文件上传成功')
// 刷新账号列表以显示更新
fetchAccounts()
} catch (error) {
ElMessage.error('Cookie文件上传失败')
} finally {
document.body.removeChild(input)
}
}
input.click()
}
// 重新登录账号
const handleReLogin = (row) => {
// 设置表单信息
dialogType.value = 'edit'
Object.assign(accountForm, {
id: row.id,
name: row.name,
platform: row.platform,
status: row.status
})
// 重置SSE状态
sseConnecting.value = false
qrCodeData.value = ''
loginStatus.value = ''
// 显示对话框
dialogVisible.value = true
// 立即开始登录流程
setTimeout(() => {
connectSSE(row.platform, row.name)
}, 300)
}
// 获取默认头像
const getDefaultAvatar = (name) => {
// 使用简单的默认头像,可以基于用户名生成不同的颜色
return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`
}
// SSE事件源对象
let eventSource = null
// 关闭SSE连接
const closeSSEConnection = () => {
if (eventSource) {
eventSource.close()
eventSource = null
}
}
// 建立SSE连接
const connectSSE = (platform, name) => {
// 关闭可能存在的连接
closeSSEConnection()
// 设置连接状态
sseConnecting.value = true
qrCodeData.value = ''
loginStatus.value = ''
// 获取平台类型编号
const platformTypeMap = {
'小红书': '1',
'视频号': '2',
'抖音': '3',
'快手': '4'
}
const type = platformTypeMap[platform] || '1'
// 创建SSE连接
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
const url = `${baseUrl}/login?type=${type}&id=${encodeURIComponent(name)}`
eventSource = new EventSource(url)
// 监听消息
eventSource.onmessage = (event) => {
const data = event.data
// 如果还没有二维码数据,且数据长度较长,认为是二维码
if (!qrCodeData.value && data.length > 100) {
try {
if (data.startsWith('data:image')) {
qrCodeData.value = data
} else {
qrCodeData.value = `data:image/png;base64,${data}`
}
} catch (error) {
// 处理二维码数据出错
}
}
// 如果收到状态码
else if (data === '200' || data === '500') {
loginStatus.value = data
// 如果登录成功
if (data === '200') {
setTimeout(() => {
// 关闭连接
closeSSEConnection()
// 1秒后关闭对话框并开始刷新
setTimeout(() => {
dialogVisible.value = false
sseConnecting.value = false
// 根据是否是重新登录显示不同提示
ElMessage.success(dialogType.value === 'edit' ? '重新登录成功' : '账号添加成功')
// 显示更新账号信息提示
ElMessage({
type: 'info',
message: '正在同步账号信息...',
duration: 0
})
// 触发刷新操作
fetchAccounts().then(() => {
// 刷新完成后关闭提示
ElMessage.closeAll()
ElMessage.success('账号信息已更新')
})
}, 1000)
}, 1000)
} else {
// 登录失败,关闭连接
closeSSEConnection()
// 2秒后重置状态,允许重试
setTimeout(() => {
sseConnecting.value = false
qrCodeData.value = ''
loginStatus.value = ''
}, 2000)
}
}
}
// 监听错误
eventSource.onerror = (error) => {
console.error('SSE连接错误:', error)
ElMessage.error('连接服务器失败,请稍后再试')
closeSSEConnection()
sseConnecting.value = false
}
}
// 提交账号表单
const submitAccountForm = () => {
accountFormRef.value.validate(async (valid) => {
if (valid) {
if (dialogType.value === 'add') {
// 建立SSE连接
connectSSE(accountForm.platform, accountForm.name)
} else {
// 编辑账号逻辑
try {
// 将平台名称转换为类型数字
const platformTypeMap = {
'小红书': 1,
'视频号': 2,
'抖音': 3,
'快手': 4
};
const type = platformTypeMap[accountForm.platform] || 1;
const res = await accountApi.updateAccount({
id: accountForm.id,
type: type,
userName: accountForm.name
})
if (res.code === 200) {
// 更新状态管理中的账号
const updatedAccount = {
id: accountForm.id,
name: accountForm.name,
platform: accountForm.platform,
status: accountForm.status // Keep the existing status
};
accountStore.updateAccount(accountForm.id, updatedAccount)
ElMessage.success('更新成功')
dialogVisible.value = false
// 刷新账号列表
fetchAccounts()
} else {
ElMessage.error(res.msg || '更新账号失败')
}
} catch (error) {
console.error('更新账号失败:', error)
ElMessage.error('更新账号失败')
}
}
} else {
return false
}
})
}
// 组件卸载前关闭SSE连接
onBeforeUnmount(() => {
closeSSEConnection()
})
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.account-management {
.page-header {
margin-bottom: 20px;
h1 {
font-size: 24px;
color: $text-primary;
margin: 0;
}
}
.account-tabs {
background-color: #fff;
border-radius: 4px;
box-shadow: $box-shadow-light;
.account-tabs-nav {
padding: 20px;
}
}
.account-list-container {
.account-search {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
.el-input {
width: 300px;
}
.action-buttons {
display: flex;
gap: 10px;
.el-icon.is-loading {
animation: rotate 1s linear infinite;
}
}
}
.account-list {
margin-bottom: 20px;
}
.empty-data {
padding: 40px 0;
}
}
// 二维码容器样式
.clickable-status {
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: scale(1.05);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
}
}
.qrcode-container {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 250px;
.qrcode-wrapper {
text-align: center;
.qrcode-tip {
margin-bottom: 15px;
color: #606266;
}
.qrcode-image {
max-width: 200px;
max-height: 200px;
border: 1px solid #ebeef5;
background-color: black;
}
}
.loading-wrapper, .success-wrapper, .error-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
.el-icon {
font-size: 48px;
&.is-loading {
animation: rotate 1s linear infinite;
}
}
span {
font-size: 16px;
}
}
.success-wrapper .el-icon {
color: #67c23a;
}
.error-wrapper .el-icon {
color: #f56c6c;
}
}
}
</style>
================================================
FILE: sau_frontend/src/views/Dashboard.vue
================================================
<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>
================================================
FILE: sau_frontend/src/views/MaterialManagement.vue
================================================
<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: sau_frontend/src/views/PublishCenter.vue
================================================
<template>
<div class="publish-center">
<!-- Tab管理区域 -->
<div class="tab-management">
<div class="tab-header">
<div class="tab-list">
<div
v-for="tab in tabs"
:key="tab.name"
:class="['tab-item', { active: activeTab === tab.name }]"
@click="activeTab = tab.name"
>
<span>{{ tab.label }}</span>
<el-icon
v-if="tabs.length > 1"
class="close-icon"
@click.stop="removeTab(tab.name)"
>
<Close />
</el-icon>
</div>
</div>
<div class="tab-actions">
<el-button
type="primary"
size="small"
@click="addTab"
class="add-tab-btn"
>
<el-icon><Plus /></el-icon>
添加Tab
</el-button>
<el-button
type="success"
size="small"
@click="batchPublish"
:loading="batchPublishing"
class="batch-publish-btn"
>
批量发布
</el-button>
</div>
</div>
</div>
<!-- 内容区域 -->
<div class="publish-content">
<div class="tab-content-wrapper">
<div
v-for="tab in tabs"
:key="tab.name"
v-show="activeTab === tab.name"
class="tab-content"
>
<!-- 发布状态提示 -->
<div v-if="tab.publishStatus" class="publish-status">
<el-alert
:title="tab.publishStatus.message"
:type="tab.publishStatus.type"
:closable="false"
show-icon
/>
</div>
<!-- 视频上传区域 -->
<div class="upload-section">
<h3>视频</h3>
<div class="upload-options">
<el-button type="primary" @click="showUploadOptions(tab)" class="upload-btn">
<el-icon><Upload /></el-icon>
上传视频
</el-button>
</div>
<!-- 已上传文件列表 -->
<div v-if="tab.fileList.length > 0" class="uploaded-files">
<h4>已上传文件:</h4>
<div class="file-list">
<div v-for="(file, index) in tab.fileList" :key="index" class="file-item">
<el-link :href="file.url" target="_blank" type="primary">{{ file.name }}</el-link>
<span class="file-size">{{ (file.size / 1024 / 1024).toFixed(2) }}MB</span>
<el-button type="danger" size="small" @click="removeFile(tab, index)">删除</el-button>
</div>
</div>
</div>
</div>
<!-- 上传选项弹窗 -->
<el-dialog
v-model="uploadOptionsVisible"
title="选择上传方式"
width="400px"
class="upload-options-dialog"
>
<div class="upload-options-content">
<el-button type="primary" @click="selectLocalUpload" class="option-btn">
<el-icon><Upload /></el-icon>
本地上传
</el-button>
<el-button type="success" @click="selectMaterialLibrary" class="option-btn">
<el-icon><Folder /></el-icon>
素材库
</el-button>
</div>
</el-dialog>
<!-- 本地上传弹窗 -->
<el-dialog
v-model="localUploadVisible"
title="本地上传"
width="600px"
class="local-upload-dialog"
>
<el-upload
class="video-upload"
drag
:auto-upload="true"
:action="`${apiBaseUrl}/upload`"
:on-success="(response, file) => handleUploadSuccess(response, file, currentUploadTab)"
:on-error="handleUploadError"
multiple
accept="video/*"
:headers="authHeaders"
>
<el-icon class="el-icon--upload"><Upload /></el-icon>
<div class="el-upload__text">
将视频文件拖到此处,或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持MP4、AVI等视频格式,可上传多个文件
</div>
</template>
</el-upload>
</el-dialog>
<!-- 批量发布进度对话框 -->
<el-dialog
v-model="batchPublishDialogVisible"
title="批量发布进度"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
>
<div class="publish-progress">
<el-progress
:percentage="publishProgress"
:status="publishProgress === 100 ? 'success' : ''"
/>
<div v-if="currentPublishingTab" class="current-publishing">
正在发布:{{ currentPublishingTab.label }}
</div>
<!-- 发布结果列表 -->
<div class="publish-results" v-if="publishResults.length > 0">
<div
v-for="(result, index) in publishResults"
:key="index"
:class="['result-item', result.status]"
>
<el-icon v-if="result.status === 'success'"><Check /></el-icon>
<el-icon v-else-if="result.status === 'error'"><Close /></el-icon>
<el-icon v-else><InfoFilled /></el-icon>
<span class="label">{{ result.label }}</span>
<span class="message">{{ result.message }}</span>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button
@click="cancelBatchPublish"
:disabled="publishProgress === 100"
>
取消发布
</el-button>
<el-button
type="primary"
@click="batchPublishDialogVisible = false"
v-if="publishProgress === 100"
>
关闭
</el-button>
</div>
</template>
</el-dialog>
<!-- 素材库选择弹窗 -->
<el-dialog
v-model="materialLibraryVisible"
title="选择素材"
width="800px"
class="material-library-dialog"
>
<div class="material-library-content">
<el-checkbox-group v-model="selectedMaterials">
<div class="material-list">
<div
v-for="material in materials"
:key="material.id"
class="material-item"
>
<el-checkbox :label="material.id" class="material-checkbox">
<div class="material-info">
<div class="material-name">{{ material.filename }}</div>
<div class="material-details">
<span class="file-size">{{ material.filesize }}MB</span>
<span class="upload-time">{{ material.upload_time }}</span>
</div>
</div>
</el-checkbox>
</div>
</div>
</el-checkbox-group>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="materialLibraryVisible = false">取消</el-button>
<el-button type="primary" @click="confirmMaterialSelection">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 账号选择 -->
<div class="account-section">
<h3>账号</h3>
<div class="account-display">
<div class="selected-accounts">
<el-tag
v-for="(account, index) in tab.selectedAccounts"
:key="index"
closable
@close="removeAccount(tab, index)"
class="account-tag"
>
{{ getAccountDisplayName(account) }}
</el-tag>
</div>
<el-button
type="primary"
plain
@click="openAccountDialog(tab)"
class="select-account-btn"
>
选择账号
</el-button>
</div>
</div>
<!-- 账号选择弹窗 -->
<el-dialog
v-model="accountDialogVisible"
title="选择账号"
width="600px"
class="account-dialog"
>
<div class="account-dialog-content">
<el-checkbox-group v-model="tempSelectedAccounts">
<div class="account-list">
<el-checkbox
v-for="account in availableAccounts"
:key="account.id"
:label="account.id"
class="account-item"
>
<div class="account-info">
<span class="account-name">{{ account.name }}</span>
</div>
</el-checkbox>
</div>
</el-checkbox-group>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="accountDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAccountSelection">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 平台选择 -->
<div class="platform-section">
<h3>平台</h3>
<el-radio-group v-model="tab.selectedPlatform" class="platform-radios">
<el-radio
v-for="platform in platforms"
:key="platform.key"
:label="platform.key"
class="platform-radio"
>
{{ platform.name }}
</el-radio>
</el-radio-group>
</div>
<!-- 原创声明 -->
<div class="original-section">
<el-checkbox
v-model="tab.isOriginal"
label="声明原创"
class="original-checkbox"
/>
</div>
<!-- 草稿选项 (仅在视频号可见) -->
<div v-if="tab.selectedPlatform === 2" class="draft-section">
<el-checkbox
v-model="tab.isDraft"
label="视频号仅保存草稿(用手机发布)"
class="draft-checkbox"
/>
</div>
<!-- 标签 (仅在抖音可见) -->
<div v-if="tab.selectedPlatform === 3" class="product-section">
<h3>商品链接</h3>
<el-input
v-model="tab.productTitle"
type="text"
:rows="1"
placeholder="请输入商品名称"
maxlength="200"
class="product-name-input"
/>
<el-input
v-model="tab.productLink"
type="text"
:rows="1"
placeholder="请输入商品链接"
maxlength="200"
class="product-link-input"
/>
</div>
<!-- 标题输入 -->
<div class="title-section">
<h3>标题</h3>
<el-input
v-model="tab.title"
type="textarea"
:rows="3"
placeholder="请输入标题"
maxlength="100"
show-word-limit
class="title-input"
/>
</div>
<!-- 话题输入 -->
<div class="topic-section">
<h3>话题</h3>
<div class="topic-display">
<div class="selected-topics">
<el-tag
v-for="(topic, index) in tab.selectedTopics"
:key="index"
closable
@close="removeTopic(tab, index)"
class="topic-tag"
>
#{{ topic }}
</el-tag>
</div>
<el-button
type="primary"
plain
@click="openTopicDialog(tab)"
class="select-topic-btn"
>
添加话题
</el-button>
</div>
</div>
<!-- 添加话题弹窗 -->
<el-dialog
v-model="topicDialogVisible"
title="添加话题"
width="600px"
class="topic-dialog"
>
<div class="topic-dialog-content">
<!-- 自定义话题输入 -->
<div class="custom-topic-input">
<el-input
v-model="customTopic"
placeholder="输入自定义话题"
class="custom-input"
>
<template #prepend>#</template>
</el-input>
<el-button type="primary" @click="addCustomTopic">添加</el-button>
</div>
<!-- 推荐话题 -->
<div class="recommended-topics">
<h4>推荐话题</h4>
<div class="topic-grid">
<el-button
v-for="topic in recommendedTopics"
:key="topic"
:type="currentTab?.selectedTopics?.includes(topic) ? 'primary' : 'default'"
@click="toggleRecommendedTopic(topic)"
class="topic-btn"
>
{{ topic }}
</el-button>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="topicDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmTopicSelection">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 定时发布 -->
<div class="schedule-section">
<h3>定时发布</h3>
<div class="schedule-controls">
<el-switch
v-model="tab.scheduleEnabled"
active-text="定时发布"
inactive-text="立即发布"
/>
<div v-if="tab.scheduleEnabled" class="schedule-settings">
<div class="schedule-item">
<span class="label">每天发布视频数:</span>
<el-select v-model="tab.videosPerDay" placeholder="选择发布数量">
<el-option
v-for="num in 55"
:key="num"
:label="num"
:value="num"
/>
</el-select>
</div>
<div class="schedule-item">
<span class="label">每天发布时间:</span>
<el-time-select
v-for="(time, index) in tab.dailyTimes"
:key="index"
v-model="tab.dailyTimes[index]"
start="00:00"
step="00:30"
end="23:30"
placeholder="选择时间"
/>
<el-button
v-if="tab.dailyTimes.length < tab.videosPerDay"
type="primary"
size="small"
@click="tab.dailyTimes.push('10:00')"
>
添加时间
</el-button>
</div>
<div class="schedule-item">
<span class="label">开始天数:</span>
<el-select v-model="tab.startDays" placeholder="选择开始天数">
<el-option :label="'明天'" :value="0" />
<el-option :label="'后天'" :value="1" />
</el-select>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button size="small" @click="cancelPublish(tab)">取消</el-button>
<el-button
size="small"
type="primary"
@click="confirmPublish(tab)"
:loading="tab.publishing || false"
>
{{ tab.publishing ? '发布中...' : '发布' }}
</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { Upload, Plus, Close, Folder } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useAccountStore } from '@/stores/account'
import { useAppStore } from '@/stores/app'
import { materialApi } from '@/api/material'
import { http } from '@/utils/request'
// API base URL
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'
// Authorization headers
const authHeaders = computed(() => ({
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
}))
// 当前激活的tab
const activeTab = ref('tab1')
// tab计数器
let tabCounter = 1
// 获取应用状态管理
const appStore = useAppStore()
// 上传相关状态
const uploadOptionsVisible = ref(false)
const localUploadVisible = ref(false)
const materialLibraryVisible = ref(false)
const currentUploadTab = ref(null)
const selectedMaterials = ref([])
const materials = computed(() => appStore.materials)
// 批量发布相关状态
const batchPublishing = ref(false)
const batchPublishMessage = ref('')
const batchPublishType = ref('info')
// 平台列表 - 对应后端type字段
const platforms = [
{ key: 3, name: '抖音' },
{ key: 4, name: '快手' },
{ key: 2, name: '视频号' },
{ key: 1, name: '小红书' }
]
const defaultTabInit = {
name: 'tab1',
label: '发布1',
fileList: [], // 后端返回的文件名列表
displayFileList: [], // 用于显示的文件列表
selectedAccounts: [], // 选中的账号ID列表
selectedPlatform: 1, // 选中的平台(单选)
title: '',
productLink: '', // 商品链接
productTitle: '', // 商品名称
selectedTopics: [], // 话题列表(不带#号)
scheduleEnabled: false, // 定时发布开关
videosPerDay: 1, // 每天发布视频数量
dailyTimes: ['10:00'], // 每天发布时间点列表
startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
publishStatus: null, // 发布状态,包含message和type
publishing: false, // 发布状态,用于控制按钮loading效果
isDraft: false, // 是否保存为草稿,仅视频号平台可见
isOriginal: false // 是否标记为原创
}
// helper to create a fresh deep-copied tab from defaultTabInit
const makeNewTab = () => {
// prefer structuredClone when available (newer browsers/node), fallback to JSON
try {
return typeof structuredClone === 'function' ? structuredClone(defaultTabInit) : JSON.parse(JSON.stringify(defaultTabInit))
} catch (e) {
return JSON.parse(JSON.stringify(defaultTabInit))
}
}
// tab页数据 - 默认只有一个tab (use deep copy to avoid shared refs)
const tabs = reactive([
makeNewTab()
])
// 账号相关状态
const accountDialogVisible = ref(false)
const tempSelectedAccounts = ref([])
const currentTab = ref(null)
// 获取账号状态管理
const accountStore = useAccountStore()
// 根据选择的平台获取可用账号列表
const availableAccounts = computed(() => {
const platformMap = {
3: '抖音',
2: '视频号',
1: '小红书',
4: '快手'
}
const currentPlatform = currentTab.value ? platformMap[currentTab.value.selectedPlatform] : null
return currentPlatform ? accountStore.accounts.filter(acc => acc.platform === currentPlatform) : []
})
// 话题相关状态
const topicDialogVisible = ref(false)
const customTopic = ref('')
// 推荐话题列表
const recommendedTopics = [
'游戏', '电影', '音乐', '美食', '旅行', '文化',
'科技', '生活', '娱乐', '体育', '教育', '艺术',
'健康', '时尚', '美妆', '摄影', '宠物', '汽车'
]
// 添加新tab
const addTab = () => {
tabCounter++
const newTab = makeNewTab()
newTab.name = `tab${tabCounter}`
newTab.label = `发布${tabCounter}`
tabs.push(newTab)
activeTab.value = newTab.name
}
// 删除tab
const removeTab = (tabName) => {
const index = tabs.findIndex(tab => tab.name === tabName)
if (index > -1) {
tabs.splice(index, 1)
// 如果删除的是当前激活的tab,切换到第一个tab
if (activeTab.value === tabName && tabs.length > 0) {
activeTab.value = tabs[0].name
}
}
}
// 处理文件上传成功
const handleUploadSuccess = (response, file, tab) => {
if (response.code === 200) {
// 获取文件路径
const filePath = response.data.path || response.data
// 从路径中提取文件名
const filename = filePath.split('/').pop()
// 保存文件信息到fileList,包含文件路径和其他信息
const fileInfo = {
name: file.name,
url: materialApi.getMaterialPreviewUrl(filename), // 使用getMaterialPreviewUrl生成预览URL
path: filePath,
size: file.size,
type: file.type
}
// 添加到文件列表
tab.fileList.push(fileInfo)
// 更新显示列表
tab.displayFileList = [...tab.fileList.map(item => ({
name: item.name,
url: item.url
}))]
ElMessage.success('文件上传成功')
} else {
ElMessage.error(response.msg || '上传失败')
}
}
// 处理文件上传失败
const handleUploadError = (error) => {
ElMessage.error('文件上传失败')
}
// 删除已上传文件
const removeFile = (tab, index) => {
// 从文件列表中删除
tab.fileList.splice(index, 1)
// 更新显示列表
tab.displayFileList = [...tab.fileList.map(item => ({
name: item.name,
url: item.url
}))]
ElMessage.success('文件删除成功')
}
// 话题相关方法
// 打开添加话题弹窗
const openTopicDialog = (tab) => {
currentTab.value = tab
topicDialogVisible.value = true
}
// 添加自定义话题
const addCustomTopic = () => {
if (!customTopic.value.trim()) {
ElMessage.warning('请输入话题内容')
return
}
if (currentTab.value && !currentTab.value.selectedTopics.includes(customTopic.value.trim())) {
currentTab.value.selectedTopics.push(customTopic.value.trim())
customTopic.value = ''
ElMessage.success('话题添加成功')
} else {
ElMessage.warning('话题已存在')
}
}
// 切换推荐话题
const toggleRecommendedTopic = (topic) => {
if (!currentTab.value) return
const index = currentTab.value.selectedTopics.indexOf(topic)
if (index > -1) {
currentTab.value.selectedTopics.splice(index, 1)
} else {
currentTab.value.selectedTopics.push(topic)
}
}
// 删除话题
const removeTopic = (tab, index) => {
tab.selectedTopics.splice(index, 1)
}
// 确认添加话题
const confirmTopicSelection = () => {
topicDialogVisible.value = false
customTopic.value = ''
currentTab.value = null
ElMessage.success('添加话题完成')
}
// 账号选择相关方法
// 打开账号选择弹窗
const openAccountDialog = (tab) => {
currentTab.value = tab
tempSelectedAccounts.value = [...tab.selectedAccounts]
accountDialogVisible.value = true
}
// 确认账号选择
const confirmAccountSelection = () => {
if (currentTab.value) {
currentTab.value.selectedAccounts = [...tempSelectedAccounts.value]
}
accountDialogVisible.value = false
currentTab.value = null
ElMessage.success('账号选择完成')
}
// 删除选中的账号
const removeAccount = (tab, index) => {
tab.selectedAccounts.splice(index, 1)
}
// 获取账号显示名称
const getAccountDisplayName = (accountId) => {
const account = accountStore.accounts.find(acc => acc.id === accountId)
return account ? account.name : accountId
}
// 取消发布
const cancelPublish = (tab) => {
ElMessage.info('已取消发布')
}
// 确认发布
const confirmPublish = async (tab) => {
// 防止重复点击
if (tab.publishing) {
throw new Error('正在发布中,请稍候...')
}
tab.publishing = true // 设置发布状态为进行中
// 数据验证
if (tab.fileList.length === 0) {
ElMessage.error('请先上传视频文件')
tab.publishing = false
throw new Error('请先上传视频文件')
}
if (!tab.title.trim()) {
ElMessage.error('请输入标题')
tab.publishing = false
throw new Error('请输入标题')
}
if (!tab.selectedPlatform) {
ElMessage.error('请选择发布平台')
tab.publishing = false
throw new Error('请选择发布平台')
}
if (tab.selectedAccounts.length === 0) {
ElMessage.error('请选择发布账号')
tab.publishing = false
throw new Error('请选择发布账号')
}
// 构造发布数据,符合后端API格式
const publishData = {
type: tab.selectedPlatform,
title: tab.title,
tags: tab.selectedTopics, // 不带#号的话题列表
fileList: tab.fileList.map(file => file.path), // 只发送文件路径
accountList: tab.selectedAccounts.map(accountId => {
const account = accountStore.accounts.find(acc => acc.id === accountId)
return account ? account.filePath : accountId
}), // 发送账号的文件路径
enableTimer: tab.scheduleEnabled ? 1 : 0,
videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1,
dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'],
startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0,
category: tab.isOriginal ? 1 : 0, // 1表示原创,0表示非原创
productLink: tab.productLink.trim() || '',
productTitle: tab.productTitle.trim() || '',
isDraft: tab.isDraft
}
// 调用后端发布API(使用统一的http封装)
try {
const data = await http.post('/postVideo', publishData)
tab.publishStatus = {
message: '发布成功',
type: 'success'
}
// 清空当前tab的数据
tab.fileList = []
tab.displayFileList = []
tab.title = ''
tab.selectedTopics = []
tab.selectedAccounts = []
tab.scheduleEnabled = false
} catch (error) {
console.error('发布错误:', error)
tab.publishStatus = {
message: `发布失败:${error.message || '请检查网络连接'}`,
type: 'error'
}
throw error
} finally {
tab.publishing = false
}
}
// 显示上传选项
const showUploadOptions = (tab) => {
currentUploadTab.value = tab
uploadOptionsVisible.value = true
}
// 选择本地上传
const selectLocalUpload = () => {
uploadOptionsVisible.value = false
localUploadVisible.value = true
}
// 选择素材库
const selectMaterialLibrary = async () => {
uploadOptionsVisible.value = false
// 如果素材库为空,先获取素材数据
if (materials.value.length === 0) {
try {
const response = await materialApi.getAllMaterials()
if (response.code === 200) {
appStore.setMaterials(response.data)
} else {
ElMessage.error('获取素材列表失败')
return
}
} catch (error) {
console.error('获取素材列表出错:', error)
ElMessage.error('获取素材列表失败')
return
}
}
selectedMaterials.value = []
materialLibraryVisible.value = true
}
// 确认素材选择
const confirmMaterialSelection = () => {
if (selectedMaterials.value.length === 0) {
ElMessage.warning('请选择至少一个素材')
return
}
if (currentUploadTab.value) {
// 将选中的素材添加到当前tab的文件列表
selectedMaterials.value.forEach(materialId => {
const material = materials.value.find(m => m.id === materialId)
if (material) {
const fileInfo = {
name: material.filename,
url: materialApi.getMaterialPreviewUrl(material.file_path.split('/').pop()),
path: material.file_path,
size: material.filesize * 1024 * 1024, // 转换为字节
type: 'video/mp4'
}
// 检查是否已存在相同文件
const exists = currentUploadTab.value.fileList.some(file => file.path === fileInfo.path)
if (!exists) {
currentUploadTab.value.fileList.push(fileInfo)
}
}
})
// 更新显示列表
currentUploadTab.value.displayFileList = [...currentUploadTab.value.fileList.map(item => ({
name: item.name,
url: item.url
}))]
}
const addedCount = selectedMaterials.value.length
materialLibraryVisible.value = false
selectedMaterials.value = []
currentUploadTab.value = null
ElMessage.success(`已添加 ${addedCount} 个素材`)
}
// 批量发布对话框状态
const batchPublishDialogVisible = ref(false)
const currentPublishingTab = ref(null)
const publishProgress = ref(0)
const publishResults = ref([])
const isCancelled = ref(false)
// 取消批量发布
const cancelBatchPublish = () => {
isCancelled.value = true
ElMessage.info('正在取消发布...')
}
// 批量发布方法
const batchPublish = async () => {
if (batchPublishing.value) return
batchPublishing.value = true
currentPublishingTab.value = null
publishProgress.value = 0
publishResults.value = []
isCancelled.value = false
batchPublishDialogVisible.value = true
try {
for (let i = 0; i < tabs.length; i++) {
if (isCancelled.value) {
publishResults.value.push({
label: tabs[i].label,
status: 'cancelled',
message: '已取消'
})
continue
}
const tab = tabs[i]
currentPublishingTab.value = tab
publishProgress.value = Math.floor((i / tabs.length) * 100)
try {
await confirmPublish(tab)
publishResults.value.push({
label: tab.label,
status: 'success',
message: '发布成功'
})
} catch (error) {
publishResults.value.push({
label: tab.label,
status: 'error',
message: error.message
})
// 不立即返回,继续显示发布结果
}
}
publishProgress.value = 100
// 统计发布结果
const successCount = publishResults.value.filter(r => r.status === 'success').length
const failCount = publishResults.value.filter(r => r.status === 'error').length
const cancelCount = publishResults.value.filter(r => r.status === 'cancelled').length
if (isCancelled.value) {
ElMessage.warning(`发布已取消:${successCount}个成功,${failCount}个失败,${cancelCount}个未执行`)
} else if (failCount > 0) {
ElMessage.error(`发布完成:${successCount}个成功,${failCount}个失败`)
} else {
ElMessage.success('所有Tab发布成功')
setTimeout(() => {
batchPublishDialogVisible.value = false
}, 1000)
}
} catch (error) {
console.error('批量发布出错:', error)
ElMessage.error('批量发布出错,请重试')
} finally {
batchPublishing.value = false
isCancelled.value = false
}
}
</script>
<style lang="scss" scoped>
@use '@/styles/variables.scss' as *;
.publish-center {
display: flex;
flex-direction: column;
height: 100%;
// Tab管理区域
.tab-management {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
padding: 15px 20px;
.tab-header {
display: flex;
align-items: flex-start;
gap: 15px;
.tab-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
flex: 1;
min-width: 0;
.tab-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background-color: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
height: 32px;
&:hover {
background-color: #ecf5ff;
border-color: #b3d8ff;
}
&.active {
background-color: #409eff;
border-color: #409eff;
color: #fff;
.close-icon {
color: #fff;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
}
}
.close-icon {
padding: 2px;
border-radius: 2px;
cursor: pointer;
transition: background-color 0.3s;
font-size: 12px;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
}
}
.tab-actions {
display: flex;
gap: 10px;
flex-shrink: 0;
.add-tab-btn,
.batch-publish-btn {
gitextract_x6olz_e2/
├── .dockerignore
├── .gitignore
├── CLAUDE.md
├── Dockerfile
├── README.md
├── __init__.py
├── cli_main.py
├── conf.example.py
├── db/
│ └── createTable.py
├── examples/
│ ├── __init__.py
│ ├── get_baijiahao_cookie.py
│ ├── get_bilibili_cookie.py
│ ├── get_douyin_cookie.py
│ ├── get_kuaishou_cookie.py
│ ├── get_tencent_cookie.py
│ ├── get_tk_cookie.py
│ ├── get_xiaohongshu_cookie.py
│ ├── upload_video_to_baijiahao.py
│ ├── upload_video_to_bilibili.py
│ ├── upload_video_to_douyin.py
│ ├── upload_video_to_kuaishou.py
│ ├── upload_video_to_tencent.py
│ ├── upload_video_to_tiktok.py
│ ├── upload_video_to_xhs.py
│ └── upload_video_to_xiaohongshu.py
├── myUtils/
│ ├── __init__.py
│ ├── auth.py
│ ├── login.py
│ └── postVideo.py
├── requirements.txt
├── sau_backend/
│ └── README.md
├── sau_backend.py
├── sau_frontend/
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── App.vue
│ │ ├── api/
│ │ │ ├── account.js
│ │ │ ├── index.js
│ │ │ ├── material.js
│ │ │ └── user.js
│ │ ├── main.js
│ │ ├── router/
│ │ │ └── index.js
│ │ ├── stores/
│ │ │ ├── account.js
│ │ │ ├── app.js
│ │ │ ├── index.js
│ │ │ └── user.js
│ │ ├── styles/
│ │ │ ├── index.scss
│ │ │ ├── reset.scss
│ │ │ └── variables.scss
│ │ ├── utils/
│ │ │ └── request.js
│ │ └── views/
│ │ ├── About.vue
│ │ ├── AccountManagement.vue
│ │ ├── Dashboard.vue
│ │ ├── MaterialManagement.vue
│ │ └── PublishCenter.vue
│ └── vite.config.js
├── start-win.bat
├── uploader/
│ ├── __init__.py
│ ├── baijiahao_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── bilibili_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── douyin_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── ks_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── tencent_uploader/
│ │ ├── __init__.py
│ │ └── main.py
│ ├── tk_uploader/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── main_chrome.py
│ │ └── tk_config.py
│ ├── xhs_uploader/
│ │ ├── __init__.py
│ │ ├── accounts.ini
│ │ ├── main.py
│ │ └── xhs_login_qrcode.py
│ └── xiaohongshu_uploader/
│ ├── __init__.py
│ └── main.py
├── utils/
│ ├── __init__.py
│ ├── base_social_media.py
│ ├── browser_hook.py
│ ├── constant.py
│ ├── files_times.py
│ ├── log.py
│ └── network.py
└── videos/
└── demo.txt
SYMBOL INDEX (164 symbols across 23 files)
FILE: cli_main.py
function parse_schedule (line 18) | def parse_schedule(schedule_raw):
function main (line 26) | async def main():
FILE: myUtils/auth.py
function cookie_auth_douyin (line 15) | async def cookie_auth_douyin(account_file):
function cookie_auth_tencent (line 43) | async def cookie_auth_tencent(account_file):
function cookie_auth_ks (line 61) | async def cookie_auth_ks(account_file):
function cookie_auth_xhs (line 80) | async def cookie_auth_xhs(account_file):
function check_cookie (line 105) | async def check_cookie(type, file_path):
FILE: myUtils/login.py
function get_browser_options (line 13) | def get_browser_options():
function douyin_cookie_gen (line 30) | async def douyin_cookie_gen(id,status_queue):
function get_tencent_cookie (line 94) | async def get_tencent_cookie(id,status_queue):
function get_ks_cookie (line 172) | async def get_ks_cookie(id,status_queue):
function xiaohongshu_cookie_gen (line 246) | async def xiaohongshu_cookie_gen(id,status_queue):
FILE: myUtils/postVideo.py
function post_video_tencent (line 13) | def post_video_tencent(title,files,tags,account_file,category=TencentZon...
function post_video_DouYin (line 32) | def post_video_DouYin(title,files,tags,account_file,category=TencentZone...
function post_video_ks (line 53) | def post_video_ks(title,files,tags,account_file,category=TencentZoneType...
function post_video_xhs (line 71) | def post_video_xhs(title,files,tags,account_file,category=TencentZoneTyp...
FILE: sau_backend.py
function custom_static (line 30) | def custom_static(filename):
function favicon (line 35) | def favicon():
function vite_svg (line 39) | def vite_svg():
function index (line 44) | def index(): # put application's code here
function upload_file (line 48) | def upload_file():
function get_file (line 73) | def get_file():
function upload_save (line 92) | def upload_save():
function get_all_files (line 154) | def get_all_files():
function getAccounts (line 194) | def getAccounts():
function getValidAccounts (line 225) | async def getValidAccounts():
function delete_file (line 256) | def delete_file():
function delete_account (line 318) | def delete_account():
function login (line 379) | def login():
function postVideo (line 403) | def postVideo():
function updateUserinfo (line 477) | def updateUserinfo():
function postVideoBatch (line 514) | def postVideoBatch():
function upload_cookie (line 563) | def upload_cookie():
function download_cookie (line 638) | def download_cookie():
function run_async_function (line 683) | def run_async_function(type,id,status_queue):
function sse_stream (line 707) | def sse_stream(status_queue):
FILE: sau_frontend/src/api/account.js
method getValidAccounts (line 6) | getValidAccounts() {
method getAccounts (line 11) | getAccounts() {
method addAccount (line 16) | addAccount(data) {
method updateAccount (line 21) | updateAccount(data) {
method deleteAccount (line 26) | deleteAccount(id) {
FILE: sau_frontend/src/utils/request.js
method get (line 74) | get(url, params) {
method post (line 78) | post(url, data, config = {}) {
method put (line 82) | put(url, data, config = {}) {
method delete (line 86) | delete(url, params) {
method upload (line 90) | upload(url, formData, onUploadProgress) {
FILE: uploader/baijiahao_uploader/main.py
function baijiahao_cookie_gen (line 16) | async def baijiahao_cookie_gen(account_file):
function cookie_auth (line 38) | async def cookie_auth(account_file):
function baijiahao_setup (line 57) | async def baijiahao_setup(account_file, handle=False):
class BaiJiaHaoVideo (line 65) | class BaiJiaHaoVideo(object):
method __init__ (line 66) | def __init__(self, title, file_path, tags, publish_date: datetime, acc...
method set_schedule_time (line 77) | async def set_schedule_time(self, page, publish_date):
method handle_upload_error (line 116) | async def handle_upload_error(self, page):
method upload (line 121) | async def upload(self, playwright: Playwright) -> None:
method uploading_video (line 189) | async def uploading_video(self, page):
method set_schedule_publish (line 208) | async def set_schedule_publish(self, page, publish_date):
method publish_video (line 224) | async def publish_video(self, page: Page, publish_date):
method direct_publish (line 232) | async def direct_publish(self, page):
method add_title_tags (line 241) | async def add_title_tags(self, page):
method main (line 247) | async def main(self):
method ai2video (line 254) | async def ai2video(self, playwright: Playwright) -> None:
method mainAi (line 504) | async def mainAi(self):
FILE: uploader/bilibili_uploader/main.py
function extract_keys_from_json (line 9) | def extract_keys_from_json(data):
function read_cookie_json_file (line 26) | def read_cookie_json_file(filepath: pathlib.Path):
function random_emoji (line 32) | def random_emoji():
class BilibiliUploader (line 42) | class BilibiliUploader(object):
method __init__ (line 43) | def __init__(self, cookie_data, file: pathlib.Path, title, desc, tid, ...
method _init_data (line 56) | def _init_data(self):
method upload (line 65) | def upload(self):
FILE: uploader/douyin_uploader/main.py
function cookie_auth (line 13) | async def cookie_auth(account_file):
function douyin_setup (line 38) | async def douyin_setup(account_file, handle=False):
function douyin_cookie_gen (line 48) | async def douyin_cookie_gen(account_file):
class DouYinVideo (line 66) | class DouYinVideo(object):
method __init__ (line 67) | def __init__(self, title, file_path, tags, publish_date: datetime, acc...
method set_schedule_time_douyin (line 80) | async def set_schedule_time_douyin(self, page, publish_date):
method handle_upload_error (line 96) | async def handle_upload_error(self, page):
method upload (line 100) | async def upload(self, playwright: Playwright) -> None:
method handle_auto_video_cover (line 229) | async def handle_auto_video_cover(self, page):
method set_thumbnail (line 265) | async def set_thumbnail(self, page: Page, thumbnail_path: str):
method set_location (line 285) | async def set_location(self, page: Page, location: str = ""):
method handle_product_dialog (line 298) | async def handle_product_dialog(self, page: Page, product_title: str):
method set_product_link (line 334) | async def set_product_link(self, page: Page, product_link: str, produc...
method main (line 390) | async def main(self):
FILE: uploader/ks_uploader/main.py
function cookie_auth (line 14) | async def cookie_auth(account_file):
function ks_setup (line 33) | async def ks_setup(account_file, handle=False):
function get_ks_cookie (line 43) | async def get_ks_cookie(account_file):
class KSVideo (line 64) | class KSVideo(object):
method __init__ (line 65) | def __init__(self, title, file_path, tags, publish_date: datetime, acc...
method handle_upload_error (line 75) | async def handle_upload_error(self, page):
method upload (line 79) | async def upload(self, playwright: Playwright) -> None:
method main (line 196) | async def main(self):
method set_schedule_time (line 200) | async def set_schedule_time(self, page, publish_date):
FILE: uploader/tencent_uploader/main.py
function format_str_for_short_title (line 14) | def format_str_for_short_title(origin_title: str) -> str:
function cookie_auth (line 34) | async def cookie_auth(account_file):
function get_tencent_cookie (line 52) | async def get_tencent_cookie(account_file):
function weixin_setup (line 73) | async def weixin_setup(account_file, handle=False):
class TencentVideo (line 84) | class TencentVideo(object):
method __init__ (line 85) | def __init__(self, title, file_path, tags, publish_date: datetime, acc...
method set_schedule_time_tencent (line 96) | async def set_schedule_time_tencent(self, page, publish_date):
method handle_upload_error (line 131) | async def handle_upload_error(self, page):
method upload (line 138) | async def upload(self, playwright: Playwright) -> None:
method add_short_title (line 179) | async def add_short_title(self, page):
method click_publish (line 187) | async def click_publish(self, page):
method detect_upload_status (line 222) | async def detect_upload_status(self, page):
method add_title_tags (line 243) | async def add_title_tags(self, page):
method add_collection (line 252) | async def add_collection(self, page):
method add_original (line 259) | async def add_original(self, page):
method main (line 283) | async def main(self):
FILE: uploader/tk_uploader/main.py
function cookie_auth (line 15) | async def cookie_auth(account_file):
function tiktok_setup (line 41) | async def tiktok_setup(account_file, handle=False):
function get_tiktok_cookie (line 51) | async def get_tiktok_cookie(account_file):
class TiktokVideo (line 72) | class TiktokVideo(object):
method __init__ (line 73) | def __init__(self, title, file_path, tags, publish_date, account_file):
method set_schedule_time (line 83) | async def set_schedule_time(self, page, publish_date):
method handle_upload_error (line 136) | async def handle_upload_error(self, page):
method upload (line 144) | async def upload(self, playwright: Playwright) -> None:
method add_title_tags (line 187) | async def add_title_tags(self, page):
method click_publish (line 220) | async def click_publish(self, page):
method detect_upload_status (line 241) | async def detect_upload_status(self, page):
method choose_base_locator (line 257) | async def choose_base_locator(self, page):
method main (line 264) | async def main(self):
FILE: uploader/tk_uploader/main_chrome.py
function cookie_auth (line 16) | async def cookie_auth(account_file):
function tiktok_setup (line 42) | async def tiktok_setup(account_file, handle=False):
function get_tiktok_cookie (line 52) | async def get_tiktok_cookie(account_file):
class TiktokVideo (line 73) | class TiktokVideo(object):
method __init__ (line 74) | def __init__(self, title, file_path, tags, publish_date, account_file,...
method set_schedule_time (line 85) | async def set_schedule_time(self, page, publish_date):
method handle_upload_error (line 141) | async def handle_upload_error(self, page):
method upload (line 149) | async def upload(self, playwright: Playwright) -> None:
method add_title_tags (line 199) | async def add_title_tags(self, page):
method upload_thumbnails (line 232) | async def upload_thumbnails(self, page):
method change_language (line 243) | async def change_language(self, page):
method click_publish (line 256) | async def click_publish(self, page):
method get_last_video_id (line 272) | async def get_last_video_id(self, page):
method detect_upload_status (line 281) | async def detect_upload_status(self, page):
method choose_base_locator (line 300) | async def choose_base_locator(self, page):
method main (line 307) | async def main(self):
FILE: uploader/tk_uploader/tk_config.py
class Tk_Locator (line 2) | class Tk_Locator(object):
FILE: uploader/xhs_uploader/main.py
function sign_local (line 15) | def sign_local(uri, data=None, a1="", web_session=""):
function sign (line 46) | def sign(uri, data=None, a1="", web_session=""):
function beauty_print (line 57) | def beauty_print(data: dict):
FILE: uploader/xiaohongshu_uploader/main.py
function cookie_auth (line 13) | async def cookie_auth(account_file):
function xiaohongshu_setup (line 38) | async def xiaohongshu_setup(account_file, handle=False):
function xiaohongshu_cookie_gen (line 48) | async def xiaohongshu_cookie_gen(account_file):
class XiaoHongShuVideo (line 66) | class XiaoHongShuVideo(object):
method __init__ (line 67) | def __init__(self, title, file_path, tags, publish_date: datetime, acc...
method set_schedule_time_xiaohongshu (line 78) | async def set_schedule_time_xiaohongshu(self, page, publish_date):
method handle_upload_error (line 105) | async def handle_upload_error(self, page):
method upload (line 109) | async def upload(self, playwright: Playwright) -> None:
method set_thumbnail (line 246) | async def set_thumbnail(self, page: Page, thumbnail_path: str):
method set_location (line 261) | async def set_location(self, page: Page, location: str = "青岛市"):
method main (line 364) | async def main(self):
FILE: utils/base_social_media.py
function get_supported_social_media (line 13) | def get_supported_social_media() -> List[str]:
function get_cli_action (line 17) | def get_cli_action() -> List[str]:
function set_init_script (line 21) | async def set_init_script(context):
FILE: utils/browser_hook.py
function get_browser_options (line 3) | def get_browser_options():
FILE: utils/constant.py
class TencentZoneTypes (line 4) | class TencentZoneTypes(enum.Enum):
class VideoZoneTypes (line 30) | class VideoZoneTypes(enum.Enum):
FILE: utils/files_times.py
function get_absolute_path (line 9) | def get_absolute_path(relative_path: str, base_dir: str = None) -> str:
function get_title_and_hashtags (line 15) | def get_title_and_hashtags(filename):
function generate_schedule_time_next_day (line 41) | def generate_schedule_time_next_day(total_videos, videos_per_day = 1, da...
FILE: utils/log.py
function log_formatter (line 8) | def log_formatter(record: dict) -> str:
function create_logger (line 26) | def create_logger(log_name: str, file_path: str):
FILE: utils/network.py
function async_retry (line 6) | def async_retry(timeout=60, max_retries=None):
Condensed preview — 86 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (340K chars).
[
{
"path": ".dockerignore",
"chars": 107,
"preview": ".venv\n.git\n.idea\ncookies\nconf.py\nsau_frontend/dist\nsau_frontend/node_modules\nsau_frontend/package-lock.json"
},
{
"path": ".gitignore",
"chars": 3384,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "CLAUDE.md",
"chars": 3256,
"preview": "## Project Overview\n\nThis project, `social-auto-upload`, is a powerful automation tool designed to help content creators"
},
{
"path": "Dockerfile",
"chars": 1388,
"preview": "FROM node:22.21.1 AS builder\n\nWORKDIR /app\n\nRUN npm config set registry https://registry.npmmirror.com\n\nCOPY sau_fronten"
},
{
"path": "README.md",
"chars": 6321,
"preview": "# social-auto-upload\n\n`social-auto-upload` 是一个强大的自动化工具,旨在帮助内容创作者和运营者高效地将视频内容一键发布到多个国内外主流社交媒体平台。\n项目实现了对 `抖音`、`Bilibili`、`"
},
{
"path": "__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "cli_main.py",
"chars": 4455,
"preview": "import argparse\nimport asyncio\nfrom datetime import datetime\nfrom os.path import exists\nfrom pathlib import Path\n\nfrom c"
},
{
"path": "conf.example.py",
"chars": 251,
"preview": "from pathlib import Path\n\nBASE_DIR = Path(__file__).parent.resolve()\nXHS_SERVER = \"http://127.0.0.1:11901\"\nLOCAL_CHROME_"
},
{
"path": "db/createTable.py",
"chars": 902,
"preview": "import sqlite3\nimport json\nimport os\n\n# 数据库文件路径(如果不存在会自动创建)\ndb_file = './database.db'\n\n# 如果数据库已存在,则删除旧的表(可选)\n# if os.pat"
},
{
"path": "examples/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "examples/get_baijiahao_cookie.py",
"chars": 367,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.baijiahao_uploader.main import baijiaha"
},
{
"path": "examples/get_bilibili_cookie.py",
"chars": 67,
"preview": "# cd uploader/bilibili_uploader\n# biliup.exe -u account.json login\n"
},
{
"path": "examples/get_douyin_cookie.py",
"chars": 355,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.douyin_uploader.main import douyin_setu"
},
{
"path": "examples/get_kuaishou_cookie.py",
"chars": 339,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.ks_uploader.main import ks_setup\n\nif __"
},
{
"path": "examples/get_tencent_cookie.py",
"chars": 357,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.tencent_uploader.main import weixin_set"
},
{
"path": "examples/get_tk_cookie.py",
"chars": 354,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.tk_uploader.main_chrome import tiktok_s"
},
{
"path": "examples/get_xiaohongshu_cookie.py",
"chars": 375,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.xiaohongshu_uploader.main import xiaoho"
},
{
"path": "examples/upload_video_to_baijiahao.py",
"chars": 1086,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.baijiahao_uploader.main import baijiaha"
},
{
"path": "examples/upload_video_to_bilibili.py",
"chars": 1693,
"preview": "import time\nfrom pathlib import Path\n\nfrom uploader.bilibili_uploader.main import read_cookie_json_file, extract_keys_fr"
},
{
"path": "examples/upload_video_to_douyin.py",
"chars": 1276,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.douyin_uploader.main import douyin_setu"
},
{
"path": "examples/upload_video_to_kuaishou.py",
"chars": 994,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.ks_uploader.main import ks_setup, KSVid"
},
{
"path": "examples/upload_video_to_tencent.py",
"chars": 1139,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.tencent_uploader.main import weixin_set"
},
{
"path": "examples/upload_video_to_tiktok.py",
"chars": 1339,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\n# from tk_uploader.main import tiktok_setup, TiktokVi"
},
{
"path": "examples/upload_video_to_xhs.py",
"chars": 2280,
"preview": "import configparser\nfrom pathlib import Path\nfrom time import sleep\n\nfrom xhs import XhsClient\n\nfrom conf import BASE_DI"
},
{
"path": "examples/upload_video_to_xiaohongshu.py",
"chars": 1317,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.xiaohongshu_uploader.main import xiaoho"
},
{
"path": "myUtils/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "myUtils/auth.py",
"chars": 4648,
"preview": "import asyncio\nimport configparser\nimport os\n\nfrom playwright.async_api import async_playwright\nfrom xhs import XhsClien"
},
{
"path": "myUtils/login.py",
"chars": 11747,
"preview": "import asyncio\nimport sqlite3\n\nfrom playwright.async_api import async_playwright\n\nfrom myUtils.auth import check_cookie\n"
},
{
"path": "myUtils/postVideo.py",
"chars": 4317,
"preview": "import asyncio\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\nfrom uploader.douyin_uploader.main import DouYinVideo"
},
{
"path": "sau_backend/README.md",
"chars": 1244,
"preview": "## 启动项目:\npython 版本:3.10\n1. 安装依赖\n pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple\n2. 删除 db"
},
{
"path": "sau_backend.py",
"chars": 22290,
"preview": "import asyncio\nimport os\nimport sqlite3\nimport threading\nimport time\nimport uuid\nfrom pathlib import Path\nfrom queue imp"
},
{
"path": "sau_frontend/README.md",
"chars": 2267,
"preview": "# Vue3 + Vite 项目\n\n一个基于 Vue3、Vite、Element Plus、Pinia、Vue Router 和 Axios 的现代化前端项目模板。\n\n## 🚀 特性\n\n- ⚡️ **Vite** - 极速的构建工具\n- 🖖"
},
{
"path": "sau_frontend/index.html",
"chars": 360,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "sau_frontend/package.json",
"chars": 507,
"preview": "{\n \"name\": \"sau-admin\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"author\": \"Edan.Lee\",\n \"type\": \"module\",\n \"scripts"
},
{
"path": "sau_frontend/src/App.vue",
"chars": 4236,
"preview": "<template>\n <div id=\"app\">\n <el-container>\n <el-aside :width=\"isCollapse ? '64px' : '200px'\">\n <div clas"
},
{
"path": "sau_frontend/src/api/account.js",
"chars": 499,
"preview": "import { http } from '@/utils/request'\n\n// 账号管理相关API\nexport const accountApi = {\n // 获取有效账号列表(带验证)\n getValidAccounts()"
},
{
"path": "sau_frontend/src/api/index.js",
"chars": 193,
"preview": "// API 统一导出\nexport * from './user'\nexport * from './account'\nexport * from './material'\n\n// 可以在这里添加其他API模块的导出\n// export "
},
{
"path": "sau_frontend/src/api/material.js",
"chars": 750,
"preview": "import { http } from '@/utils/request'\n\n// 素材管理API\nexport const materialApi = {\n // 获取所有素材\n getAllMaterials: () => {\n "
},
{
"path": "sau_frontend/src/api/user.js",
"chars": 108,
"preview": "import { http } from '@/utils/request'\n\n// 用户相关API(预留)\n// 注意:当前后端暂无用户认证接口,以下为预留定义\nexport const userApi = {}\n"
},
{
"path": "sau_frontend/src/main.js",
"chars": 513,
"preview": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport router from './router'\nimport pinia from './stores'\ni"
},
{
"path": "sau_frontend/src/router/index.js",
"chars": 902,
"preview": "import { createRouter, createWebHashHistory } from 'vue-router'\nimport Dashboard from '../views/Dashboard.vue'\nimport Ac"
},
{
"path": "sau_frontend/src/stores/account.js",
"chars": 1354,
"preview": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useAccountStore = defineStore('account', () "
},
{
"path": "sau_frontend/src/stores/app.js",
"chars": 1541,
"preview": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useAppStore = defineStore('app', () => {\n /"
},
{
"path": "sau_frontend/src/stores/index.js",
"chars": 258,
"preview": "import { createPinia } from 'pinia'\nimport { useUserStore } from './user'\nimport { useAccountStore } from './account'\nim"
},
{
"path": "sau_frontend/src/stores/user.js",
"chars": 507,
"preview": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useUserStore = defineStore('user', () => {\n "
},
{
"path": "sau_frontend/src/styles/index.scss",
"chars": 2499,
"preview": "// 导入重置样式\n@use './reset.scss';\n\n// 导入变量\n@use './variables.scss' as *;\n\n// 全局样式\nbody {\n font-family: -apple-system, Blin"
},
{
"path": "sau_frontend/src/styles/reset.scss",
"chars": 2015,
"preview": "/* CSS Reset - 删除浏览器默认样式 */\n\n/* 1. Use a more-intuitive box-sizing model */\n*, *::before, *::after {\n box-sizing: borde"
},
{
"path": "sau_frontend/src/styles/variables.scss",
"chars": 1099,
"preview": "// 颜色变量\n$primary-color: #409eff;\n$success-color: #67c23a;\n$warning-color: #e6a23c;\n$danger-color: #f56c6c;\n$info-color: "
},
{
"path": "sau_frontend/src/utils/request.js",
"chars": 2091,
"preview": "import axios from 'axios'\nimport { ElMessage } from 'element-plus'\n\n// 创建axios实例\nconst request = axios.create({\n baseUR"
},
{
"path": "sau_frontend/src/views/About.vue",
"chars": 2359,
"preview": "<template>\n <div class=\"about\">\n <el-card class=\"about-card\">\n <div class=\"about-header\">\n <h1>自媒体自动化运营系"
},
{
"path": "sau_frontend/src/views/AccountManagement.vue",
"chars": 33042,
"preview": "<template>\n <div class=\"account-management\">\n <div class=\"page-header\">\n <h1>账号管理</h1>\n </div>\n \n <div"
},
{
"path": "sau_frontend/src/views/Dashboard.vue",
"chars": 12399,
"preview": "<template>\n <div class=\"dashboard\">\n <div class=\"page-header\">\n <h1>自媒体自动化运营系统</h1>\n </div>\n\n <div class="
},
{
"path": "sau_frontend/src/views/MaterialManagement.vue",
"chars": 14406,
"preview": "<template>\n <div class=\"material-management\">\n <div class=\"page-header\">\n <h1>素材管理</h1>\n </div>\n \n <di"
},
{
"path": "sau_frontend/src/views/PublishCenter.vue",
"chars": 37483,
"preview": "<template>\n <div class=\"publish-center\">\n <!-- Tab管理区域 -->\n <div class=\"tab-management\">\n <div class=\"tab-he"
},
{
"path": "sau_frontend/vite.config.js",
"chars": 863,
"preview": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport { resolve } from 'path'\n\n// https://vite"
},
{
"path": "start-win.bat",
"chars": 993,
"preview": "@echo off\nTITLE One-Click Starter for social-auto-upload\n\nECHO ==================================================\nECHO "
},
{
"path": "uploader/__init__.py",
"chars": 100,
"preview": "from pathlib import Path\n\nfrom conf import BASE_DIR\n\nPath(BASE_DIR / \"cookies\").mkdir(exist_ok=True)"
},
{
"path": "uploader/baijiahao_uploader/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "uploader/baijiahao_uploader/main.py",
"chars": 22096,
"preview": "# -*- coding: utf-8 -*-\nimport random\nfrom datetime import datetime\n\nfrom playwright.async_api import Playwright, async_"
},
{
"path": "uploader/bilibili_uploader/__init__.py",
"chars": 122,
"preview": "from pathlib import Path\n\nfrom conf import BASE_DIR\n\nPath(BASE_DIR / \"cookies\" / \"bilibili_uploader\").mkdir(exist_ok=Tru"
},
{
"path": "uploader/bilibili_uploader/main.py",
"chars": 3044,
"preview": "import json\nimport pathlib\nimport random\nfrom biliup.plugins.bili_webup import BiliBili, Data\n\nfrom utils.log import bil"
},
{
"path": "uploader/douyin_uploader/__init__.py",
"chars": 120,
"preview": "from pathlib import Path\n\nfrom conf import BASE_DIR\n\nPath(BASE_DIR / \"cookies\" / \"douyin_uploader\").mkdir(exist_ok=True)"
},
{
"path": "uploader/douyin_uploader/main.py",
"chars": 16815,
"preview": "# -*- coding: utf-8 -*-\nfrom datetime import datetime\n\nfrom playwright.async_api import Playwright, async_playwright, Pa"
},
{
"path": "uploader/ks_uploader/__init__.py",
"chars": 116,
"preview": "from pathlib import Path\n\nfrom conf import BASE_DIR\n\nPath(BASE_DIR / \"cookies\" / \"ks_uploader\").mkdir(exist_ok=True)"
},
{
"path": "uploader/ks_uploader/main.py",
"chars": 8135,
"preview": "# -*- coding: utf-8 -*-\nfrom datetime import datetime\n\nfrom playwright.async_api import Playwright, async_playwright\nimp"
},
{
"path": "uploader/tencent_uploader/__init__.py",
"chars": 121,
"preview": "from pathlib import Path\n\nfrom conf import BASE_DIR\n\nPath(BASE_DIR / \"cookies\" / \"tencent_uploader\").mkdir(exist_ok=True"
},
{
"path": "uploader/tencent_uploader/main.py",
"chars": 12502,
"preview": "# -*- coding: utf-8 -*-\nfrom datetime import datetime\n\nfrom playwright.async_api import Playwright, async_playwright\nimp"
},
{
"path": "uploader/tk_uploader/__init__.py",
"chars": 116,
"preview": "from pathlib import Path\n\nfrom conf import BASE_DIR\n\nPath(BASE_DIR / \"cookies\" / \"tk_uploader\").mkdir(exist_ok=True)"
},
{
"path": "uploader/tk_uploader/main.py",
"chars": 10940,
"preview": "# -*- coding: utf-8 -*-\nimport re\nfrom datetime import datetime\n\nfrom playwright.async_api import Playwright, async_play"
},
{
"path": "uploader/tk_uploader/main_chrome.py",
"chars": 13403,
"preview": "# -*- coding: utf-8 -*-\nimport re\nfrom datetime import datetime\n\nfrom playwright.async_api import Playwright, async_play"
},
{
"path": "uploader/tk_uploader/tk_config.py",
"chars": 98,
"preview": "\nclass Tk_Locator(object):\n tk_iframe = '[data-tt=\"Upload_index_iframe\"]'\n default = 'body'\n"
},
{
"path": "uploader/xhs_uploader/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "uploader/xhs_uploader/accounts.ini",
"chars": 30,
"preview": "[account1]\ncookies = changeme\n"
},
{
"path": "uploader/xhs_uploader/main.py",
"chars": 2058,
"preview": "import configparser\nimport json\nimport pathlib\nfrom time import sleep\n\nimport requests\nfrom playwright.sync_api import s"
},
{
"path": "uploader/xhs_uploader/xhs_login_qrcode.py",
"chars": 944,
"preview": "import datetime\nimport json\nimport qrcode\nfrom time import sleep\n\nfrom xhs import XhsClient\n\nfrom uploader.xhs_uploader."
},
{
"path": "uploader/xiaohongshu_uploader/__init__.py",
"chars": 125,
"preview": "from pathlib import Path\n\nfrom conf import BASE_DIR\n\nPath(BASE_DIR / \"cookies\" / \"xiaohongshu_uploader\").mkdir(exist_ok="
},
{
"path": "uploader/xiaohongshu_uploader/main.py",
"chars": 15228,
"preview": "# -*- coding: utf-8 -*-\nfrom datetime import datetime\n\nfrom playwright.async_api import Playwright, async_playwright, Pa"
},
{
"path": "utils/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "utils/base_social_media.py",
"chars": 642,
"preview": "from pathlib import Path\nfrom typing import List\n\nfrom conf import BASE_DIR\n\nSOCIAL_MEDIA_DOUYIN = \"douyin\"\nSOCIAL_MEDIA"
},
{
"path": "utils/browser_hook.py",
"chars": 500,
"preview": "from conf import LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH\n\ndef get_browser_options():\n options = {\n 'headless'"
},
{
"path": "utils/constant.py",
"chars": 7156,
"preview": "import enum\n\n\nclass TencentZoneTypes(enum.Enum):\n LIFESTYLE = '生活'\n CUTE_KIDS = '萌娃'\n MUSIC = '音乐'\n KNOWLEDG"
},
{
"path": "utils/files_times.py",
"chars": 2601,
"preview": "from datetime import timedelta\n\nfrom datetime import datetime\nfrom pathlib import Path\n\nfrom conf import BASE_DIR\n\n\ndef "
},
{
"path": "utils/log.py",
"chars": 1910,
"preview": "from pathlib import Path\nfrom sys import stdout\nfrom loguru import logger\n\nfrom conf import BASE_DIR\n\n\ndef log_formatter"
},
{
"path": "utils/network.py",
"chars": 1133,
"preview": "import asyncio\nimport time\nfrom functools import wraps\n\n\ndef async_retry(timeout=60, max_retries=None):\n def decorato"
},
{
"path": "videos/demo.txt",
"chars": 39,
"preview": "男子为了心爱之人每天坚守❤️🩹\n#坚持不懈 #爱情执着 #奋斗使者 #短视频"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the dreammis/social-auto-upload GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 86 files (313.5 KB), approximately 84.2k tokens, and a symbol index with 164 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.