first commit

This commit is contained in:
sjk
2025-12-19 22:36:48 +08:00
commit 6802624e59
185 changed files with 43430 additions and 0 deletions

View File

@@ -0,0 +1,247 @@
# 小红书发布脚本 - 功能总结
## 🎉 最新更新v1.1.0
已成功增强发布脚本,现在支持**网络图片 URL**
## ✨ 核心功能
### 1. Cookie 认证
- 支持验证码登录
- 支持 Cookie 注入
- 自动验证登录状态
### 2. 内容发布
- ✅ 标题和正文
- ✅ 多图上传(最多 9 张)
- ✅ 标签自动添加
- ✅ 发布状态追踪
### 3. 图片支持(新增)
-**本地文件路径**(绝对/相对路径)
-**网络图片 URL**HTTP/HTTPS
-**混合使用**(本地 + 网络)
- ✅ **自动下载**网络图片
- ✅ **自动清理**临时文件
## 📝 使用示例
### 基础使用
```json
{
"cookies": [...],
"title": "笔记标题",
"content": "笔记内容",
"images": [
"https://picsum.photos/800/600?random=1",
"D:/local/image.jpg"
],
"tags": ["标签1", "标签2"]
}
```
### 命令行
```bash
# 安装依赖
pip install -r requirements.txt
# 从配置文件发布
python xhs_publish.py --config my_config.json
# 测试网络图片功能
python test_network_images.py
```
## 🔧 技术实现
### 图片处理流程
```python
1. 判断是否为网络 URL
2. 使用 aiohttp 下载图片
3. 保存到 temp_downloads/
4. 返回本地路径
5. 上传到小红书
6. 发布完成后清理临时文件
```
### 关键代码
```python
class XHSPublishService:
async def download_image(self, url: str) -> str:
"""下载网络图片"""
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=30) as response:
content = await response.read()
# 保存文件
return local_path
async def process_images(self, images: List[str]) -> List[str]:
"""处理图片列表(下载网络图片)"""
local_images = []
for img in images:
if self.is_url(img):
local_path = await self.download_image(img)
local_images.append(local_path)
else:
local_images.append(img)
return local_images
```
## 📦 文件结构
```
backend/
├── xhs_publish.py # 发布脚本(已增强)
├── xhs_login.py # 登录服务
├── xhs_cli.py # 命令行工具
├── test_publish.py # 基础测试
├── test_network_images.py # 网络图片测试(新增)
├── publish_config_example.json # 配置示例(已更新)
├── requirements.txt # 依赖列表(已更新)
├── temp_downloads/ # 临时下载目录(自动创建)
└── 文档/
├── XHS_PUBLISH_README.md # 发布脚本文档
├── NETWORK_IMAGE_SUPPORT.md # 网络图片文档(新增)
└── PUBLISH_FEATURE_SUMMARY.md # 功能总结(本文档)
```
## 🚀 快速开始
### 1. 安装依赖
```bash
cd backend
pip install -r requirements.txt
playwright install chromium
```
### 2. 获取 Cookie
```bash
python xhs_cli.py login 13800138000 123456
```
### 3. 准备配置文件
```bash
cp publish_config_example.json my_publish.json
# 编辑 my_publish.json填入实际数据
```
### 4. 执行发布
```bash
python xhs_publish.py --config my_publish.json
```
## 📊 性能指标
| 指标 | 数值 |
|------|------|
| 图片下载超时 | 30 秒/张 |
| 最大图片数 | 9 张 |
| 建议图片大小 | < 5MB |
| 临时文件清理 | 自动 |
## 🔍 实际应用
### 从数据库获取图片 URL 发布
```python
# 查询文章信息
article = db.query("SELECT * FROM ai_articles WHERE id = ?", article_id)
images = db.query("SELECT image_url FROM ai_article_images WHERE article_id = ?", article_id)
tags = db.query("SELECT coze_tag FROM ai_article_tags WHERE article_id = ?", article_id)
# 准备发布
publisher = XHSPublishService(cookies)
result = await publisher.publish(
title=article['title'],
content=article['content'],
images=[img['image_url'] for img in images], # 使用数据库中的 URL
tags=tags[0]['coze_tag'].split(',') if tags else []
)
```
### Go 后端调用示例
```go
func PublishArticle(articleID int) error {
// 1. 查询文章信息
article := db.GetArticle(articleID)
images := db.GetArticleImages(articleID)
// 2. 构造配置
config := map[string]interface{}{
"cookies": loadCookies(),
"title": article.Title,
"content": article.Content,
"images": images, // 直接使用 URL 数组
"tags": splitTags(article.Tags),
}
// 3. 保存配置文件
configFile := fmt.Sprintf("temp/publish_%d.json", articleID)
saveJSON(configFile, config)
// 4. 调用 Python 脚本
cmd := exec.Command("python", "backend/xhs_publish.py", "--config", configFile)
output, err := cmd.CombinedOutput()
// 5. 解析结果
var result map[string]interface{}
json.Unmarshal(output, &result)
return checkResult(result)
}
```
## ⚠️ 注意事项
### 1. 网络图片
- 确保 URL 可公开访问
- 避免使用需要认证的图片
- 注意图片服务器的访问速度
### 2. 临时文件
- 默认保存在 `temp_downloads/`
- 发布完成后自动清理
- 可设置 `cleanup=False` 保留文件
### 3. 错误处理
- 单张图片下载失败不影响其他图片
- 会跳过失败的图片继续发布
- 详细的错误日志输出
## 📚 相关文档
- [XHS_PUBLISH_README.md](XHS_PUBLISH_README.md) - 详细使用文档
- [NETWORK_IMAGE_SUPPORT.md](NETWORK_IMAGE_SUPPORT.md) - 网络图片支持文档
- [XHS_CLI_README.md](XHS_CLI_README.md) - 命令行工具文档
## 🐛 问题反馈
如遇到问题请检查
1. 是否安装了 `aiohttp`
2. 网络连接是否正常
3. Cookie 是否有效
4. 图片 URL 是否可访问
5. 磁盘空间是否充足
## 🎯 下一步计划
- [ ] 支持视频发布
- [ ] 批量发布功能
- [ ] 定时发布功能
- [ ] 发布结果追踪
- [ ] 图片压缩优化
- [ ] 并发下载优化

313
backend/QUICK_START.md Normal file
View File

