app.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 多平台视频发布服务 - 统一入口
  5. 支持平台: 抖音、小红书、视频号、快手
  6. 参考项目: matrix (https://github.com/kebenxiaoming/matrix)
  7. 使用方式:
  8. python app.py # 启动 HTTP 服务 (端口 5005)
  9. python app.py --port 8080 # 指定端口
  10. python app.py --headless false # 显示浏览器窗口
  11. """
  12. import asyncio
  13. import os
  14. import sys
  15. import argparse
  16. import traceback
  17. from datetime import datetime
  18. from pathlib import Path
  19. # 确保当前目录在 Python 路径中
  20. CURRENT_DIR = Path(__file__).parent.resolve()
  21. if str(CURRENT_DIR) not in sys.path:
  22. sys.path.insert(0, str(CURRENT_DIR))
  23. from flask import Flask, request, jsonify
  24. from flask_cors import CORS
  25. from platforms import get_publisher, PLATFORM_MAP
  26. from platforms.base import PublishParams
  27. def parse_datetime(date_str: str):
  28. """解析日期时间字符串"""
  29. if not date_str:
  30. return None
  31. formats = [
  32. "%Y-%m-%d %H:%M:%S",
  33. "%Y-%m-%d %H:%M",
  34. "%Y/%m/%d %H:%M:%S",
  35. "%Y/%m/%d %H:%M",
  36. "%Y-%m-%dT%H:%M:%S",
  37. "%Y-%m-%dT%H:%M:%SZ",
  38. ]
  39. for fmt in formats:
  40. try:
  41. return datetime.strptime(date_str, fmt)
  42. except ValueError:
  43. continue
  44. return None
  45. def validate_video_file(video_path: str) -> bool:
  46. """验证视频文件是否有效"""
  47. if not video_path:
  48. return False
  49. if not os.path.exists(video_path):
  50. return False
  51. if not os.path.isfile(video_path):
  52. return False
  53. valid_extensions = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.webm']
  54. ext = os.path.splitext(video_path)[1].lower()
  55. if ext not in valid_extensions:
  56. return False
  57. if os.path.getsize(video_path) < 1024:
  58. return False
  59. return True
  60. # 创建 Flask 应用
  61. app = Flask(__name__)
  62. CORS(app)
  63. # 全局配置
  64. HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
  65. # ==================== 签名相关(小红书专用) ====================
  66. @app.route("/sign", methods=["POST"])
  67. def sign_endpoint():
  68. """小红书签名接口"""
  69. try:
  70. from platforms.xiaohongshu import XiaohongshuPublisher
  71. data = request.json
  72. publisher = XiaohongshuPublisher(headless=True)
  73. result = asyncio.run(publisher.get_sign(
  74. data.get("uri", ""),
  75. data.get("data"),
  76. data.get("a1", ""),
  77. data.get("web_session", "")
  78. ))
  79. return jsonify(result)
  80. except Exception as e:
  81. traceback.print_exc()
  82. return jsonify({"error": str(e)}), 500
  83. # ==================== 统一发布接口 ====================
  84. @app.route("/publish", methods=["POST"])
  85. def publish_video():
  86. """
  87. 统一发布接口
  88. 请求体:
  89. {
  90. "platform": "douyin", # douyin | xiaohongshu | weixin | kuaishou
  91. "cookie": "cookie字符串或JSON",
  92. "title": "视频标题",
  93. "description": "视频描述(可选)",
  94. "video_path": "视频文件绝对路径",
  95. "cover_path": "封面图片绝对路径(可选)",
  96. "tags": ["话题1", "话题2"],
  97. "post_time": "定时发布时间(可选,格式:2024-01-20 12:00:00)",
  98. "location": "位置(可选,默认:重庆市)"
  99. }
  100. 响应:
  101. {
  102. "success": true,
  103. "platform": "douyin",
  104. "video_id": "xxx",
  105. "video_url": "xxx",
  106. "message": "发布成功"
  107. }
  108. """
  109. try:
  110. data = request.json
  111. # 获取参数
  112. platform = data.get("platform", "").lower()
  113. cookie_str = data.get("cookie", "")
  114. title = data.get("title", "")
  115. description = data.get("description", "")
  116. video_path = data.get("video_path", "")
  117. cover_path = data.get("cover_path")
  118. tags = data.get("tags", [])
  119. post_time = data.get("post_time")
  120. location = data.get("location", "重庆市")
  121. # 调试日志
  122. print(f"[Publish] 收到请求: platform={platform}, title={title}, video_path={video_path}")
  123. # 参数验证
  124. if not platform:
  125. print("[Publish] 错误: 缺少 platform 参数")
  126. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  127. if platform not in PLATFORM_MAP:
  128. print(f"[Publish] 错误: 不支持的平台 {platform}")
  129. return jsonify({
  130. "success": False,
  131. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  132. }), 400
  133. if not cookie_str:
  134. print("[Publish] 错误: 缺少 cookie 参数")
  135. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  136. if not title:
  137. print("[Publish] 错误: 缺少 title 参数")
  138. return jsonify({"success": False, "error": "缺少 title 参数"}), 400
  139. if not video_path:
  140. print("[Publish] 错误: 缺少 video_path 参数")
  141. return jsonify({"success": False, "error": "缺少 video_path 参数"}), 400
  142. # 视频文件验证(增加详细信息)
  143. if not os.path.exists(video_path):
  144. print(f"[Publish] 错误: 视频文件不存在: {video_path}")
  145. return jsonify({"success": False, "error": f"视频文件不存在: {video_path}"}), 400
  146. if not os.path.isfile(video_path):
  147. print(f"[Publish] 错误: 路径不是文件: {video_path}")
  148. return jsonify({"success": False, "error": f"路径不是文件: {video_path}"}), 400
  149. # 解析发布时间
  150. publish_date = parse_datetime(post_time) if post_time else None
  151. # 创建发布参数
  152. params = PublishParams(
  153. title=title,
  154. video_path=video_path,
  155. description=description,
  156. cover_path=cover_path,
  157. tags=tags,
  158. publish_date=publish_date,
  159. location=location
  160. )
  161. print("=" * 60)
  162. print(f"[Publish] 平台: {platform}")
  163. print(f"[Publish] 标题: {title}")
  164. print(f"[Publish] 视频: {video_path}")
  165. print(f"[Publish] 封面: {cover_path}")
  166. print(f"[Publish] 话题: {tags}")
  167. print(f"[Publish] 定时: {publish_date}")
  168. print("=" * 60)
  169. # 获取对应平台的发布器
  170. PublisherClass = get_publisher(platform)
  171. publisher = PublisherClass(headless=HEADLESS_MODE)
  172. # 执行发布
  173. result = asyncio.run(publisher.run(cookie_str, params))
  174. response_data = {
  175. "success": result.success,
  176. "platform": result.platform,
  177. "video_id": result.video_id,
  178. "video_url": result.video_url,
  179. "message": result.message,
  180. "error": result.error,
  181. "need_captcha": result.need_captcha,
  182. "captcha_type": result.captcha_type,
  183. "screenshot_base64": result.screenshot_base64,
  184. "page_url": result.page_url,
  185. "status": result.status
  186. }
  187. # 如果需要验证码,打印明确的日志
  188. if result.need_captcha:
  189. print(f"[Publish] 需要验证码: type={result.captcha_type}")
  190. return jsonify(response_data)
  191. except Exception as e:
  192. traceback.print_exc()
  193. return jsonify({"success": False, "error": str(e)}), 500
  194. # ==================== AI 辅助发布接口 ====================
  195. # 存储活跃的发布会话
  196. active_publish_sessions = {}
  197. @app.route("/publish/ai-assisted", methods=["POST"])
  198. def publish_ai_assisted():
  199. """
  200. AI 辅助发布接口
  201. 与普通发布接口的区别:
  202. 1. 发布过程中会返回截图供 AI 分析
  203. 2. 如果检测到需要验证码,返回截图和状态,等待外部处理
  204. 3. 支持继续发布(输入验证码后)
  205. 请求体:
  206. {
  207. "platform": "douyin",
  208. "cookie": "cookie字符串",
  209. "title": "视频标题",
  210. "video_path": "视频文件路径",
  211. ...
  212. "return_screenshot": true // 是否返回截图
  213. }
  214. 响应:
  215. {
  216. "success": true/false,
  217. "status": "success|failed|need_captcha|processing",
  218. "screenshot_base64": "...", // 当前页面截图
  219. "page_url": "...",
  220. ...
  221. }
  222. """
  223. try:
  224. data = request.json
  225. # 获取参数
  226. platform = data.get("platform", "").lower()
  227. cookie_str = data.get("cookie", "")
  228. title = data.get("title", "")
  229. description = data.get("description", "")
  230. video_path = data.get("video_path", "")
  231. cover_path = data.get("cover_path")
  232. tags = data.get("tags", [])
  233. post_time = data.get("post_time")
  234. location = data.get("location", "重庆市")
  235. return_screenshot = data.get("return_screenshot", True)
  236. # 参数验证
  237. if not platform:
  238. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  239. if platform not in PLATFORM_MAP:
  240. return jsonify({"success": False, "error": f"不支持的平台: {platform}"}), 400
  241. if not cookie_str:
  242. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  243. if not title:
  244. return jsonify({"success": False, "error": "缺少 title 参数"}), 400
  245. if not video_path or not os.path.exists(video_path):
  246. return jsonify({"success": False, "error": f"视频文件不存在: {video_path}"}), 400
  247. # 解析发布时间
  248. publish_date = parse_datetime(post_time) if post_time else None
  249. # 创建发布参数
  250. params = PublishParams(
  251. title=title,
  252. video_path=video_path,
  253. description=description,
  254. cover_path=cover_path,
  255. tags=tags,
  256. publish_date=publish_date,
  257. location=location
  258. )
  259. print("=" * 60)
  260. print(f"[AI Publish] 平台: {platform}")
  261. print(f"[AI Publish] 标题: {title}")
  262. print(f"[AI Publish] 视频: {video_path}")
  263. print("=" * 60)
  264. # 获取对应平台的发布器
  265. PublisherClass = get_publisher(platform)
  266. publisher = PublisherClass(headless=HEADLESS_MODE)
  267. # 执行发布
  268. result = asyncio.run(publisher.run(cookie_str, params))
  269. response_data = {
  270. "success": result.success,
  271. "platform": result.platform,
  272. "video_id": result.video_id,
  273. "video_url": result.video_url,
  274. "message": result.message,
  275. "error": result.error,
  276. "need_captcha": result.need_captcha,
  277. "captcha_type": result.captcha_type,
  278. "status": result.status or ("success" if result.success else "failed"),
  279. "page_url": result.page_url
  280. }
  281. # 如果请求返回截图
  282. if return_screenshot and result.screenshot_base64:
  283. response_data["screenshot_base64"] = result.screenshot_base64
  284. return jsonify(response_data)
  285. except Exception as e:
  286. traceback.print_exc()
  287. return jsonify({"success": False, "error": str(e), "status": "error"}), 500
  288. # ==================== 批量发布接口 ====================
  289. @app.route("/publish/batch", methods=["POST"])
  290. def publish_batch():
  291. """
  292. 批量发布接口 - 发布到多个平台
  293. 请求体:
  294. {
  295. "platforms": ["douyin", "xiaohongshu"],
  296. "cookies": {
  297. "douyin": "cookie字符串",
  298. "xiaohongshu": "cookie字符串"
  299. },
  300. "title": "视频标题",
  301. "video_path": "视频文件绝对路径",
  302. ...
  303. }
  304. """
  305. try:
  306. data = request.json
  307. platforms = data.get("platforms", [])
  308. cookies = data.get("cookies", {})
  309. if not platforms:
  310. return jsonify({"success": False, "error": "缺少 platforms 参数"}), 400
  311. results = []
  312. for platform in platforms:
  313. platform = platform.lower()
  314. cookie_str = cookies.get(platform, "")
  315. if not cookie_str:
  316. results.append({
  317. "platform": platform,
  318. "success": False,
  319. "error": f"缺少 {platform} 的 cookie"
  320. })
  321. continue
  322. try:
  323. # 创建参数
  324. params = PublishParams(
  325. title=data.get("title", ""),
  326. video_path=data.get("video_path", ""),
  327. description=data.get("description", ""),
  328. cover_path=data.get("cover_path"),
  329. tags=data.get("tags", []),
  330. publish_date=parse_datetime(data.get("post_time")),
  331. location=data.get("location", "重庆市")
  332. )
  333. # 发布
  334. PublisherClass = get_publisher(platform)
  335. publisher = PublisherClass(headless=HEADLESS_MODE)
  336. result = asyncio.run(publisher.run(cookie_str, params))
  337. results.append({
  338. "platform": result.platform,
  339. "success": result.success,
  340. "video_id": result.video_id,
  341. "message": result.message,
  342. "error": result.error
  343. })
  344. except Exception as e:
  345. results.append({
  346. "platform": platform,
  347. "success": False,
  348. "error": str(e)
  349. })
  350. # 统计成功/失败数量
  351. success_count = sum(1 for r in results if r.get("success"))
  352. return jsonify({
  353. "success": success_count > 0,
  354. "total": len(platforms),
  355. "success_count": success_count,
  356. "fail_count": len(platforms) - success_count,
  357. "results": results
  358. })
  359. except Exception as e:
  360. traceback.print_exc()
  361. return jsonify({"success": False, "error": str(e)}), 500
  362. # ==================== Cookie 验证接口 ====================
  363. @app.route("/check_cookie", methods=["POST"])
  364. def check_cookie():
  365. """检查 cookie 是否有效"""
  366. try:
  367. data = request.json
  368. platform = data.get("platform", "").lower()
  369. cookie_str = data.get("cookie", "")
  370. if not cookie_str:
  371. return jsonify({"valid": False, "error": "缺少 cookie 参数"}), 400
  372. # 目前只支持小红书的 cookie 验证
  373. if platform == "xiaohongshu":
  374. try:
  375. from platforms.xiaohongshu import XiaohongshuPublisher, XHS_SDK_AVAILABLE
  376. if XHS_SDK_AVAILABLE:
  377. from xhs import XhsClient
  378. publisher = XiaohongshuPublisher()
  379. xhs_client = XhsClient(cookie_str, sign=publisher.sign_sync)
  380. info = xhs_client.get_self_info()
  381. if info:
  382. return jsonify({
  383. "valid": True,
  384. "user_info": {
  385. "user_id": info.get("user_id"),
  386. "nickname": info.get("nickname"),
  387. "avatar": info.get("images")
  388. }
  389. })
  390. except Exception as e:
  391. return jsonify({"valid": False, "error": str(e)})
  392. # 其他平台返回格式正确但未验证
  393. return jsonify({
  394. "valid": True,
  395. "message": "Cookie 格式正确,但未进行在线验证"
  396. })
  397. except Exception as e:
  398. traceback.print_exc()
  399. return jsonify({"valid": False, "error": str(e)})
  400. # ==================== 获取作品列表接口 ====================
  401. @app.route("/works", methods=["POST"])
  402. def get_works():
  403. """
  404. 获取作品列表
  405. 请求体:
  406. {
  407. "platform": "douyin", # douyin | xiaohongshu | kuaishou
  408. "cookie": "cookie字符串或JSON",
  409. "page": 0, # 页码(从0开始,可选,默认0)
  410. "page_size": 20 # 每页数量(可选,默认20)
  411. }
  412. 响应:
  413. {
  414. "success": true,
  415. "platform": "douyin",
  416. "works": [...],
  417. "total": 100,
  418. "has_more": true
  419. }
  420. """
  421. try:
  422. data = request.json
  423. platform = data.get("platform", "").lower()
  424. cookie_str = data.get("cookie", "")
  425. page = data.get("page", 0)
  426. page_size = data.get("page_size", 20)
  427. print(f"[Works] 收到请求: platform={platform}, page={page}, page_size={page_size}")
  428. if not platform:
  429. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  430. if platform not in PLATFORM_MAP:
  431. return jsonify({
  432. "success": False,
  433. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  434. }), 400
  435. if not cookie_str:
  436. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  437. # 获取对应平台的发布器
  438. PublisherClass = get_publisher(platform)
  439. publisher = PublisherClass(headless=HEADLESS_MODE)
  440. # 执行获取作品
  441. result = asyncio.run(publisher.run_get_works(cookie_str, page, page_size))
  442. return jsonify(result.to_dict())
  443. except Exception as e:
  444. traceback.print_exc()
  445. return jsonify({"success": False, "error": str(e)}), 500
  446. # ==================== 获取评论列表接口 ====================
  447. @app.route("/comments", methods=["POST"])
  448. def get_comments():
  449. """
  450. 获取作品评论
  451. 请求体:
  452. {
  453. "platform": "douyin", # douyin | xiaohongshu | kuaishou
  454. "cookie": "cookie字符串或JSON",
  455. "work_id": "作品ID",
  456. "cursor": "" # 分页游标(可选)
  457. }
  458. 响应:
  459. {
  460. "success": true,
  461. "platform": "douyin",
  462. "work_id": "xxx",
  463. "comments": [...],
  464. "total": 50,
  465. "has_more": true,
  466. "cursor": "xxx"
  467. }
  468. """
  469. try:
  470. data = request.json
  471. platform = data.get("platform", "").lower()
  472. cookie_str = data.get("cookie", "")
  473. work_id = data.get("work_id", "")
  474. cursor = data.get("cursor", "")
  475. print(f"[Comments] 收到请求: platform={platform}, work_id={work_id}")
  476. if not platform:
  477. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  478. if platform not in PLATFORM_MAP:
  479. return jsonify({
  480. "success": False,
  481. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  482. }), 400
  483. if not cookie_str:
  484. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  485. if not work_id:
  486. return jsonify({"success": False, "error": "缺少 work_id 参数"}), 400
  487. # 获取对应平台的发布器
  488. PublisherClass = get_publisher(platform)
  489. publisher = PublisherClass(headless=HEADLESS_MODE)
  490. # 执行获取评论
  491. result = asyncio.run(publisher.run_get_comments(cookie_str, work_id, cursor))
  492. result_dict = result.to_dict()
  493. # 添加 cursor 到响应
  494. if hasattr(result, '__dict__') and 'cursor' in result.__dict__:
  495. result_dict['cursor'] = result.__dict__['cursor']
  496. return jsonify(result_dict)
  497. except Exception as e:
  498. traceback.print_exc()
  499. return jsonify({"success": False, "error": str(e)}), 500
  500. # ==================== 获取所有作品评论接口 ====================
  501. @app.route("/all_comments", methods=["POST"])
  502. def get_all_comments():
  503. """
  504. 获取所有作品的评论(一次性获取)
  505. 请求体:
  506. {
  507. "platform": "douyin", # douyin | xiaohongshu
  508. "cookie": "cookie字符串或JSON"
  509. }
  510. 响应:
  511. {
  512. "success": true,
  513. "platform": "douyin",
  514. "work_comments": [
  515. {
  516. "work_id": "xxx",
  517. "title": "作品标题",
  518. "cover_url": "封面URL",
  519. "comments": [...]
  520. }
  521. ],
  522. "total": 5
  523. }
  524. """
  525. try:
  526. data = request.json
  527. platform = data.get("platform", "").lower()
  528. cookie_str = data.get("cookie", "")
  529. print(f"[AllComments] 收到请求: platform={platform}")
  530. if not platform:
  531. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  532. if platform not in ['douyin', 'xiaohongshu']:
  533. return jsonify({
  534. "success": False,
  535. "error": f"该接口只支持 douyin 和 xiaohongshu 平台"
  536. }), 400
  537. if not cookie_str:
  538. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  539. # 获取对应平台的发布器
  540. PublisherClass = get_publisher(platform)
  541. publisher = PublisherClass(headless=HEADLESS_MODE)
  542. # 执行获取所有评论
  543. result = asyncio.run(publisher.get_all_comments(cookie_str))
  544. return jsonify(result)
  545. except Exception as e:
  546. traceback.print_exc()
  547. return jsonify({"success": False, "error": str(e)}), 500
  548. # ==================== 登录状态检查接口 ====================
  549. @app.route("/check_login", methods=["POST"])
  550. def check_login():
  551. """
  552. 检查 Cookie 登录状态(通过浏览器访问后台页面检测)
  553. 请求体:
  554. {
  555. "platform": "douyin", # douyin | xiaohongshu | kuaishou | weixin
  556. "cookie": "cookie字符串或JSON"
  557. }
  558. 响应:
  559. {
  560. "success": true,
  561. "valid": true, # Cookie 是否有效
  562. "need_login": false, # 是否需要重新登录
  563. "message": "登录状态有效"
  564. }
  565. """
  566. try:
  567. data = request.json
  568. platform = data.get("platform", "").lower()
  569. cookie_str = data.get("cookie", "")
  570. print(f"[CheckLogin] 收到请求: platform={platform}")
  571. if not platform:
  572. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  573. if platform not in PLATFORM_MAP:
  574. return jsonify({
  575. "success": False,
  576. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  577. }), 400
  578. if not cookie_str:
  579. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  580. # 获取对应平台的发布器
  581. PublisherClass = get_publisher(platform)
  582. publisher = PublisherClass(headless=HEADLESS_MODE)
  583. # 执行登录检查
  584. result = asyncio.run(publisher.check_login_status(cookie_str))
  585. return jsonify(result)
  586. except Exception as e:
  587. traceback.print_exc()
  588. return jsonify({
  589. "success": False,
  590. "valid": False,
  591. "need_login": True,
  592. "error": str(e)
  593. }), 500
  594. # ==================== 获取账号信息接口 ====================
  595. @app.route("/account_info", methods=["POST"])
  596. def get_account_info():
  597. """
  598. 获取账号信息
  599. 请求体:
  600. {
  601. "platform": "baijiahao", # 平台
  602. "cookie": "cookie字符串或JSON"
  603. }
  604. 响应:
  605. {
  606. "success": true,
  607. "account_id": "xxx",
  608. "account_name": "用户名",
  609. "avatar_url": "头像URL",
  610. "fans_count": 0,
  611. "works_count": 0
  612. }
  613. """
  614. try:
  615. data = request.json
  616. platform = data.get("platform", "").lower()
  617. cookie_str = data.get("cookie", "")
  618. print(f"[AccountInfo] 收到请求: platform={platform}")
  619. if not platform:
  620. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  621. if platform not in PLATFORM_MAP:
  622. return jsonify({
  623. "success": False,
  624. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  625. }), 400
  626. if not cookie_str:
  627. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  628. # 获取对应平台的发布器
  629. PublisherClass = get_publisher(platform)
  630. publisher = PublisherClass(headless=HEADLESS_MODE)
  631. # 检查是否有 get_account_info 方法
  632. if hasattr(publisher, 'get_account_info'):
  633. result = asyncio.run(publisher.get_account_info(cookie_str))
  634. return jsonify(result)
  635. else:
  636. return jsonify({
  637. "success": False,
  638. "error": f"平台 {platform} 不支持获取账号信息"
  639. }), 400
  640. except Exception as e:
  641. traceback.print_exc()
  642. return jsonify({"success": False, "error": str(e)}), 500
  643. # ==================== 健康检查 ====================
  644. @app.route("/health", methods=["GET"])
  645. def health_check():
  646. """健康检查"""
  647. # 检查 xhs SDK 是否可用
  648. xhs_available = False
  649. try:
  650. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  651. xhs_available = XHS_SDK_AVAILABLE
  652. except:
  653. pass
  654. return jsonify({
  655. "status": "ok",
  656. "xhs_sdk": xhs_available,
  657. "supported_platforms": list(PLATFORM_MAP.keys()),
  658. "headless_mode": HEADLESS_MODE
  659. })
  660. @app.route("/", methods=["GET"])
  661. def index():
  662. """首页"""
  663. return jsonify({
  664. "name": "多平台视频发布服务",
  665. "version": "1.1.0",
  666. "endpoints": {
  667. "GET /": "服务信息",
  668. "GET /health": "健康检查",
  669. "POST /publish": "发布视频",
  670. "POST /publish/batch": "批量发布",
  671. "POST /works": "获取作品列表",
  672. "POST /comments": "获取作品评论",
  673. "POST /all_comments": "获取所有作品评论",
  674. "POST /check_cookie": "检查 Cookie",
  675. "POST /sign": "小红书签名"
  676. },
  677. "supported_platforms": list(PLATFORM_MAP.keys())
  678. })
  679. # ==================== 命令行启动 ====================
  680. def main():
  681. parser = argparse.ArgumentParser(description='多平台视频发布服务')
  682. parser.add_argument('--port', type=int, default=5005, help='服务端口 (默认: 5005)')
  683. parser.add_argument('--host', type=str, default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
  684. parser.add_argument('--headless', type=str, default='true', help='是否无头模式 (默认: true)')
  685. parser.add_argument('--debug', action='store_true', help='调试模式')
  686. args = parser.parse_args()
  687. global HEADLESS_MODE
  688. HEADLESS_MODE = args.headless.lower() == 'true'
  689. # 检查 xhs SDK
  690. xhs_status = "未安装"
  691. try:
  692. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  693. xhs_status = "已安装" if XHS_SDK_AVAILABLE else "未安装"
  694. except:
  695. pass
  696. print("=" * 60)
  697. print("多平台视频发布服务")
  698. print("=" * 60)
  699. print(f"XHS SDK: {xhs_status}")
  700. print(f"Headless 模式: {HEADLESS_MODE}")
  701. print(f"支持平台: {', '.join(PLATFORM_MAP.keys())}")
  702. print("=" * 60)
  703. print(f"启动服务: http://{args.host}:{args.port}")
  704. print("=" * 60)
  705. app.run(host=args.host, port=args.port, debug=args.debug, threaded=True)
  706. if __name__ == '__main__':
  707. main()