From 031e08570266d1797177677cb4fdb6bc8fd9ccce Mon Sep 17 00:00:00 2001 From: GuDong <1735590698@qq.com> Date: Mon, 25 May 2026 21:27:02 +0800 Subject: [PATCH] =?UTF-8?q?add(=E6=B1=82=E5=B0=8F=E7=BA=A2=E8=8A=B1):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=8A=A8=E6=B1=82=E5=B0=8F=E7=BA=A2?= =?UTF-8?q?=E8=8A=B1=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto_red_flower_task.py | 185 ++++++++++++++++++++++++++++ db_manager.py | 239 +++++++++++++++++++++++++++++++++++- reply_server.py | 172 ++++++++++++++++++++++++++ static/css/accounts.css | 168 +++++++++++++++++++++++++ static/index.html | 20 +-- static/js/app.js | 173 ++++++++++++++++++++------ utils/red_flower_service.py | 216 ++++++++++++++++++++++++++++++++ 7 files changed, 1120 insertions(+), 53 deletions(-) create mode 100644 auto_red_flower_task.py create mode 100644 utils/red_flower_service.py diff --git a/auto_red_flower_task.py b/auto_red_flower_task.py new file mode 100644 index 0000000..7afe09f --- /dev/null +++ b/auto_red_flower_task.py @@ -0,0 +1,185 @@ +"""自动求小红花任务。 + +轻量本地调度版本:不引入上游独立 scheduler 服务,直接复用当前 SQLite、Cookie 和订单数据。 +""" + +from __future__ import annotations + +import asyncio +import os +import time +import uuid +from typing import Any, Dict, Optional + +from loguru import logger + +from db_manager import db_manager +from utils.red_flower_service import RedFlowerService + + +DEFAULT_INTERVAL_SECONDS = int(os.getenv("AUTO_RED_FLOWER_TASK_INTERVAL_SECONDS", "300") or 300) +DEFAULT_BATCH_LIMIT = int(os.getenv("AUTO_RED_FLOWER_TASK_BATCH_LIMIT", "5") or 5) +DEFAULT_LOOKBACK_DAYS = int(os.getenv("AUTO_RED_FLOWER_TASK_LOOKBACK_DAYS", "10") or 10) +DEFAULT_COOLDOWN_MINUTES = int(os.getenv("AUTO_RED_FLOWER_TASK_COOLDOWN_MINUTES", "30") or 30) + + +def _status_from_result(result: Dict[str, Any]) -> str: + if result.get("success"): + return "success" + if result.get("session_expired"): + return "cookie_expired" + return "failed" + + +async def request_red_flower_once( + cookie_id: str, + order_id: str, + *, + batch_id: Optional[str] = None, + source: str = "manual", +) -> Dict[str, Any]: + """对单个订单执行一次求小红花,并写入日志。""" + cookie_id = str(cookie_id or "").strip() + order_id = str(order_id or "").strip() + batch_id = batch_id or f"{source}_{uuid.uuid4()}" + + order_info = db_manager.get_order_by_id(order_id) + if not order_info: + message = "订单不存在" + db_manager.add_scheduled_red_flower_log(batch_id, cookie_id, order_id=order_id, status="skipped", message=message) + return {"success": False, "message": message, "status": "skipped"} + + order_cookie_id = str(order_info.get("cookie_id") or "").strip() + if order_cookie_id and order_cookie_id != cookie_id: + message = "订单不属于当前账号" + db_manager.add_scheduled_red_flower_log( + batch_id, cookie_id, order_id=order_id, item_id=order_info.get("item_id"), + buyer_id=order_info.get("buyer_id"), buyer_nick=order_info.get("buyer_nick"), + status="skipped", message=message, + ) + return {"success": False, "message": message, "status": "skipped"} + + if order_info.get("is_red_flower"): + message = "订单已标记为已求小红花" + db_manager.add_scheduled_red_flower_log( + batch_id, cookie_id, order_id=order_id, item_id=order_info.get("item_id"), + buyer_id=order_info.get("buyer_id"), buyer_nick=order_info.get("buyer_nick"), + status="already_red_flower", message=message, + ) + return {"success": True, "message": message, "status": "already_red_flower", "already_red_flower": True} + + cookie_string = db_manager.get_cookie(cookie_id) + if not cookie_string: + message = "账号 Cookie 为空或不存在" + db_manager.add_scheduled_red_flower_log( + batch_id, cookie_id, order_id=order_id, item_id=order_info.get("item_id"), + buyer_id=order_info.get("buyer_id"), buyer_nick=order_info.get("buyer_nick"), + status="cookie_expired", message=message, + ) + return {"success": False, "message": message, "status": "cookie_expired"} + + service = RedFlowerService(cookie_string, account_id=cookie_id) + result = await service.request_red_flower(order_id) + status = _status_from_result(result) + message = str(result.get("message") or "") + + db_manager.add_scheduled_red_flower_log( + batch_id=batch_id, + cookie_id=cookie_id, + order_id=order_id, + item_id=order_info.get("item_id"), + buyer_id=order_info.get("buyer_id"), + buyer_nick=order_info.get("buyer_nick"), + status=status, + message=message, + raw_response=result.get("raw") or result, + ) + + if result.get("success"): + db_manager.mark_order_red_flower(order_id, True) + else: + db_manager.mark_order_red_flower(order_id, False, message) + + return { + "success": bool(result.get("success")), + "message": message, + "status": status, + "order_id": order_id, + "cookie_id": cookie_id, + "already_red_flower": bool(result.get("already_red_flower")), + } + + +async def run_auto_red_flower_batch( + *, + batch_limit: int = DEFAULT_BATCH_LIMIT, + lookback_days: int = DEFAULT_LOOKBACK_DAYS, + cooldown_minutes: int = DEFAULT_COOLDOWN_MINUTES, +) -> Dict[str, Any]: + """执行一轮自动求小红花。""" + batch_id = str(uuid.uuid4()) + started_at = time.time() + stats = { + "batch_id": batch_id, + "accounts": 0, + "orders": 0, + "success": 0, + "failed": 0, + "skipped": 0, + } + + all_cookies = db_manager.get_all_cookies() + for cookie_id in list(all_cookies.keys()): + try: + if not db_manager.get_auto_red_flower(cookie_id): + continue + stats["accounts"] += 1 + + orders = db_manager.get_pending_red_flower_orders( + cookie_id, + limit=batch_limit, + days=lookback_days, + cooldown_minutes=cooldown_minutes, + ) + if not orders: + continue + + logger.info(f"【{cookie_id}】自动求小红花找到 {len(orders)} 个待处理订单") + for order in orders: + stats["orders"] += 1 + result = await request_red_flower_once( + cookie_id, + order.get("order_id"), + batch_id=batch_id, + source="scheduled_red_flower", + ) + if result.get("success"): + stats["success"] += 1 + elif result.get("status") in {"skipped", "already_red_flower"}: + stats["skipped"] += 1 + else: + stats["failed"] += 1 + await asyncio.sleep(1) + except Exception as exc: + stats["failed"] += 1 + logger.error(f"【{cookie_id}】自动求小红花账号处理异常: {exc}") + + stats["duration_seconds"] = round(time.time() - started_at, 2) + if stats["orders"]: + logger.info(f"自动求小红花批次完成: {stats}") + return stats + + +async def auto_red_flower_task_loop(interval_seconds: int = DEFAULT_INTERVAL_SECONDS): + """后台自动求小红花循环。""" + interval_seconds = max(60, int(interval_seconds or DEFAULT_INTERVAL_SECONDS)) + logger.info(f"自动求小红花任务已启动,检查间隔 {interval_seconds} 秒") + while True: + try: + await run_auto_red_flower_batch() + except asyncio.CancelledError: + logger.info("自动求小红花任务已取消") + raise + except Exception as exc: + logger.error(f"自动求小红花任务异常: {exc}") + await asyncio.sleep(interval_seconds) diff --git a/db_manager.py b/db_manager.py index 4e88529..cf822e4 100644 --- a/db_manager.py +++ b/db_manager.py @@ -323,6 +323,7 @@ class DBManager: value TEXT NOT NULL, user_id INTEGER NOT NULL, auto_confirm INTEGER DEFAULT 1, + auto_red_flower INTEGER DEFAULT 0, remark TEXT DEFAULT '', status_note TEXT DEFAULT '', qr_login_grace_until INTEGER DEFAULT 0, @@ -471,6 +472,9 @@ class DBManager: is_rated INTEGER DEFAULT 0, rated_at TIMESTAMP, rate_error TEXT, + is_red_flower INTEGER DEFAULT 0, + red_flower_at TIMESTAMP, + red_flower_error TEXT, cookie_id TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -941,6 +945,7 @@ Cookie数量: {cookie_count} ('verification_email_api_url', '', '验证码邮件 API 地址(留空则仅使用 SMTP,不再向旧硬编码地址外发)'), ('qq_notification_api_url', '', 'QQ 私信通知 API 地址(留空则禁用 QQ 私信通知)'), ('auto_comment_api_url', '', '自动好评辅助 API 地址(留空则禁用此功能,避免 Cookie 外发)'), + ('auto_red_flower_interval_seconds', '300', '自动求小红花后台任务检查间隔秒数'), ('qq_reply_secret_key', 'xianyu_qq_reply_2024', 'QQ回复消息API秘钥') ''') @@ -1003,10 +1008,16 @@ Cookie数量: {cookie_count} cursor.execute("ALTER TABLE cookies ADD COLUMN auto_comment INTEGER DEFAULT 0") logger.info("数据库迁移完成:添加auto_comment列") + if 'auto_red_flower' not in cookie_columns: + logger.info("添加cookies表的auto_red_flower列...") + cursor.execute("ALTER TABLE cookies ADD COLUMN auto_red_flower INTEGER DEFAULT 0") + logger.info("数据库迁移完成:添加auto_red_flower列") + # 历史版本可能缺少订单平台时间字段,不能再依赖旧版本号分支触发 self._ensure_orders_platform_time_columns(cursor) self._ensure_orders_auto_comment_columns(cursor) self._ensure_scheduled_rate_logs_table(cursor) + self._ensure_scheduled_red_flower_logs_table(cursor) # 迁移notification_templates表以支持新的模板类型 self._migrate_notification_templates(cursor) @@ -1079,6 +1090,9 @@ Cookie数量: {cookie_count} "is_rated": "INTEGER DEFAULT 0", "rated_at": "TIMESTAMP", "rate_error": "TEXT", + "is_red_flower": "INTEGER DEFAULT 0", + "red_flower_at": "TIMESTAMP", + "red_flower_error": "TEXT", } for column_name, column_def in column_defs.items(): try: @@ -1087,6 +1101,7 @@ Cookie数量: {cookie_count} self._execute_sql(cursor, f"ALTER TABLE orders ADD COLUMN {column_name} {column_def}") logger.info(f"为orders表添加自动评价字段({column_name})") self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_orders_auto_comment ON orders(cookie_id, order_status, is_rated, updated_at)") + self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_orders_red_flower ON orders(cookie_id, order_status, is_red_flower, updated_at)") def _ensure_scheduled_rate_logs_table(self, cursor): """创建自动评价执行日志表。""" @@ -1111,6 +1126,28 @@ Cookie数量: {cookie_count} self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_rate_logs_batch ON scheduled_rate_logs(batch_id)") self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_rate_logs_order ON scheduled_rate_logs(order_id)") + def _ensure_scheduled_red_flower_logs_table(self, cursor): + """创建求小红花执行日志表。""" + self._execute_sql(cursor, ''' + CREATE TABLE IF NOT EXISTS scheduled_red_flower_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + batch_id TEXT NOT NULL, + cookie_id TEXT NOT NULL, + order_id TEXT, + item_id TEXT, + buyer_id TEXT, + buyer_nick TEXT, + status TEXT NOT NULL, + message TEXT, + raw_response TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE + ) + ''') + self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_red_flower_logs_cookie_time ON scheduled_red_flower_logs(cookie_id, created_at DESC)") + self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_red_flower_logs_batch ON scheduled_red_flower_logs(batch_id)") + self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_red_flower_logs_order ON scheduled_red_flower_logs(order_id)") + def _update_cards_table_constraints(self, cursor): """更新cards表的CHECK约束以支持image和yifan_api类型""" try: @@ -2498,6 +2535,39 @@ Cookie数量: {cookie_count} logger.error(f"获取自动确认发货设置失败: {e}") return True # 出错时默认开启 + # -------------------- 自动求小红花操作 -------------------- + def get_auto_red_flower(self, cookie_id: str) -> bool: + """获取Cookie的自动求小红花设置。""" + with self.lock: + try: + cursor = self.conn.cursor() + self._execute_sql(cursor, "SELECT auto_red_flower FROM cookies WHERE id = ?", (cookie_id,)) + result = cursor.fetchone() + if result and result[0] is not None: + return bool(result[0]) + return False + except Exception as e: + logger.error(f"获取自动求小红花设置失败: {e}") + return False + + def update_auto_red_flower(self, cookie_id: str, auto_red_flower: bool) -> bool: + """更新Cookie的自动求小红花设置。""" + with self.lock: + try: + cursor = self.conn.cursor() + self._execute_sql( + cursor, + "UPDATE cookies SET auto_red_flower = ? WHERE id = ?", + (int(auto_red_flower), cookie_id) + ) + self.conn.commit() + logger.info(f"更新账号 {cookie_id} 自动求小红花设置: {'开启' if auto_red_flower else '关闭'}") + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"更新自动求小红花设置失败: {e}") + self.conn.rollback() + return False + # -------------------- 自动好评操作 -------------------- def get_auto_comment(self, cookie_id: str) -> bool: """获取Cookie的自动好评设置""" @@ -7049,7 +7119,8 @@ Cookie数量: {cookie_count} SELECT order_id, item_id, buyer_id, buyer_nick, sid, spec_name, spec_value, spec_name_2, spec_value_2, quantity, amount, bargain_flow_detected, bargain_success_detected, order_status, pre_refund_status, cookie_id, platform_created_at, platform_paid_at, - platform_completed_at, is_rated, rated_at, rate_error, created_at, updated_at + platform_completed_at, is_rated, rated_at, rate_error, + is_red_flower, red_flower_at, red_flower_error, created_at, updated_at FROM orders WHERE order_id = ? ''', (order_id,)) @@ -7078,8 +7149,11 @@ Cookie数量: {cookie_count} 'is_rated': bool(row[19]), 'rated_at': row[20], 'rate_error': row[21], - 'created_at': row[22], - 'updated_at': row[23] + 'is_red_flower': bool(row[22]), + 'red_flower_at': row[23], + 'red_flower_error': row[24], + 'created_at': row[25], + 'updated_at': row[26] } return None @@ -7141,7 +7215,8 @@ Cookie数量: {cookie_count} SELECT order_id, item_id, buyer_id, buyer_nick, sid, spec_name, spec_value, spec_name_2, spec_value_2, quantity, amount, order_status, platform_created_at, platform_paid_at, platform_completed_at, - is_rated, rated_at, rate_error, created_at, updated_at + is_rated, rated_at, rate_error, + is_red_flower, red_flower_at, red_flower_error, created_at, updated_at FROM orders WHERE cookie_id = ? ORDER BY created_at DESC LIMIT ? ''', (cookie_id, limit)) @@ -7170,8 +7245,11 @@ Cookie数量: {cookie_count} 'is_rated': bool(row[15]), 'rated_at': row[16], 'rate_error': row[17], - 'created_at': row[18], - 'updated_at': row[19] + 'is_red_flower': bool(row[18]), + 'red_flower_at': row[19], + 'red_flower_error': row[20], + 'created_at': row[21], + 'updated_at': row[22] }) return orders @@ -7204,6 +7282,30 @@ Cookie数量: {cookie_count} self.conn.rollback() return False + def mark_order_red_flower(self, order_id: str, is_red_flower: bool = True, error_message: str = None) -> bool: + """更新订单求小红花状态。""" + with self.lock: + try: + cursor = self.conn.cursor() + if is_red_flower: + cursor.execute(''' + UPDATE orders + SET is_red_flower = 1, red_flower_at = CURRENT_TIMESTAMP, red_flower_error = NULL, updated_at = CURRENT_TIMESTAMP + WHERE order_id = ? + ''', (order_id,)) + else: + cursor.execute(''' + UPDATE orders + SET red_flower_error = ?, updated_at = CURRENT_TIMESTAMP + WHERE order_id = ? + ''', (str(error_message or '')[:1000], order_id)) + self.conn.commit() + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"更新订单求小红花状态失败: order_id={order_id}, error={e}") + self.conn.rollback() + return False + def add_scheduled_rate_log(self, batch_id: str, cookie_id: str, order_id: str = None, item_id: str = None, buyer_id: str = None, buyer_nick: str = None, comment: str = None, status: str = 'failed', message: str = None, @@ -7330,6 +7432,131 @@ Cookie数量: {cookie_count} logger.error(f"查询待自动评价订单失败: cookie_id={cookie_id}, error={e}") return [] + def add_scheduled_red_flower_log(self, batch_id: str, cookie_id: str, order_id: str = None, + item_id: str = None, buyer_id: str = None, buyer_nick: str = None, + status: str = 'failed', message: str = None, + raw_response: Any = None) -> Optional[int]: + """写入求小红花执行日志。""" + with self.lock: + try: + cursor = self.conn.cursor() + if raw_response is None: + raw_text = None + elif isinstance(raw_response, str): + raw_text = raw_response + else: + raw_text = json.dumps(raw_response, ensure_ascii=False, default=str) + cursor.execute(''' + INSERT INTO scheduled_red_flower_logs ( + batch_id, cookie_id, order_id, item_id, buyer_id, buyer_nick, + status, message, raw_response + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + batch_id, cookie_id, order_id, item_id, buyer_id, buyer_nick, + status, message, raw_text + )) + log_id = cursor.lastrowid + self.conn.commit() + return log_id + except Exception as e: + logger.error(f"写入求小红花日志失败: {e}") + self.conn.rollback() + return None + + def get_scheduled_red_flower_logs(self, user_id: int = None, cookie_id: str = None, + limit: int = 100, offset: int = 0) -> List[Dict]: + """查询求小红花日志。""" + with self.lock: + try: + cursor = self.conn.cursor() + conditions = [] + params = [] + if user_id is not None: + conditions.append("c.user_id = ?") + params.append(user_id) + if cookie_id: + conditions.append("l.cookie_id = ?") + params.append(cookie_id) + where_sql = f"WHERE {' AND '.join(conditions)}" if conditions else "" + params.extend([max(1, min(int(limit or 100), 500)), max(0, int(offset or 0))]) + cursor.execute(f''' + SELECT l.id, l.batch_id, l.cookie_id, l.order_id, l.item_id, l.buyer_id, + l.buyer_nick, l.status, l.message, l.raw_response, l.created_at + FROM scheduled_red_flower_logs l + LEFT JOIN cookies c ON c.id = l.cookie_id + {where_sql} + ORDER BY l.created_at DESC, l.id DESC + LIMIT ? OFFSET ? + ''', params) + logs = [] + for row in cursor.fetchall(): + logs.append({ + 'id': row[0], + 'batch_id': row[1], + 'cookie_id': row[2], + 'order_id': row[3], + 'item_id': row[4], + 'buyer_id': row[5], + 'buyer_nick': row[6], + 'status': row[7], + 'message': row[8], + 'raw_response': row[9], + 'created_at': row[10], + }) + return logs + except Exception as e: + logger.error(f"查询求小红花日志失败: {e}") + return [] + + def get_pending_red_flower_orders(self, cookie_id: str, limit: int = 5, + days: int = 10, cooldown_minutes: int = 30) -> List[Dict]: + """获取待自动求小红花订单。""" + with self.lock: + try: + cursor = self.conn.cursor() + cursor.execute(''' + SELECT o.order_id, o.item_id, o.buyer_id, o.buyer_nick, o.sid, + o.order_status, o.cookie_id, o.platform_completed_at, o.created_at, o.updated_at, + o.is_red_flower, o.red_flower_at, o.red_flower_error + FROM orders o + WHERE o.cookie_id = ? + AND o.order_status NOT IN ('cancelled', 'processing', 'pending_payment') + AND COALESCE(o.is_red_flower, 0) = 0 + AND datetime(COALESCE(o.platform_created_at, o.platform_paid_at, o.created_at)) >= datetime('now', ?) + AND NOT EXISTS ( + SELECT 1 FROM scheduled_red_flower_logs l + WHERE l.order_id = o.order_id + AND l.status IN ('failed', 'cookie_expired') + AND datetime(l.created_at) >= datetime('now', ?) + ) + ORDER BY datetime(COALESCE(o.platform_created_at, o.platform_paid_at, o.created_at)) ASC + LIMIT ? + ''', (cookie_id, f'-{max(1, int(days or 10))} days', f'-{max(1, int(cooldown_minutes or 30))} minutes', max(1, min(int(limit or 5), 50)))) + orders = [] + for row in cursor.fetchall(): + buyer_nick = self._sanitize_order_buyer_nick(row[3]) + if not buyer_nick: + buyer_nick = self._lookup_buyer_nick_from_chat_messages(cookie_id, row[4], row[2]) + orders.append({ + 'order_id': row[0], + 'item_id': row[1], + 'buyer_id': row[2], + 'buyer_nick': buyer_nick, + 'sid': row[4], + 'order_status': row[5], + 'cookie_id': row[6], + 'platform_completed_at': row[7], + 'created_at': row[8], + 'updated_at': row[9], + 'is_red_flower': bool(row[10]), + 'red_flower_at': row[11], + 'red_flower_error': row[12], + }) + return orders + except Exception as e: + logger.error(f"查询待求小红花订单失败: cookie_id={cookie_id}, error={e}") + return [] + def delete_order(self, order_id: str, cookie_id: str = None) -> bool: """删除订单,可选限定所属账号。""" with self.lock: diff --git a/reply_server.py b/reply_server.py index 284d98a..15fe1c0 100644 --- a/reply_server.py +++ b/reply_server.py @@ -1101,6 +1101,12 @@ async def start_scheduled_task_checker(): logger.info("自动补评价任务已启动") except Exception as exc: logger.error(f"自动补评价任务启动失败: {exc}") + try: + from auto_red_flower_task import auto_red_flower_task_loop + asyncio.create_task(auto_red_flower_task_loop()) + logger.info("自动求小红花任务已启动") + except Exception as exc: + logger.error(f"自动求小红花任务启动失败: {exc}") # 添加请求日志中间件 @@ -3558,6 +3564,7 @@ def get_cookies_details(current_user: Dict[str, Any] = Depends(get_current_user) cookie_enabled = cookie_manager.manager.get_cookie_status(cookie_id) auto_confirm = db_manager.get_auto_confirm(cookie_id) auto_comment = db_manager.get_auto_comment(cookie_id) + auto_red_flower = db_manager.get_auto_red_flower(cookie_id) # 获取备注信息 cookie_details = db_manager.get_cookie_details(cookie_id) remark = cookie_details.get('remark', '') if cookie_details else '' @@ -3572,6 +3579,7 @@ def get_cookies_details(current_user: Dict[str, Any] = Depends(get_current_user) 'enabled': cookie_enabled, 'auto_confirm': auto_confirm, 'auto_comment': auto_comment, + 'auto_red_flower': auto_red_flower, 'remark': remark, 'status_note': status_note, 'username': username, @@ -7393,11 +7401,19 @@ class AutoCommentUpdate(BaseModel): auto_comment: bool +class AutoRedFlowerUpdate(BaseModel): + auto_red_flower: bool + + class AutoCommentOrderRequest(BaseModel): cookie_id: Optional[str] = None comment: Optional[str] = None +class RedFlowerOrderRequest(BaseModel): + cookie_id: Optional[str] = None + + class CommentTemplateCreate(BaseModel): name: str content: str @@ -7478,6 +7494,56 @@ def get_auto_confirm(cid: str, current_user: Dict[str, Any] = Depends(get_curren raise HTTPException(status_code=500, detail=str(e)) +# ==================== 自动求小红花相关API ==================== + +@app.put("/cookies/{cid}/auto-red-flower") +def update_auto_red_flower(cid: str, update_data: AutoRedFlowerUpdate, current_user: Dict[str, Any] = Depends(get_current_user)): + """更新账号的自动求小红花设置""" + if cookie_manager.manager is None: + raise HTTPException(status_code=500, detail="CookieManager 未就绪") + try: + user_id = current_user['user_id'] + user_cookies = db_manager.get_all_cookies(user_id) + if cid not in user_cookies: + raise HTTPException(status_code=403, detail="无权限操作该Cookie") + + success = db_manager.update_auto_red_flower(cid, update_data.auto_red_flower) + if not success: + raise HTTPException(status_code=500, detail="更新自动求小红花设置失败") + + return { + "msg": "success", + "auto_red_flower": update_data.auto_red_flower, + "message": f"自动求小红花已{'开启' if update_data.auto_red_flower else '关闭'}" + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/cookies/{cid}/auto-red-flower") +def get_auto_red_flower(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)): + """获取账号的自动求小红花设置""" + if cookie_manager.manager is None: + raise HTTPException(status_code=500, detail="CookieManager 未就绪") + try: + user_id = current_user['user_id'] + user_cookies = db_manager.get_all_cookies(user_id) + if cid not in user_cookies: + raise HTTPException(status_code=403, detail="无权限操作该Cookie") + + auto_red_flower = db_manager.get_auto_red_flower(cid) + return { + "auto_red_flower": auto_red_flower, + "message": f"自动求小红花当前{'开启' if auto_red_flower else '关闭'}" + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + # ==================== 自动好评相关API ==================== @app.put("/cookies/{cid}/auto-comment") @@ -11562,6 +11628,7 @@ def get_user_orders(current_user: Dict[str, Any] = Depends(get_current_user)): raise HTTPException(status_code=500, detail=f"查询订单失败: {str(e)}") + @app.get('/api/auto-comment/logs') def get_auto_comment_logs( cookie_id: str = None, @@ -11673,6 +11740,111 @@ async def run_auto_comment_once( raise HTTPException(status_code=500, detail=f"手动触发自动补评价失败: {str(e)}") +@app.get('/api/auto-red-flower/logs') +def get_auto_red_flower_logs( + cookie_id: str = None, + limit: int = 100, + offset: int = 0, + current_user: Dict[str, Any] = Depends(get_current_user), +): + """查询求小红花执行日志。""" + try: + if cookie_id: + cookie_id = _ensure_cookie_access(cookie_id, current_user) + logs = db_manager.get_scheduled_red_flower_logs( + user_id=current_user['user_id'], + cookie_id=cookie_id, + limit=limit, + offset=offset, + ) + return {"success": True, "data": logs} + except HTTPException: + raise + except Exception as e: + log_with_user('error', f"查询求小红花日志失败: {str(e)}", current_user) + raise HTTPException(status_code=500, detail=f"查询求小红花日志失败: {str(e)}") + + +@app.post('/api/orders/{order_id}/red-flower') +async def request_order_red_flower_once( + order_id: str, + request: RedFlowerOrderRequest = RedFlowerOrderRequest(), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """手动对指定订单执行一次求小红花。""" + try: + order = db_manager.get_order_by_id(order_id) + if not order: + raise HTTPException(status_code=404, detail='订单不存在') + + cookie_id = str(request.cookie_id or order.get('cookie_id') or '').strip() + cookie_id = _ensure_cookie_access(cookie_id, current_user) + if order.get('cookie_id') and order.get('cookie_id') != cookie_id: + raise HTTPException(status_code=403, detail='订单不属于该账号') + + from auto_red_flower_task import request_red_flower_once + + result = await request_red_flower_once( + cookie_id=cookie_id, + order_id=order_id, + batch_id=f"manual_red_flower_{uuid.uuid4()}", + source='manual', + ) + log_with_user('info', f"手动求小红花: order_id={order_id}, cookie_id={cookie_id}, result={result}", current_user) + return {"success": bool(result.get('success')), "data": result, "message": result.get('message')} + except HTTPException: + raise + except Exception as e: + log_with_user('error', f"手动求小红花失败: order_id={order_id}, error={str(e)}", current_user) + raise HTTPException(status_code=500, detail=f"手动求小红花失败: {str(e)}") + + +@app.post('/api/auto-red-flower/run-once') +async def run_auto_red_flower_once( + request: RedFlowerOrderRequest = RedFlowerOrderRequest(), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """手动触发一轮当前用户范围内的自动求小红花。""" + try: + from auto_red_flower_task import request_red_flower_once + + user_cookies = db_manager.get_all_cookies(current_user['user_id']) + target_cookie_ids = [request.cookie_id] if request.cookie_id else list(user_cookies.keys()) + batch_id = f"manual_red_flower_batch_{uuid.uuid4()}" + results = [] + stats = {"batch_id": batch_id, "accounts": 0, "orders": 0, "success": 0, "failed": 0, "skipped": 0} + + for raw_cookie_id in target_cookie_ids: + cookie_id = _ensure_cookie_access(raw_cookie_id, current_user) + if not db_manager.get_auto_red_flower(cookie_id): + continue + stats['accounts'] += 1 + orders = db_manager.get_pending_red_flower_orders(cookie_id, limit=5, days=10, cooldown_minutes=0) + for order in orders: + stats['orders'] += 1 + result = await request_red_flower_once( + cookie_id=cookie_id, + order_id=order.get('order_id'), + batch_id=batch_id, + source='manual_batch', + ) + results.append(result) + if result.get('success'): + stats['success'] += 1 + elif result.get('status') in {'skipped', 'already_red_flower'}: + stats['skipped'] += 1 + else: + stats['failed'] += 1 + await asyncio.sleep(1) + + return {"success": True, "data": {"stats": stats, "results": results}} + except HTTPException: + raise + except Exception as e: + log_with_user('error', f"手动触发自动求小红花失败: {str(e)}", current_user) + raise HTTPException(status_code=500, detail=f"手动触发自动求小红花失败: {str(e)}") + + @app.get('/api/orders/stream') def stream_user_orders(current_user: Dict[str, Any] = Depends(get_current_user)): """订单实时事件流,仅在订单页激活时使用。""" diff --git a/static/css/accounts.css b/static/css/accounts.css index 3324883..5895df4 100644 --- a/static/css/accounts.css +++ b/static/css/accounts.css @@ -71,6 +71,30 @@ input:checked + .status-slider:before { border: 1px solid #fecaca; } +.account-status-cell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.35rem; + min-width: 92px; +} + +.account-status-main { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + flex-wrap: nowrap; + white-space: nowrap; +} + +.account-status-cell .account-status-note-badge { + max-width: 100%; + justify-content: center; + text-align: center; +} + .account-status-note-badge { display: inline-flex; align-items: center; @@ -89,6 +113,14 @@ input:checked + .status-slider:before { font-size: 0.8rem; } +#cookieTable thead th { + white-space: nowrap; +} + +#cookieTable thead th:nth-child(5) { + min-width: 4.5rem; +} + .account-row.disabled { opacity: 0.6; background-color: #f9fafb; @@ -98,6 +130,107 @@ input:checked + .status-slider:before { background-color: #f3f4f6; } +.account-actions-cell { + min-width: 340px; +} + +.account-actions-toolbar { + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + gap: 0.35rem; + width: 100%; + overflow-x: auto; + overflow-y: hidden; + padding: 0.1rem 0; + scrollbar-width: thin; +} + +.account-actions-toolbar::-webkit-scrollbar { + height: 4px; +} + +.account-action-group { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 0.18rem; + padding: 0.18rem; + border: 1px solid #e5e7eb; + border-radius: 0.65rem; + background: #f8fafc; +} + +.account-action-group-label, +.account-action-btn .action-text { + display: none; +} + +.account-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + min-width: 28px; + height: 28px; + border-radius: 0.5rem !important; + margin-left: 0 !important; + padding: 0; + font-size: 0.78rem; + line-height: 1; + white-space: nowrap; +} + +.account-action-btn .bi { + font-size: 0.95rem; +} + +.account-action-group-danger { + margin-left: auto; +} + +.account-action-delete { + min-width: 28px; +} + +.account-action-group-basic { + background: #f8fafc; + border-color: #e2e8f0; +} + +.account-action-group-reply { + background: #f0fdf4; + border-color: #bbf7d0; +} + +.account-action-group-item { + background: #eff6ff; + border-color: #bfdbfe; +} + +.account-action-group-flower { + background: #fff7ed; + border-color: #fed7aa; +} + +.account-action-group-danger { + background: #fef2f2; + border-color: #fecaca; +} + +.account-actions-group { + display: flex !important; + flex-wrap: wrap; + justify-content: center; + gap: 0.25rem; +} + +.account-actions-group > .btn { + border-radius: 0.375rem !important; + margin-left: 0 !important; +} + /* 关键词管理界面的状态提示 */ .account-badge .badge.bg-warning { animation: pulse 2s infinite; @@ -1237,6 +1370,41 @@ input:checked + .status-slider:before { border-color: rgba(245, 158, 11, 0.32); } +[data-theme="dark"] .account-action-group { + background: rgba(15, 23, 42, 0.72); + border-color: rgba(148, 163, 184, 0.22); +} + +[data-theme="dark"] .account-action-group-label { + background: rgba(51, 65, 85, 0.9); + color: #cbd5e1; +} + +[data-theme="dark"] .account-action-group-basic { + background: rgba(30, 41, 59, 0.55); + border-color: rgba(148, 163, 184, 0.22); +} + +[data-theme="dark"] .account-action-group-reply { + background: rgba(6, 78, 59, 0.24); + border-color: rgba(16, 185, 129, 0.24); +} + +[data-theme="dark"] .account-action-group-item { + background: rgba(30, 64, 175, 0.2); + border-color: rgba(59, 130, 246, 0.24); +} + +[data-theme="dark"] .account-action-group-flower { + background: rgba(146, 64, 14, 0.2); + border-color: rgba(251, 146, 60, 0.24); +} + +[data-theme="dark"] .account-action-group-danger { + background: rgba(127, 29, 29, 0.2); + border-color: rgba(248, 113, 113, 0.24); +} + /* 禁用账号提示 */ [data-theme="dark"] .disabled-account-notice { background: rgba(245, 158, 11, 0.15); diff --git a/static/index.html b/static/index.html index c41eef9..1c287ae 100644 --- a/static/index.html +++ b/static/index.html @@ -642,23 +642,23 @@ - - - - - - - + + + + + + + - - + - + diff --git a/static/js/app.js b/static/js/app.js index eaa26da..49ff11c 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -4180,6 +4180,9 @@ async function loadCookies() { // 自动好评状态(默认关闭) const autoComment = cookie.auto_comment === undefined ? false : cookie.auto_comment; + // 自动求小红花状态(默认关闭) + const autoRedFlower = cookie.auto_red_flower === undefined ? false : cookie.auto_red_flower; + tr.innerHTML = ` @@ -4253,30 +4258,49 @@ async function loadCookies() { - `; @@ -4727,7 +4751,7 @@ function cancelCookieEdit(id) { cookieValueCell.innerHTML = window.editingCookieData.originalContent; // 恢复按钮状态 - const actionButtons = row.querySelectorAll('.btn-group button'); + const actionButtons = row.querySelectorAll('.account-actions-toolbar button, .btn-group button'); actionButtons.forEach(btn => btn.disabled = false); // 清理全局数据 @@ -4816,7 +4840,7 @@ function updateAccountRowStatus(accountId, enabled, statusNote = '') { const row = toggle.closest('tr'); const statusBadge = row.querySelector('.status-badge'); const statusCell = row.querySelector('.account-status-cell'); - const actionButtons = row.querySelectorAll('.btn-group .btn:not(.btn-outline-info):not(.btn-outline-danger)'); + const actionButtons = row.querySelectorAll('.account-actions-toolbar .btn[data-requires-enabled="true"], .btn-group .btn:not(.btn-outline-info):not(.btn-outline-danger)'); // 更新行样式 row.className = `account-row ${enabled ? 'enabled' : 'disabled'}`; @@ -4837,17 +4861,22 @@ function updateAccountRowStatus(accountId, enabled, statusNote = '') { statusCell.insertAdjacentHTML('beforeend', renderedStatusNote); } - // 更新按钮状态(只禁用编辑Cookie按钮,其他按钮保持可用) + // 更新依赖账号启用状态的按钮;自动回复规则入口始终可用 actionButtons.forEach(btn => { - if (btn.onclick && btn.onclick.toString().includes('editCookieInline')) { + if (btn.dataset.requiresEnabled === 'true') { btn.disabled = !enabled; } - // 设置自动回复按钮始终可用,但更新提示文本 if (btn.onclick && btn.onclick.toString().includes('goToAutoReply')) { btn.title = enabled ? '设置自动回复' : '配置关键词 (账号已禁用)'; } }); + const redFlowerRunButton = row.querySelector('[data-red-flower-run]'); + if (redFlowerRunButton) { + const redFlowerEnabled = redFlowerRunButton.dataset.redFlowerActive === 'true'; + redFlowerRunButton.disabled = !enabled || !redFlowerEnabled; + } + // 更新切换按钮的提示 const label = toggle.closest('.status-toggle'); label.title = enabled ? '点击禁用' : '点击启用'; @@ -4987,6 +5016,76 @@ function updateAutoCommentRowStatus(accountId, enabled) { } } +// 切换自动求小红花状态 +async function toggleAutoRedFlower(accountId, enabled) { + try { + toggleLoading(true); + + const response = await fetch(`${apiBase}/cookies/${accountId}/auto-red-flower`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ auto_red_flower: enabled }) + }); + + if (response.ok) { + const result = await response.json(); + showToast(result.message || (enabled ? '已开启自动求小红花' : '已关闭自动求小红花'), 'success'); + await loadCookies(); + } else { + const error = await response.json().catch(() => ({})); + showToast(error.detail || '更新自动求小红花设置失败', 'error'); + } + } catch (error) { + console.error('切换自动求小红花状态失败:', error); + showToast('网络错误,请稍后重试', 'error'); + } finally { + toggleLoading(false); + } +} + +// 立即执行当前账号的求小红花补偿 +async function runAutoRedFlowerForAccount(accountId) { + if (!accountId) { + showToast('缺少账号ID', 'warning'); + return; + } + + const confirmed = confirm(`确定要立即为账号「${accountId}」执行一轮求小红花吗?`); + if (!confirmed) return; + + toggleLoading(true); + showToast('正在执行求小红花,请稍候...', 'info'); + try { + const response = await fetch(`${apiBase}/api/auto-red-flower/run-once`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ cookie_id: accountId }) + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.success === false) { + showToast(data.detail || data.message || '求小红花执行失败', 'danger'); + return; + } + const stats = data?.data?.stats || {}; + showToast( + `求小红花完成:处理 ${stats.orders || 0} 单,成功 ${stats.success || 0},失败 ${stats.failed || 0},跳过 ${stats.skipped || 0}`, + (stats.failed || 0) > 0 ? 'warning' : 'success' + ); + await loadCookies(); + } catch (error) { + console.error('执行求小红花失败:', error); + showToast(`求小红花请求异常: ${error.message}`, 'danger'); + } finally { + toggleLoading(false); + } +} + // 当前编辑的好评模板账号ID let currentCommentTemplateAccountId = null; diff --git a/utils/red_flower_service.py b/utils/red_flower_service.py new file mode 100644 index 0000000..33c2b13 --- /dev/null +++ b/utils/red_flower_service.py @@ -0,0 +1,216 @@ +"""闲鱼求小红花服务。 + +参考上游 red_flower_task 的核心请求逻辑,适配当前单体项目: +- 调用 mtop.taobao.idlemessage.red.flower; +- 令牌过期时合并响应 Set-Cookie、保存 Cookie 后重试一次; +- 返回统一结果供实时接口、手动接口和后台补偿任务复用。 +""" + +from __future__ import annotations + +import asyncio +import hashlib +import json +import time +from typing import Any, Dict, Tuple + +import aiohttp +from loguru import logger + + +APP_KEY = "34839810" +RED_FLOWER_API_URL = "https://h5api.m.goofish.com/h5/mtop.taobao.idlemessage.red.flower/1.0/" +RED_FLOWER_API_NAME = "mtop.taobao.idlemessage.red.flower" + + +class RedFlowerService: + """闲鱼求小红花服务。""" + + def __init__(self, cookie_string: str, account_id: str | None = None): + self.cookie_string = str(cookie_string or "").strip() + self.account_id = account_id + self.cookies_dict = self._parse_cookies(self.cookie_string) + + @staticmethod + def _parse_cookies(cookies_str: str) -> Dict[str, str]: + cookies: Dict[str, str] = {} + for part in str(cookies_str or "").replace("\ufeff", "").split(";"): + part = part.strip() + if not part or "=" not in part: + continue + key, value = part.split("=", 1) + key = key.strip() + if key: + cookies[key] = value.strip() + return cookies + + @staticmethod + def _cookie_dict_to_string(cookies: Dict[str, str]) -> str: + return "; ".join(f"{key}={value}" for key, value in cookies.items()) + + @staticmethod + def _generate_sign(t: str, token: str, data: str) -> str: + msg = f"{token}&{t}&{APP_KEY}&{data}" + md5_hash = hashlib.md5() + md5_hash.update(msg.encode("utf-8")) + return md5_hash.hexdigest() + + @staticmethod + def _ret_to_text(ret: Any) -> str: + if isinstance(ret, list): + return "; ".join(str(item) for item in ret) + return str(ret or "") + + @classmethod + def _is_token_expired(cls, ret: Any) -> bool: + ret_text = cls._ret_to_text(ret) + return "FAIL_SYS_TOKEN_EXOIRED" in ret_text or "令牌过期" in ret_text + + @classmethod + def _is_session_expired(cls, ret: Any) -> bool: + ret_text = cls._ret_to_text(ret) + return "FAIL_SYS_SESSION_EXPIRED" in ret_text or "Session过期" in ret_text + + @classmethod + def _is_already_requested(cls, ret: Any, result: Any) -> bool: + text = cls._ret_to_text(ret) + " " + json.dumps(result, ensure_ascii=False, default=str) + return any(keyword in text for keyword in ( + "已送出小红花", "已收下", "已求过", "已赠送", "已经送", "重复", "不能重复", + )) + + @staticmethod + def _extract_set_cookies(response: aiohttp.ClientResponse) -> Dict[str, str]: + new_cookies: Dict[str, str] = {} + try: + for cookie_header in response.headers.getall("set-cookie", []): + first_part = cookie_header.split(";", 1)[0] + if "=" not in first_part: + continue + name, value = first_part.split("=", 1) + name = name.strip() + if name: + new_cookies[name] = value.strip() + except Exception as exc: + logger.warning(f"提取求小红花接口 Set-Cookie 失败: {exc}") + return new_cookies + + def _merge_response_cookies(self, response: aiohttp.ClientResponse) -> Tuple[bool, str]: + new_cookies = self._extract_set_cookies(response) + if not new_cookies: + return False, self.cookie_string + merged = dict(self.cookies_dict) + merged.update(new_cookies) + merged_string = self._cookie_dict_to_string(merged) + logger.info( + f"【{self.account_id or '未知账号'}】求小红花接口返回新 Cookie,已合并 {len(new_cookies)} 个字段" + ) + return True, merged_string + + async def _persist_cookie_if_needed(self, new_cookie_string: str) -> None: + if not self.account_id or not new_cookie_string or new_cookie_string == self.cookie_string: + return + try: + from db_manager import db_manager + + db_manager.save_cookie(self.account_id, new_cookie_string) + logger.info(f"【{self.account_id}】求小红花接口刷新后的 Cookie 已保存到数据库") + except Exception as exc: + logger.warning(f"【{self.account_id}】保存求小红花刷新 Cookie 失败: {exc}") + + async def request_red_flower(self, order_id: str, is_retry: bool = False) -> Dict[str, Any]: + """对指定订单发送求小红花请求。""" + order_id = str(order_id or "").strip() + if not order_id: + return {"success": False, "message": "缺少订单号"} + if not self.cookie_string: + return {"success": False, "message": "账号 Cookie 为空"} + + m_h5_tk = self.cookies_dict.get("_m_h5_tk", "") + token = m_h5_tk.split("_", 1)[0] if m_h5_tk else "" + if not token: + return {"success": False, "message": "Cookie 中缺少 _m_h5_tk,无法生成求小红花签名"} + + timestamp = str(int(time.time() * 1000)) + data_obj = { + "orderId": order_id, + "channel": "list", + } + data_val = json.dumps(data_obj, separators=(",", ":"), ensure_ascii=False) + sign = self._generate_sign(timestamp, token, data_val) + + params = { + "jsv": "2.7.2", + "appKey": APP_KEY, + "t": timestamp, + "sign": sign, + "v": "4.0", + "type": "originaljson", + "accountSite": "xianyu", + "dataType": "json", + "timeout": "20000", + "api": RED_FLOWER_API_NAME, + "sessionOption": "AutoLoginOnly", + } + headers = { + "accept": "application/json", + "content-type": "application/x-www-form-urlencoded", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "referer": "https://www.goofish.com/", + "origin": "https://www.goofish.com", + "cookie": self.cookie_string, + } + + try: + timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.post(RED_FLOWER_API_URL, params=params, headers=headers, data={"data": data_val}) as response: + try: + result = await response.json(content_type=None) + except Exception: + body = await response.text() + return { + "success": False, + "message": f"求小红花接口返回非 JSON: HTTP {response.status}", + "raw": body[:1000], + } + + # 无论是否成功,先合并响应中的新 Cookie,供后续请求使用。 + has_cookie_update, merged_cookie_string = self._merge_response_cookies(response) + if has_cookie_update: + await self._persist_cookie_if_needed(merged_cookie_string) + self.cookie_string = merged_cookie_string + self.cookies_dict = self._parse_cookies(merged_cookie_string) + + ret = result.get("ret", []) if isinstance(result, dict) else [] + ret_text = self._ret_to_text(ret) or str(result) + retry_tag = "[令牌过期重试] " if is_retry else "" + + if ret_text == "SUCCESS::调用成功" or "SUCCESS" in ret_text: + logger.info( + f"【{self.account_id or '未知账号'}】{retry_tag}求小红花成功: order_id={order_id}" + ) + return {"success": True, "message": "求小红花成功", "raw": result} + + if self._is_already_requested(ret, result): + logger.info( + f"【{self.account_id or '未知账号'}】订单已求过小红花,按成功处理: order_id={order_id}, ret={ret}" + ) + return {"success": True, "already_red_flower": True, "message": "订单已求过小红花", "raw": result} + + if not is_retry and self._is_token_expired(ret): + if has_cookie_update: + return await self.request_red_flower(order_id, is_retry=True) + return {"success": False, "message": "令牌过期且响应未返回新 Cookie", "raw": result} + + if self._is_session_expired(ret): + return {"success": False, "session_expired": True, "message": ret_text, "raw": result} + + logger.warning( + f"【{self.account_id or '未知账号'}】{retry_tag}求小红花失败: order_id={order_id}, ret={ret}" + ) + return {"success": False, "message": ret_text, "raw": result} + except asyncio.TimeoutError: + return {"success": False, "message": "求小红花接口请求超时"} + except Exception as exc: + logger.error(f"【{self.account_id or '未知账号'}】求小红花异常: order_id={order_id}, error={exc}") + return {"success": False, "message": str(exc)}
账号IDCookie值关键词状态默认回复AI回复自动确认发货账号IDCookie值关键词状态默认回复AI回复自动确认发货 自动好评备注 + 备注 暂停时间 操作操作
- -
- - - - - - - - +