mirror of
https://github.com/GuDong2003/xianyu-auto-reply-fix.git
synced 2026-08-28 17:40:45 +08:00
fix: 修复买家昵称污染与停止脚本清理
This commit is contained in:
+112
-12
@@ -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 '未知',
|
||||
|
||||
+99
-8
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user