@@ -0,0 +1,313 @@
# 快速开始指南
## 问题说明
如果您在使用 `xhs_publish.py` 时遇到 JSON 解析错误:
```bash
python xhs_publish.py --cookies '[...]' --title "标题" --content "内容"
# 错误: JSON解析失败: Expecting property name enclosed in double quotes
```
这是因为命令行中的 JSON 字符串转义问题。
## 解决方案
我们提供了 **3 种简单的使用方式**
---
## 方式 1快速发布脚本推荐
使用 `quick_publish.py`,无需处理 JSON 转义。
### 用法
```bash
python quick_publish.py "标题" "内容" "图片1,图片2,图片3" "标签1,标签2"
```
### Windows PowerShell 用法
**在 Windows PowerShell 中,强烈推荐使用此方式,避免 JSON 转义问题:**
```powershell
python quick_publish.py "测试笔记" "这是测试内容" "https://picsum.photos/800/600,https://picsum.photos/800/600" "测试,自动化"
```
### 参数说明
1. **标题**(必需)
2. **内容**(必需)
3. **图片**(可选)- 用逗号分隔,支持本地路径和网络 URL
4. **标签**(可选)- 用逗号分隔
### 示例
**1. 发布纯文字**
```bash
python quick_publish.py "测试笔记" "这是测试内容" "" "测试,自动化"
```
**2. 使用网络图片**
```bash
python quick_publish.py "测试笔记" "这是测试内容" "https://picsum.photos/800/600,https://picsum.photos/800/600" "测试,图片"
```
**3. 使用本地图片**
```bash
python quick_publish.py "测试笔记" "这是测试内容" "D:/image1.jpg,D:/image2.jpg" "测试"
```
**4. 混合使用**
```bash
python quick_publish.py "测试笔记" "这是测试内容" "https://picsum.photos/800/600,D:/local.jpg" "测试"
```
### Cookie 文件
脚本默认从 `cookies.json` 读取 Cookie。
如果 Cookie 在其他文件,可以指定:
```bash
python quick_publish.py "标题" "内容" "图片" "标签" "my_cookies.json"
```
---
## 方式 2配置文件发布
### 步骤 1准备 Cookie 文件
将您的 Cookie 保存为 `test_cookies.json`(已为您创建)。
### 步骤 2创建配置文件
创建 `my_publish.json`
```json
{
"cookies": "@test_cookies.json",
"title": "💧夏日必备2元一杯的柠檬水竟然这么好喝",
"content": "今天给大家分享一个超级实惠的夏日饮品!",
"images": [
"https://picsum.photos/800/600?random=1",
"https://picsum.photos/800/600?random=2"
],
"tags": ["夏日清爽", "饮品", "柠檬水"]
}
```
### 步骤 3执行发布
```bash
python xhs_publish.py --config my_publish.json
```
---
## 方式 3使用命令行参数支持Cookie文件
### 用法
```bash
# Cookie 使用文件路径(推荐)
python xhs_publish.py --cookies test_cookies.json --title "标题" --content "内容" --images '["https://picsum.photos/800/600"]' --tags '["标签1"]'
# Cookie 使用 JSON 字符串(需要转义,不推荐)
python xhs_publish.py --cookies '[{...}]' --title "标题" --content "内容"
```
### 参数说明
- `--cookies`: **Cookie JSON 字符串 或 Cookie 文件路径**(推荐使用文件路径)
- `--title`: 标题
- `--content`: 内容
- `--images`: 图片列表的 JSON 字符串(可选)
- `--tags`: 标签列表的 JSON 字符串(可选)
### 示例
**1. 使用 Cookie 文件(推荐)**
```bash
python xhs_publish.py --cookies test_cookies.json --title "测试笔记" --content "这是测试内容" --images '["https://picsum.photos/800/600"]' --tags '["测试","自动化"]'
```
**2. 只发布文字**
```bash
python xhs_publish.py --cookies test_cookies.json --title "纯文字笔记" --content "这是一条纯文字笔记"
```
## 方式 4Python 脚本调用
```python
import asyncio
import json
from xhs_publish import XHSPublishService
async def publish_my_note():
# 读取 Cookie
with open('test_cookies.json', 'r') as f:
cookies = json.load(f)
# 创建发布服务
publisher = XHSPublishService(cookies)
# 发布笔记
result = await publisher.publish(
title="测试笔记",
content="这是测试内容",
images=[
"https://picsum.photos/800/600?random=1",
"https://picsum.photos/800/600?random=2"
],
tags=["测试", "自动化"]
)
print(json.dumps(result, ensure_ascii=False, indent=2))
# 运行
asyncio.run(publish_my_note())
```
---
## 完整示例
### 使用您提供的 Cookie
Cookie 已保存到 `test_cookies.json`,现在可以直接发布:
```bash
# 方式 1快速发布推荐
cd backend
python quick_publish.py "测试笔记" "这是一条测试笔记,使用网络图片自动发布" "https://picsum.photos/800/600,https://picsum.photos/800/600" "测试,自动化" test_cookies.json
```
### 输出示例
```
==================================================
快速发布小红书笔记
==================================================
标题: 测试笔记
内容: 这是一条测试笔记,使用网络图片自动发布
图片: 2 张
1. https://picsum.photos/800/600
2. https://picsum.photos/800/600
标签: ['测试', '自动化']
Cookie: test_cookies.json
==================================================
正在处理 2 张图片...
正在下载图片 [1]: https://picsum.photos/800/600
✅ 下载成功: image_0_xxx.jpg (45.2KB)
正在下载图片 [2]: https://picsum.photos/800/600
✅ 下载成功: image_1_xxx.jpg (52.8KB)
成功处理 2/2 张图片
1. 初始化浏览器...
浏览器初始化成功
2. 验证登录状态...
✅ 登录状态有效
3. 开始发布笔记...
[...]
==================================================
发布结果:
{
"success": true,
"message": "笔记发布成功",
"url": "https://www.xiaohongshu.com/explore/..."
}
==================================================
✅ 发布成功!
📎 笔记链接: https://www.xiaohongshu.com/explore/...
```
---
## 常见问题
### Q: 为什么会出现 JSON 解析错误?
A: 在命令行中直接使用 JSON 字符串时Shell 会对引号进行特殊处理,导致解析失败。建议使用:
- **方式 1**`quick_publish.py`(无需处理 JSON
- **方式 2**配置文件JSON 在文件中)
### Q: Cookie 从哪里获取?
A: 三种方式:
1. 使用 `xhs_cli.py login` 登录获取
2. 从浏览器开发者工具复制
3. 使用现有的 Cookie JSON
### Q: 图片支持哪些格式?
A:
- **本地文件**JPG、PNG、GIF、WEBP
- **网络 URL**HTTP/HTTPS 链接
- **混合使用**:可以同时使用本地和网络图片
### Q: 如何验证 Cookie 是否有效?
A: 使用快速发布脚本,会自动验证:
```bash
python quick_publish.py "测试" "测试内容" "" ""
```
如果 Cookie 失效,会提示:
```
❌ 发布失败: Cookie已失效或未登录
```
---
## 进阶使用
### 批量发布
创建脚本 `batch_publish.py`
```python
import asyncio
from quick_publish import quick_publish
async def main():
articles = [
{
"title": "文章1",
"content": "内容1",
"images": ["https://picsum.photos/800/600"],
"tags": ["标签1"]
},
{
"title": "文章2",
"content": "内容2",
"images": ["https://picsum.photos/800/600"],
"tags": ["标签2"]
}
]
for article in articles:
result = await quick_publish(**article)
print(f"发布结果: {result['success']}")
await asyncio.sleep(60) # 间隔60秒
asyncio.run(main())
```
---
## 总结
**推荐使用 `quick_publish.py`**
- 无需处理 JSON 转义
- 参数简单直观
- 自动读取 Cookie 文件
现在您可以尝试使用快速发布脚本了!🚀

170
backend/README.md Normal file
View File

@@ -0,0 +1,170 @@
# 小红书登录后端服务
基于 Playwright 的小红书登录服务,支持手机号+验证码登录。
## 功能特性
- ✅ 手机号+验证码登录
- ✅ 自动化浏览器操作
- ✅ 获取登录后的 Cookies 和用户信息
- ✅ RESTful API 接口
## 技术栈
- Python 3.8+
- FastAPI - Web 框架
- Playwright - 浏览器自动化
- Uvicorn - ASGI 服务器
## 安装步骤
### 1. 创建虚拟环境(如果还没有)
```bash
cd backend
python -m venv venv
```
### 2. 激活虚拟环境
**Windows:**
```bash
venv\Scripts\activate
```
**Linux/Mac:**
```bash
source venv/bin/activate
```
### 3. 安装依赖
```bash
pip install -r requirements.txt
```
### 4. 安装 Playwright 浏览器
```bash
playwright install chromium
```
## 使用方法
### 启动服务
```bash
python main.py
```
服务将在 `http://localhost:8000` 启动。
### API 接口
#### 1. 发送验证码
**POST** `/api/xhs/send-code`
请求体:
```json
{
"phone": "13800138000",
"country_code": "+86"
}
```
响应:
```json
{
"code": 0,
"message": "验证码已发送请在小红书APP中查看",
"data": {
"sent_at": "2025-12-10T10:00:00"
}
}
```
#### 2. 登录验证
**POST** `/api/xhs/login`
请求体:
```json
{
"phone": "13800138000",
"code": "123456",
"country_code": "+86"
}
```
响应:
```json
{
"code": 0,
"message": "登录成功",
"data": {
"user_info": {...},
"cookies": {...},
"login_time": "2025-12-10T10:01:00"
}
}
```
#### 3. 健康检查
**GET** `/`
响应:
```json
{
"status": "ok",
"message": "小红书登录服务运行中"
}
```
## 注意事项
1. **滑块验证**: 小红书可能会要求滑块验证,需要手动完成
2. **验证码**: 验证码会发送到小红书 APP需要在 APP 中查看
3. **浏览器模式**:
- 开发时使用 `headless=False` 可以看到浏览器操作
- 生产环境可设置 `headless=True` 在后台运行
4. **反爬虫**: 小红书有反爬虫机制,可能需要调整策略
## 开发调试
### 查看 API 文档
访问 `http://localhost:8000/docs` 可以看到自动生成的 Swagger 文档。
### 日志输出
服务会在控制台输出详细的操作日志,便于调试。
## 项目结构
```
backend/
├── main.py # FastAPI 主程序
├── xhs_login.py # 小红书登录服务
├── requirements.txt # Python 依赖
├── venv/ # Python 虚拟环境
└── README.md # 说明文档
```
## 常见问题
### Q: 验证码发送失败?
A: 检查手机号格式是否正确,确保网络连接正常。
### Q: 登录失败?
A: 确认验证码是否正确,验证码有时效性,请及时输入。
### Q: 浏览器无法启动?
A: 确保已经运行 `playwright install chromium` 安装浏览器。
## 安全提示
- 不要在公网暴露此服务
- 生产环境建议添加认证机制
- 妥善保管获取到的 Cookies 和用户信息

123
backend/XHS_CLI_README.md Normal file
View File

