app.py 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428
  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, date
  18. from pathlib import Path
  19. import pymysql
  20. from pymysql.cursors import DictCursor
  21. # 确保当前目录在 Python 路径中
  22. CURRENT_DIR = Path(__file__).parent.resolve()
  23. if str(CURRENT_DIR) not in sys.path:
  24. sys.path.insert(0, str(CURRENT_DIR))
  25. # 从 server/.env 文件加载环境变量
  26. def load_env_file():
  27. """从 server/.env 文件加载环境变量"""
  28. env_path = CURRENT_DIR.parent / '.env'
  29. if env_path.exists():
  30. print(f"[Config] Loading env from: {env_path}")
  31. with open(env_path, 'r', encoding='utf-8') as f:
  32. for line in f:
  33. line = line.strip()
  34. if line and not line.startswith('#') and '=' in line:
  35. key, value = line.split('=', 1)
  36. key = key.strip()
  37. value = value.strip()
  38. # 移除引号
  39. if value.startswith('"') and value.endswith('"'):
  40. value = value[1:-1]
  41. elif value.startswith("'") and value.endswith("'"):
  42. value = value[1:-1]
  43. # 只在环境变量未设置时加载
  44. if key not in os.environ:
  45. os.environ[key] = value
  46. print(f"[Config] Loaded: {key}=***" if 'PASSWORD' in key or 'SECRET' in key else f"[Config] Loaded: {key}={value}")
  47. else:
  48. print(f"[Config] .env file not found: {env_path}")
  49. # 加载环境变量
  50. load_env_file()
  51. from flask import Flask, request, jsonify
  52. from flask_cors import CORS
  53. from platforms import get_publisher, PLATFORM_MAP
  54. from platforms.base import PublishParams
  55. def parse_datetime(date_str: str):
  56. """解析日期时间字符串"""
  57. if not date_str:
  58. return None
  59. formats = [
  60. "%Y-%m-%d %H:%M:%S",
  61. "%Y-%m-%d %H:%M",
  62. "%Y/%m/%d %H:%M:%S",
  63. "%Y/%m/%d %H:%M",
  64. "%Y-%m-%dT%H:%M:%S",
  65. "%Y-%m-%dT%H:%M:%SZ",
  66. ]
  67. for fmt in formats:
  68. try:
  69. return datetime.strptime(date_str, fmt)
  70. except ValueError:
  71. continue
  72. return None
  73. def validate_video_file(video_path: str) -> bool:
  74. """验证视频文件是否有效"""
  75. if not video_path:
  76. return False
  77. if not os.path.exists(video_path):
  78. return False
  79. if not os.path.isfile(video_path):
  80. return False
  81. valid_extensions = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.webm']
  82. ext = os.path.splitext(video_path)[1].lower()
  83. if ext not in valid_extensions:
  84. return False
  85. if os.path.getsize(video_path) < 1024:
  86. return False
  87. return True
  88. # 创建 Flask 应用
  89. app = Flask(__name__)
  90. CORS(app)
  91. # 全局配置
  92. HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
  93. print(f"[Config] HEADLESS env value: '{os.environ.get('HEADLESS', 'NOT SET')}'", flush=True)
  94. print(f"[Config] HEADLESS_MODE: {HEADLESS_MODE}", flush=True)
  95. # 数据库配置
  96. DB_CONFIG = {
  97. 'host': os.environ.get('DB_HOST', 'localhost'),
  98. 'port': int(os.environ.get('DB_PORT', 3306)),
  99. 'user': os.environ.get('DB_USERNAME', 'root'),
  100. 'password': os.environ.get('DB_PASSWORD', ''),
  101. 'database': os.environ.get('DB_DATABASE', 'media_manager'),
  102. 'charset': 'utf8mb4',
  103. 'cursorclass': DictCursor
  104. }
  105. print(f"[DB_CONFIG] host={DB_CONFIG['host']}, port={DB_CONFIG['port']}, user={DB_CONFIG['user']}, db={DB_CONFIG['database']}, pwd_len={len(DB_CONFIG['password'])}", flush=True)
  106. def get_db_connection():
  107. """获取数据库连接"""
  108. print(f"[DEBUG DB] 正在连接数据库...", flush=True)
  109. print(f"[DEBUG DB] host={DB_CONFIG['host']}, port={DB_CONFIG['port']}, user={DB_CONFIG['user']}, db={DB_CONFIG['database']}", flush=True)
  110. try:
  111. conn = pymysql.connect(**DB_CONFIG)
  112. print(f"[DEBUG DB] 数据库连接成功!", flush=True)
  113. return conn
  114. except Exception as e:
  115. print(f"[DEBUG DB] 数据库连接失败: {e}", flush=True)
  116. raise
  117. # ==================== 签名相关(小红书专用) ====================
  118. @app.route("/sign", methods=["POST"])
  119. def sign_endpoint():
  120. """小红书签名接口"""
  121. try:
  122. from platforms.xiaohongshu import XiaohongshuPublisher
  123. data = request.json
  124. publisher = XiaohongshuPublisher(headless=True)
  125. result = asyncio.run(publisher.get_sign(
  126. data.get("uri", ""),
  127. data.get("data"),
  128. data.get("a1", ""),
  129. data.get("web_session", "")
  130. ))
  131. return jsonify(result)
  132. except Exception as e:
  133. traceback.print_exc()
  134. return jsonify({"error": str(e)}), 500
  135. # ==================== 统一发布接口 ====================
  136. @app.route("/publish", methods=["POST"])
  137. def publish_video():
  138. """
  139. 统一发布接口
  140. 请求体:
  141. {
  142. "platform": "douyin", # douyin | xiaohongshu | weixin | kuaishou
  143. "cookie": "cookie字符串或JSON",
  144. "title": "视频标题",
  145. "description": "视频描述(可选)",
  146. "video_path": "视频文件绝对路径",
  147. "cover_path": "封面图片绝对路径(可选)",
  148. "tags": ["话题1", "话题2"],
  149. "post_time": "定时发布时间(可选,格式:2024-01-20 12:00:00)",
  150. "location": "位置(可选,默认:重庆市)"
  151. }
  152. 响应:
  153. {
  154. "success": true,
  155. "platform": "douyin",
  156. "video_id": "xxx",
  157. "video_url": "xxx",
  158. "message": "发布成功"
  159. }
  160. """
  161. try:
  162. data = request.json
  163. # 获取参数
  164. platform = data.get("platform", "").lower()
  165. cookie_str = data.get("cookie", "")
  166. title = data.get("title", "")
  167. description = data.get("description", "")
  168. video_path = data.get("video_path", "")
  169. cover_path = data.get("cover_path")
  170. tags = data.get("tags", [])
  171. post_time = data.get("post_time")
  172. location = data.get("location", "重庆市")
  173. # 调试日志
  174. print(f"[Publish] 收到请求: platform={platform}, title={title}, video_path={video_path}")
  175. # 参数验证
  176. if not platform:
  177. print("[Publish] 错误: 缺少 platform 参数")
  178. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  179. if platform not in PLATFORM_MAP:
  180. print(f"[Publish] 错误: 不支持的平台 {platform}")
  181. return jsonify({
  182. "success": False,
  183. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  184. }), 400
  185. if not cookie_str:
  186. print("[Publish] 错误: 缺少 cookie 参数")
  187. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  188. if not title:
  189. print("[Publish] 错误: 缺少 title 参数")
  190. return jsonify({"success": False, "error": "缺少 title 参数"}), 400
  191. if not video_path:
  192. print("[Publish] 错误: 缺少 video_path 参数")
  193. return jsonify({"success": False, "error": "缺少 video_path 参数"}), 400
  194. # 视频文件验证(增加详细信息)
  195. if not os.path.exists(video_path):
  196. print(f"[Publish] 错误: 视频文件不存在: {video_path}")
  197. return jsonify({"success": False, "error": f"视频文件不存在: {video_path}"}), 400
  198. if not os.path.isfile(video_path):
  199. print(f"[Publish] 错误: 路径不是文件: {video_path}")
  200. return jsonify({"success": False, "error": f"路径不是文件: {video_path}"}), 400
  201. # 解析发布时间
  202. publish_date = parse_datetime(post_time) if post_time else None
  203. # 创建发布参数
  204. params = PublishParams(
  205. title=title,
  206. video_path=video_path,
  207. description=description,
  208. cover_path=cover_path,
  209. tags=tags,
  210. publish_date=publish_date,
  211. location=location
  212. )
  213. print("=" * 60)
  214. print(f"[Publish] 平台: {platform}")
  215. print(f"[Publish] 标题: {title}")
  216. print(f"[Publish] 视频: {video_path}")
  217. print(f"[Publish] 封面: {cover_path}")
  218. print(f"[Publish] 话题: {tags}")
  219. print(f"[Publish] 定时: {publish_date}")
  220. print("=" * 60)
  221. # 获取对应平台的发布器
  222. PublisherClass = get_publisher(platform)
  223. publisher = PublisherClass(headless=HEADLESS_MODE)
  224. # 执行发布
  225. result = asyncio.run(publisher.run(cookie_str, params))
  226. response_data = {
  227. "success": result.success,
  228. "platform": result.platform,
  229. "video_id": result.video_id,
  230. "video_url": result.video_url,
  231. "message": result.message,
  232. "error": result.error,
  233. "need_captcha": result.need_captcha,
  234. "captcha_type": result.captcha_type,
  235. "screenshot_base64": result.screenshot_base64,
  236. "page_url": result.page_url,
  237. "status": result.status
  238. }
  239. # 如果需要验证码,打印明确的日志
  240. if result.need_captcha:
  241. print(f"[Publish] 需要验证码: type={result.captcha_type}")
  242. return jsonify(response_data)
  243. except Exception as e:
  244. traceback.print_exc()
  245. return jsonify({"success": False, "error": str(e)}), 500
  246. # ==================== AI 辅助发布接口 ====================
  247. # 存储活跃的发布会话
  248. active_publish_sessions = {}
  249. @app.route("/publish/ai-assisted", methods=["POST"])
  250. def publish_ai_assisted():
  251. """
  252. AI 辅助发布接口
  253. 与普通发布接口的区别:
  254. 1. 发布过程中会返回截图供 AI 分析
  255. 2. 如果检测到需要验证码,返回截图和状态,等待外部处理
  256. 3. 支持继续发布(输入验证码后)
  257. 请求体:
  258. {
  259. "platform": "douyin",
  260. "cookie": "cookie字符串",
  261. "title": "视频标题",
  262. "video_path": "视频文件路径",
  263. ...
  264. "return_screenshot": true // 是否返回截图
  265. }
  266. 响应:
  267. {
  268. "success": true/false,
  269. "status": "success|failed|need_captcha|processing",
  270. "screenshot_base64": "...", // 当前页面截图
  271. "page_url": "...",
  272. ...
  273. }
  274. """
  275. try:
  276. data = request.json
  277. # 获取参数
  278. platform = data.get("platform", "").lower()
  279. cookie_str = data.get("cookie", "")
  280. title = data.get("title", "")
  281. description = data.get("description", "")
  282. video_path = data.get("video_path", "")
  283. cover_path = data.get("cover_path")
  284. tags = data.get("tags", [])
  285. post_time = data.get("post_time")
  286. location = data.get("location", "重庆市")
  287. return_screenshot = data.get("return_screenshot", True)
  288. # 参数验证
  289. if not platform:
  290. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  291. if platform not in PLATFORM_MAP:
  292. return jsonify({"success": False, "error": f"不支持的平台: {platform}"}), 400
  293. if not cookie_str:
  294. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  295. if not title:
  296. return jsonify({"success": False, "error": "缺少 title 参数"}), 400
  297. if not video_path or not os.path.exists(video_path):
  298. return jsonify({"success": False, "error": f"视频文件不存在: {video_path}"}), 400
  299. # 解析发布时间
  300. publish_date = parse_datetime(post_time) if post_time else None
  301. # 创建发布参数
  302. params = PublishParams(
  303. title=title,
  304. video_path=video_path,
  305. description=description,
  306. cover_path=cover_path,
  307. tags=tags,
  308. publish_date=publish_date,
  309. location=location
  310. )
  311. print("=" * 60)
  312. print(f"[AI Publish] 平台: {platform}")
  313. print(f"[AI Publish] 标题: {title}")
  314. print(f"[AI Publish] 视频: {video_path}")
  315. print("=" * 60)
  316. # 获取对应平台的发布器
  317. PublisherClass = get_publisher(platform)
  318. publisher = PublisherClass(headless=HEADLESS_MODE)
  319. # 执行发布
  320. result = asyncio.run(publisher.run(cookie_str, params))
  321. response_data = {
  322. "success": result.success,
  323. "platform": result.platform,
  324. "video_id": result.video_id,
  325. "video_url": result.video_url,
  326. "message": result.message,
  327. "error": result.error,
  328. "need_captcha": result.need_captcha,
  329. "captcha_type": result.captcha_type,
  330. "status": result.status or ("success" if result.success else "failed"),
  331. "page_url": result.page_url
  332. }
  333. # 如果请求返回截图
  334. if return_screenshot and result.screenshot_base64:
  335. response_data["screenshot_base64"] = result.screenshot_base64
  336. return jsonify(response_data)
  337. except Exception as e:
  338. traceback.print_exc()
  339. return jsonify({"success": False, "error": str(e), "status": "error"}), 500
  340. # ==================== 批量发布接口 ====================
  341. @app.route("/publish/batch", methods=["POST"])
  342. def publish_batch():
  343. """
  344. 批量发布接口 - 发布到多个平台
  345. 请求体:
  346. {
  347. "platforms": ["douyin", "xiaohongshu"],
  348. "cookies": {
  349. "douyin": "cookie字符串",
  350. "xiaohongshu": "cookie字符串"
  351. },
  352. "title": "视频标题",
  353. "video_path": "视频文件绝对路径",
  354. ...
  355. }
  356. """
  357. try:
  358. data = request.json
  359. platforms = data.get("platforms", [])
  360. cookies = data.get("cookies", {})
  361. if not platforms:
  362. return jsonify({"success": False, "error": "缺少 platforms 参数"}), 400
  363. results = []
  364. for platform in platforms:
  365. platform = platform.lower()
  366. cookie_str = cookies.get(platform, "")
  367. if not cookie_str:
  368. results.append({
  369. "platform": platform,
  370. "success": False,
  371. "error": f"缺少 {platform} 的 cookie"
  372. })
  373. continue
  374. try:
  375. # 创建参数
  376. params = PublishParams(
  377. title=data.get("title", ""),
  378. video_path=data.get("video_path", ""),
  379. description=data.get("description", ""),
  380. cover_path=data.get("cover_path"),
  381. tags=data.get("tags", []),
  382. publish_date=parse_datetime(data.get("post_time")),
  383. location=data.get("location", "重庆市")
  384. )
  385. # 发布
  386. PublisherClass = get_publisher(platform)
  387. publisher = PublisherClass(headless=HEADLESS_MODE)
  388. result = asyncio.run(publisher.run(cookie_str, params))
  389. results.append({
  390. "platform": result.platform,
  391. "success": result.success,
  392. "video_id": result.video_id,
  393. "message": result.message,
  394. "error": result.error
  395. })
  396. except Exception as e:
  397. results.append({
  398. "platform": platform,
  399. "success": False,
  400. "error": str(e)
  401. })
  402. # 统计成功/失败数量
  403. success_count = sum(1 for r in results if r.get("success"))
  404. return jsonify({
  405. "success": success_count > 0,
  406. "total": len(platforms),
  407. "success_count": success_count,
  408. "fail_count": len(platforms) - success_count,
  409. "results": results
  410. })
  411. except Exception as e:
  412. traceback.print_exc()
  413. return jsonify({"success": False, "error": str(e)}), 500
  414. # ==================== Cookie 验证接口 ====================
  415. @app.route("/check_cookie", methods=["POST"])
  416. def check_cookie():
  417. """检查 cookie 是否有效"""
  418. try:
  419. data = request.json
  420. platform = data.get("platform", "").lower()
  421. cookie_str = data.get("cookie", "")
  422. if not cookie_str:
  423. return jsonify({"valid": False, "error": "缺少 cookie 参数"}), 400
  424. # 目前只支持小红书的 cookie 验证
  425. if platform == "xiaohongshu":
  426. try:
  427. from platforms.xiaohongshu import XiaohongshuPublisher, XHS_SDK_AVAILABLE
  428. if XHS_SDK_AVAILABLE:
  429. from xhs import XhsClient
  430. publisher = XiaohongshuPublisher()
  431. xhs_client = XhsClient(cookie_str, sign=publisher.sign_sync)
  432. info = xhs_client.get_self_info()
  433. if info:
  434. return jsonify({
  435. "valid": True,
  436. "user_info": {
  437. "user_id": info.get("user_id"),
  438. "nickname": info.get("nickname"),
  439. "avatar": info.get("images")
  440. }
  441. })
  442. except Exception as e:
  443. return jsonify({"valid": False, "error": str(e)})
  444. # 其他平台返回格式正确但未验证
  445. return jsonify({
  446. "valid": True,
  447. "message": "Cookie 格式正确,但未进行在线验证"
  448. })
  449. except Exception as e:
  450. traceback.print_exc()
  451. return jsonify({"valid": False, "error": str(e)})
  452. # ==================== 获取作品列表接口 ====================
  453. @app.route("/works", methods=["POST"])
  454. def get_works():
  455. """
  456. 获取作品列表
  457. 请求体:
  458. {
  459. "platform": "douyin", # douyin | xiaohongshu | kuaishou
  460. "cookie": "cookie字符串或JSON",
  461. "page": 0, # 页码(从0开始,可选,默认0)
  462. "page_size": 20 # 每页数量(可选,默认20)
  463. }
  464. 响应:
  465. {
  466. "success": true,
  467. "platform": "douyin",
  468. "works": [...],
  469. "total": 100,
  470. "has_more": true
  471. }
  472. """
  473. try:
  474. data = request.json
  475. platform = data.get("platform", "").lower()
  476. cookie_str = data.get("cookie", "")
  477. page = data.get("page", 0)
  478. page_size = data.get("page_size", 20)
  479. print(f"[Works] 收到请求: platform={platform}, page={page}, page_size={page_size}")
  480. if not platform:
  481. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  482. if platform not in PLATFORM_MAP:
  483. return jsonify({
  484. "success": False,
  485. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  486. }), 400
  487. if not cookie_str:
  488. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  489. # 获取对应平台的发布器
  490. PublisherClass = get_publisher(platform)
  491. publisher = PublisherClass(headless=HEADLESS_MODE)
  492. # 执行获取作品
  493. result = asyncio.run(publisher.run_get_works(cookie_str, page, page_size))
  494. return jsonify(result.to_dict())
  495. except Exception as e:
  496. traceback.print_exc()
  497. return jsonify({"success": False, "error": str(e)}), 500
  498. # ==================== 保存作品日统计数据接口 ====================
  499. @app.route("/work_day_statistics", methods=["POST"])
  500. def save_work_day_statistics():
  501. """
  502. 保存作品每日统计数据
  503. 当天的数据走更新流,日期变化走新增流
  504. 请求体:
  505. {
  506. "statistics": [
  507. {
  508. "work_id": 1,
  509. "fans_count": 1000,
  510. "play_count": 5000,
  511. "like_count": 200,
  512. "comment_count": 50,
  513. "share_count": 30,
  514. "collect_count": 100
  515. },
  516. ...
  517. ]
  518. }
  519. 响应:
  520. {
  521. "success": true,
  522. "inserted": 5,
  523. "updated": 3,
  524. "message": "保存成功"
  525. }
  526. """
  527. print("=" * 60, flush=True)
  528. print("[DEBUG] ===== 进入 save_work_day_statistics 方法 =====", flush=True)
  529. print(f"[DEBUG] 请求方法: {request.method}", flush=True)
  530. print(f"[DEBUG] 请求数据: {request.json}", flush=True)
  531. print("=" * 60, flush=True)
  532. try:
  533. data = request.json
  534. statistics_list = data.get("statistics", [])
  535. if not statistics_list:
  536. return jsonify({"success": False, "error": "缺少 statistics 参数"}), 400
  537. today = date.today()
  538. inserted_count = 0
  539. updated_count = 0
  540. print(f"[WorkDayStatistics] 收到请求: {len(statistics_list)} 条统计数据")
  541. conn = get_db_connection()
  542. try:
  543. with conn.cursor() as cursor:
  544. for stat in statistics_list:
  545. work_id = stat.get("work_id")
  546. if not work_id:
  547. continue
  548. fans_count = stat.get("fans_count", 0)
  549. play_count = stat.get("play_count", 0)
  550. like_count = stat.get("like_count", 0)
  551. comment_count = stat.get("comment_count", 0)
  552. share_count = stat.get("share_count", 0)
  553. collect_count = stat.get("collect_count", 0)
  554. # 检查当天是否已有记录
  555. cursor.execute(
  556. "SELECT id FROM work_day_statistics WHERE work_id = %s AND record_date = %s",
  557. (work_id, today)
  558. )
  559. existing = cursor.fetchone()
  560. if existing:
  561. # 更新已有记录
  562. cursor.execute(
  563. """UPDATE work_day_statistics
  564. SET fans_count = %s, play_count = %s, like_count = %s,
  565. comment_count = %s, share_count = %s, collect_count = %s,
  566. updated_at = NOW()
  567. WHERE id = %s""",
  568. (fans_count, play_count, like_count, comment_count,
  569. share_count, collect_count, existing['id'])
  570. )
  571. updated_count += 1
  572. else:
  573. # 插入新记录
  574. cursor.execute(
  575. """INSERT INTO work_day_statistics
  576. (work_id, record_date, fans_count, play_count, like_count,
  577. comment_count, share_count, collect_count, created_at, updated_at)
  578. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())""",
  579. (work_id, today, fans_count, play_count, like_count,
  580. comment_count, share_count, collect_count)
  581. )
  582. inserted_count += 1
  583. conn.commit()
  584. finally:
  585. conn.close()
  586. print(f"[WorkDayStatistics] 完成: 新增 {inserted_count} 条, 更新 {updated_count} 条")
  587. return jsonify({
  588. "success": True,
  589. "inserted": inserted_count,
  590. "updated": updated_count,
  591. "message": f"保存成功: 新增 {inserted_count} 条, 更新 {updated_count} 条"
  592. })
  593. except Exception as e:
  594. traceback.print_exc()
  595. return jsonify({"success": False, "error": str(e)}), 500
  596. @app.route("/work_day_statistics/trend", methods=["GET"])
  597. def get_statistics_trend():
  598. """
  599. 获取数据趋势(用于 Dashboard 数据看板 和 数据分析页面)
  600. 查询参数:
  601. user_id: 用户ID (必填)
  602. days: 天数 (可选,默认7天,最大30天) - 与 start_date/end_date 二选一
  603. start_date: 开始日期 (可选,格式 YYYY-MM-DD)
  604. end_date: 结束日期 (可选,格式 YYYY-MM-DD)
  605. account_id: 账号ID (可选,不填则查询所有账号)
  606. 响应:
  607. {
  608. "success": true,
  609. "data": {
  610. "dates": ["01-16", "01-17", "01-18", ...],
  611. "fans": [100, 120, 130, ...],
  612. "views": [1000, 1200, 1500, ...],
  613. "likes": [50, 60, 70, ...],
  614. "comments": [10, 12, 15, ...],
  615. "shares": [5, 6, 8, ...],
  616. "collects": [20, 25, 30, ...]
  617. }
  618. }
  619. """
  620. try:
  621. user_id = request.args.get("user_id")
  622. days = request.args.get("days")
  623. start_date = request.args.get("start_date")
  624. end_date = request.args.get("end_date")
  625. account_id = request.args.get("account_id")
  626. if not user_id:
  627. return jsonify({"success": False, "error": "缺少 user_id 参数"}), 400
  628. conn = get_db_connection()
  629. try:
  630. with conn.cursor() as cursor:
  631. # 构建查询:关联 works 表获取用户的作品,然后汇总统计数据
  632. # 注意:粉丝数是账号级别的数据,每个账号每天只取一个值(使用 MAX)
  633. # 其他指标(播放、点赞等)是作品级别的数据,需要累加
  634. sql = """
  635. SELECT
  636. record_date,
  637. SUM(account_fans) as total_fans,
  638. SUM(account_views) as total_views,
  639. SUM(account_likes) as total_likes,
  640. SUM(account_comments) as total_comments,
  641. SUM(account_shares) as total_shares,
  642. SUM(account_collects) as total_collects
  643. FROM (
  644. SELECT
  645. wds.record_date,
  646. w.accountId,
  647. MAX(wds.fans_count) as account_fans,
  648. SUM(wds.play_count) as account_views,
  649. SUM(wds.like_count) as account_likes,
  650. SUM(wds.comment_count) as account_comments,
  651. SUM(wds.share_count) as account_shares,
  652. SUM(wds.collect_count) as account_collects
  653. FROM work_day_statistics wds
  654. INNER JOIN works w ON wds.work_id = w.id
  655. WHERE w.userId = %s
  656. """
  657. params = [user_id]
  658. # 支持两种日期筛选方式:start_date/end_date 或 days
  659. if start_date and end_date:
  660. sql += " AND wds.record_date >= %s AND wds.record_date <= %s"
  661. params.extend([start_date, end_date])
  662. else:
  663. # 默认使用 days 参数
  664. days_value = min(int(days or 7), 30)
  665. sql += " AND wds.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)"
  666. params.append(days_value)
  667. if account_id:
  668. sql += " AND w.accountId = %s"
  669. params.append(account_id)
  670. sql += """
  671. GROUP BY wds.record_date, w.accountId
  672. ) as account_stats
  673. GROUP BY record_date
  674. ORDER BY record_date ASC
  675. """
  676. cursor.execute(sql, params)
  677. results = cursor.fetchall()
  678. # 构建响应数据
  679. dates = []
  680. fans = []
  681. views = []
  682. likes = []
  683. comments = []
  684. shares = []
  685. collects = []
  686. for row in results:
  687. # 格式化日期为 "MM-DD" 格式
  688. record_date = row['record_date']
  689. if isinstance(record_date, str):
  690. dates.append(record_date[5:10]) # "2026-01-16" -> "01-16"
  691. else:
  692. dates.append(record_date.strftime("%m-%d"))
  693. # 确保返回整数类型
  694. fans.append(int(row['total_fans'] or 0))
  695. views.append(int(row['total_views'] or 0))
  696. likes.append(int(row['total_likes'] or 0))
  697. comments.append(int(row['total_comments'] or 0))
  698. shares.append(int(row['total_shares'] or 0))
  699. collects.append(int(row['total_collects'] or 0))
  700. # 如果没有数据,生成空的日期范围
  701. if not dates:
  702. from datetime import timedelta
  703. today = date.today()
  704. for i in range(days, 0, -1):
  705. d = today - timedelta(days=i-1)
  706. dates.append(d.strftime("%m-%d"))
  707. fans.append(0)
  708. views.append(0)
  709. likes.append(0)
  710. comments.append(0)
  711. shares.append(0)
  712. collects.append(0)
  713. return jsonify({
  714. "success": True,
  715. "data": {
  716. "dates": dates,
  717. "fans": fans,
  718. "views": views,
  719. "likes": likes,
  720. "comments": comments,
  721. "shares": shares,
  722. "collects": collects
  723. }
  724. })
  725. finally:
  726. conn.close()
  727. except Exception as e:
  728. traceback.print_exc()
  729. return jsonify({"success": False, "error": str(e)}), 500
  730. @app.route("/work_day_statistics/platforms", methods=["GET"])
  731. def get_statistics_by_platform():
  732. """
  733. 按平台分组获取统计数据(用于数据分析页面的平台对比)
  734. 数据来源:
  735. - 粉丝数:从 platform_accounts 表获取(账号级别数据)
  736. - 播放量/点赞/评论/收藏:从 work_day_statistics 表按平台汇总
  737. - 粉丝增量:通过比较区间内最早和最新的粉丝数计算
  738. 查询参数:
  739. user_id: 用户ID (必填)
  740. days: 天数 (可选,默认30天,最大30天) - 与 start_date/end_date 二选一
  741. start_date: 开始日期 (可选,格式 YYYY-MM-DD)
  742. end_date: 结束日期 (可选,格式 YYYY-MM-DD)
  743. 响应:
  744. {
  745. "success": true,
  746. "data": [
  747. {
  748. "platform": "douyin",
  749. "fansCount": 1000,
  750. "fansIncrease": 50,
  751. "viewsCount": 5000,
  752. "likesCount": 200,
  753. "commentsCount": 30,
  754. "collectsCount": 100
  755. },
  756. ...
  757. ]
  758. }
  759. """
  760. try:
  761. user_id = request.args.get("user_id")
  762. days = request.args.get("days")
  763. start_date = request.args.get("start_date")
  764. end_date = request.args.get("end_date")
  765. if not user_id:
  766. return jsonify({"success": False, "error": "缺少 user_id 参数"}), 400
  767. conn = get_db_connection()
  768. try:
  769. with conn.cursor() as cursor:
  770. # 简化查询:按平台分组获取统计数据
  771. # 1. 从 platform_accounts 获取当前粉丝数(注意:字段名是下划线命名 fans_count, user_id)
  772. # 2. 从 work_day_statistics 获取播放量等累计数据
  773. # 根据日期参数构建日期条件
  774. if start_date and end_date:
  775. date_condition = "wds.record_date >= %s AND wds.record_date <= %s"
  776. date_params = [start_date, end_date]
  777. else:
  778. days_value = min(int(days or 30), 30)
  779. date_condition = "wds.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)"
  780. date_params = [days_value]
  781. # 注意:数据库存储的是累积值,所以区间增量 = 最后一天 - 第一天
  782. sql = f"""
  783. SELECT
  784. pa.platform,
  785. pa.fans_count as current_fans,
  786. COALESCE(last_day.views, 0) - COALESCE(first_day.views, 0) as viewsIncrease,
  787. COALESCE(last_day.likes, 0) - COALESCE(first_day.likes, 0) as likesIncrease,
  788. COALESCE(last_day.comments, 0) - COALESCE(first_day.comments, 0) as commentsIncrease,
  789. COALESCE(last_day.collects, 0) - COALESCE(first_day.collects, 0) as collectsIncrease,
  790. COALESCE(first_day.fans, pa.fans_count) as earliest_fans
  791. FROM platform_accounts pa
  792. LEFT JOIN (
  793. -- 获取每个账号在区间内第一天的累积值
  794. SELECT
  795. accountId, fans, views, likes, comments, collects
  796. FROM (
  797. SELECT
  798. w.accountId,
  799. MAX(wds.fans_count) as fans,
  800. SUM(wds.play_count) as views,
  801. SUM(wds.like_count) as likes,
  802. SUM(wds.comment_count) as comments,
  803. SUM(wds.collect_count) as collects,
  804. wds.record_date,
  805. ROW_NUMBER() OVER (PARTITION BY w.accountId ORDER BY wds.record_date ASC) as rn
  806. FROM work_day_statistics wds
  807. INNER JOIN works w ON wds.work_id = w.id
  808. WHERE w.userId = %s
  809. AND {date_condition}
  810. GROUP BY w.accountId, wds.record_date
  811. ) ranked
  812. WHERE rn = 1
  813. ) first_day ON pa.id = first_day.accountId
  814. LEFT JOIN (
  815. -- 获取每个账号在区间内最后一天的累积值
  816. SELECT
  817. accountId, fans, views, likes, comments, collects
  818. FROM (
  819. SELECT
  820. w.accountId,
  821. MAX(wds.fans_count) as fans,
  822. SUM(wds.play_count) as views,
  823. SUM(wds.like_count) as likes,
  824. SUM(wds.comment_count) as comments,
  825. SUM(wds.collect_count) as collects,
  826. wds.record_date,
  827. ROW_NUMBER() OVER (PARTITION BY w.accountId ORDER BY wds.record_date DESC) as rn
  828. FROM work_day_statistics wds
  829. INNER JOIN works w ON wds.work_id = w.id
  830. WHERE w.userId = %s
  831. AND {date_condition}
  832. GROUP BY w.accountId, wds.record_date
  833. ) ranked
  834. WHERE rn = 1
  835. ) last_day ON pa.id = last_day.accountId
  836. WHERE pa.user_id = %s
  837. ORDER BY current_fans DESC
  838. """
  839. # 构建参数列表:first_day子查询(user_id + date_params) + last_day子查询(user_id + date_params) + 主查询(user_id)
  840. params = [user_id] + date_params + [user_id] + date_params + [user_id]
  841. cursor.execute(sql, params)
  842. results = cursor.fetchall()
  843. # 构建响应数据
  844. platform_data = []
  845. for row in results:
  846. current_fans = int(row['current_fans'] or 0)
  847. earliest_fans = int(row['earliest_fans'] or current_fans)
  848. fans_increase = current_fans - earliest_fans
  849. platform_data.append({
  850. "platform": row['platform'],
  851. "fansCount": current_fans,
  852. "fansIncrease": fans_increase,
  853. "viewsCount": int(row['viewsIncrease'] or 0), # 区间增量
  854. "likesCount": int(row['likesIncrease'] or 0), # 区间增量
  855. "commentsCount": int(row['commentsIncrease'] or 0), # 区间增量
  856. "collectsCount": int(row['collectsIncrease'] or 0), # 区间增量
  857. })
  858. print(f"[PlatformStats] 返回 {len(platform_data)} 个平台的数据")
  859. return jsonify({
  860. "success": True,
  861. "data": platform_data
  862. })
  863. finally:
  864. conn.close()
  865. except Exception as e:
  866. traceback.print_exc()
  867. return jsonify({"success": False, "error": str(e)}), 500
  868. @app.route("/work_day_statistics/batch", methods=["POST"])
  869. def get_work_statistics_history():
  870. """
  871. 批量获取作品的历史统计数据
  872. 请求体:
  873. {
  874. "work_ids": [1, 2, 3],
  875. "start_date": "2025-01-01", # 可选
  876. "end_date": "2025-01-21" # 可选
  877. }
  878. 响应:
  879. {
  880. "success": true,
  881. "data": {
  882. "1": [
  883. {"record_date": "2025-01-20", "play_count": 100, ...},
  884. {"record_date": "2025-01-21", "play_count": 150, ...}
  885. ],
  886. ...
  887. }
  888. }
  889. """
  890. try:
  891. data = request.json
  892. work_ids = data.get("work_ids", [])
  893. start_date = data.get("start_date")
  894. end_date = data.get("end_date")
  895. if not work_ids:
  896. return jsonify({"success": False, "error": "缺少 work_ids 参数"}), 400
  897. conn = get_db_connection()
  898. try:
  899. with conn.cursor() as cursor:
  900. # 构建查询
  901. placeholders = ', '.join(['%s'] * len(work_ids))
  902. sql = f"""SELECT work_id, record_date, fans_count, play_count, like_count,
  903. comment_count, share_count, collect_count
  904. FROM work_day_statistics
  905. WHERE work_id IN ({placeholders})"""
  906. params = list(work_ids)
  907. if start_date:
  908. sql += " AND record_date >= %s"
  909. params.append(start_date)
  910. if end_date:
  911. sql += " AND record_date <= %s"
  912. params.append(end_date)
  913. sql += " ORDER BY work_id, record_date"
  914. cursor.execute(sql, params)
  915. results = cursor.fetchall()
  916. finally:
  917. conn.close()
  918. # 按 work_id 分组
  919. grouped_data = {}
  920. for row in results:
  921. work_id = str(row['work_id'])
  922. if work_id not in grouped_data:
  923. grouped_data[work_id] = []
  924. grouped_data[work_id].append({
  925. 'record_date': row['record_date'].strftime('%Y-%m-%d') if row['record_date'] else None,
  926. 'fans_count': row['fans_count'],
  927. 'play_count': row['play_count'],
  928. 'like_count': row['like_count'],
  929. 'comment_count': row['comment_count'],
  930. 'share_count': row['share_count'],
  931. 'collect_count': row['collect_count']
  932. })
  933. return jsonify({
  934. "success": True,
  935. "data": grouped_data
  936. })
  937. except Exception as e:
  938. traceback.print_exc()
  939. return jsonify({"success": False, "error": str(e)}), 500
  940. # ==================== 获取评论列表接口 ====================
  941. @app.route("/comments", methods=["POST"])
  942. def get_comments():
  943. """
  944. 获取作品评论
  945. 请求体:
  946. {
  947. "platform": "douyin", # douyin | xiaohongshu | kuaishou
  948. "cookie": "cookie字符串或JSON",
  949. "work_id": "作品ID",
  950. "cursor": "" # 分页游标(可选)
  951. }
  952. 响应:
  953. {
  954. "success": true,
  955. "platform": "douyin",
  956. "work_id": "xxx",
  957. "comments": [...],
  958. "total": 50,
  959. "has_more": true,
  960. "cursor": "xxx"
  961. }
  962. """
  963. try:
  964. data = request.json
  965. platform = data.get("platform", "").lower()
  966. cookie_str = data.get("cookie", "")
  967. work_id = data.get("work_id", "")
  968. cursor = data.get("cursor", "")
  969. print(f"[Comments] 收到请求: platform={platform}, work_id={work_id}")
  970. if not platform:
  971. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  972. if platform not in PLATFORM_MAP:
  973. return jsonify({
  974. "success": False,
  975. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  976. }), 400
  977. if not cookie_str:
  978. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  979. if not work_id:
  980. return jsonify({"success": False, "error": "缺少 work_id 参数"}), 400
  981. # 获取对应平台的发布器
  982. PublisherClass = get_publisher(platform)
  983. publisher = PublisherClass(headless=HEADLESS_MODE)
  984. # 执行获取评论
  985. result = asyncio.run(publisher.run_get_comments(cookie_str, work_id, cursor))
  986. result_dict = result.to_dict()
  987. # 添加 cursor 到响应
  988. if hasattr(result, '__dict__') and 'cursor' in result.__dict__:
  989. result_dict['cursor'] = result.__dict__['cursor']
  990. return jsonify(result_dict)
  991. except Exception as e:
  992. traceback.print_exc()
  993. return jsonify({"success": False, "error": str(e)}), 500
  994. # ==================== 获取所有作品评论接口 ====================
  995. @app.route("/all_comments", methods=["POST"])
  996. def get_all_comments():
  997. """
  998. 获取所有作品的评论(一次性获取)
  999. 请求体:
  1000. {
  1001. "platform": "douyin", # douyin | xiaohongshu
  1002. "cookie": "cookie字符串或JSON"
  1003. }
  1004. 响应:
  1005. {
  1006. "success": true,
  1007. "platform": "douyin",
  1008. "work_comments": [
  1009. {
  1010. "work_id": "xxx",
  1011. "title": "作品标题",
  1012. "cover_url": "封面URL",
  1013. "comments": [...]
  1014. }
  1015. ],
  1016. "total": 5
  1017. }
  1018. """
  1019. try:
  1020. data = request.json
  1021. platform = data.get("platform", "").lower()
  1022. cookie_str = data.get("cookie", "")
  1023. print(f"[AllComments] 收到请求: platform={platform}")
  1024. if not platform:
  1025. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  1026. if platform not in ['douyin', 'xiaohongshu']:
  1027. return jsonify({
  1028. "success": False,
  1029. "error": f"该接口只支持 douyin 和 xiaohongshu 平台"
  1030. }), 400
  1031. if not cookie_str:
  1032. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  1033. # 获取对应平台的发布器
  1034. PublisherClass = get_publisher(platform)
  1035. publisher = PublisherClass(headless=HEADLESS_MODE)
  1036. # 执行获取所有评论
  1037. result = asyncio.run(publisher.get_all_comments(cookie_str))
  1038. return jsonify(result)
  1039. except Exception as e:
  1040. traceback.print_exc()
  1041. return jsonify({"success": False, "error": str(e)}), 500
  1042. # ==================== 登录状态检查接口 ====================
  1043. @app.route("/check_login", methods=["POST"])
  1044. def check_login():
  1045. """
  1046. 检查 Cookie 登录状态(通过浏览器访问后台页面检测)
  1047. 请求体:
  1048. {
  1049. "platform": "douyin", # douyin | xiaohongshu | kuaishou | weixin
  1050. "cookie": "cookie字符串或JSON"
  1051. }
  1052. 响应:
  1053. {
  1054. "success": true,
  1055. "valid": true, # Cookie 是否有效
  1056. "need_login": false, # 是否需要重新登录
  1057. "message": "登录状态有效"
  1058. }
  1059. """
  1060. try:
  1061. data = request.json
  1062. platform = data.get("platform", "").lower()
  1063. cookie_str = data.get("cookie", "")
  1064. print(f"[CheckLogin] 收到请求: platform={platform}")
  1065. if not platform:
  1066. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  1067. if platform not in PLATFORM_MAP:
  1068. return jsonify({
  1069. "success": False,
  1070. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  1071. }), 400
  1072. if not cookie_str:
  1073. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  1074. # 获取对应平台的发布器
  1075. PublisherClass = get_publisher(platform)
  1076. publisher = PublisherClass(headless=HEADLESS_MODE)
  1077. # 执行登录检查
  1078. result = asyncio.run(publisher.check_login_status(cookie_str))
  1079. return jsonify(result)
  1080. except Exception as e:
  1081. traceback.print_exc()
  1082. return jsonify({
  1083. "success": False,
  1084. "valid": False,
  1085. "need_login": True,
  1086. "error": str(e)
  1087. }), 500
  1088. # ==================== 获取账号信息接口 ====================
  1089. @app.route("/account_info", methods=["POST"])
  1090. def get_account_info():
  1091. """
  1092. 获取账号信息
  1093. 请求体:
  1094. {
  1095. "platform": "baijiahao", # 平台
  1096. "cookie": "cookie字符串或JSON"
  1097. }
  1098. 响应:
  1099. {
  1100. "success": true,
  1101. "account_id": "xxx",
  1102. "account_name": "用户名",
  1103. "avatar_url": "头像URL",
  1104. "fans_count": 0,
  1105. "works_count": 0
  1106. }
  1107. """
  1108. try:
  1109. data = request.json
  1110. platform = data.get("platform", "").lower()
  1111. cookie_str = data.get("cookie", "")
  1112. print(f"[AccountInfo] 收到请求: platform={platform}")
  1113. if not platform:
  1114. return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
  1115. if platform not in PLATFORM_MAP:
  1116. return jsonify({
  1117. "success": False,
  1118. "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
  1119. }), 400
  1120. if not cookie_str:
  1121. return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
  1122. # 获取对应平台的发布器
  1123. PublisherClass = get_publisher(platform)
  1124. publisher = PublisherClass(headless=HEADLESS_MODE)
  1125. # 检查是否有 get_account_info 方法
  1126. if hasattr(publisher, 'get_account_info'):
  1127. result = asyncio.run(publisher.get_account_info(cookie_str))
  1128. return jsonify(result)
  1129. else:
  1130. return jsonify({
  1131. "success": False,
  1132. "error": f"平台 {platform} 不支持获取账号信息"
  1133. }), 400
  1134. except Exception as e:
  1135. traceback.print_exc()
  1136. return jsonify({"success": False, "error": str(e)}), 500
  1137. # ==================== 健康检查 ====================
  1138. @app.route("/health", methods=["GET"])
  1139. def health_check():
  1140. """健康检查"""
  1141. # 检查 xhs SDK 是否可用
  1142. xhs_available = False
  1143. try:
  1144. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  1145. xhs_available = XHS_SDK_AVAILABLE
  1146. except:
  1147. pass
  1148. return jsonify({
  1149. "status": "ok",
  1150. "xhs_sdk": xhs_available,
  1151. "supported_platforms": list(PLATFORM_MAP.keys()),
  1152. "headless_mode": HEADLESS_MODE
  1153. })
  1154. @app.route("/", methods=["GET"])
  1155. def index():
  1156. """首页"""
  1157. return jsonify({
  1158. "name": "多平台视频发布服务",
  1159. "version": "1.2.0",
  1160. "endpoints": {
  1161. "GET /": "服务信息",
  1162. "GET /health": "健康检查",
  1163. "POST /publish": "发布视频",
  1164. "POST /publish/batch": "批量发布",
  1165. "POST /works": "获取作品列表",
  1166. "POST /comments": "获取作品评论",
  1167. "POST /all_comments": "获取所有作品评论",
  1168. "POST /work_day_statistics": "保存作品每日统计数据",
  1169. "POST /work_day_statistics/batch": "获取作品历史统计数据",
  1170. "POST /check_cookie": "检查 Cookie",
  1171. "POST /sign": "小红书签名"
  1172. },
  1173. "supported_platforms": list(PLATFORM_MAP.keys())
  1174. })
  1175. # ==================== 命令行启动 ====================
  1176. def main():
  1177. parser = argparse.ArgumentParser(description='多平台视频发布服务')
  1178. parser.add_argument('--port', type=int, default=5005, help='服务端口 (默认: 5005)')
  1179. parser.add_argument('--host', type=str, default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
  1180. parser.add_argument('--headless', type=str, default='true', help='是否无头模式 (默认: true)')
  1181. parser.add_argument('--debug', action='store_true', help='调试模式')
  1182. args = parser.parse_args()
  1183. global HEADLESS_MODE
  1184. HEADLESS_MODE = args.headless.lower() == 'true'
  1185. # 检查 xhs SDK
  1186. xhs_status = "未安装"
  1187. try:
  1188. from platforms.xiaohongshu import XHS_SDK_AVAILABLE
  1189. xhs_status = "已安装" if XHS_SDK_AVAILABLE else "未安装"
  1190. except:
  1191. pass
  1192. print("=" * 60)
  1193. print("多平台视频发布服务")
  1194. print("=" * 60)
  1195. print(f"XHS SDK: {xhs_status}")
  1196. print(f"Headless 模式: {HEADLESS_MODE}")
  1197. print(f"支持平台: {', '.join(PLATFORM_MAP.keys())}")
  1198. print("=" * 60)
  1199. print(f"启动服务: http://{args.host}:{args.port}")
  1200. print("=" * 60)
  1201. app.run(host=args.host, port=args.port, debug=args.debug, threaded=True)
  1202. if __name__ == '__main__':
  1203. main()