Compare commits

...

2 Commits

Author SHA1 Message Date
houhuan 69046d1ccc merge: 保留本地最新版本 2026-05-12 17:07:12 +08:00
houhuan f0bfd9dbbe v4: 改用本地IndexTTS2 + 新增飞书推送
- TTS从小爱音箱云API改为本地IndexTTS2模型
- 新增飞书群Webhook推送
- 新增notify_all统一通知(企业微信+飞书)
- 新增local_tts.py本地TTS封装模块
- 新增run.bat一键启动脚本
- 新增README.md项目说明
- 新增.gitignore排除敏感文件
- 更新CLAUDE.md文档
2026-05-12 17:06:49 +08:00
6 changed files with 260 additions and 144 deletions
+2 -13
View File
@@ -1,23 +1,12 @@
# Sensitive data - NEVER commit # Sensitive
CLAUDE.md
cookies.json cookies.json
xiaomi_raw_cookies.json
xiaomi_config.json xiaomi_config.json
xiaomi_raw_cookies.json
order_data.json order_data.json
*.mi.token
# Python # Python
__pycache__/ __pycache__/
*.pyc *.pyc
*.pyo
# IDE # IDE
.vscode/
.idea/
# OS
Thumbs.db
Desktop.ini
# Claude
.claude/ .claude/
+82
View File
@@ -0,0 +1,82 @@
# 丰享订单监控项目
## 项目概述
自动监控丰享商家端(fs.szfx.com)的新订单,同时推送到企业微信、飞书群和本地TTS语音播报**。
## 文件说明
| 文件 | 用途 |
|------|------|
| `order_monitor.py` | 主程序:监控订单 + 企业微信 + 飞书 + 本地TTS播报 |
| `push_latest_order.py` | 单次获取最新订单并推送企业微信 |
| `fetch_orders.py` | 复用 Cookie 获取订单数据(命令行) |
| `fengxiang_scraper.py` | 首次登录获取 CookiePlaywright 半自动) |
| `setup_xiaomi.py` | 小米账号登录获取 serviceTokenPlaywright |
| `cookies.json` | 丰享平台的登录 Cookie |
| `xiaomi_config.json` | 小米账号和音箱配置 |
| `order_data.json` | 订单数据样例 |
## 已逆向的 API
### 丰享平台
- **登录**: `POST https://fspass.szfx.com/api/login`
- 参数: `uname`(手机号), `upass`(密码Base64反转), `ticket`, `randstr`(腾讯验证码)
- 登录后通过 `https://fs.szfx.com/saasmerchant/setstoken` 设置 Cookie
- Cookie 保存在浏览器中,主要: `USS`, `PTOKEN`, `CPTOKEN`, `STOKEN`
- **订单列表**: `POST https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list`
- Content-Type: application/json
- Body: `{"shopId": "20434543575189", "page": 1, "pageSize": 20}`
- 需要登录 Cookie,未登录返回 `errno: 110003`
- 返回: `{errno: 0, data: {total, list: [...], count}}`
- **关键字段**:
- `shopId`: `20434543575189` (益选便利店)
- `orderId`: 订单号
- `amountPayable`: 实付金额(单位:分)
- `allPrice`: 订单金额(单位:分)
- `shopName`, `city`, `companyName`, `workplaceName`: 业务信息
- `createTime`, `payTime`, `finishTime`: 时间戳(秒)
### 本地 TTSIndexTTS2
- 使用本地部署的 IndexTTS2 模型,无需联网
- 模型路径: `E:\2025Code\python\IndexTT\index-tts\`
- 语音样本: `examples/voice_01.wav`
- 封装模块: `local_tts.py`
- 调用方式: `_local_speak(text)` → 合成 WAV → 本地播放
### 企业微信 Webhook
- URL: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=644ab6d9-3b66-4166-88e9-5a8a89e3731d`
- 只能发消息,不能收消息
## order_monitor.py 核心逻辑
1. Playwright 打开浏览器,用户手动登录丰享平台
2. 登录成功后,通过浏览器页面的 `fetch()` 调用订单 API(复用浏览器会话,最自然)
3. 每次拉取最近 N 条订单,和上次最新订单号对比,找出所有新订单
4. 新订单同时推送企业微信 + 飞书 + 本地TTS播报
5. 高峰期(11-13点、17-19点)10-20秒轮询,闲时 30-60 秒
6. 每 5 分钟刷新页面防 Session 过期
7. Session 过期自动检测 → 通知企业微信 → 等待用户重新登录
## 消息格式
- 企业微信: `【丰享丰食】订单收款成功,收款24.00元`
- 小爱音箱: `收到新订单,收款24元`(口语化,整数不带小数点)
## 已安装的依赖
```
pip install playwright requests
playwright install chromium
```
## 注意事项
- 丰享登录有**腾讯验证码 TCaptcha**,无法纯接口绕过,必须用 Playwright 浏览器登录
- 小米 passToken 会过期,过期后需重新运行 `setup_xiaomi.py` 登录
- Windows 终端输出中文会乱码,但实际发送的内容是正确的 UTF-8
- MiService 的 `miaccount.py` 第 68 行已修改:`self.password.encode()` 加了 None 检查(passToken 登录时 password 为 None
+39 -81
View File
@@ -1,106 +1,64 @@
# 丰享订单监控 # 丰享订单监控
自动监控丰享商家端(fs.szfx.com新订单,同时推送到**企业微信群**和**小爱音箱语音播报**。 自动监控丰享商家端(fs.szfx.com)新订单,同时推送到**企业微信**、**飞书群**并进行**本地TTS语音播报**。
## 功能特性
- **自动轮询**:高峰期(11-13点、17-19点)10-20秒/次,闲时30-60秒/次
- **双通道推送**:新订单同时推送企业微信消息 + 小爱音箱 TTS 语音播报
- **Cookie 复用**:首次登录后保存 Cookie,后续启动无需重复登录
- **夜间暂停**21:00 ~ 07:40 自动暂停轮询,节省资源
- **Session 保活**:每5分钟自动发请求维持 Cookie 活性
- **过期自动恢复**:Cookie 过期时弹浏览器提示重新登录,并推送企业微信通知
- **开机自启**:支持 Windows 计划任务,登录系统后自动运行
## 快速开始 ## 快速开始
### 1. 安装依赖 双击 `run.bat` 启动。首次运行会自动打开浏览器登录丰享平台,后续复用 Cookie 无需反复登录。
```bash
pip install playwright requests miservice_fork aiohttp
playwright install chromium
```
### 2. 配置小米音箱(可选)
首次使用需要获取小米 passToken:
```bash
python setup_xiaomi.py
```
浏览器打开后手动登录小米账号,Token 自动保存到 `~/.mi.token`
### 3. 运行监控
```bash
python order_monitor.py
```
首次运行会打开浏览器,手动完成丰享平台登录(有腾讯验证码,需人工操作)。登录成功后 Cookie 自动保存,之后重启无需再登录。
### 4. 测试推送
```bash
python test_push.py
```
拉取最新一条订单,测试企业微信和小爱音箱是否正常。
## 文件说明 ## 文件说明
| 文件 | 用途 | | 文件 | 用途 |
|------|------| |------|------|
| `order_monitor.py` | 主监控程序,轮询订单 + 推送 + 播报 | | `order_monitor.py` | 主程序:监控订单 + 企业微信/飞书推送 + 本地TTS播报 |
| `test_push.py` | 测试脚本,验证企业微信和音箱通道 | | `local_tts.py` | 本地 TTS 封装模块(基于 IndexTTS2 |
| `push_latest_order.py` | 单次获取最新订单并推送企业微信 | | `run.bat` | 一键启动脚本 |
| `fetch_orders.py` | 命令行获取订单数据(JSON 输出) | | `push_latest_order.py` | 单次获取最新订单并推送 |
| `fengxiang_scraper.py` | Playwright 半自动登录获取 Cookie | | `fetch_orders.py` | 复用 Cookie 获取订单数据(命令行) |
| `setup_xiaomi.py` | 小米账号登录获取 serviceToken | | `fengxiang_scraper.py` | 首次登录获取 CookiePlaywright 半自动) |
| `run_monitor.bat` | Windows 启动批处理 | | `setup_xiaomi.py` | 小米账号登录(已弃用,TTS 改为本地) |
| `setup_autostart.ps1` | Windows 开机自启配置脚本 | | `cookies.json` | 丰享平台登录 Cookie(自动生成) |
## 已逆向的 API ## 运行环境
### 丰享平台 - Python 虚拟环境: `E:\2025Code\python\IndexTT\index-tts\.venv`
- 依赖: `playwright``requests`
- 浏览器: ChromiumPlaywright 自动管理)
- TTS 模型: IndexTTS2(本地部署,首次加载需 1-2 分钟)
- **登录**`POST https://fspass.szfx.com/api/login` ## 通知渠道
- 参数:`uname`(手机号)、`upass`(密码 Base64 反转)、`ticket``randstr`(腾讯验证码)
- Cookie`USS``PTOKEN``CPTOKEN``STOKEN`
- **订单列表**`POST https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list` | 渠道 | 说明 |
- Content-Type: application/json |------|------|
- Body: `{"shopId": "...", "page": 1, "pageSize": 20}` | 企业微信 | Webhook 推送消息 |
- 返回:`{errno: 0, data: {total, list: [...], count}}` | 飞书群 | Webhook 推送消息 |
| 本地 TTS | IndexTTS2 合成语音 + 扬声器播放 |
### 小米/小爱音箱 ## 轮询策略
- 使用 [miservice_fork](https://github.com/nicepkg/miservice_fork) Python 库 | 时段 | 间隔 | 拉取数量 |
- TTS 播报:`MiNAService.text_to_speech(deviceId, text)` |------|------|---------|
- Token 保存在 `~/.mi.token` | 高峰期 11:00-13:00, 17:00-19:00 | 5-10秒 | 20条 |
| 闲时 | 5-10秒 | 5条 |
| 夜间 21:00-07:40 | 自动暂停 | — |
## 消息格式 ## 消息格式
企业微信和小爱音箱使用统一格式: - 推送: `【丰享丰食】订单收款成功,收款24.00元`
- TTS: 同上文案语音播报
> 【丰享丰食】订单收款成功,收款24.00元 ## 已逆向 API
## 开机自启(Windows ### 丰享平台
已配置计划任务 `FXOrderMonitor`,用户登录时自动运行。 - **登录**: `POST https://fspass.szfx.com/api/login`
- 腾讯验证码 TCaptcha,必须浏览器手动登录
```powershell - **订单列表**: `POST https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list`
# 手动配置 - Body: `{"shopId": "20434543575189", "page": 1, "pageSize": 20}`
.\setup_autostart.ps1 - 返回: `{errno: 0, data: {total, list: [...], count}}`
```
## 注意事项 ## 注意事项
- 丰享登录有腾讯验证码 TCaptcha,无法纯接口绕过,首次必须用浏览器登录 - Cookie 过期时自动弹浏览器提示重新登录,并推送通知
- Cookie 有效期较长,持续使用可维持;过期会自动弹窗提示重新登录 - 每 5 分钟自动保活防 Session 过期
- 小米 passToken 会过期,过期后需重新运行 `setup_xiaomi.py` - 无需小米账号或小爱音箱,TTS 完全本地运行
- 配置文件(cookies.json、xiaomi_config.json)含敏感信息,已加入 .gitignore
## License
MIT
+67
View File
@@ -0,0 +1,67 @@
"""
本地TTS语音播报模块 - 基于IndexTTS2
"""
import os
import sys
import time
import tempfile
import winsound
# IndexTTS2 路径配置
_INDEXTTS_HOME = r"E:\2025Code\python\IndexTT\index-tts"
_INDEXTTS_MODEL_DIR = os.path.join(_INDEXTTS_HOME, "checkpoints")
_INDEXTTS_CFG = os.path.join(_INDEXTTS_MODEL_DIR, "config.yaml")
_INDEXTTS_VENV_PYTHON = os.path.join(_INDEXTTS_HOME, ".venv", "Scripts", "python.exe")
# 默认语音样本
_DEFAULT_VOICE_PROMPT = os.path.join(_INDEXTTS_HOME, "examples", "voice_01.wav")
# 全局 TTS 实例(延迟初始化)
_tts_instance = None
def _get_tts():
"""延迟初始化 IndexTTS2 实例"""
global _tts_instance
if _tts_instance is None:
sys.path.insert(0, _INDEXTTS_HOME)
from indextts.infer_v2 import IndexTTS2
print("[TTS] 正在加载 IndexTTS2 模型(首次加载较慢,请耐心等待)...")
_tts_instance = IndexTTS2(
cfg_path=_INDEXTTS_CFG,
model_dir=_INDEXTTS_MODEL_DIR,
use_fp16=True,
use_cuda_kernel=False,
use_deepspeed=False
)
print("[TTS] IndexTTS2 模型加载完成")
return _tts_instance
def speak(text, voice_prompt=None):
"""使用本地 IndexTTS2 合成语音并播放
Args:
text: 要播报的文本
voice_prompt: 语音样本路径,默认使用 voice_01.wav
"""
if voice_prompt is None:
voice_prompt = _DEFAULT_VOICE_PROMPT
tts = _get_tts()
output_path = os.path.join(tempfile.gettempdir(), f"fx_tts_{int(time.time() * 1000)}.wav")
tts.infer(
spk_audio_prompt=voice_prompt,
text=text,
output_path=output_path,
verbose=False
)
# 播放生成的音频
winsound.PlaySound(output_path, winsound.SND_FILENAME)
return output_path
if __name__ == "__main__":
speak("本地语音播报测试成功")
+27 -47
View File
@@ -10,25 +10,20 @@ import json
import random import random
import time import time
import datetime import datetime
import asyncio
import threading
import requests import requests
from pathlib import Path from pathlib import Path
from aiohttp import ClientSession
from miservice import MiAccount, MiNAService
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
from local_tts import speak as _local_speak
# ============ 配置 ============ # ============ 配置 ============
LOGIN_URL = "https://fspass.szfx.com/ucenter/userlogin?platform=saas&redirect_url=https%3A%2F%2Ffs.szfx.com%2FMMS%23%2F&return_url=https%3A%2F%2Ffs.szfx.com%2Fsaasmerchant%2Fsetstoken" LOGIN_URL = "https://fspass.szfx.com/ucenter/userlogin?platform=saas&redirect_url=https%3A%2F%2Ffs.szfx.com%2FMMS%23%2F&return_url=https%3A%2F%2Ffs.szfx.com%2Fsaasmerchant%2Fsetstoken"
DATA_API = "https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list" DATA_API = "https://fs.szfx.com/saasmerchant/pcweb/order/quickpayorder/list"
SHOP_ID = "20434543575189" SHOP_ID = "20434543575189"
WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=644ab6d9-3b66-4166-88e9-5a8a89e3731d" WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=644ab6d9-3b66-4166-88e9-5a8a89e3731d"
FEISHU_WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/oc_d5ef8b1abf66842c28ef57e1658dc096"
COOKIES_FILE = "cookies.json" COOKIES_FILE = "cookies.json"
# 小爱音箱配置 # 本地 TTS 配置(使用 IndexTTS2,无需额外配置)
XIAOMI_USER_ID = "1136458602"
XIAOMI_TOKEN_PATH = str(Path.home() / ".mi.token")
XIAOMI_SPEAKER_DID = "3ba2c1e8-d8cb-45c5-b88a-15624e7a02f3"
# 高峰时段配置 # 高峰时段配置
PEAK_HOURS = [(11, 13), (17, 19)] PEAK_HOURS = [(11, 13), (17, 19)]
@@ -86,6 +81,12 @@ def format_msg(order):
return f"【丰享丰食】订单收款成功,收款{fmt_money(order['amountPayable'])}" return f"【丰享丰食】订单收款成功,收款{fmt_money(order['amountPayable'])}"
def notify_all(text):
"""统一发送通知到企业微信 + 飞书"""
send_to_wecom(text)
send_to_feishu(text)
def send_to_wecom(text): def send_to_wecom(text):
payload = {"msgtype": "text", "text": {"content": text}} payload = {"msgtype": "text", "text": {"content": text}}
try: try:
@@ -94,53 +95,32 @@ def send_to_wecom(text):
if result.get("errcode") == 0: if result.get("errcode") == 0:
log("企业微信推送成功") log("企业微信推送成功")
else: else:
log(f"推送失败: {result}") log(f"企业微信推送失败: {result}")
except Exception as e: except Exception as e:
log(f"推送异常: {e}") log(f"企业微信推送异常: {e}")
def _run_async_in_thread(coro): def send_to_feishu(text):
"""在独立线程中运行协程,避免与 Playwright 事件循环冲突""" payload = {"msg_type": "text", "content": {"text": text}}
result = None
error = None
def _target():
nonlocal result, error
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try: try:
result = loop.run_until_complete(coro) resp = requests.post(FEISHU_WEBHOOK_URL, json=payload, timeout=10)
result = resp.json()
if result.get("code") == 0:
log("飞书推送成功")
else:
log(f"飞书推送失败: {result}")
except Exception as e: except Exception as e:
error = e log(f"飞书推送异常: {e}")
finally:
loop.close()
t = threading.Thread(target=_target)
t.start()
t.join(timeout=15)
if error:
raise error
return result
def speak(text): def speak(text):
"""调用小爱音箱语音播报""" """使用本地 IndexTTS2 语音播报"""
try: try:
async def _tts(): _local_speak(text)
async with ClientSession() as session:
account = MiAccount(session, XIAOMI_USER_ID, None, XIAOMI_TOKEN_PATH)
mina = MiNAService(account)
return await mina.text_to_speech(XIAOMI_SPEAKER_DID, text)
result = _run_async_in_thread(_tts())
if result and result.get("code") == 0:
log("语音播报成功") log("语音播报成功")
else:
log(f"语音播报失败: {result}")
except Exception as e: except Exception as e:
log(f"语音播报异常: {e}") log(f"语音播报异常: {e}")
def load_cookies(): def load_cookies():
"""加载本地 Cookie 文件""" """加载本地 Cookie 文件"""
cookie_path = Path(__file__).parent / COOKIES_FILE cookie_path = Path(__file__).parent / COOKIES_FILE
@@ -227,7 +207,7 @@ def main():
# 2. 没有有效 Cookie → 打开浏览器登录 # 2. 没有有效 Cookie → 打开浏览器登录
if not cookies: if not cookies:
send_to_wecom("【监控通知】需要登录丰享平台,请查看电脑浏览器完成登录") notify_all("【监控通知】需要登录丰享平台,请查看电脑浏览器完成登录")
cookies = browser_login() cookies = browser_login()
if not cookies: if not cookies:
log("登录失败,退出") log("登录失败,退出")
@@ -259,7 +239,7 @@ def main():
test = fetch_orders_via_requests(cookies, page_size=1) test = fetch_orders_via_requests(cookies, page_size=1)
if test == "SESSION_EXPIRED": if test == "SESSION_EXPIRED":
log("Cookie 在夜间过期,需要重新登录") log("Cookie 在夜间过期,需要重新登录")
send_to_wecom("【监控通知】Cookie 过期,请查看电脑浏览器完成登录") notify_all("【监控通知】Cookie 过期,请查看电脑浏览器完成登录")
cookies = browser_login() cookies = browser_login()
if not cookies: if not cookies:
log("登录失败,退出") log("登录失败,退出")
@@ -285,10 +265,10 @@ def main():
# Cookie 过期 → 浏览器重新登录 # Cookie 过期 → 浏览器重新登录
if orders == "SESSION_EXPIRED": if orders == "SESSION_EXPIRED":
log("Cookie 已过期!") log("Cookie 已过期!")
send_to_wecom("【监控异常】Cookie 已过期,请查看电脑浏览器完成登录") notify_all("【监控异常】Cookie 已过期,请查看电脑浏览器完成登录")
cookies = browser_login() cookies = browser_login()
if not cookies: if not cookies:
send_to_wecom("登录超时,监控已退出。请手动重新运行。") notify_all("登录超时,监控已退出。请手动重新运行。")
return return
send_to_wecom("重新登录成功,监控已恢复") send_to_wecom("重新登录成功,监控已恢复")
last_keepalive = time.time() last_keepalive = time.time()
@@ -312,7 +292,7 @@ def main():
# 从旧到新依次推送 # 从旧到新依次推送
for o in reversed(new_orders): for o in reversed(new_orders):
msg = format_msg(o) msg = format_msg(o)
send_to_wecom(msg) notify_all(msg)
speak(msg) speak(msg)
time.sleep(0.5) time.sleep(0.5)
last_order_id = new_orders[0]["orderId"] last_order_id = new_orders[0]["orderId"]
+40
View File
@@ -0,0 +1,40 @@
@echo off
chcp 65001 >nul
title 丰享订单监控
echo ============================================
echo 丰享订单监控系统 v4
echo 推送: 企业微信 + 本地TTS语音播报
echo ============================================
echo.
cd /d "%~dp0"
:: Python路径(IndexTTS虚拟环境)
set PYTHON=E:\2025Code\python\IndexTT\index-tts\.venv\Scripts\python.exe
:: 检查Python是否存在
if not exist "%PYTHON%" (
echo [错误] 未找到Python: %PYTHON%
echo 请确认IndexTTS已正确安装
pause
exit /b 1
)
:: 检查cookie文件
if not exist "cookies.json" (
echo [提示] 未检测到登录Cookie,首次运行将自动打开浏览器
echo 请在浏览器中手动完成登录(含验证码)
echo.
)
echo [启动] 正在启动订单监控...
echo.
"%PYTHON%" order_monitor.py
if %errorlevel% neq 0 (
echo.
echo [异常] 监控程序异常退出,错误码: %errorlevel%
pause
)