@@ -0,0 +1,123 @@
# 小红书登录 CLI 工具
## 概述
这是一个可以被 Go 服务直接调用的 Python CLI 工具,用于小红书登录功能。
使用此工具后,不再需要单独启动 Python Web 服务。
## 使用方式
### 1. 发送验证码
```bash
python xhs_cli.py send_code <手机号> [国家区号]
```
示例:
```bash
python xhs_cli.py send_code 13800138000 +86
```
返回 JSON 格式:
```json
{
"success": true,
"message": "验证码发送成功"
}
```
### 2. 登录
```bash
python xhs_cli.py login <手机号> <验证码> [国家区号]
```
示例:
```bash
python xhs_cli.py login 13800138000 123456 +86
```
返回 JSON 格式:
```json
{
"success": true,
"user_info": {...},
"cookies": {...},
"url": "https://www.xiaohongshu.com/"
}
```
### 3. 注入 Cookie (验证登录状态)
```bash
python xhs_cli.py inject_cookies '<cookies_json>'
```
示例:
```bash
python xhs_cli.py inject_cookies '[{"name":"web_session","value":"xxx","domain":".xiaohongshu.com"}]'
```
返回 JSON 格式:
```json
{
"success": true,
"logged_in": true,
"cookies": {...},
"user_info": {...}
}
```
## Go 服务集成
Go 服务已经修改为直接调用 Python CLI 脚本,无需启动 Python Web 服务。
### 修改的文件
1. **backend/xhs_cli.py** (新增)
- 命令行接口工具
2. **go_backend/service/xhs_service.go** (修改)
- 使用 `exec.Command` 调用 Python 脚本
- 不再通过 HTTP 调用 Python 服务
3. **go_backend/service/employee_service.go** (修改)
- 使用 `exec.Command` 调用 Python 脚本
### 优点
- ✅ 只需启动一个 Go 服务
- ✅ 部署更简单,不需要管理多个服务进程
- ✅ 减少网络开销
- ✅ 更容易调试和维护
## 依赖要求
确保已安装 Python 依赖:
```bash
cd backend
pip install -r requirements.txt
```
主要依赖:
- playwright
- asyncio
## 注意事项
1. Python 命令需要在系统 PATH 中可用
2. 确保 `xhs_login.py``xhs_cli.py` 在同一目录
3. Go 服务会在相对路径 `../backend` 下查找 Python 脚本
4. 所有输出均为 JSON 格式,便于 Go 服务解析
## 错误处理
如果执行失败,会返回包含错误信息的 JSON:
```json
{
"success": false,
"error": "错误描述信息"
}
```
Go 服务会捕获 stderr 输出并作为错误信息的一部分返回。

View File

