From 2d075eaa335ea349f17de105c809472e28f7feb9 Mon Sep 17 00:00:00 2001 From: GuDong <1735590698@qq.com> Date: Mon, 18 May 2026 17:48:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B9=B0=E5=AE=B6?= =?UTF-8?q?=E6=98=B5=E7=A7=B0=E6=B1=A1=E6=9F=93=E4=B8=8E=E5=81=9C=E6=AD=A2?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XianyuAutoAsync.py | 124 ++++++++++++++++++++++++++++++++++++++++----- db_manager.py | 107 +++++++++++++++++++++++++++++++++++--- stop.sh | 100 +++++++++++++++++++++++++++++++----- 3 files changed, 297 insertions(+), 34 deletions(-) diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 5ce1db4..3d54a43 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -2333,6 +2333,8 @@ class XianyuLive: "全部", "交易消息", "等待你发货", + "买家", + "工作台通知", "你人真不错,送你闲鱼小红花", "卖家人不错?送Ta闲鱼小红花", "快给ta一个评价吧~", @@ -4607,7 +4609,8 @@ class XianyuLive: send_user_id=user_id, item_id=item_id, error_message=finalize_result.get('error') or '检测到已发送记录,但补完成发货收尾失败', - chat_id=chat_id + chat_id=chat_id, + order_id=order_id ) return @@ -4640,7 +4643,8 @@ class XianyuLive: send_user_id=user_id, item_id=item_id, error_message="发货成功", - chat_id=chat_id + chat_id=chat_id, + order_id=order_id ) logger.info(f'[{msg_time}] 【{self.cookie_id}】[{msg_id}] ✅ 简化消息自动发货补完成收尾成功') return @@ -4741,7 +4745,8 @@ class XianyuLive: send_user_id=user_id, item_id=item_id, error_message=finalize_result.get('error') or '发送成功但提交发货副作用失败', - chat_id=chat_id + chat_id=chat_id, + order_id=order_id ) return @@ -4789,7 +4794,8 @@ class XianyuLive: send_user_id=user_id, item_id=item_id, error_message="发货成功", - chat_id=chat_id + chat_id=chat_id, + order_id=order_id ) logger.info(f'[{msg_time}] 【{self.cookie_id}】[{msg_id}] ✅ 简化消息自动发货完成') @@ -4809,7 +4815,8 @@ class XianyuLive: send_user_id=user_id, item_id=item_id, error_message="未找到匹配的发货规则或获取发货内容失败", - chat_id=chat_id + chat_id=chat_id, + order_id=order_id ) except Exception as e: @@ -5511,9 +5518,9 @@ class XianyuLive: ) else: notify_message = f"多数量发货成功,共完成 {finalized_count}/{quantity_to_send} 个卡券" - await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, notify_message, chat_id) + await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, notify_message, chat_id, order_id=order_id) else: - await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, "发货成功", chat_id) + await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, "发货成功", chat_id, order_id=order_id) else: logger.warning(f'[{msg_time}] 【自动发货】未找到匹配的发货规则或获取发货内容失败') self._record_delivery_log( @@ -5525,7 +5532,7 @@ class XianyuLive: reason=last_delivery_error or "未找到匹配的发货规则或获取发货内容失败", channel='auto' ) - await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, last_delivery_error or "未找到匹配的发货规则或获取发货内容失败", chat_id) + await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, last_delivery_error or "未找到匹配的发货规则或获取发货内容失败", chat_id, order_id=order_id) except Exception as e: self._record_delivery_log( @@ -5539,7 +5546,7 @@ class XianyuLive: ) logger.error(f"自动发货处理异常: {self._safe_str(e)}") # 发送自动发货异常通知 - await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, f"自动发货处理异常: {str(e)}", chat_id) + await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, f"自动发货处理异常: {str(e)}", chat_id, order_id=order_id) logger.info(f'[{msg_time}] 【{self.cookie_id}】订单锁释放: {lock_key},自动发货处理完成') @@ -9861,13 +9868,106 @@ class XianyuLive: return "Token定时刷新失败,将自动重试" - async def send_delivery_failure_notification(self, send_user_name: str, send_user_id: str, item_id: str, error_message: str, chat_id: str = None): - """发送自动发货失败通知""" + def _resolve_delivery_notification_buyer_name( + self, + buyer_name: Any = None, + *, + buyer_id: str = None, + chat_id: str = None, + order_id: str = None, + log_prefix: str = "", + ) -> str: + """为自动发货通知解析可信买家昵称,避免使用“等待你发货”等系统标题。""" + normalized_buyer_id = self._normalize_buyer_id_value(buyer_id) + normalized_chat_id = str(chat_id or '').strip() + try: + if order_id: + order_info = db_manager.get_order_by_id(str(order_id).strip()) + if order_info: + order_cookie_id = str(order_info.get('cookie_id') or '').strip() + if not order_cookie_id or order_cookie_id == str(self.cookie_id).strip(): + order_buyer_nick = self._sanitize_buyer_nick( + order_info.get('buyer_nick'), + source='delivery_notification_order', + log_prefix=log_prefix, + ) + if order_buyer_nick: + return order_buyer_nick + + if not normalized_buyer_id: + normalized_buyer_id = self._normalize_buyer_id_value(order_info.get('buyer_id')) + + if not normalized_chat_id: + sid = str(order_info.get('sid') or '').strip() + normalized_chat_id = sid.split('@')[0].strip() if sid else '' + + if normalized_chat_id: + chat_messages = db_manager.get_chat_messages(self.cookie_id, normalized_chat_id, limit=80) + for chat_message in reversed(chat_messages or []): + if int(chat_message.get('direction') or 0) != 2: + continue + + sender_id = self._normalize_buyer_id_value(chat_message.get('sender_id')) + if sender_id and sender_id == self.myid: + continue + if normalized_buyer_id and sender_id and sender_id != normalized_buyer_id: + continue + + chat_buyer_nick = self._sanitize_buyer_nick( + chat_message.get('sender_name'), + source='delivery_notification_chat', + log_prefix=log_prefix, + ) + if chat_buyer_nick: + return chat_buyer_nick + + if normalized_buyer_id: + recent_order = db_manager.get_recent_order_by_buyer_id( + normalized_buyer_id, + cookie_id=self.cookie_id, + minutes=24 * 60, + ) + if recent_order: + recent_buyer_nick = self._sanitize_buyer_nick( + recent_order.get('buyer_nick'), + source='delivery_notification_recent_order', + log_prefix=log_prefix, + ) + if recent_buyer_nick: + return recent_buyer_nick + except Exception as resolve_error: + logger.warning(f"{log_prefix} 自动发货通知买家昵称解析失败: {self._safe_str(resolve_error)}") + + fallback_buyer_name = self._sanitize_buyer_nick( + buyer_name, + source='delivery_notification_raw', + log_prefix=log_prefix, + ) + return fallback_buyer_name or '买家' + + async def send_delivery_failure_notification( + self, + send_user_name: str, + send_user_id: str, + item_id: str, + error_message: str, + chat_id: str = None, + order_id: str = None, + ): + """发送自动发货通知。""" + try: + resolved_buyer_name = self._resolve_delivery_notification_buyer_name( + send_user_name, + buyer_id=send_user_id, + chat_id=chat_id, + order_id=order_id, + log_prefix=f"【{self.cookie_id}】", + ) notification_message = render_notification_template( 'delivery', account_id=self.cookie_id, - buyer_name=send_user_name, + buyer_name=resolved_buyer_name, buyer_id=send_user_id, item_id=item_id, chat_id=chat_id or '未知', diff --git a/db_manager.py b/db_manager.py index 3812e2c..1c54c87 100644 --- a/db_manager.py +++ b/db_manager.py @@ -6781,6 +6781,54 @@ Cookie数量: {cookie_count} return False return True + def _sanitize_order_buyer_nick(self, buyer_nick: str = None) -> str: + """过滤订单买家昵称中的系统通知标题,避免订单列表展示“工作台通知”等文案。""" + if buyer_nick is None: + return None + + text = str(buyer_nick).strip() + if not text: + return None + + invalid_exact_titles = { + "订单", + "全部", + "交易消息", + "等待你发货", + "买家", + "工作台通知", + "我完成了评价", + "你人真不错,送你闲鱼小红花", + "卖家人不错?送Ta闲鱼小红花", + "快给ta一个评价吧~", + "快给ta一个评价吧~", + } + if text in invalid_exact_titles: + logger.info(f"忽略系统标题型订单买家昵称: {text}") + return None + + invalid_keywords = ( + "小红花", "待付款", "待发货", "待刀成", "成功小刀", "闲鱼", + "交易", "收货", "退款", "评价", "发货", "付款", "拍下", + "确认", "关闭", "鼓励", "真不错", "全部", "订单", + ) + if any(keyword in text for keyword in invalid_keywords): + logger.info(f"忽略系统关键词型订单买家昵称: {text}") + return None + + return text + + def _resolve_order_buyer_nick_for_write(self, order_id: str, buyer_nick: str = None, existing_buyer_nick: str = None) -> str: + sanitized_incoming = self._sanitize_order_buyer_nick(buyer_nick) + if sanitized_incoming: + return sanitized_incoming + + sanitized_existing = self._sanitize_order_buyer_nick(existing_buyer_nick) + if sanitized_existing: + return sanitized_existing + + return None + def insert_or_update_order(self, order_id: str, item_id: str = None, buyer_id: str = None, spec_name: str = None, spec_value: str = None, quantity: str = None, amount: str = None, order_status: str = None, cookie_id: str = None, @@ -6824,8 +6872,10 @@ Cookie数量: {cookie_count} return False # 检查订单是否已存在 - cursor.execute("SELECT order_id FROM orders WHERE order_id = ?", (order_id,)) + cursor.execute("SELECT order_id, buyer_nick FROM orders WHERE order_id = ?", (order_id,)) existing = cursor.fetchone() + existing_buyer_nick = existing[1] if existing else None + resolved_buyer_nick = self._resolve_order_buyer_nick_for_write(order_id, buyer_nick, existing_buyer_nick) if existing: # 更新现有订单 @@ -6842,8 +6892,11 @@ Cookie数量: {cookie_count} else: logger.debug(f"跳过无效buyer_id覆盖: order_id={order_id}, invalid_buyer_id={buyer_id}") if buyer_nick is not None: - update_fields.append("buyer_nick = ?") - update_values.append(buyer_nick) + if resolved_buyer_nick is not None: + update_fields.append("buyer_nick = ?") + update_values.append(resolved_buyer_nick) + elif existing_buyer_nick and self._sanitize_order_buyer_nick(existing_buyer_nick) is None: + update_fields.append("buyer_nick = NULL") if sid is not None: update_fields.append("sid = ?") update_values.append(sid) @@ -6907,7 +6960,7 @@ Cookie数量: {cookie_count} 'spec_name_2', 'spec_value_2', 'quantity', 'amount', 'order_status', 'cookie_id' ] insert_values = [ - order_id, item_id, sanitized_buyer_id, buyer_nick, sid, spec_name, spec_value, + order_id, item_id, sanitized_buyer_id, resolved_buyer_nick, sid, spec_name, spec_value, spec_name_2, spec_value_2, quantity, amount, normalized_order_status or 'unknown', cookie_id ] @@ -7002,6 +7055,37 @@ Cookie数量: {cookie_count} logger.error(f"获取订单退款前状态失败: {order_id} - {e}") return None + def _lookup_buyer_nick_from_chat_messages(self, cookie_id: str, sid: str = None, buyer_id: str = None) -> str: + chat_id = str(sid or '').strip().split('@')[0] + normalized_buyer_id = str(buyer_id or '').strip() + if not chat_id: + return None + + try: + cursor = self.conn.cursor() + params = [cookie_id, chat_id] + buyer_filter = '' + if normalized_buyer_id: + buyer_filter = ' AND sender_id = ?' + params.append(normalized_buyer_id) + + cursor.execute(f''' + SELECT sender_name + FROM chat_messages + WHERE cookie_id = ? AND chat_id = ? AND direction = 2 + AND sender_name IS NOT NULL AND sender_name != ''{buyer_filter} + ORDER BY id DESC + LIMIT 80 + ''', params) + for row in cursor.fetchall(): + buyer_nick = self._sanitize_order_buyer_nick(row[0]) + if buyer_nick: + return buyer_nick + except Exception as e: + logger.debug(f"从聊天记录兜底买家昵称失败: cookie_id={cookie_id}, sid={sid}, buyer_id={buyer_id}, error={e}") + + return None + def get_orders_by_cookie(self, cookie_id: str, limit: int = 100): """根据Cookie ID获取订单列表""" with self.lock: @@ -7017,11 +7101,14 @@ Cookie数量: {cookie_count} 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': row[3], + 'buyer_nick': buyer_nick, 'sid': row[4], 'spec_name': row[5], 'spec_value': row[6], @@ -7082,6 +7169,10 @@ Cookie数量: {cookie_count} if not buyer_id or not buyer_nick: return 0 + sanitized_buyer_nick = self._sanitize_order_buyer_nick(buyer_nick) + if not sanitized_buyer_nick: + return 0 + with self.lock: try: cursor = self.conn.cursor() @@ -7091,18 +7182,18 @@ Cookie数量: {cookie_count} cursor.execute(''' UPDATE orders SET buyer_nick = ? WHERE buyer_id = ? AND cookie_id = ? - ''', (buyer_nick, buyer_id, cookie_id)) + ''', (sanitized_buyer_nick, buyer_id, cookie_id)) else: cursor.execute(''' UPDATE orders SET buyer_nick = ? WHERE buyer_id = ? - ''', (buyer_nick, buyer_id)) + ''', (sanitized_buyer_nick, buyer_id)) updated_count = cursor.rowcount self.conn.commit() if updated_count > 0: - logger.info(f"已更新买家 {buyer_id} 的 {updated_count} 个订单昵称为: {buyer_nick}") + logger.info(f"已更新买家 {buyer_id} 的 {updated_count} 个订单昵称为: {sanitized_buyer_nick}") return updated_count diff --git a/stop.sh b/stop.sh index 278526d..cddde82 100755 --- a/stop.sh +++ b/stop.sh @@ -1,26 +1,98 @@ #!/bin/bash # 闲鱼自动回复系统停止脚本 +PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" + echo "正在停止闲鱼自动回复系统..." -if pgrep -f "Start.py" > /dev/null; then - # 先停掉项目内由 execjs/扫码链路拉起的 node 子进程。 - # 如果先杀 Python,node 往已关闭的 stdout/pipe 写数据时会抛 write EPIPE。 - pkill -TERM -f "utils/gen_tfstk.js" 2>/dev/null || true - pkill -TERM -f "utils/et_f.js" 2>/dev/null || true - sleep 0.3 +# 递归获取某个 PID 的所有子孙进程。 +get_descendants() { + local parent="$1" + local children child + children=$(pgrep -P "$parent" 2>/dev/null || true) + for child in $children; do + echo "$child" + get_descendants "$child" + done +} - pkill -TERM -f "Start.py" - sleep 2 +is_alive() { + kill -0 "$1" 2>/dev/null +} - if pgrep -f "Start.py" > /dev/null; then - echo "正在强制停止..." - pkill -9 -f "Start.py" +wait_pids_exit() { + local timeout_seconds="$1" + shift + local pids="$*" + local loops=$((timeout_seconds * 10)) + local i pid alive + + [ -z "$pids" ] && return 0 + + for ((i = 0; i < loops; i++)); do + alive=0 + for pid in $pids; do + if is_alive "$pid"; then + alive=1 + break + fi + done + [ "$alive" -eq 0 ] && return 0 + sleep 0.1 + done + + return 1 +} + +unique_pids() { + printf '%s\n' "$@" | awk 'NF && !seen[$1]++' +} + +# 只匹配当前项目的 Start.py,避免误杀其它同名项目。 +start_pids=$(pgrep -f "${PROJECT_DIR}/Start.py" 2>/dev/null || true) +if [ -z "$start_pids" ]; then + # 兼容从项目目录内以相对路径启动的情况。 + start_pids=$(pgrep -f "python.*Start.py" 2>/dev/null || true) +fi + +if [ -n "$start_pids" ]; then + # 先停止 Python 主进程派生出来的 node 子进程。 + # ExecJS/扫码登录链路的 node 命令行不一定包含固定 js 文件名;如果先停 Python, + # node 可能继续向已关闭的 stdout/stderr pipe 写入并打印 write EPIPE。 + node_pids="" + for start_pid in $start_pids; do + descendants=$(get_descendants "$start_pid") + for pid in $descendants; do + comm=$(ps -p "$pid" -o comm= 2>/dev/null | awk '{print $1}') + args=$(ps -p "$pid" -o command= 2>/dev/null || true) + if [ "$comm" = "node" ] || echo "$args" | grep -qE '(^|/)node( |$)'; then + node_pids="$node_pids $pid" + fi + done + done + + # 兜底:项目内已知的 node 脚本名。 + known_node_pids=$(pgrep -f "${PROJECT_DIR}/utils/gen_tfstk.js|${PROJECT_DIR}/utils/et_f.js|utils/gen_tfstk.js|utils/et_f.js" 2>/dev/null || true) + node_pids=$(unique_pids $node_pids $known_node_pids) + + if [ -n "$node_pids" ]; then + kill -TERM $node_pids 2>/dev/null || true + if ! wait_pids_exit 2 $node_pids; then + kill -KILL $node_pids 2>/dev/null || true + wait_pids_exit 1 $node_pids >/dev/null 2>&1 || true + fi fi - # 兜底清理 lite 扫码登录可能残留的 node 子进程 - pkill -9 -f "utils/gen_tfstk.js" 2>/dev/null || true - pkill -9 -f "utils/et_f.js" 2>/dev/null || true + # 再停止 Python 主进程。 + kill -TERM $start_pids 2>/dev/null || true + if ! wait_pids_exit 5 $start_pids; then + echo "正在强制停止..." + kill -KILL $start_pids 2>/dev/null || true + wait_pids_exit 2 $start_pids >/dev/null 2>&1 || true + fi + + # 最后兜底清理启动过程中可能残留的已知 node 子进程。 + pkill -KILL -f "${PROJECT_DIR}/utils/gen_tfstk.js|${PROJECT_DIR}/utils/et_f.js|utils/gen_tfstk.js|utils/et_f.js" 2>/dev/null || true echo "已停止" else