@@ -0,0 +1,313 @@
# 小红书笔记发布脚本使用说明
## 功能介绍
`xhs_publish.py` 是一个用于自动发布小红书笔记的 Python 脚本,支持通过 Cookie 认证,自动完成图文笔记发布。
## 环境准备
### 1. 安装依赖
```bash
cd backend
pip install -r requirements.txt
```
主要依赖:
- playwright (浏览器自动化)
- asyncio (异步处理)
### 2. 安装浏览器驱动
```bash
playwright install chromium
```
## 使用方式
### 方式一:使用配置文件(推荐)
#### 1. 准备配置文件
复制 `publish_config_example.json` 并修改为实际参数:
```json
{
"cookies": [
{
"name": "a1",
"value": "your_cookie_value_here",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": -1,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
}
],
"title": "笔记标题",
"content": "笔记内容",
"images": [
"D:/path/to/image1.jpg",
"D:/path/to/image2.jpg"
],
"tags": [
"标签1",
"标签2"
]
}
```
#### 2. 执行发布
```bash
python xhs_publish.py --config publish_config.json
```
### 方式二:命令行参数
```bash
python xhs_publish.py \
--cookies '[{"name":"a1","value":"xxx","domain":".xiaohongshu.com"}]' \
--title "笔记标题" \
--content "笔记内容" \
--images '["D:/image1.jpg","D:/image2.jpg"]' \
--tags '["标签1","标签2"]'
```
## 参数说明
### cookies (必需)
Cookie 数组,每个 Cookie 对象包含以下字段:
- `name`: Cookie 名称
- `value`: Cookie 值
- `domain`: 域名(通常为 `.xiaohongshu.com`
- `path`: 路径(通常为 `/`
- `expires`: 过期时间(-1 表示会话 Cookie
- `httpOnly`: 是否仅 HTTP
- `secure`: 是否安全
- `sameSite`: 同站策略Lax/Strict/None
**重要 Cookie必需**
- `a1`: 用户身份认证
- `webId`: 设备标识
- `web_session`: 会话信息
### title (必需)
笔记标题,字符串类型。
**示例:**
```
"💧夏日必备2元一杯的柠檬水竟然这么好喝"
```
### content (必需)
笔记正文内容,字符串类型,支持换行符 `\n`
**示例:**
```
"今天给大家分享一个超级实惠的夏日饮品!\n\n蜜雪冰城的柠檬水只要2元一杯性价比真的太高了"
```
### images (可选)
图片文件路径数组,支持本地绝对路径。
**要求:**
- 图片必须是本地文件
- 支持 jpg、png、gif 等格式
- 最多上传 9 张图片
- 建议尺寸800x600 或更高
**示例:**
```json
[
"D:/project/Work/ai_xhs/backend/temp_uploads/image1.jpg",
"D:/project/Work/ai_xhs/backend/temp_uploads/image2.jpg"
]
```
### tags (可选)
标签数组,会自动添加 `#` 前缀。
**示例:**
```json
["夏日清爽", "饮品", "柠檬水"]
```
## 获取 Cookie 的方法
### 方法一:使用登录脚本
```bash
python xhs_cli.py login <手机号> <验证码>
```
登录成功后会自动保存 Cookie 到 `cookies.json` 文件。
### 方法二:浏览器手动获取
1. 在浏览器中登录小红书网页版
2. 打开开发者工具F12
3. 切换到 Network网络标签
4. 刷新页面
5. 找到任意请求,查看 Request Headers
6. 复制 Cookie 字段内容
7. 使用在线工具或脚本转换为 JSON 格式
### 方法三:使用 Cookie 注入验证
```bash
python xhs_cli.py inject_cookies '<cookies_json>'
```
## 返回结果
### 成功示例
```json
{
"success": true,
"message": "笔记发布成功",
"url": "https://www.xiaohongshu.com/explore/xxxx"
}
```
### 失败示例
```json
{
"success": false,
"error": "Cookie已失效或未登录"
}
```
## 注意事项
### 1. Cookie 有效期
- Cookie 会在一段时间后失效
- 需要定期重新登录获取新 Cookie
- 建议使用 Cookie 注入验证接口检查状态
### 2. 图片上传
- 确保图片文件存在且可访问
- 图片路径使用绝对路径
- Windows 系统路径使用 `/``\\` 分隔符
### 3. 发布限制
- 小红书可能有发布频率限制
- 建议控制发布间隔,避免被限流
- 内容需符合小红书社区规范
### 4. 错误处理
常见错误及解决方法:
- **"Cookie已失效"**: 重新登录获取新 Cookie
- **"图片文件不存在"**: 检查图片路径是否正确
- **"未找到发布按钮"**: 小红书页面结构可能变化,需要更新选择器
- **"输入内容失败"**: 等待时间不足,增加延迟时间
## 与 Go 后端集成
在 Go 后端中调用此脚本:
```go
import (
"os/exec"
"encoding/json"
)
// 发布笔记
func PublishNote(cookies []Cookie, title, content string, images, tags []string) error {
// 构造配置文件
config := map[string]interface{}{
"cookies": cookies,
"title": title,
"content": content,
"images": images,
"tags": tags,
}
// 保存到临时文件
configFile := "temp_publish_config.json"
data, _ := json.Marshal(config)
ioutil.WriteFile(configFile, data, 0644)
// 调用 Python 脚本
cmd := exec.Command("python", "backend/xhs_publish.py", "--config", configFile)
output, err := cmd.CombinedOutput()
if err != nil {
return err
}
// 解析结果
var result map[string]interface{}
json.Unmarshal(output, &result)
if !result["success"].(bool) {
return errors.New(result["error"].(string))
}
return nil
}
```
## 开发调试
### 启用浏览器可视模式
修改 `xhs_login.py` 中的 `headless` 参数:
```python
self.browser = await self.playwright.chromium.launch(
headless=False, # 改为 False 可以看到浏览器操作过程
args=['--disable-blink-features=AutomationControlled']
)
```
### 查看详细日志
脚本会在控制台输出详细的执行日志,包括:
- 浏览器初始化
- 登录状态验证
- 图片上传进度
- 内容输入状态
- 发布结果
## 常见问题
### Q: 为什么上传图片后没有显示?
A: 可能是图片上传时间较长,脚本已经增加了等待时间。如果仍有问题,可以调整 `xhs_login.py` 中的等待时间。
### Q: 如何批量发布多条笔记?
A: 准备多个配置文件,使用循环调用脚本:
```bash
for config in publish_config_*.json; do
python xhs_publish.py --config "$config"
sleep 60 # 间隔60秒
done
```
### Q: Cookie 多久失效?
A: 小红书 Cookie 通常在 7-30 天后失效,具体取决于 Cookie 的过期时间设置。
## 技术支持
如有问题,请查看:
1. 脚本执行日志
2. 小红书页面结构是否变化
3. Cookie 是否有效
4. 图片文件是否存在

0
backend/fix_print.py Normal file
View File

282
backend/main.py Normal file
View File

@@ -0,0 +1,282 @@
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional, Dict, Any, List
import asyncio
from datetime import datetime
import os
import shutil
from pathlib import Path
from xhs_login import XHSLoginService
app = FastAPI(title="小红书登录API")
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 生产环境应该限制具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 全局登录服务实例
login_service = XHSLoginService()
# 临时文件存储目录
TEMP_DIR = Path("temp_uploads")
TEMP_DIR.mkdir(exist_ok=True)
# 请求模型
class SendCodeRequest(BaseModel):
phone: str
country_code: str = "+86"
class LoginRequest(BaseModel):
phone: str
code: str
country_code: str = "+86"
class PublishNoteRequest(BaseModel):
title: str
content: str
images: Optional[list] = None
topics: Optional[list] = None
class InjectCookiesRequest(BaseModel):
cookies: list
# 响应模型
class BaseResponse(BaseModel):
code: int
message: str
data: Optional[Dict[str, Any]] = None
@app.on_event("startup")
async def startup_event():
"""启动时不初始化浏览器,等待第一次请求时再初始化"""
pass
@app.on_event("shutdown")
async def shutdown_event():
"""关闭时清理浏览器"""
await login_service.close_browser()
@app.post("/api/xhs/send-code", response_model=BaseResponse)
async def send_code(request: SendCodeRequest):
"""
发送验证码
通过playwright访问小红书官网输入手机号并触发验证码发送
"""
try:
# 调用登录服务发送验证码
result = await login_service.send_verification_code(
phone=request.phone,
country_code=request.country_code
)
if result["success"]:
return BaseResponse(
code=0,
message="验证码已发送请在小红书APP中查看",
data={"sent_at": datetime.now().isoformat()}
)
else:
return BaseResponse(
code=1,
message=result.get("error", "发送验证码失败"),
data=None
)
except Exception as e:
print(f"发送验证码异常: {str(e)}")
return BaseResponse(
code=1,
message=f"发送验证码失败: {str(e)}",
data=None
)
@app.post("/api/xhs/login", response_model=BaseResponse)
async def login(request: LoginRequest):
"""
登录验证
用户填写验证码后,完成登录并获取小红书返回的数据
"""
try:
# 调用登录服务进行登录
result = await login_service.login(
phone=request.phone,
code=request.code,
country_code=request.country_code
)
if result["success"]:
return BaseResponse(
code=0,
message="登录成功",
data={
"user_info": result.get("user_info"),
"cookies": result.get("cookies"), # 键值对格式(前端展示)
"cookies_full": result.get("cookies_full"), # Playwright完整格式数据库存储/脚本使用)
"login_time": datetime.now().isoformat()
}
)
else:
return BaseResponse(
code=1,
message=result.get("error", "登录失败"),
data=None
)
except Exception as e:
print(f"登录异常: {str(e)}")
return BaseResponse(
code=1,
message=f"登录失败: {str(e)}",
data=None
)
@app.get("/")
async def root():
"""健康检查"""
return {"status": "ok", "message": "小红书登录服务运行中"}
@app.post("/api/xhs/inject-cookies", response_model=BaseResponse)
async def inject_cookies(request: InjectCookiesRequest):
"""
注入Cookies并验证登录状态
允许使用之前保存的Cookies跳过登录
"""
try:
# 关闭旧的浏览器(如果有)
if login_service.browser:
await login_service.close_browser()
# 使用Cookies初始化浏览器
await login_service.init_browser(cookies=request.cookies)
# 验证登录状态
result = await login_service.verify_login_status()
if result.get("logged_in"):
return BaseResponse(
code=0,
message="Cookie注入成功已登录",
data={
"logged_in": True,
"user_info": result.get("user_info"),
"cookies": result.get("cookies"), # 键值对格式
"cookies_full": result.get("cookies_full"), # Playwright完整格式
"url": result.get("url")
}
)
else:
return BaseResponse(
code=1,
message=result.get("message", "Cookie已失效请重新登录"),
data={
"logged_in": False
}
)
except Exception as e:
print(f"注入Cookies异常: {str(e)}")
return BaseResponse(
code=1,
message=f"注入失败: {str(e)}",
data=None
)
@app.post("/api/xhs/publish", response_model=BaseResponse)
async def publish_note(request: PublishNoteRequest):
"""
发布笔记
登录后可以发布图文笔记到小红书
"""
try:
# 调用登录服务发布笔记
result = await login_service.publish_note(
title=request.title,
content=request.content,
images=request.images,
topics=request.topics
)
if result["success"]:
return BaseResponse(
code=0,
message="笔记发布成功",
data={
"url": result.get("url"),
"publish_time": datetime.now().isoformat()
}
)
else:
return BaseResponse(
code=1,
message=result.get("error", "发布失败"),
data=None
)
except Exception as e:
print(f"发布笔记异常: {str(e)}")
return BaseResponse(
code=1,
message=f"发布失败: {str(e)}",
data=None
)
@app.post("/api/xhs/upload-images")
async def upload_images(files: List[UploadFile] = File(...)):
"""
上传图片到服务器临时目录
返回图片的服务器路径
"""
try:
uploaded_paths = []
for file in files:
# 检查文件类型
if not file.content_type.startswith('image/'):
return {
"code": 1,
"message": f"文件 {file.filename} 不是图片类型",
"data": None
}
# 生成唯一文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
file_ext = os.path.splitext(file.filename)[1]
safe_filename = f"{timestamp}{file_ext}"
file_path = TEMP_DIR / safe_filename
# 保存文件
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# 使用绝对路径
abs_path = str(file_path.absolute())
uploaded_paths.append(abs_path)
print(f"已上传图片: {abs_path}")
return {
"code": 0,
"message": f"成功上传 {len(uploaded_paths)} 张图片",
"data": {
"paths": uploaded_paths,
"count": len(uploaded_paths)
}
}
except Exception as e:
print(f"上传图片异常: {str(e)}")
return {
"code": 1,
"message": f"上传失败: {str(e)}",
"data": None
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,38 @@
{
"cookies": [
{
"name": "a1",
"value": "your_cookie_value_here",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": -1,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
},
{
"name": "webId",
"value": "your_webid_here",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": -1,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
}
],
"title": "💧夏日必备2元一杯的柠檬水竟然这么好喝",
"content": "今天给大家分享一个超级实惠的夏日饮品!\n\n蜜雪冰城的柠檬水只要2元一杯性价比真的太高了\n酸酸甜甜的口感冰冰凉凉超解渴~\n\n夏天必备强烈推荐给大家",
"images": [
"https://picsum.photos/800/600?random=1",
"https://picsum.photos/800/600?random=2",
"D:/project/Work/ai_xhs/backend/temp_uploads/image3.jpg"
],
"tags": [
"夏日清爽",
"饮品",
"柠檬水",
"性价比",
"蜜雪冰城"
]
}

View File

@@ -0,0 +1,15 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 小红书快速发布示例
echo ========================================
echo.
REM 使用 Cookie 文件路径发布
python xhs_publish.py --cookies "./cookies.json" --title "【测试】批处理发布" --content "这是通过批处理文件发布的测试笔记" --images "[\"https://picsum.photos/800/600\",\"https://picsum.photos/800/600\"]" --tags "[\"测试\",\"批处理\",\"自动化\"]"
echo.
echo ========================================
echo 发布完成
echo ========================================
pause

136
backend/quick_publish.py Normal file
View File

@@ -0,0 +1,136 @@
"""
快速发布脚本
简化命令行调用,避免 JSON 转义问题
"""
import sys
import json
import asyncio
from xhs_publish import XHSPublishService
def load_cookies_from_file(filepath='cookies.json'):
"""从文件加载 Cookie"""
try:
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
print(f"❌ Cookie 文件不存在: {filepath}")
return None
except json.JSONDecodeError as e:
print(f"❌ Cookie 文件格式错误: {e}")
return None
async def quick_publish(
title: str,
content: str,
images: list = None,
tags: list = None,
cookies_file: str = 'cookies.json'
):
"""
快速发布
Args:
title: 标题
content: 内容
images: 图片列表(支持本地路径和网络 URL
tags: 标签列表
cookies_file: Cookie 文件路径
"""
# 加载 Cookie
cookies = load_cookies_from_file(cookies_file)
if not cookies:
return {
"success": False,
"error": "无法加载 Cookie"
}
# 创建发布服务
publisher = XHSPublishService(cookies)
# 执行发布
result = await publisher.publish(
title=title,
content=content,
images=images,
tags=tags
)
return result
def main():
"""
命令行入口
使用方式:
python quick_publish.py "标题" "内容" "图片1,图片2,图片3" "标签1,标签2"
python quick_publish.py "标题" "内容" "" "标签1,标签2" # 不使用图片
"""
if len(sys.argv) < 3:
print("使用方式:")
print(' python quick_publish.py "标题" "内容" ["图片1,图片2"] ["标签1,标签2"]')
print()
print("示例:")
print(' python quick_publish.py "测试笔记" "这是内容" "https://picsum.photos/800/600,D:/test.jpg" "测试,自动化"')
sys.exit(1)
# 解析参数
title = sys.argv[1]
content = sys.argv[2]
# 解析图片(逗号分隔)
images = []
if len(sys.argv) > 3 and sys.argv[3].strip():
images = [img.strip() for img in sys.argv[3].split(',') if img.strip()]
# 解析标签(逗号分隔)
tags = []
if len(sys.argv) > 4 and sys.argv[4].strip():
tags = [tag.strip() for tag in sys.argv[4].split(',') if tag.strip()]
# Cookie 文件路径(可选)
cookies_file = sys.argv[5] if len(sys.argv) > 5 else 'cookies.json'
print("="*50)
print("快速发布小红书笔记")
print("="*50)
print(f"标题: {title}")
print(f"内容: {content[:100]}{'...' if len(content) > 100 else ''}")
print(f"图片: {len(images)}")
if images:
for i, img in enumerate(images, 1):
print(f" {i}. {img}")
print(f"标签: {tags}")
print(f"Cookie: {cookies_file}")
print("="*50)
print()
# 执行发布
result = asyncio.run(quick_publish(
title=title,
content=content,
images=images if images else None,
tags=tags if tags else None,
cookies_file=cookies_file
))
# 输出结果
print()
print("="*50)
print("发布结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
print("="*50)
if result.get('success'):
print("\n✅ 发布成功!")
if 'url' in result:
print(f"📎 笔记链接: {result['url']}")
else:
print(f"\n❌ 发布失败: {result.get('error')}")
sys.exit(1)
if __name__ == "__main__":
main()

6
backend/requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
fastapi==0.104.1
uvicorn==0.24.0
playwright==1.40.0
pydantic==2.5.0
python-multipart==0.0.6
aiohttp==3.9.1

8
backend/start.bat Normal file
View File

@@ -0,0 +1,8 @@
@echo off
echo 正在激活虚拟环境...
venv\Scripts\activate
echo 正在启动小红书登录服务...
python main.py
pause

7
backend/start.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
echo "正在激活虚拟环境..."
source venv/bin/activate
echo "正在启动小红书登录服务..."
python main.py

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -0,0 +1,188 @@
"""
测试 API 返回格式
验证登录 API 是否正确返回 cookies 和 cookies_full
"""
import json
def test_api_response_format():
"""测试 API 响应格式"""
# 模拟 API 返回的数据
mock_response = {
"code": 0,
"message": "登录成功",
"data": {
"user_info": {},
"cookies": {
"a1": "xxx",
"webId": "yyy",
"web_session": "zzz"
},
"cookies_full": [
{
"name": "a1",
"value": "xxx",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": 1797066496,
"httpOnly": False,
"secure": False,
"sameSite": "Lax"
},
{
"name": "webId",
"value": "yyy",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": 1797066496,
"httpOnly": False,
"secure": False,
"sameSite": "Lax"
},
{
"name": "web_session",
"value": "zzz",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": 1797066497,
"httpOnly": True,
"secure": True,
"sameSite": "Lax"
}
],
"login_time": "2025-12-12T23:30:00"
}
}
print("="*60)
print("API 响应格式测试")
print("="*60)
print()
# 检查响应结构
assert "code" in mock_response, "缺少 code 字段"
assert "message" in mock_response, "缺少 message 字段"
assert "data" in mock_response, "缺少 data 字段"
data = mock_response["data"]
# 检查 cookies 字段(键值对格式)
print("✅ 检查 cookies 字段(键值对格式):")
assert "cookies" in data, "缺少 cookies 字段"
assert isinstance(data["cookies"], dict), "cookies 应该是字典类型"
print(f" 类型: {type(data['cookies']).__name__}")
print(f" 示例: {json.dumps(data['cookies'], ensure_ascii=False, indent=2)}")
print()
# 检查 cookies_full 字段Playwright 完整格式)
print("✅ 检查 cookies_full 字段Playwright 完整格式):")
assert "cookies_full" in data, "缺少 cookies_full 字段"
assert isinstance(data["cookies_full"], list), "cookies_full 应该是列表类型"
print(f" 类型: {type(data['cookies_full']).__name__}")
print(f" 数量: {len(data['cookies_full'])} 个 Cookie")
print(f" 示例(第一个):")
print(f"{json.dumps(data['cookies_full'][0], ensure_ascii=False, indent=6)}")
print()
# 检查 cookies_full 的每个元素
print("✅ 检查 cookies_full 的结构:")
for i, cookie in enumerate(data["cookies_full"]):
assert "name" in cookie, f"Cookie[{i}] 缺少 name 字段"
assert "value" in cookie, f"Cookie[{i}] 缺少 value 字段"
assert "domain" in cookie, f"Cookie[{i}] 缺少 domain 字段"
assert "path" in cookie, f"Cookie[{i}] 缺少 path 字段"
assert "expires" in cookie, f"Cookie[{i}] 缺少 expires 字段"
assert "httpOnly" in cookie, f"Cookie[{i}] 缺少 httpOnly 字段"
assert "secure" in cookie, f"Cookie[{i}] 缺少 secure 字段"
assert "sameSite" in cookie, f"Cookie[{i}] 缺少 sameSite 字段"
print(f" Cookie[{i}] ({cookie['name']}): ✅ 所有字段完整")
print()
print("="*60)
print("🎉 所有检查通过API 返回格式正确")
print("="*60)
print()
# 使用场景说明
print("📝 使用场景:")
print()
print("1. 前端展示 - 使用 cookies键值对格式:")
print(" const cookies = response.data.cookies;")
print(" console.log(cookies.a1, cookies.webId);")
print()
print("2. 数据库存储 - 使用 cookies_full完整格式:")
print(" const cookiesFull = response.data.cookies_full;")
print(" await db.saveCookies(userId, JSON.stringify(cookiesFull));")
print()
print("3. Python 脚本使用 - 使用 cookies_full:")
print(" cookies_full = response['data']['cookies_full']")
print(" publisher = XHSPublishService(cookies_full)")
print()
def compare_formats():
"""对比两种格式"""
print("="*60)
print("格式对比分析")
print("="*60)
print()
# 键值对格式
cookies_dict = {
"a1": "xxx",
"webId": "yyy",
"web_session": "zzz"
}
# Playwright 完整格式
cookies_full = [
{
"name": "a1",
"value": "xxx",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": 1797066496,
"httpOnly": False,
"secure": False,
"sameSite": "Lax"
}
]
print("📊 键值对格式:")
dict_str = json.dumps(cookies_dict, ensure_ascii=False, indent=2)
print(dict_str)
print(f" 大小: {len(dict_str)} 字符")
print()
print("📊 Playwright 完整格式:")
full_str = json.dumps(cookies_full, ensure_ascii=False, indent=2)
print(full_str)
print(f" 大小: {len(full_str)} 字符")
print()
print("📊 对比结果:")
print(f" 完整格式 vs 键值对格式: {len(full_str)} / {len(dict_str)} = {len(full_str)/len(dict_str):.1f}x")
print(f" 每个 Cookie 完整格式约增加: {(len(full_str) - len(dict_str)) // len(cookies_dict)} 字符")
print()
print("✅ 结论:")
print(" - 完整格式虽然较大,但包含所有必要属性")
print(" - 对于数据库存储,建议使用完整格式")
print(" - 对于前端展示,可以使用键值对格式")
print()
if __name__ == "__main__":
# 测试 API 响应格式
test_api_response_format()
# 对比两种格式
compare_formats()
print("="*60)
print("✅ 测试完成!")
print("="*60)

162
backend/test_cookie_file.py Normal file
View File

@@ -0,0 +1,162 @@
"""
测试 Cookie 文件路径支持
"""
import subprocess
import sys
import json
def test_cookie_file_param():
"""测试 --cookies 参数支持文件路径"""
print("="*60)
print("测试 Cookie 文件路径参数支持")
print("="*60)
print()
# 测试命令
cmd = [
sys.executable,
"xhs_publish.py",
"--cookies", "test_cookies.json", # 使用文件路径
"--title", "【测试】Cookie文件路径参数",
"--content", "测试使用 --cookies 参数传递文件路径,而不是 JSON 字符串",
"--images", '["https://picsum.photos/800/600","https://picsum.photos/800/600"]',
"--tags", '["测试","Cookie文件","自动化"]'
]
print("执行命令:")
print(" ".join(cmd))
print()
print("-"*60)
print()
# 执行命令
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8'
)
# 输出结果
print("标准输出:")
print(result.stdout)
if result.stderr:
print("\n标准错误:")
print(result.stderr)
print()
print("-"*60)
# 解析结果
try:
# 尝试从输出中提取 JSON 结果
lines = result.stdout.strip().split('\n')
for i, line in enumerate(lines):
if line.strip().startswith('{'):
json_str = '\n'.join(lines[i:])
response = json.loads(json_str)
print("\n解析结果:")
print(json.dumps(response, ensure_ascii=False, indent=2))
if response.get('success'):
print("\n✅ 测试成功Cookie 文件路径参数工作正常")
if 'url' in response:
print(f"📎 笔记链接: {response['url']}")
else:
print(f"\n❌ 测试失败: {response.get('error')}")
break
except json.JSONDecodeError:
print("⚠️ 无法解析 JSON 输出")
return result.returncode == 0
except Exception as e:
print(f"❌ 执行失败: {str(e)}")
return False
def test_quick_publish():
"""测试 quick_publish.py 脚本"""
print("\n")
print("="*60)
print("测试 quick_publish.py 脚本")
print("="*60)
print()
cmd = [
sys.executable,
"quick_publish.py",
"【测试】快速发布脚本",
"测试 quick_publish.py 的简化调用方式",
"https://picsum.photos/800/600,https://picsum.photos/800/600",
"测试,快速发布,自动化",
"test_cookies.json"
]
print("执行命令:")
print(" ".join(cmd))
print()
print("-"*60)
print()
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8'
)
print(result.stdout)
if result.stderr:
print("\n标准错误:")
print(result.stderr)
return result.returncode == 0
except Exception as e:
print(f"❌ 执行失败: {str(e)}")
return False
if __name__ == "__main__":
print()
print("🧪 Cookie 文件路径支持测试")
print()
# 检查 Cookie 文件是否存在
import os
if not os.path.exists('test_cookies.json'):
print("❌ 错误: test_cookies.json 文件不存在")
print("请先创建 Cookie 文件")
sys.exit(1)
print("✅ 找到 Cookie 文件: test_cookies.json")
print()
# 测试1: xhs_publish.py 使用文件路径
success1 = test_cookie_file_param()
# 测试2: quick_publish.py
success2 = test_quick_publish()
# 总结
print()
print("="*60)
print("测试总结")
print("="*60)
print(f"xhs_publish.py (Cookie文件): {'✅ 通过' if success1 else '❌ 失败'}")
print(f"quick_publish.py: {'✅ 通过' if success2 else '❌ 失败'}")
print()
if success1 and success2:
print("🎉 所有测试通过!")
else:
print("⚠️ 部分测试失败,请检查错误信息")

View File

@@ -0,0 +1,143 @@
"""
测试两种 Cookie 格式支持
"""
import asyncio
import json
from xhs_publish import XHSPublishService
# 格式1: Playwright 完整格式(从文件读取)
playwright_cookies = [
{
"name": "a1",
"value": "19b11d16e24t3h3xmlvojbrw1cr55xwamiacluw3c50000231766",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": 1797066496,
"httpOnly": False,
"secure": False,
"sameSite": "Lax"
},
{
"name": "web_session",
"value": "030037ae088f0acf2c81329d432e4a12fcb0ca",
"domain": ".xiaohongshu.com",
"path": "/",
"expires": 1797066497.112584,
"httpOnly": True,
"secure": True,
"sameSite": "Lax"
}
]
# 格式2: 键值对格式(从数据库读取)
keyvalue_cookies = {
"a1": "19b11d16e24t3h3xmlvojbrw1cr55xwamiacluw3c50000231766",
"abRequestId": "b273b4d0-3ef7-5b8f-bba4-2d19e63ad883",
"acw_tc": "0a4ae09717655304937202738e4b75c08d6eb78f2c8d30d7dc5a465429e1e6",
"gid": "yjDyyfyKiD6DyjDyyfyKd37EJ49qxqC61hlV0qSDFEySFS2822CE01888JqyWKK8Djdi8d2j",
"loadts": "1765530496548",
"sec_poison_id": "a589e333-c364-477c-9d14-53af8a1e7f1c",
"unread": "{%22ub%22:%22648455690000000014025d90%22%2C%22ue%22:%2264b34737000000002f0262f9%22%2C%22uc%22:22}",
"webBuild": "5.0.6",
"webId": "fdf2dccee4bec7534aff5581310c0e26",
"web_session": "030037ae088f0acf2c81329d432e4a12fcb0ca",
"websectiga": "984412fef754c018e472127b8effd174be8a5d51061c991aadd200c69a2801d6",
"xsecappid": "xhs-pc-web"
}
async def test_playwright_format():
"""测试 Playwright 格式"""
print("="*60)
print("测试 1: Playwright 格式(完整格式)")
print("="*60)
try:
publisher = XHSPublishService(playwright_cookies)
print("✅ 初始化成功")
print(f" 转换后的 Cookie 数量: {len(publisher.cookies)}")
return True
except Exception as e:
print(f"❌ 初始化失败: {e}")
return False
async def test_keyvalue_format():
"""测试键值对格式"""
print("\n" + "="*60)
print("测试 2: 键值对格式(数据库格式)")
print("="*60)
try:
publisher = XHSPublishService(keyvalue_cookies)
print("✅ 初始化成功")
print(f" 转换后的 Cookie 数量: {len(publisher.cookies)}")
# 显示转换后的一个示例
print("\n转换示例(第一个 Cookie:")
print(json.dumps(publisher.cookies[0], ensure_ascii=False, indent=2))
return True
except Exception as e:
print(f"❌ 初始化失败: {e}")
return False
async def test_from_file():
"""从文件读取测试"""
print("\n" + "="*60)
print("测试 3: 从 cookies.json 文件读取")
print("="*60)
try:
with open('cookies.json', 'r', encoding='utf-8') as f:
cookies = json.load(f)
publisher = XHSPublishService(cookies)
print("✅ 初始化成功")
print(f" Cookie 数量: {len(publisher.cookies)}")
return True
except FileNotFoundError:
print("⚠️ cookies.json 文件不存在,跳过此测试")
return None
except Exception as e:
print(f"❌ 初始化失败: {e}")
return False
async def main():
print("\n🧪 Cookie 格式兼容性测试\n")
# 测试1: Playwright格式
result1 = await test_playwright_format()
# 测试2: 键值对格式
result2 = await test_keyvalue_format()
# 测试3: 从文件读取
result3 = await test_from_file()
# 总结
print("\n" + "="*60)
print("测试总结")
print("="*60)
print(f"Playwright 格式: {'✅ 通过' if result1 else '❌ 失败'}")
print(f"键值对格式: {'✅ 通过' if result2 else '❌ 失败'}")
if result3 is not None:
print(f"文件读取: {'✅ 通过' if result3 else '❌ 失败'}")
else:
print(f"文件读取: ⚠️ 跳过")
if result1 and result2:
print("\n🎉 所有格式测试通过!")
print("\n💡 使用说明:")
print(" - 从 Python 脚本保存的 cookies.json → Playwright 格式")
print(" - 从数据库读取的 Cookie → 键值对格式")
print(" - 两种格式都可以正常使用!")
else:
print("\n⚠️ 部分测试失败")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,80 @@
"""
测试网络图片下载功能
"""
import asyncio
import json
from xhs_publish import XHSPublishService
async def test_network_images():
"""测试网络图片功能"""
print("="*50)
print("网络图片下载功能测试")
print("="*50)
print()
# 1. 准备测试 Cookie从 cookies.json 读取)
try:
with open('cookies.json', 'r', encoding='utf-8') as f:
cookies = json.load(f)
print(f"✅ 成功读取 {len(cookies)} 个 Cookie")
except FileNotFoundError:
print("❌ cookies.json 文件不存在")
print("请先运行登录获取 Cookie:")
print(" python xhs_cli.py login <手机号> <验证码>")
return
# 2. 准备测试数据
title = "【测试】网络图片发布测试"
content = """测试使用网络 URL 图片发布笔记 📸
本次测试使用了:
✅ 网络 URL 图片picsum.photos
✅ 自动下载功能
✅ 临时文件管理
如果你看到这条笔记,说明网络图片功能正常!"""
# 3. 使用网络图片 URL
images = [
"https://picsum.photos/800/600?random=test1",
"https://picsum.photos/800/600?random=test2",
"https://picsum.photos/800/600?random=test3"
]
print(f"\n测试图片 URL:")
for i, url in enumerate(images, 1):
print(f" {i}. {url}")
tags = ["测试", "网络图片", "自动发布"]
# 4. 创建发布服务
print("\n开始测试发布...")
publisher = XHSPublishService(cookies)
# 5. 执行发布
result = await publisher.publish(
title=title,
content=content,
images=images,
tags=tags,
cleanup=True # 自动清理临时文件
)
# 6. 显示结果
print("\n" + "="*50)
print("测试结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
print("="*50)
if result.get('success'):
print("\n✅ 测试成功!网络图片功能正常")
if 'url' in result:
print(f"📎 笔记链接: {result['url']}")
else:
print(f"\n❌ 测试失败: {result.get('error')}")
if __name__ == "__main__":
asyncio.run(test_network_images())

90
backend/test_publish.py Normal file
View File

@@ -0,0 +1,90 @@
"""
小红书发布功能测试脚本
快速测试发布功能是否正常工作
"""
import asyncio
import json
import os
from xhs_publish import XHSPublishService
async def test_publish():
"""测试发布功能"""
# 1. 从 cookies.json 读取 Cookie
try:
with open('cookies.json', 'r', encoding='utf-8') as f:
cookies = json.load(f)
print(f"✅ 成功读取 {len(cookies)} 个 Cookie")
except FileNotFoundError:
print("❌ cookies.json 文件不存在")
print("请先运行登录获取 Cookie:")
print(" python xhs_cli.py login <手机号> <验证码>")
return
except Exception as e:
print(f"❌ 读取 cookies.json 失败: {e}")
return
# 2. 准备测试数据
title = "【测试】小红书发布功能测试"
content = """这是一条测试笔记 📝
今天测试一下自动发布功能是否正常~
如果你看到这条笔记,说明发布成功了!
#测试 #自动化"""
# 3. 准备测试图片(可选)
images = []
test_image_dir = "temp_uploads"
if os.path.exists(test_image_dir):
for file in os.listdir(test_image_dir):
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
img_path = os.path.abspath(os.path.join(test_image_dir, file))
images.append(img_path)
if len(images) >= 3: # 最多3张测试图片
break
if images:
print(f"✅ 找到 {len(images)} 张测试图片")
else:
print("⚠️ 未找到测试图片,将只发布文字")
# 4. 准备标签
tags = ["测试", "自动化发布"]
# 5. 创建发布服务
print("\n开始发布测试笔记...")
publisher = XHSPublishService(cookies)
# 6. 执行发布
result = await publisher.publish(
title=title,
content=content,
images=images if images else None,
tags=tags
)
# 7. 显示结果
print("\n" + "="*50)
print("发布结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
print("="*50)
if result.get('success'):
print("\n✅ 测试成功!笔记已发布")
if 'url' in result:
print(f"📎 笔记链接: {result['url']}")
else:
print(f"\n❌ 测试失败: {result.get('error')}")
if __name__ == "__main__":
print("="*50)
print("小红书发布功能测试")
print("="*50)
print()
# 运行测试
asyncio.run(test_publish())

138
backend/xhs_cli.py Normal file
View File

@@ -0,0 +1,138 @@
"""
小红书登录CLI工具
供Go服务调用的命令行脚本
"""
import sys
import json
import asyncio
import io
import os
from xhs_login import XHSLoginService
# 设置标准输出为UTF-8编码
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# 将xhs_login.py中的print输出重定向到stderr
original_print = print
def silent_print(*args, **kwargs):
""" 将print输出重定向到stderr """
kwargs['file'] = sys.stderr
original_print(*args, **kwargs)
# 替换全局print函数
import builtins
builtins.print = silent_print
async def send_code(phone: str, country_code: str = "+86"):
"""发送验证码"""
service = XHSLoginService()
try:
result = await service.send_verification_code(phone, country_code)
return result
finally:
await service.close_browser()
async def login(phone: str, code: str, country_code: str = "+86"):
"""登录"""
service = XHSLoginService()
try:
# 先发送验证码(页面已打开)
await service.send_verification_code(phone, country_code)
# 等待一下确保验证码发送完成
await asyncio.sleep(1)
# 执行登录
result = await service.login(phone, code, country_code)
return result
finally:
await service.close_browser()
async def inject_cookies(cookies_json: str):
"""注入Cookie并验证登录状态"""
service = XHSLoginService()
try:
cookies = json.loads(cookies_json)
await service.init_browser(cookies=cookies)
result = await service.verify_login_status()
return result
finally:
await service.close_browser()
def main():
"""主函数"""
if len(sys.argv) < 2:
print(json.dumps({
"success": False,
"error": "缺少命令参数"
}))
sys.exit(1)
command = sys.argv[1]
try:
if command == "send_code":
# python xhs_cli.py send_code <phone> [country_code]
if len(sys.argv) < 3:
print(json.dumps({
"success": False,
"error": "缺少手机号参数"
}))
sys.exit(1)
phone = sys.argv[2]
country_code = sys.argv[3] if len(sys.argv) > 3 else "+86"
result = asyncio.run(send_code(phone, country_code))
sys.stdout.write(json.dumps(result, ensure_ascii=False))
elif command == "login":
# python xhs_cli.py login <phone> <code> [country_code]
if len(sys.argv) < 4:
print(json.dumps({
"success": False,
"error": "缺少手机号或验证码参数"
}))
sys.exit(1)
phone = sys.argv[2]
code = sys.argv[3]
country_code = sys.argv[4] if len(sys.argv) > 4 else "+86"
result = asyncio.run(login(phone, code, country_code))
sys.stdout.write(json.dumps(result, ensure_ascii=False))
elif command == "inject_cookies":
# python xhs_cli.py inject_cookies <cookies_json>
if len(sys.argv) < 3:
print(json.dumps({
"success": False,
"error": "缺少Cookie参数"
}))
sys.exit(1)
cookies_json = sys.argv[2]
result = asyncio.run(inject_cookies(cookies_json))
sys.stdout.write(json.dumps(result, ensure_ascii=False))
else:
print(json.dumps({
"success": False,
"error": f"未知命令: {command}"
}))
sys.exit(1)
except Exception as e:
print(json.dumps({
"success": False,
"error": str(e)
}, ensure_ascii=False))
sys.exit(1)
if __name__ == "__main__":
main()

1414
backend/xhs_login.py Normal file

File diff suppressed because it is too large Load Diff

571
backend/xhs_publish.py Normal file
View File

@@ -0,0 +1,571 @@
"""
小红书笔记发布脚本
提供Cookie、文案标题、内容、标签、图片完成发布操作
支持本地图片路径和网络URL图片
"""
import sys
import json
import asyncio
import io
import os
import re
import aiohttp
import hashlib
import unicodedata
from typing import List, Dict, Any, Union
from pathlib import Path
from xhs_login import XHSLoginService
class XHSPublishService:
"""小红书笔记发布服务"""
def __init__(self, cookies: Union[List[Dict[str, Any]], Dict[str, str]], proxy: str | None = None, user_agent: str | None = None):
"""
初始化发布服务
Args:
cookies: Cookie数据支持两种格式
1. Playwright格式列表: [{"name": "a1", "value": "xxx", "domain": "...", ...}]
2. 键值对格式(字典): {"a1": "xxx", "webId": "yyy", ...}
proxy: 可选的代理地址(例如 http://user:pass@ip:port
user_agent: 可选的自定义User-Agent
"""
# 转换Cookie格式
self.cookies = self._normalize_cookies(cookies)
self.proxy = proxy
self.user_agent = user_agent
self.service = XHSLoginService()
self.temp_dir = "temp_downloads" # 临时下载目录
self.downloaded_files = [] # 记录下载的文件,用于清理
def _normalize_cookies(self, cookies: Union[List[Dict[str, Any]], Dict[str, str]]) -> List[Dict[str, Any]]:
"""
将Cookie标准化为Playwright格式
Args:
cookies: 输入的Cookie支持两种格式
Returns:
Playwright格式的Cookie列表
"""
# 如果已经是列表格式Playwright格式
if isinstance(cookies, list):
# 检查是否包含必要字段
if cookies and 'name' in cookies[0] and 'value' in cookies[0]:
print("✅ 使用 Playwright 格式的 Cookie", file=sys.stderr)
return cookies
# 如果是字典格式键值对格式转换为Playwright格式
if isinstance(cookies, dict):
print("✅ 检测到键值对格式的 Cookie转换为 Playwright 格式", file=sys.stderr)
playwright_cookies = []
for name, value in cookies.items():
cookie = {
"name": name,
"value": str(value),
"domain": ".xiaohongshu.com",
"path": "/",
"expires": -1, # 会话Cookie
"httpOnly": False,
"secure": False,
"sameSite": "Lax"
}
# 特殊处理某些Cookie的属性
if name == "web_session":
cookie["httpOnly"] = True
cookie["secure"] = True
elif name in ["acw_tc"]:
cookie["httpOnly"] = True
playwright_cookies.append(cookie)
print(f" 转换了 {len(playwright_cookies)} 个 Cookie", file=sys.stderr)
return playwright_cookies
# 如果格式不支持,抛出异常
raise ValueError(f"不支持的Cookie格式: {type(cookies)}。请使用列表或字典格式。")
def _calculate_title_width(self, title: str) -> int:
width = 0
for ch in title:
if unicodedata.east_asian_width(ch) in ("F", "W"):
width += 2
else:
width += 1
return width
def is_url(self, path: str) -> bool:
"""
判断是否为网络URL
Args:
path: 图片路径或URL
Returns:
是否为URL
"""
url_pattern = re.compile(r'^https?://', re.IGNORECASE)
return bool(url_pattern.match(path))
async def download_image(self, url: str, index: int = 0) -> str:
"""
下载网络图片到本地临时目录
Args:
url: 图片URL
index: 图片索引(用于命名)
Returns:
本地文件路径
"""
try:
print(f" 正在下载图片 [{index + 1}]: {url}", file=sys.stderr)
# 创建临时目录
Path(self.temp_dir).mkdir(exist_ok=True)
# 生成文件名使用URL的hash值
url_hash = hashlib.md5(url.encode()).hexdigest()[:10]
# 从URL提取文件扩展名
ext = '.jpg' # 默认扩展名
url_path = url.split('?')[0] # 去除URL参数
if '.' in url_path:
ext = '.' + url_path.split('.')[-1].lower()
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
ext = '.jpg'
filename = f"image_{index}_{url_hash}{ext}"
filepath = os.path.join(self.temp_dir, filename)
# 下载图片
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
if response.status == 200:
content = await response.read()
# 保存文件
with open(filepath, 'wb') as f:
f.write(content)
# 记录已下载文件
self.downloaded_files.append(filepath)
# 获取文件大小
file_size = len(content) / 1024 # KB
print(f" ✅ 下载成功: {filename} ({file_size:.1f}KB)", file=sys.stderr)
return os.path.abspath(filepath)
else:
raise Exception(f"下载失败HTTP状态码: {response.status}")
except asyncio.TimeoutError:
raise Exception(f"下载超时: {url}")
except Exception as e:
raise Exception(f"下载图片失败 ({url}): {str(e)}")
async def process_images(self, images: List[str]) -> List[str]:
"""
处理图片列表将网络URL下载到本地
Args:
images: 图片路径列表可以是本地路径或网络URL
Returns:
本地图片路径列表
"""
if not images:
return []
local_images = []
print(f"\n正在处理 {len(images)} 张图片...", file=sys.stderr)
for i, img in enumerate(images):
if self.is_url(img):
# 网络URL需要下载
try:
local_path = await self.download_image(img, i)
local_images.append(local_path)
except Exception as e:
print(f" ⚠️ 图片下载失败: {str(e)}", file=sys.stderr)
# 继续处理其他图片
continue
else:
# 本地路径
if os.path.exists(img):
local_images.append(os.path.abspath(img))
print(f" ✅ 本地图片 [{i + 1}]: {os.path.basename(img)}", file=sys.stderr)
else:
print(f" ⚠️ 本地图片不存在: {img}", file=sys.stderr)
print(f"\n成功处理 {len(local_images)}/{len(images)} 张图片", file=sys.stderr)
return local_images
def cleanup_temp_files(self):
"""
清理临时下载的文件
"""
if not self.downloaded_files:
return
print(f"\n清理 {len(self.downloaded_files)} 个临时文件...", file=sys.stderr)
for filepath in self.downloaded_files:
try:
if os.path.exists(filepath):
os.remove(filepath)
print(f" 已删除: {os.path.basename(filepath)}", file=sys.stderr)
except Exception as e:
print(f" 删除失败 {filepath}: {e}", file=sys.stderr)
# 清空记录
self.downloaded_files = []
async def publish(
self,
title: str,
content: str,
images: List[str] = None,
tags: List[str] = None,
cleanup: bool = True
) -> Dict[str, Any]:
"""
发布笔记
Args:
title: 笔记标题
content: 笔记内容
images: 图片路径列表支持本地文件路径或网络URL
tags: 标签列表(例如:["美食", "探店"]
cleanup: 是否清理临时下载的图片文件默认True
Returns:
Dict containing success status, message, and publish result
"""
try:
print("\n========== 开始发布小红书笔记 ==========", file=sys.stderr)
print(f"标题: {title}", file=sys.stderr)
print(f"内容: {content[:100]}{'...' if len(content) > 100 else ''}", file=sys.stderr)
print(f"图片: {len(images) if images else 0}", file=sys.stderr)
print(f"标签: {tags if tags else []}", file=sys.stderr)
width = self._calculate_title_width(title)
if width > 40:
return {
"success": False,
"error": f"标题长度超过限制(当前宽度 {width},平台限制 40"
}
if tags:
if len(tags) > 10:
tags = tags[:10]
print("⚠️ 标签数量超过10已截取前10个标签", file=sys.stderr)
local_images = None
if images:
local_images = await self.process_images(images)
if not local_images:
print("⚠️ 警告:没有可用的图片", file=sys.stderr)
return {
"success": False,
"error": "没有可用的图片,无法发布笔记"
}
# 初始化浏览器并注入Cookie
print("\n1. 初始化浏览器...", file=sys.stderr)
await self.service.init_browser(cookies=self.cookies, proxy=self.proxy, user_agent=self.user_agent)
# 验证登录状态
print("\n2. 验证登录状态...", file=sys.stderr)
verify_result = await self.service.verify_login_status()
if not verify_result.get('logged_in'):
return {
"success": False,
"error": "Cookie已失效或未登录",
"details": verify_result
}
print("✅ 登录状态有效", file=sys.stderr)
# 发布笔记
print("\n3. 开始发布笔记...", file=sys.stderr)
result = await self.service.publish_note(
title=title,
content=content,
images=local_images,
topics=tags
)
print("\n========== 发布完成 ==========", file=sys.stderr)
return result
except Exception as e:
print(f"\n发布异常: {str(e)}", file=sys.stderr)
return {
"success": False,
"error": str(e)
}
finally:
# 关闭浏览器
await self.service.close_browser()
# 清理临时文件
if cleanup:
self.cleanup_temp_files()
async def publish_from_config(config_file: str) -> Dict[str, Any]:
"""
从配置文件读取参数并发布
Args:
config_file: JSON配置文件路径
Returns:
发布结果
"""
try:
# 读取配置文件
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
# 提取参数
cookies = config.get('cookies', [])
title = config.get('title', '')
content = config.get('content', '')
images = config.get('images', [])
tags = config.get('tags', [])
proxy = config.get('proxy')
user_agent = config.get('user_agent')
# 验证必需参数
if not cookies:
return {
"success": False,
"error": "缺少Cookie参数"
}
if not title or not content:
return {
"success": False,
"error": "标题和内容不能为空"
}
# 注意不再验证图片文件是否存在因为可能是网络URL
# 图片验证交给 process_images 方法处理
# 创建发布服务并执行
publisher = XHSPublishService(cookies, proxy=proxy, user_agent=user_agent)
result = await publisher.publish(
title=title,
content=content,
images=images,
tags=tags
)
return result
except Exception as e:
return {
"success": False,
"error": f"读取配置文件失败: {str(e)}"
}
async def publish_from_params(
cookies_json: str,
title: str,
content: str,
images_json: str = None,
tags_json: str = None
) -> Dict[str, Any]:
"""
从命令行参数发布
Args:
cookies_json: Cookie JSON字符串 或 Cookie文件路径
title: 标题
content: 内容
images_json: 图片路径数组的JSON字符串 (可选)
tags_json: 标签数组的JSON字符串 (可选)
Returns:
发布结果
"""
try:
# 解析Cookie - 支持JSON字符串或文件路径
cookies = None
# 检查是否为文件路径
if os.path.isfile(cookies_json):
# 从文件读取
try:
with open(cookies_json, 'r', encoding='utf-8') as f:
cookies = json.load(f)
print(f"✅ 从文件加载 Cookie: {cookies_json}")
except Exception as e:
return {
"success": False,
"error": f"读取 Cookie 文件失败: {str(e)}"
}
else:
# 解析JSON字符串
try:
cookies = json.loads(cookies_json)
print("✅ 从 JSON 字符串解析 Cookie")
except json.JSONDecodeError as e:
return {
"success": False,
"error": f"Cookie 参数既不是有效文件路径,也不是有效 JSON 字符串: {str(e)}"
}
if not cookies:
return {
"success": False,
"error": "Cookie 为空"
}
# 解析图片列表
images = []
if images_json:
images = json.loads(images_json)
# 解析标签列表
tags = []
if tags_json:
tags = json.loads(tags_json)
# 创建发布服务并执行命令行模式暂不支持传入代理和自定义UA
publisher = XHSPublishService(cookies)
result = await publisher.publish(
title=title,
content=content,
images=images,
tags=tags
)
return result
except json.JSONDecodeError as e:
return {
"success": False,
"error": f"JSON解析失败: {str(e)}"
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
def main():
"""
命令行主函数
使用方式:
1. 从配置文件发布:
python xhs_publish.py --config publish_config.json
2. 从命令行参数发布:
python xhs_publish.py --cookies '<cookies_json>' --title '标题' --content '内容' [--images '<images_json>'] [--tags '<tags_json>']
"""
# 设置标准输出为UTF-8编码
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
if len(sys.argv) < 2:
print(json.dumps({
"success": False,
"error": "缺少参数,请使用 --config 或 --cookies"
}, ensure_ascii=False))
sys.exit(1)
try:
# 解析命令行参数
args = sys.argv[1:]
# 方式1: 从配置文件读取
if args[0] == '--config':
if len(args) < 2:
print(json.dumps({
"success": False,
"error": "缺少配置文件路径"
}, ensure_ascii=False))
sys.exit(1)
config_file = args[1]
result = asyncio.run(publish_from_config(config_file))
print(json.dumps(result, ensure_ascii=False, indent=2))
# 方式2: 从命令行参数
elif args[0] == '--cookies':
# 解析参数
params = {}
i = 0
while i < len(args):
if args[i] == '--cookies' and i + 1 < len(args):
params['cookies'] = args[i + 1]
i += 2
elif args[i] == '--title' and i + 1 < len(args):
params['title'] = args[i + 1]
i += 2
elif args[i] == '--content' and i + 1 < len(args):
params['content'] = args[i + 1]
i += 2
elif args[i] == '--images' and i + 1 < len(args):
params['images'] = args[i + 1]
i += 2
elif args[i] == '--tags' and i + 1 < len(args):
params['tags'] = args[i + 1]
i += 2
else:
i += 1
# 验证必需参数
if 'cookies' not in params:
print(json.dumps({
"success": False,
"error": "缺少 --cookies 参数"
}, ensure_ascii=False))
sys.exit(1)
if 'title' not in params or 'content' not in params:
print(json.dumps({
"success": False,
"error": "缺少 --title 或 --content 参数"
}, ensure_ascii=False))
sys.exit(1)
result = asyncio.run(publish_from_params(
cookies_json=params['cookies'],
title=params['title'],
content=params['content'],
images_json=params.get('images'),
tags_json=params.get('tags')
))
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(json.dumps({
"success": False,
"error": f"未知参数: {args[0]},请使用 --config 或 --cookies"
}, ensure_ascii=False))
sys.exit(1)
except Exception as e:
print(json.dumps({
"success": False,
"error": str(e)
}, ensure_ascii=False))
sys.exit(1)
if __name__ == "__main__":
main()