初始化项目 v2.0:中国邮政揽收系统自动填单工具

基于 HTTP API + Node.js token bridge 方案,不依赖浏览器自动化。
This commit is contained in:
2026-05-15 14:34:35 +08:00
commit e99dfaef4d
20 changed files with 4432 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# Python
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
*.spec
# Virtual environments
.venv/
# Node.js
token_bridge/node_modules/
# HAR captures (large binary data)
docs/*.har
# Runtime data
session.json
# IDE
.vscode/
.idea/
# Claude Code
CLAUDE.md
+1
View File
@@ -0,0 +1 @@
3.10
+155
View File
@@ -0,0 +1,155 @@
# 中国邮政揽收系统 - 自动填单工具
通过 HTTP API + Node.js token bridge 实现,**不依赖浏览器自动化**。读取 Excel 运单数据,逐条自动填写产品、客户、运单信息并提交。
## 运行环境
- Windows 10/11 64 位
- **Node.js 18+**(用于 token bridge,计算 $_ts 安全框架的 c93l5Fya 动态 token)
## 快速开始
```bash
# 安装 Python 依赖
uv sync
# 安装 token bridge 依赖
cd token_bridge && npm install && cd ..
# 运行
uv run python main.py
```
## 打包
```bash
build.bat # Windows
bash build.sh # Linux
```
输出 `dist/PickupAuto.exe`,用户端需安装 Node.js 18+。
## 架构
```
┌─────────────────────────────────────────┐
│ Python (requests.Session) │
│ ├─ engine.py 引擎/表单组装/提交 │
│ ├─ pickup_api.py HTTP API 封装(18个) │
│ ├─ login_helper.py cookies 导入 │
│ ├─ field_mapping.py 中文字段映射 │
│ ├─ excel_reader.py Excel 读写 │
│ ├─ main.py Tkinter GUI │
│ └─ log.py 统一日志 │
├─────────────────────────────────────────┤
│ Node.js (HTTP server, 同进程启动) │
│ └─ token_bridge/server.js │
│ jsdom 加载 $_ts 安全 JS │
│ 计算 c93l5Fya token │
└─────────────────────────────────────────┘
```
### 数据流
```
Excel 行数据
→ 产品搜索 (autocomplete API) → 用户选择
→ 客户搜索 (autocomplete API) → 用户选择
→ 运单查询 → 获取收寄件人/地址/电话
→ 地址匹配 → 获取区划代码
→ 组装表单 (5层优先级) → 确认弹窗 → 提交
```
### 表单字段优先级
```
强制固定值 (force=true)
> API 获取值 (运单/产品/客户/地址匹配)
> 固定值 (force=false)
> Excel 表格值
> 默认值 (config.json defaults)
```
### token bridge 原理
揽收系统使用 `$_ts` 安全框架(954KB 混淆 JS),所有 AJAX 请求需附带 `c93l5Fya=TOKEN` 参数。token bridge 在 jsdom 中运行该 JS,通过 HTTP API 对外提供 token 计算服务,Python 拿到 token 后用自己的 `requests.Session` 发请求。
## 使用流程
### 1. 登录(获取 cookies)
点击 `🔑 登录` → 弹出对话框 → 用浏览器 cookies 导出工具导出 `10.4.188.1` 的所有 cookies 为 JSON 数组 → 粘贴到对话框 → 保存。
cookies 保存到 `session.json`(与 exe 同目录)。
### 2. 选择 Excel
点击 `选择文件`,选择要处理的 Excel 文件。
### 3. 配置固定值(可选)
在 GUI 固定值表格中设置常用字段,勾选 "强制覆盖" 可覆盖 API 获取的值。
### 4. 开始处理
点击 `▶ 开始处理` → 弹出产品/客户选择框 → 确认提交信息 → 自动批量处理后续行。
## Excel 格式
| 字段 | 说明 | 来源 |
|------|------|------|
| 单号 | 运单号 | Excel 必填 |
| 重量 | 单位克 | Excel |
| 产品名称 | 用于 autocomplete 搜索 | Excel/固定值 |
| 客户名称 | 用于 autocomplete 搜索 | Excel/固定值 |
| 收件人/手机/地址 | 运单 API 自动填充,Excel 可覆盖 | API/Excel |
| 寄件人/手机/地址 | 客户资料 API 自动填充 | API/固定值 |
| 寄达地/寄达地区划 | 地址匹配 API 自动获取,固定值可强制覆盖 | API/固定值 |
| 电商平台 | 默认 "无"(WU) | 默认值 |
| 付款方式 | 默认 "寄付"(1) | 默认值 |
| 内件属性 | 产品 API 返回 `innerpartsType` | API |
| 运输类型 | 默认 "陆运"(1) | 默认值 |
## 配置说明 (config.json)
### system
- `url`: 揽收系统地址
### timing
- `delay_between_rows_ms`: 行间延时(毫秒)
### fixed_values
```
"字段名": {"value": "值", "force": false}
```
`force: true` 时覆盖 API 获取值。
### defaults
字段的默认值(中文名,自动翻译为系统编码)。
### field_mapping
Excel 列名 → 表单参数名的自定义映射。内置映射覆盖常见字段,仅需在此配置自定义列名。
## 文档
项目级文档见 `../docs/`:
| 文件 | 说明 |
|------|------|
| `分析报告.md` | 系统 API 分析 |
| `字段覆盖检查报告.md` | 字段来源和覆盖关系 |
| `har-successful-submit-fields.json` | 成功提交的完整表单字段模板 |
| `portal-to-pickup-full-flow.har` | 门户→揽收页面完整网络流 |
| `product-customer-selection-flow.har` | 产品/客户选择 API 流 |
| `successful-submit-with-address-change.har` | 改寄达地后成功提交 |
## 开发
### 日志等级
`log.py` 中修改 `LEVEL`:
- `0` = 仅用户级消息
- `1` = 调试级
- `2` = 追踪级(API 请求/响应)
修改后重新打包生效。
+94
View File
@@ -0,0 +1,94 @@
@echo off
setlocal enabledelayedexpansion
chcp 65001 >nul 2>&1
echo ============================================
echo PickupAuto Build Script (HTTP mode)
echo ============================================
echo.
:: Check Python
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Python not found. Please install Python 3.10+
echo https://www.python.org/downloads/
pause
exit /b 1
)
:: Check Node.js (for token bridge)
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Node.js not found. Please install Node.js 18+
echo https://nodejs.org/
pause
exit /b 1
)
:: Check uv, install if missing
uv --version >nul 2>&1
if %errorlevel% neq 0 (
echo [INFO] Installing uv...
pip install uv
if %errorlevel% neq 0 (
echo [ERROR] Failed to install uv
pause
exit /b 1
)
)
echo [1/4] Installing Python dependencies...
uv sync
if %errorlevel% neq 0 (
echo [ERROR] Python dependency install failed
pause
exit /b 1
)
echo [2/4] Installing token bridge dependencies...
cd token_bridge
call npm install
if %errorlevel% neq 0 (
echo [ERROR] npm install failed in token_bridge
pause
exit /b 1
)
cd ..
echo [3/4] Cleaning old build...
if exist dist rmdir /s /q dist
if exist build rmdir /s /q build
echo [4/4] Building exe with PyInstaller...
uv run pyinstaller ^
--onefile ^
--windowed ^
--name "PickupAuto" ^
--icon="pickup.ico" ^
--add-data "config.json;." ^
--add-data "pickup.ico;." ^
--add-data "token_bridge;token_bridge" ^
--hidden-import=openpyxl ^
--hidden-import=requests ^
main.py
if %errorlevel% neq 0 (
echo [ERROR] PyInstaller build failed
pause
exit /b 1
)
:: Copy config to dist for reference
copy config.json dist\config.json >nul 2>&1
echo.
echo ============================================
echo Build complete!
echo Output: dist\PickupAuto.exe
echo.
echo Requirements for end users:
echo - Windows 10/11 64-bit
echo - Node.js 18+ (for security token computation)
echo - Access to internal network (10.4.188.1)
echo ============================================
pause
Executable
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
echo "============================================"
echo " PickupAuto Build Script (HTTP mode)"
echo "============================================"
echo
# Check dependencies
command -v python3 >/dev/null 2>&1 || { echo "[ERROR] Python 3.10+ required"; exit 1; }
command -v node >/dev/null 2>&1 || { echo "[ERROR] Node.js 18+ required"; exit 1; }
command -v npm >/dev/null 2>&1 || { echo "[ERROR] npm required"; exit 1; }
# Check uv
if ! command -v uv &>/dev/null; then
echo "[INFO] Installing uv..."
pip install uv
fi
echo "[1/4] Installing Python dependencies..."
uv sync
echo "[2/4] Installing token bridge dependencies..."
cd token_bridge
npm install
cd ..
echo "[3/4] Cleaning old build..."
rm -rf dist build
echo "[4/4] Building with PyInstaller..."
uv run pyinstaller \
--onefile \
--windowed \
--name "PickupAuto" \
--add-data "config.json:." \
--add-data "pickup.ico:." \
--add-data "token_bridge:token_bridge" \
--hidden-import=openpyxl \
--hidden-import=requests \
main.py
cp config.json dist/config.json 2>/dev/null || true
echo
echo "============================================"
echo " Build complete!"
echo " Output: dist/PickupAuto"
echo
echo " Requirements for end users:"
echo " - Node.js 18+ (for security token computation)"
echo " - Access to internal network (10.4.188.1)"
echo "============================================"
+34
View File
@@ -0,0 +1,34 @@
{
"system": {
"url": "https://10.4.188.1/pickup-web/a/postwaybill/main"
},
"timing": {
"delay_between_rows_ms": 3000
},
"field_mapping": {
"_comment": "仅需在此配置与内置映射不同的自定义列名,默认使用内置映射"
},
"fixed_values": {
"客户名称": {"value": "", "force": false},
"寄件人": {"value": "", "force": false},
"寄件地址": {"value": "", "force": false},
"寄达地": {"value": "", "force": false},
"寄达地区划": {"value": "", "force": false},
"产品名称": {"value": "", "force": false},
"内件品名": {"value": "", "force": false}
},
"defaults": {
"电商平台": "无",
"付款方式": "寄付",
"证件类型": "身份证",
"保价金额": "",
"代收货款": "",
"运输类型": "陆运",
"内件属性": "其他",
"件数": "1",
"保价标志": "不保价",
"回单标志": "无回单",
"代收标志": "不代收",
"投递方式": "普通投递"
}
}
+1032
View File
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
"""Excel 读取模块 — 读取输入 Excel 并写入结果 Excel。"""
import openpyxl
def read_input_excel(file_path: str) -> tuple[list[dict], list[str]]:
"""读取输入 Excel 文件,返回 (行数据列表, 列名列表)。
每行数据为 {列名: 值} 的字典。跳过全空行。
"""
wb = openpyxl.load_workbook(file_path, read_only=True)
ws = wb.active
rows = list(ws.iter_rows(min_row=1, values_only=True))
if not rows:
raise ValueError("Excel 文件为空")
headers = [str(h).strip() if h is not None else "" for h in rows[0]]
if not any(headers):
raise ValueError("未找到表头行")
data = []
for row in rows[1:]:
values = [str(c).strip() if c is not None else "" for c in row]
if not any(values): # 跳过全空行
continue
row_dict = {}
for i, header in enumerate(headers):
if i < len(values):
row_dict[header] = values[i]
else:
row_dict[header] = ""
data.append(row_dict)
wb.close()
return data, headers
def validate_required_columns(
headers: list[str],
fixed_values: dict[str, str] = None,
defaults: dict[str, str] = None,
) -> list[str]:
"""验证必要字段是否有数据来源(固定值 > Excel > API > 默认值)。
数据来源优先级:
1. 固定值设置(客户名称、寄件人、寄件地址、产品名称、寄达地)
2. Excel 表格(运单号、重量、内件品名)
3. API 自动获取(querypostwaybillmanager 返回收件人/电话/地址等全部运单信息)
4. 默认值(电商平台、付款方式等系统编码)
只在所有来源都没有值时警告。
"""
fixed_values = fixed_values or {}
defaults = defaults or {}
# querypostwaybillmanager API 自动返回的字段,Excel 无需填写
api_fields = [
"收件人", "收件人手机", "收件地址", "收件邮编", "收件人固话",
"寄件人", "寄件人手机", "寄件地址", "寄件邮编",
]
# 其余需要人工提供数据的字段
check_fields = [
"单号", # Excel 必填(API 不返回)
"重量", # Excel 必填(API 返回 orderWeight 但可能为 0)
"产品名称", # 固定值
"客户名称", # 固定值
]
from field_mapping import get_fixed_value_str
missing = []
for field in check_fields:
if get_fixed_value_str(fixed_values, field):
continue
if field in headers:
continue
if defaults.get(field, ""):
continue
missing.append(field)
return missing
+225
View File
@@ -0,0 +1,225 @@
"""字段映射模块 — 将 Excel 列名映射到页面 DOM 元素 ID。
所有中文名→DOM ID 的对应关系写死在 BUILTIN_FIELD_MAP 中。
config.json 的 field_mapping 用于追加/覆盖自定义映射。
"""
import json
from pathlib import Path
from typing import Any
# 内置字段映射:中文名 → 页面 DOM 元素 ID(写死在代码中)
BUILTIN_FIELD_MAP: dict[str, str] = {
# ── 业务字段 ──
"单号": "waybillNo",
"运单号": "waybillNo",
"产品名称": "bizProductName",
"客户名称": "senders",
# ── 收件人 ──
"收件人": "receiverLinker",
"收件人手机": "receiverMobile",
"收件地址": "receiverAddr",
"收件邮编": "receiverPostcode",
"收件人固话": "receiverFixtel",
# ── 寄件人 ──
"寄件人": "senderLinker",
"寄件人手机": "senderMobile",
"寄件地址": "senderAddr",
"寄件邮编": "senderPostcode",
"寄件人固话": "senderFixtel",
# ── 货物 ──
"重量": "realWeight",
"内件品名": "reserved5",
# ── 电商 / 支付 ──
"电商平台": "ecommerceNo",
"付款方式": "paymentMode",
"证件类型": "consignorIdType",
"保价金额": "insuranceAmount",
"代收货款": "codAmount",
# ── 系统保留字段(通常不需要在 Excel 中填写,只用默认值)──
"运输类型": "transferType",
"内件属性": "contentsAttribute",
"件数": "quantity",
"保价标志": "insuranceFlag",
"回单标志": "receiptFlag",
"代收标志": "codFlag",
"投递方式": "deliverType",
# ── 其他 ──
"寄达地": "receiverArriveOrgName",
"寄达地区划": "receiverDistrictNo",
"备注": "senderNotes",
}
def load_config(config_path: str = "config.json") -> dict[str, Any]:
"""加载配置文件。"""
# 支持开发环境和 PyInstaller 打包后的路径
paths_to_try = [
Path(config_path),
Path(__file__).parent / config_path,
Path.cwd() / config_path,
]
# PyInstaller _MEIPASS
import sys
if getattr(sys, "frozen", False):
paths_to_try.insert(0, Path(sys._MEIPASS) / config_path)
for p in paths_to_try:
if p.exists():
with open(p, "r", encoding="utf-8") as f:
return json.load(f)
raise FileNotFoundError(f"配置文件未找到: {config_path}")
def get_field_mapping(config: dict) -> dict[str, str]:
"""获取 Excel 列名 → DOM ID 的映射(内置 + 配置覆盖)。"""
mapping = dict(BUILTIN_FIELD_MAP)
mapping.update(config.get("field_mapping", {}))
return mapping
def get_defaults(config: dict) -> dict[str, str]:
"""获取默认值(中文键 → 值)。"""
return config.get("defaults", {})
def get_system_config(config: dict) -> dict[str, Any]:
"""获取系统配置。"""
return config.get("system", {})
def get_timing_config(config: dict) -> dict[str, Any]:
"""获取延时配置。"""
return config.get("timing", {})
def get_fixed_values(config: dict) -> dict[str, dict]:
"""获取固定值设置,返回 {字段名: {value, force}}。"""
return config.get("fixed_values", {})
def get_fixed_value_str(fixed_values: dict[str, dict], field: str) -> str:
"""从固定值配置中提取字段的字符串值。"""
return fixed_values.get(field, {}).get("value", "")
# 中文显示名 → 系统内部编码(用于默认值/固定值的翻译)
# 数据来源:data.har 抓包分析
CODE_MAP: dict[str, dict[str, str]] = {
"电商平台": {
"无": "WU",
"菜鸟电子面单": "TAOBAOWAYBILL",
"淘宝": "TAOBAO",
"拼多多": "PINDUODUO",
"电子面单": "DZMD",
"大客户订单接口": "DKH",
"云打印": "CLOUD",
"批量导入": "IMPO",
"客户端": "EMSRM",
"公安部": "GAB",
"电子返单": "DZFD",
"仓库配送": "CKPS",
"无": "WU",
},
"付款方式": {
"寄付": "1",
"到付": "2",
},
"证件类型": {
"身份证": "01",
},
"运输类型": {
"陆运": "1",
},
"内件属性": {
"其他": "00",
"身份证": "01",
"录取通知书": "02",
"法院专递": "03",
"护照": "04",
"通行证": "05",
"签证": "06",
"驾驶证": "07",
"保险文书": "08",
"银行票据": "09",
"行驶证": "10",
"交通违章通知书": "11",
"纳税申报": "12",
"到货通知书": "13",
"公证书": "14",
"学生档案": "15",
"准考证": "16",
"成绩单": "17",
"药监批件": "18",
"移动配送": "19",
"劳动仲裁": "20",
"合格证": "21",
"健康证": "22",
"火车票": "23",
"房证": "24",
"企业代码证": "25",
"教材": "26",
"福利彩票": "27",
"病历": "28",
"银企对账单": "29",
"学位证书": "30",
"车辆档案": "31",
"交房通知书": "32",
"车牌": "36",
"信用卡": "41",
"检察专递": "53",
"法院专递回执": "54",
},
"保价标志": {
"不保价": "1",
},
"回单标志": {
"无回单": "1",
},
"代收标志": {
"不代收": "9",
},
"投递方式": {
"普通投递": "3",
},
}
def translate_value(excel_col: str, chinese_value: str) -> str:
"""将中文显示名翻译为系统内部编码(如 "寄付" → "1")。
如果该字段没有翻译表,或值已经是编码格式,直接返回原值。
"""
if not chinese_value:
return chinese_value
mapping = CODE_MAP.get(excel_col, {})
return mapping.get(chinese_value, chinese_value)
def reverse_translate(excel_col: str, code_value: str) -> str:
"""将系统编码翻译回中文显示名(如 "1" → "寄付")。"""
if not code_value:
return code_value
mapping = CODE_MAP.get(excel_col, {})
for chinese, code in mapping.items():
if code == code_value:
return chinese
return code_value
def resolve_field_value(
row: dict[str, str],
excel_col: str,
defaults: dict[str, str],
fixed_values: dict[str, str] = None,
) -> str:
"""解析字段值,优先级:固定值 > Excel > 默认值。结果自动翻译为系统编码。"""
fixed_values = fixed_values or {}
fixed = fixed_values.get(excel_col, "")
if fixed:
return translate_value(excel_col, fixed)
value = row.get(excel_col, "").strip()
if value:
return translate_value(excel_col, value)
return translate_value(excel_col, defaults.get(excel_col, ""))
+44
View File
@@ -0,0 +1,44 @@
"""统一日志控制 — 编译前修改 LEVEL 控制输出量。
等级:
0 = 用户级:只显示处理结果和错误
1 = 调试级:显示所有步骤细节
2 = 追踪级:显示 API 请求/响应内容
"""
import sys
from datetime import datetime
LEVEL = 2 # 编译时修改此值
_gui_callback = None # GUI 注册的回调: (msg: str) -> None
def set_gui_callback(cb):
"""注册 GUI 日志回调,所有日志同步输出到 GUI。"""
global _gui_callback
_gui_callback = cb
def _fmt(msg: str) -> str:
return f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] {msg}"
def _emit(msg: str):
print(_fmt(msg), file=sys.stderr)
if _gui_callback:
_gui_callback(msg)
def user(msg: str):
_emit(msg)
def debug(msg: str):
if LEVEL >= 1:
_emit(msg)
def trace(msg: str):
if LEVEL >= 2:
_emit(f"[trace] {msg}")
+63
View File
@@ -0,0 +1,63 @@
"""登录助手 — 解析浏览器导出的 cookies JSON,保存为 session.json。"""
from __future__ import annotations
import json
import os
import sys
def get_session_path() -> str:
if getattr(sys, "frozen", False):
base = os.path.dirname(sys.executable)
else:
base = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base, "session.json")
def parse_json_and_save(json_text: str) -> int:
"""解析浏览器导出的 cookies JSON 数组,提取目标域名的 cookie 并保存。
返回保存的 cookie 数量。
"""
raw = json.loads(json_text)
if not isinstance(raw, list):
raise ValueError("JSON 必须是数组格式")
# 过滤目标域名
target = "10.4.188.1"
cookies = []
seen = set()
for c in raw:
domain = c.get("domain", "")
if target not in domain:
continue
name = c.get("name", "").strip()
path = c.get("path", "/")
if not name:
continue
key = (name, domain, path)
if key in seen:
continue
seen.add(key)
cookies.append({
"name": name,
"value": c.get("value", ""),
"domain": domain,
"path": path,
})
data = {"cookies": cookies, "saved_from": f"https://{target}"}
session_path = get_session_path()
os.makedirs(os.path.dirname(session_path) or ".", exist_ok=True)
with open(session_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return len(cookies)
def load_session_info() -> dict | None:
"""读取已保存的 session 信息用于 UI 展示。"""
try:
with open(get_session_path(), "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return None
+813
View File
@@ -0,0 +1,813 @@
"""中国邮政揽收系统 - Excel 自动填单工具
主程序入口,提供 Tkinter GUI 界面。
"""
import asyncio
import json
import os
import sys
import threading
from datetime import datetime
from tkinter import (
BOTH,
BOTTOM,
DISABLED,
END,
HORIZONTAL,
LEFT,
NORMAL,
RIGHT,
VERTICAL,
X,
Y,
BooleanVar,
Button,
Entry,
Frame,
Label,
LabelFrame,
Listbox,
Scrollbar,
StringVar,
Text,
Tk,
Toplevel,
filedialog,
messagebox,
ttk,
)
from excel_reader import read_input_excel, validate_required_columns
from field_mapping import get_defaults, get_field_mapping, get_fixed_values, get_timing_config, load_config
# ─── 全局引擎引用 ──────────────────────────────────────────
_engine = None
_loop = None
def _get_writable_path(name: str) -> str:
"""获取可写文件路径(config.json, session.json)。
开发模式:脚本所在目录。
打包模式:exe 所在目录(_MEIPASS 是只读的)。
"""
if getattr(sys, 'frozen', False):
return os.path.join(os.path.dirname(sys.executable), name)
else:
return os.path.join(os.path.dirname(os.path.abspath(__file__)), name)
def _get_config_path():
return _get_writable_path("config.json")
def _get_session_path():
return _get_writable_path("session.json")
def _resource_path(relative_path):
"""获取只读资源文件路径(pickup.ico 等)。
开发模式:脚本所在目录。
打包模式:_MEIPASS(PyInstaller 解压临时目录)。
"""
if getattr(sys, 'frozen', False):
return os.path.join(sys._MEIPASS, relative_path)
else:
return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)
def _get_session_path():
return _resource_path("session.json")
# ─── 异步引擎后台线程 ──────────────────────────────────────
def _run_async(coro):
"""在新线程中运行异步协程。"""
global _loop
_loop = asyncio.new_event_loop()
asyncio.set_event_loop(_loop)
_loop.run_until_complete(coro)
def _run_in_thread(coro):
"""在后台线程启动异步任务。"""
t = threading.Thread(target=_run_async, args=(coro,), daemon=True)
t.start()
return t
# ─── 主窗口 ────────────────────────────────────────────────
class App:
def __init__(self):
self.root = Tk()
self.root.title("自动填单工具 v2.0")
self.root.geometry("900x650")
self.root.minsize(800, 550)
if sys.platform == "win32":
self.root.iconbitmap(_resource_path("pickup.ico"))
else:
# 在 Linux 上什么都不做,或者用 .xbm 文件
# self.root.iconbitmap(_resource_path("pickup.xbm"))
pass
self.config = {}
self.excel_path = StringVar()
self.delay_var = StringVar(value="3")
self.status_var = StringVar(value="就绪")
self.engine_status_var = StringVar(value="未启动")
self.running = False
self.paused = False
self.results = []
self._build_ui()
# 关闭窗口时清理
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
# 将 log 模块输出重定向到 GUI 日志窗口
from log import set_gui_callback
set_gui_callback(self._log)
self._load_config()
def _build_ui(self):
"""构建界面。"""
# ── 配置区 ──
config_frame = LabelFrame(self.root, text="配置", padx=10, pady=5)
config_frame.pack(fill=X, padx=10, pady=5)
# 引擎状态
row_status = Frame(config_frame)
row_status.pack(fill=X, pady=2)
Label(row_status, text="引擎状态:", width=10, anchor="e").pack(side="left")
self.status_label = Label(row_status, textvariable=self.engine_status_var, fg="gray")
self.status_label.pack(side="left", padx=5)
# Excel 文件
row2 = Frame(config_frame)
row2.pack(fill=X, pady=2)
Label(row2, text="Excel文件:", width=10, anchor="e").pack(side="left")
self.file_label = Label(row2, text="未选择", fg="gray", anchor="w")
self.file_label.pack(side="left", fill=X, expand=True, padx=5)
Button(row2, text="选择文件", command=self._pick_file, width=10).pack(
side="right"
)
# 延时设置
row3 = Frame(config_frame)
row3.pack(fill=X, pady=2)
Label(row3, text="间隔(秒):", width=10, anchor="e").pack(side="left")
Entry(row3, textvariable=self.delay_var, width=8).pack(side="left", padx=5)
Label(row3, text="(每单提交之间的等待时间)", fg="gray").pack(side="left")
# 按钮
btn_frame = Frame(config_frame)
btn_frame.pack(fill=X, pady=5)
self.login_btn = Button(
btn_frame, text="🔑 登录", command=self._do_login, width=8
)
self.login_btn.pack(side="left", padx=2)
self.start_btn = Button(
btn_frame, text="▶ 开始处理", command=self._start, bg="#4CAF50",
fg="white", width=12, height=1
)
self.start_btn.pack(side="left", padx=2)
self.pause_btn = Button(
btn_frame, text="⏸ 暂停", command=self._pause, width=10, state=DISABLED
)
self.pause_btn.pack(side="left", padx=2)
self.stop_btn = Button(
btn_frame, text="⏹ 停止", command=self._stop, width=10, state=DISABLED
)
self.stop_btn.pack(side="left", padx=2)
self.config_btn = Button(
btn_frame, text="编辑配置", command=self._edit_config, width=10
)
self.config_btn.pack(side="right", padx=2)
# 状态
Label(
btn_frame, textvariable=self.status_var, fg="blue"
).pack(side="right", padx=10)
# ── 固定值设置 ──
fixed_frame = LabelFrame(self.root, text="固定值设置(优先级:强制覆盖 > API获取值 > 固定值 > 表格 > 默认值)", padx=5, pady=5)
fixed_frame.pack(fill=X, padx=10, pady=5)
tree_f = Frame(fixed_frame)
tree_f.pack(fill=X)
columns = ("field", "value", "force")
self.fixed_tree = ttk.Treeview(tree_f, columns=columns, show="headings", height=4)
self.fixed_tree.heading("field", text="字段名")
self.fixed_tree.heading("value", text="固定值")
self.fixed_tree.heading("force", text="强制覆盖")
self.fixed_tree.column("field", width=100)
self.fixed_tree.column("value", width=260)
self.fixed_tree.column("force", width=70, anchor="center")
self.fixed_tree.pack(side="left", fill=X, expand=True)
btn_f = Frame(fixed_frame)
btn_f.pack(fill=X, pady=2)
Button(btn_f, text="添加", command=self._add_fixed_value, width=6).pack(side="left", padx=2)
Button(btn_f, text="删除", command=lambda: [self.fixed_tree.delete(i) for i in self.fixed_tree.selection()], width=6).pack(side="left", padx=2)
Button(btn_f, text="保存", command=self._save_fixed_values, width=6, bg="#4CAF50", fg="white").pack(side="left", padx=2)
Label(btn_f, text="双击编辑值 | 单击'强制覆盖'列切换 | 留空则不生效", fg="gray").pack(side="left", padx=5)
self.fixed_tree.bind("<Double-1>", self._edit_fixed_cell)
self.fixed_tree.bind("<ButtonRelease-1>", self._toggle_force)
# ── 进度条 ──
progress_frame = Frame(self.root)
progress_frame.pack(fill=X, padx=10, pady=2)
self.progress = ttk.Progressbar(
progress_frame, mode="determinate", length=100
)
self.progress.pack(fill=X)
self.progress_label = Label(progress_frame, text="", fg="gray")
self.progress_label.pack()
# ── 日志区 ──
log_frame = LabelFrame(self.root, text="运行日志", padx=10, pady=5)
log_frame.pack(fill="both", expand=True, padx=10, pady=5)
self.log_text = Text(log_frame, wrap="word", state=DISABLED, font=("Consolas", 9))
self.log_text.pack(side="left", fill="both", expand=True)
scroll_y = Scrollbar(log_frame, orient=VERTICAL, command=self.log_text.yview)
scroll_y.pack(side=RIGHT, fill=Y)
self.log_text.configure(yscrollcommand=scroll_y.set)
# ─── 配置管理 ───────────────────────────────────────────
def _load_config(self):
"""加载配置文件。首次运行时从 _MEIPASS 复制到 exe 目录。"""
try:
config_path = _get_config_path()
# 打包模式首次运行:从 _MEIPASS 复制 config.json 到可写目录
if getattr(sys, 'frozen', False) and not os.path.exists(config_path):
bundled = os.path.join(sys._MEIPASS, "config.json")
if os.path.exists(bundled):
import shutil
shutil.copy2(bundled, config_path)
self.config = load_config(config_path)
timing = get_timing_config(self.config)
delay_sec = timing.get("delay_between_rows_ms", 3000) // 1000
self.delay_var.set(str(delay_sec))
# 加载固定值到表格
for item in self.fixed_tree.get_children():
self.fixed_tree.delete(item)
for field, cfg in self.config.get("fixed_values", {}).items():
val = cfg.get("value", "")
force = "✓" if cfg.get("force") else ""
self.fixed_tree.insert("", "end", values=(field, val, force))
self._log("配置加载成功")
# 检查已保存的登录会话
self._check_session_status()
except Exception as e:
self._log(f"[error] 配置加载失败: {e}")
messagebox.showwarning("配置错误", f"配置文件加载失败:\n{e}")
def _check_session_status(self):
"""检查已保存的登录会话状态并更新 UI。"""
try:
from login_helper import load_session_info
info = load_session_info()
if info and len(info.get("cookies", [])) > 0:
self.engine_status_var.set(f"已登录 ({len(info['cookies'])} cookies)")
self.status_label.configure(fg="green")
else:
self.engine_status_var.set("未登录")
self.status_label.configure(fg="orange")
except Exception:
self.engine_status_var.set("未登录")
self.status_label.configure(fg="orange")
def _edit_config(self):
"""打开配置编辑窗口。"""
config_path = _get_config_path()
# os.startfile(config_path) if sys.platform == "win32" else None
win = Toplevel(self.root)
win.title("编辑配置")
win.geometry("700x500")
text = Text(win, wrap="none", font=("Consolas", 10))
text.pack(fill="both", expand=True, padx=5, pady=5)
with open(config_path, "r", encoding="utf-8") as f:
text.insert("1.0", f.read())
def save():
new_config = text.get("1.0", "end-1c")
try:
json.loads(new_config) # 验证 JSON
with open(config_path, "w", encoding="utf-8") as f:
f.write(new_config)
self._load_config()
win.destroy()
self._log("配置已保存并重新加载")
except json.JSONDecodeError as e:
messagebox.showerror("JSON 错误", f"配置格式错误:\n{e}")
Button(win, text="保存", command=save, bg="#4CAF50", fg="white").pack(
side="bottom", pady=5
)
# ─── 固定值设置 ────────────────────────────────────────
def _edit_fixed_cell(self, event):
"""双击编辑固定值单元格(跳过强制覆盖列)。"""
region = self.fixed_tree.identify_region(event.x, event.y)
if region != "cell":
return
column = self.fixed_tree.identify_column(event.x)
if column == "#3": # 强制覆盖列不双击编辑,单击切换
return
item = self.fixed_tree.identify_row(event.y)
if not item:
return
col_idx = int(column.replace("#", "")) - 1
old_val = self.fixed_tree.item(item, "values")[col_idx]
edit = Entry(self.root)
edit.insert(0, old_val)
edit.select_range(0, "end")
bbox = self.fixed_tree.bbox(item, column)
if bbox:
edit.place(x=bbox[0] + 185, y=bbox[1] + 310, width=bbox[2], height=bbox[3])
def save_edit(e=None):
vals = list(self.fixed_tree.item(item, "values"))
vals[col_idx] = edit.get()
self.fixed_tree.item(item, values=vals)
edit.destroy()
edit.bind("<Return>", save_edit)
edit.bind("<FocusOut>", lambda e: edit.destroy())
edit.focus_set()
def _add_fixed_value(self):
"""添加新的固定值行(默认不强制覆盖)。"""
self.fixed_tree.insert("", "end", values=("新固定值", "", ""))
def _toggle_force(self, event):
"""单击'强制覆盖'列切换开关状态。"""
region = self.fixed_tree.identify_region(event.x, event.y)
if region != "cell":
return
column = self.fixed_tree.identify_column(event.x)
if column != "#3": # force 是第3列
return
item = self.fixed_tree.identify_row(event.y)
if not item:
return
vals = list(self.fixed_tree.item(item, "values"))
vals[2] = "" if vals[2] == "✓" else "✓"
self.fixed_tree.item(item, values=vals)
def _save_fixed_values(self):
"""保存固定值到 config.json(新格式:{value, force})。"""
new_fixed = {}
for item in self.fixed_tree.get_children():
vals = self.fixed_tree.item(item, "values")
field_name = vals[0].strip()
if field_name:
new_fixed[field_name] = {
"value": vals[1].strip() if len(vals) > 1 else "",
"force": vals[2] == "✓" if len(vals) > 2 else False,
}
self.config["fixed_values"] = new_fixed
config_path = _get_config_path()
with open(config_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
cfg["fixed_values"] = new_fixed
with open(config_path, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=4)
self._log(f"固定值已保存: {len(new_fixed)} 项")
# ─── 文件选择 ───────────────────────────────────────────
def _pick_file(self):
path = filedialog.askopenfilename(
title="选择 Excel 文件",
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
)
if path:
self.excel_path.set(path)
self.file_label.configure(text=path, fg="black")
def _on_close(self):
"""关闭窗口;atexit 会强制清理 Node.js 子进程。"""
global _engine
if _engine:
_engine.request_stop()
self.root.destroy()
# ─── 控制按钮 ───────────────────────────────────────────
def _do_login(self):
"""弹窗让用户粘贴一键导出的 cookies JSON。"""
from login_helper import parse_json_and_save, load_session_info
win = Toplevel(self.root)
win.title("登录揽收系统")
win.geometry("620x360")
win.transient(self.root)
win.grab_set()
Label(win, text="登录揽收系统", font=("", 12, "bold"), pady=8).pack()
Label(win, text="使用 Cookies 导出工具,将 10.4.188.1 的所有 cookies 导出为 JSON,粘贴到下方:").pack(anchor="w", padx=10)
json_text = Text(win, height=12, wrap="word", font=("Consolas", 9))
json_text.pack(fill=BOTH, expand=True, padx=10, pady=5)
def do_save():
try:
n = parse_json_and_save(json_text.get("1.0", "end-1c"))
self._log(f"已保存 {n} 个 cookies 到 session.json")
self.engine_status_var.set("已登录")
self.status_label.configure(fg="green")
self._check_session_status()
win.destroy()
except Exception as e:
messagebox.showerror("解析失败", f"JSON 解析失败:\n{e}", parent=win)
btn_f = Frame(win)
btn_f.pack(pady=10)
Button(btn_f, text="保存并关闭", command=do_save,
bg="#4CAF50", fg="white", width=15, height=1).pack()
info = load_session_info()
if info:
Label(win, text=f"当前已保存: {len(info.get('cookies', []))} 个 cookies",
fg="gray").pack()
def _start(self):
if self.running:
return
if not self.excel_path.get():
messagebox.showwarning("提示", "请先选择 Excel 文件")
return
try:
rows, headers = read_input_excel(self.excel_path.get())
except Exception as e:
messagebox.showerror("读取错误", f"读取 Excel 失败:\n{e}")
return
if not rows:
messagebox.showwarning("提示", "Excel 中没有数据")
return
# 验证必要字段是否有数据来源(固定值 > Excel > 默认值)
fixed_values = get_fixed_values(self.config)
defaults = get_defaults(self.config)
missing = validate_required_columns(headers, fixed_values, defaults)
if missing:
msg = (
f"以下字段在所有来源中均未找到值:\n"
+ "\n".join(f" - {c}(请填入Excel / 固定值 / 默认值任一)" for c in missing)
)
messagebox.showwarning("缺少必要字段", msg)
return
self.running = True
self.paused = False
self._set_controls_state("running")
# 创建结果工作簿
self.results = []
# 更新配置中的延时
try:
delay_ms = int(self.delay_var.get()) * 1000
self.config.setdefault("timing", {})["delay_between_rows_ms"] = delay_ms
except ValueError:
pass
self._log(f"开始处理 {len(rows)} 条记录...")
self.progress["maximum"] = len(rows)
self.progress["value"] = 0
self.progress_label.configure(text=f"0 / {len(rows)}")
# 在后台线程启动异步引擎
_run_in_thread(self._run_engine(rows, headers))
async def _run_engine(self, rows, headers):
"""后台运行引擎(在 async 线程中执行)。"""
global _engine
try:
from engine import PickupEngine
field_mapping = get_field_mapping(self.config)
defaults = get_defaults(self.config)
timing = get_timing_config(self.config)
fixed_values = get_fixed_values(self.config)
_engine = PickupEngine(
url="https://10.4.188.1/pickup-web/a/postwaybill/main",
field_mapping=field_mapping,
defaults=defaults,
timing=timing,
log_callback=self._log_async,
fixed_values=fixed_values,
choose_callback=self._choose_callback,
confirm_callback=self._confirm_callback,
)
self._update_status("启动中...")
try:
await _engine.start()
except Exception as e:
self._log_async(f"[error] 启动失败: {e}")
self._update_status("启动失败")
await _engine.stop()
return
# 验证 engine.start() 中已加载了 session,且包含关键 cookie
if not _engine._sess or not _engine._sess._http.cookies:
self._log_async("[提示] 未找到登录会话,请点击'登录'按钮完成登录后再开始处理")
self._update_status("未登录")
await _engine.stop()
return
cookie_names = {c.name for c in _engine._sess._http.cookies}
missing = [n for n in ("JSESSIONID", "jdpt.session.id") if n not in cookie_names]
if missing:
self._log_async(f"[提示] 缺少关键 cookie: {', '.join(missing)},请重新登录并手动填写")
self._update_status("未登录")
await _engine.stop()
return
# 初始化页面资源和 token bridge
try:
await _engine.begin_processing()
self._update_status("后台运行中")
self._log_async("HTTP API 引擎已就绪")
except Exception as e:
self._log_async(f"[error] 初始化失败: {e}")
self._update_status("启动失败")
await _engine.stop()
return
for i, row in enumerate(rows):
if _engine._stop_requested:
break
result = await _engine.process_single_row(row)
self.results.append(result)
# 更新进度
self.root.after(0, lambda cur=i+1, total=len(rows): self._update_progress(cur, total))
# 行间延时
delay = timing.get("delay_between_rows_ms", 3000) / 1000
if i < len(rows) - 1:
await asyncio.sleep(delay)
self._log_async(f"\n处理完成!共处理 {len(self.results)} 条记录")
except Exception as e:
self._log_async(f"[error] 引擎异常: {e}")
import traceback
traceback.print_exc()
finally:
if _engine:
await _engine.stop()
self.root.after(0, self._on_finished)
def _update_progress(self, current, total):
self.progress["value"] = current
self.progress_label.configure(text=f"{current} / {total}")
self.status_var.set(f"处理中: {current}/{total}")
def _on_finished(self):
global _engine
self.running = False
self.paused = False
_engine = None
self._set_controls_state("stopped")
self.status_var.set("就绪")
self._log("处理结束")
def _pause(self):
global _engine
if not self.running:
return
if self.paused:
self.paused = False
if _engine:
_engine.resume()
self.pause_btn.configure(text="⏸ 暂停")
self.status_var.set("处理中...")
self._log("已恢复")
else:
self.paused = True
if _engine:
_engine.request_pause()
self.pause_btn.configure(text="▶ 继续")
self.status_var.set("已暂停")
self._log("已暂停")
def _stop(self):
global _engine
self.running = False
if _engine:
_engine.request_stop()
self._set_controls_state("stopped")
self.status_var.set("正在停止...")
self._log("用户停止")
def _set_controls_state(self, state):
if state == "running":
self.start_btn.configure(state=DISABLED)
self.pause_btn.configure(state=NORMAL)
self.stop_btn.configure(state=NORMAL)
else:
self.start_btn.configure(state=NORMAL)
self.pause_btn.configure(state=DISABLED, text="⏸ 暂停")
self.stop_btn.configure(state=DISABLED)
# ─── 日志 ───────────────────────────────────────────────
def _log(self, msg):
"""线程安全的日志输出。"""
self.root.after(0, self._log_sync, msg)
def _log_async(self, msg):
"""从异步线程调用(直接入队到 GUI 线程)。"""
self.root.after(0, self._log_sync, msg)
def _update_status(self, status: str):
"""更新引擎状态显示。"""
color_map = {"后台运行中": "green", "启动失败": "red", "未登录": "orange"}
self.root.after(0, lambda: self.engine_status_var.set(status))
self.root.after(0, lambda s=status: self.status_label.configure(
fg=color_map.get(s, "gray")
))
async def _choose_callback(self, title: str, options: list[str]) -> int | None:
"""弹出选择框让用户从 autocomplete 结果中选择。返回索引或 None。"""
loop = asyncio.get_event_loop()
future = loop.create_future()
result = {"index": None}
def show():
win = Toplevel(self.root)
win.title(title)
win.geometry("400x300")
win.transient(self.root)
win.grab_set()
Label(win, text=f"请选择{title},选择后批量复用:", pady=10).pack()
frame = Frame(win)
frame.pack(fill="both", expand=True, padx=10)
listbox = Listbox(frame)
scroll = Scrollbar(frame, orient=VERTICAL, command=listbox.yview)
listbox.configure(yscrollcommand=scroll.set)
scroll.pack(side=RIGHT, fill=Y)
listbox.pack(side=LEFT, fill=BOTH, expand=True)
for i, opt in enumerate(options):
listbox.insert(END, f"[{i+1}] {opt}")
def on_select():
sel = listbox.curselection()
if sel:
result["index"] = sel[0]
win.destroy()
listbox.bind("<Double-1>", lambda e: on_select())
# 关闭窗口 → result["index"] 为 None → engine 停止 batch
def on_cancel():
result["index"] = None
win.destroy()
btn_frame = Frame(win)
btn_frame.pack(pady=10)
Button(btn_frame, text="确认选择", command=on_select,
bg="#4CAF50", fg="white", width=12).pack(side=LEFT, padx=5)
Button(btn_frame, text="取消", command=on_cancel,
bg="red", fg="white", width=8).pack(side=LEFT, padx=5)
win.protocol("WM_DELETE_WINDOW", on_cancel)
listbox.focus_set()
# 等窗口关闭后通知 async 线程
def wait_then_resolve():
win.wait_window()
loop.call_soon_threadsafe(lambda: future.set_result(result["index"]))
threading.Thread(target=wait_then_resolve, daemon=True).start()
self.root.after(0, show)
return await future
async def _confirm_callback(self, fields: dict[str, str]) -> bool:
"""弹出确认框显示表单数据,用户确认后返回 True。"""
loop = asyncio.get_event_loop()
future = loop.create_future()
result = {"confirmed": False}
def show():
win = Toplevel(self.root)
win.title("确认提交信息 - 确认后自动批量处理")
win.geometry("450x400")
win.transient(self.root)
win.grab_set()
Label(win, text="请确认以下信息无误后点击确认:", pady=10,
font=("", 10, "bold")).pack()
frame = Frame(win)
frame.pack(fill=BOTH, expand=True, padx=20, pady=5)
text = Text(frame, wrap="word", font=("Consolas", 10), height=16)
text.pack(side=LEFT, fill=BOTH, expand=True)
scroll = Scrollbar(frame, command=text.yview)
scroll.pack(side=RIGHT, fill=Y)
text.configure(yscrollcommand=scroll.set)
# 系统标记(不展示)
from engine import _SYSTEM_FLAGS
sys_keys = set(_SYSTEM_FLAGS.keys())
# 关键字段:始终展示,值也翻译为中文
from field_mapping import reverse_translate
key_fields = [
("产品名称", "bizProductName", ""),
("客户名称", "senders", ""),
("客户代码", "senderNo", ""),
("邮件号", "waybillNo", ""),
("重量(g)", "realWeight", ""),
("寄达地", "receiverArriveOrgName", ""),
("收件人", "receiverLinker", ""),
("收件人手机", "receiverMobile", ""),
("收件人地址", "receiverAddr", ""),
("寄件人", "senderLinker", ""),
("寄件人手机", "senderMobile", ""),
("寄件人地址", "senderAddr", ""),
("运输类型", "transferType", "运输类型"),
("内件属性", "contentsAttribute", "内件属性"),
("电商平台", "ecommerceNo", "电商平台"),
("付款方式", "paymentMode", "付款方式"),
("内件品名", "reserved5", ""),
("保价金额", "insuranceAmount", ""),
("代收货款", "codAmount", ""),
("备注", "senderNotes", ""),
]
if not isinstance(fields, dict):
text.insert(END, f"错误: 表单数据异常 ({type(fields).__name__})")
text.configure(state=DISABLED)
return
shown = set()
for label, key, col_name in key_fields:
val = fields.get(key, "") or "空"
if col_name and val != "空":
val = reverse_translate(col_name, val)
text.insert(END, f"{label}: {val}\n")
shown.add(key)
# 展示其余非系统标记的有值字段
for k, v in fields.items():
if v and k not in shown and k not in sys_keys:
text.insert(END, f"{k}: {v}\n")
text.configure(state=DISABLED)
btn_frame = Frame(win)
btn_frame.pack(pady=10)
Button(btn_frame, text="确认提交",
command=lambda: (result.__setitem__("confirmed", True), win.destroy()),
bg="#4CAF50", fg="white", width=12, height=1).pack(side=LEFT, padx=10)
Button(btn_frame, text="取消",
command=win.destroy,
bg="red", fg="white", width=10).pack(side=LEFT, padx=10)
def wait_then_resolve():
win.wait_window()
loop.call_soon_threadsafe(lambda: future.set_result(result["confirmed"]))
threading.Thread(target=wait_then_resolve, daemon=True).start()
self.root.after(0, show)
return await future
def _log_sync(self, msg):
timestamp = datetime.now().strftime("%H:%M:%S")
line = f"[{timestamp}] {msg}\n"
self.log_text.configure(state=NORMAL)
self.log_text.insert(END, line)
self.log_text.see(END)
self.log_text.configure(state=DISABLED)
# ─── 入口 ──────────────────────────────────────────────────
def main():
app = App()
app.root.mainloop()
if __name__ == "__main__":
main()
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

+604
View File
@@ -0,0 +1,604 @@
"""揽收系统 HTTP API 封装 — 直接调用后端接口,不依赖浏览器。
依赖 Node.js token bridge 计算 $_ts 安全框架的 c93l5Fya 动态 token。
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
from typing import Any
from urllib.parse import urlparse, urljoin
from log import debug
# ─── Token Bridge 管理 ────────────────────────────────────────────────
def _get_bridge_dir() -> str:
"""获取 token_bridge 目录路径(支持 PyInstaller 打包)。"""
if getattr(sys, "frozen", False):
base = sys._MEIPASS
else:
base = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base, "token_bridge")
class TokenBridge:
"""管理 Node.js token 计算子进程(HTTP 通信)。"""
def __init__(self):
self._proc: subprocess.Popen | None = None
self._port: int = 0
self._base_url: str = ""
def start(self):
bridge_dir = _get_bridge_dir()
kwargs = dict(
cwd=bridge_dir,
stderr=subprocess.PIPE,
text=True, encoding="utf-8", errors="replace",
)
if sys.platform == "win32":
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
self._proc = subprocess.Popen(["node", "server.js", "0"], **kwargs)
# 进程退出时强制杀子进程,防止 _MEIPASS 目录无法删除
import atexit as _atexit
_atexit.register(self.stop)
# 读取就绪信号(包含端口号)
line = self._proc.stderr.readline()
try:
msg = json.loads(line.strip())
if not msg.get("ready"):
raise RuntimeError(f"Bridge startup failed: {line}")
self._port = msg["port"]
self._base_url = f"http://127.0.0.1:{self._port}"
except json.JSONDecodeError:
raise RuntimeError(f"Bridge unexpected stderr: {line}")
# 后台线程持续读取 stderr 并输出到 log
import threading as _threading
def _read_stderr():
from log import trace
for line in self._proc.stderr:
trace(f"bridge stderr: {line.strip()}")
_threading.Thread(target=_read_stderr, daemon=True).start()
def stop(self):
if self._proc:
try:
self._proc.kill()
self._proc.wait(timeout=3)
except Exception:
pass
self._proc = None
self._port = 0
def _request(self, path: str, body: dict = None) -> dict:
"""向 bridge HTTP 服务器发送请求。"""
from log import debug, trace
import requests as _requests
url = f"{self._base_url}{path}"
trace(f"bridge -> {path} {str(body)[:100] if body else ''}")
try:
if body:
resp = _requests.post(url, json=body, timeout=30)
else:
resp = _requests.get(url, timeout=30)
resp.raise_for_status()
result = resp.json()
trace(f"bridge <- {str(result)[:100]}")
if "error" in result:
raise RuntimeError(f"Bridge error: {result['error']}")
return result
except _requests.RequestException as e:
# 读取 stderr
err = ""
try:
while True:
chunk = self._proc.stderr.readline()
if not chunk:
break
err += chunk
except Exception:
pass
if err:
raise RuntimeError(f"Bridge request failed: {e}, stderr={err[:300]}")
raise
def initialize(self, html: str, scripts: list[dict], base_url: str):
return self._request("/init", {
"html": html,
"scripts": scripts,
"baseUrl": base_url,
})
def compute_token(self, method: str, path: str) -> str:
result = self._request("/token", {
"method": method,
"path": path,
})
return result["token"]
def ping(self) -> bool:
try:
self._request("/ping")
return True
except Exception:
return False
# ─── HTTP 会话 & 页面资源管理 ─────────────────────────────────────────
class PickupSession:
"""管理 HTTP 会话(cookies)和页面资源加载。"""
def __init__(self, base_url: str):
self.base_url = base_url.rstrip("/")
parsed = urlparse(self.base_url)
self._origin = f"{parsed.scheme}://{parsed.netloc}"
# 使用 requests 保持 cookie 会话(同步,足够简单可靠)
import requests as _requests
self._http: _requests.Session = _requests.Session()
self._http.verify = False # 内部自签证书
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
self._http.headers.update({
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/109.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
})
self._scripts: list[dict] = []
self._html: str = ""
self._pickup_path: str = ""
self._ts_cd: str = ""
self._ts_nsd: int = 0
@property
def cookies_str(self) -> str:
"""将 requests cookies 转为 Cookie 请求头字符串。"""
pairs = []
for cookie in self._http.cookies:
pairs.append(f"{cookie.name}={cookie.value}")
return "; ".join(pairs)
def load_session(self, session_path: str) -> bool:
"""从文件加载之前保存的 cookies。返回是否加载成功。"""
import requests as _requests
try:
with open(session_path, "r") as f:
data = json.load(f)
seen = set()
for cookie_data in data.get("cookies", []):
key = (cookie_data["name"], cookie_data.get("domain", ""), cookie_data.get("path", "/"))
if key in seen:
continue
seen.add(key)
self._http.cookies.set(
cookie_data["name"],
cookie_data["value"],
domain=cookie_data.get("domain", ""),
path=cookie_data.get("path", "/"),
)
return len(seen) > 0
except (FileNotFoundError, json.JSONDecodeError, KeyError):
return False
def save_session(self, session_path: str):
"""保存 cookies 到文件。"""
data = {
"cookies": [
{"name": c.name, "value": c.value, "domain": c.domain, "path": c.path}
for c in self._http.cookies
]
}
with open(session_path, "w") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _abs_url(self, path_or_url: str) -> str:
if path_or_url.startswith("http"):
return path_or_url
return urljoin(self._origin, path_or_url)
def fetch_page_resources(self) -> dict:
"""加载 pickup 页面及所需 JS 文件。返回 {html, scripts, base_url, nsd}。"""
import requests as _requests
pickup_path = urlparse(self.base_url).path
self._pickup_path = pickup_path
# 加载 pickup 页面(需要 Referer 满足安全框架要求)
portal_url = f"{self._origin}/portal/a"
debug(f"fetch_page: loading {self.base_url}, cookies={list(self._http.cookies.keys())}")
resp = self._http.get(
self.base_url,
headers={
"Referer": portal_url,
"Sec-Fetch-Dest": "iframe",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Upgrade-Insecure-Requests": "1",
},
timeout=30,
)
debug(f"fetch_page: status={resp.status_code}")
if resp.status_code != 200:
debug(f"fetch_page: resp_headers={dict(resp.headers)}")
debug(f"fetch_page: resp_body={resp.text[:500]}")
resp.raise_for_status()
self._html = resp.text
# 从页面 HTML 中提取 $_ts 安全参数
self._ts_cd = ""
self._ts_nsd = 0
m = re.search(r'\$_ts\.cd="([^"]*)"', self._html)
if m:
self._ts_cd = m.group(1)
m = re.search(r'\$_ts\.nsd=(\d+)', self._html)
if m:
self._ts_nsd = int(m.group(1))
if not self._ts_cd or not self._ts_nsd:
raise RuntimeError("Failed to extract $_ts.cd/nsd from page")
# 2. 提取外部 JS 引用,只保留 security JS(cfd114a.js)
# 页面业务脚本(jQuery、main.js 等)会导致 jsdom 卡死,不需要
script_urls = re.findall(
r'<script[^>]+src="([^"]+)"[^>]*>', self._html, re.IGNORECASE
)
self._scripts = []
for url in script_urls:
if "cfd114a.js" not in url:
continue
abs_url = self._abs_url(url)
try:
script_resp = self._http.get(abs_url, timeout=15)
if script_resp.status_code == 200:
self._scripts.append({"url": url, "content": script_resp.text})
debug(f"fetch_page: loaded security JS {url}")
except _requests.RequestException:
debug(f"Failed to fetch script: {abs_url}")
if not self._scripts:
raise RuntimeError("Security JS (cfd114a.js) not found on page")
return {
"html": self._html,
"scripts": self._scripts,
"base_url": self.base_url,
"ts_cd": self._ts_cd,
"ts_nsd": self._ts_nsd,
}
def request(self, method: str, path: str, token: str,
data: dict[str, str] = None) -> dict[str, Any]:
"""向 pickup-web 后端发送 API 请求。
Args:
method: HTTP 方法 (GET/POST)
path: API 路径 (如 /pickup-web/a/common/customerautocomplete)
token: c93l5Fya 安全 token
data: POST 表单数据
Returns:
JSON 响应(如果是 JSON),否则返回 {"_text": response_text}
"""
full_url = self._abs_url(path)
if "?" in full_url:
full_url += f"&c93l5Fya={token}"
else:
full_url += f"?c93l5Fya={token}"
headers = {
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Referer": self.base_url,
}
if data:
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
debug(f"API {method} {path}")
resp = self._http.request(
method, full_url,
headers=headers,
data=data or None,
timeout=30,
)
debug(f"API {method} {path} → {resp.status_code}")
if resp.status_code >= 400:
debug(f"API error body: {resp.text[:500]}")
resp.raise_for_status()
text = resp.text
if text and ("<html" in text[:300].lower() or "<!doctype" in text[:300].lower()):
raise RuntimeError("Session 已过期,返回了登录页面,请重新登录并获取 cookies")
content_type = resp.headers.get("Content-Type", "")
if "json" in content_type or text.strip().startswith(("[", "{")):
try:
return resp.json()
except ValueError:
pass
return {"_text": text}
# ─── 业务 API ─────────────────────────────────────────────────────────
class PickupAPI:
"""揽收系统业务 API 封装。
每个方法对应页面上的一个 AJAX 操作。
所有需要 token 的方法都通过 token_bridge 计算 c93l5Fya 参数。
"""
def __init__(self, session: PickupSession, bridge: TokenBridge):
self._sess = session
self._bridge = bridge
def _token(self, method: str, path: str) -> str:
return self._bridge.compute_token(method, path)
def _post(self, path: str, data: dict[str, str] = None) -> dict:
token = self._token("POST", path)
return self._sess.request("POST", path, token, data)
def _get(self, path: str) -> dict:
token = self._token("GET", path)
return self._sess.request("GET", path, token)
# ── 产品 ──────────────────────────────────────────────────
def query_product(self, keyword: str) -> list[dict]:
"""搜索产品名称,返回候选列表(data=id|bizProductNo, value=显示名)。"""
result = self._post(
"/pickup-web/a/postwaybilledit/queryproductmanager",
data={"no": "on", "query": keyword},
)
from log import trace
trace(f"query_product raw: {str(result)[:300]}")
suggestions = result.get("suggestions", []) if isinstance(result, dict) else []
# 解析 data 字段: "productId|bizProductNo"
parsed = []
for s in suggestions:
data_parts = s.get("data", "").split("|")
parsed.append({
"productId": data_parts[0] if len(data_parts) > 0 else "",
"bizProductNo": data_parts[1] if len(data_parts) > 1 else "",
"productName": s.get("value", ""),
})
return parsed
def select_product(self, biz_product_id: str, biz_product_no: str) -> dict:
"""选择产品后获取产品详情(模板、材料等)。"""
product_info = {"bizProductId": biz_product_id, "bizProductNo": biz_product_no}
# 获取模板(返回数组,取第一个)
template = self._post(
"/pickup-web/a/common/templatecodemodel",
data={"bizProductNo": biz_product_no},
)
tmpl = template[0] if isinstance(template, list) and template else template
if isinstance(tmpl, dict):
product_info.update(tmpl)
# 查询产品详情
detail = self._post(
"/pickup-web/a/common/queryproductbymanager",
data={
"bizProductId": biz_product_id,
"bizProductNo": biz_product_no,
},
)
if isinstance(detail, dict):
product_info.update(detail)
elif isinstance(detail, list) and detail:
product_info.update(detail[0])
# 查询物料信息(返回数组或 dict)
material = self._post(
"/pickup-web/a/common/findppoductmaterialid",
data={"productCode": biz_product_no},
)
mat = material[0] if isinstance(material, list) and material else material
if isinstance(mat, dict):
product_info["material"] = mat
return product_info
# ── 客户 ──────────────────────────────────────────────────
def query_customer(self, keyword: str) -> list[dict]:
"""搜索客户名称,返回候选列表(data=customerId, valueName=短名, value=全名)。"""
result = self._post(
"/pickup-web/a/common/customerautocomplete",
data={"query": keyword},
)
from log import trace
trace(f"query_customer raw: {str(result)[:300]}")
suggestions = result.get("suggestions", []) if isinstance(result, dict) else []
parsed = []
for s in suggestions:
parsed.append({
"customerId": s.get("data", ""),
"customerName": s.get("valueName", s.get("value", "")),
"customerFullName": s.get("value", ""),
})
return parsed
def select_customer(self, customer_id: str) -> dict:
"""选择客户后获取客户详情。"""
result = self._post(
"/pickup-web/a/common/customerins",
data={"customerId": customer_id},
)
if isinstance(result, dict):
return result
return {}
def find_sender_history(self, sender_no: str) -> dict:
"""根据寄件人编号查询实名历史。"""
result = self._post(
"/pickup-web/a/postwaybillargs/findrealnamehistorybysender",
data={"senderNo": sender_no},
)
if isinstance(result, dict):
return result
return {}
# ── 运单 ──────────────────────────────────────────────────
def query_waybill(self, waybill_no: str, ecommerce_no: str,
biz_product_no: str, payment_mode: str,
biz_product_id: str = "",
post_blend: str = "0",
lock_customer: str = "0") -> dict:
"""查询运单信息。"""
return self._post(
"/pickup-web/a/postwaybill/querypostwaybillmanager",
data={
"waybillNo": waybill_no,
"ecommerceNo": ecommerce_no,
"bizProductNo": biz_product_no,
"postBlend": post_blend,
"lockCustomer": lock_customer,
"paymentMode": payment_mode,
"bizProductId": biz_product_id,
},
)
# ── 地址 ──────────────────────────────────────────────────
def address_match_raw(self, receiver_addr: str) -> dict:
"""根据地址文本查询区划代码(addressMatch API)。"""
return self._post(
"/pickup-web/a/postwaybill/addressMatch",
data={"receiverAddr": receiver_addr},
)
def query_district(self, dist_code: str) -> dict:
"""根据区划代码查询地址基本信息。"""
return self._post(
"/pickup-web/a/postwaybilledit/addressbasicno",
data={"dto": dist_code},
)
def match_address(self, receiver_arrive_org_no: str,
base_product_no: str,
waybill_no: str,
receiver_addr: str,
sender_no: str = "",
transfer_type: str = "3",
contents_attribute: str = "3",
single_seal_flag: str = "0",
inter_flag: str = "0",
reserved29: str = "0",
safety_type: str = "0",
io_type: str = "10") -> dict:
"""匹配收件地址,获取寄达地网格等信息。"""
return self._post(
"/pickup-web/a/common/findGridPrammer",
data={
"receiverArriveOrgNo": receiver_arrive_org_no,
"baseProductNo": base_product_no,
"transferType": transfer_type,
"contentsAttribute": contents_attribute,
"singleSealFlag": single_seal_flag,
"interFlag": inter_flag,
"reserved29": reserved29,
"waybillNo": waybill_no,
"receiverAddr": receiver_addr,
"senderNo": sender_no,
"safetyType": safety_type,
"ioType": io_type,
},
)
# ── 校验 ──────────────────────────────────────────────────
def check_product_material(self, cargo_names: str, waybill_no: str,
product_code: str, dist_code: str) -> dict:
"""校验产品物料。"""
return self._post(
"/pickup-web/a/common/checkppoductmaterialidnew",
data={
"cargoNames": cargo_names,
"cargoOptions": "",
"waybillNo": waybill_no,
"productCode": product_code,
"distCode": dist_code,
},
)
def check_reach_area(self, product_code: str, dist_code: str,
cust_code: str, weight: str, waybill_no: str) -> dict:
"""校验可达区域。"""
return self._post(
"/pickup-web/a/common/reachareaverification",
data={
"productCode": product_code,
"distCode": dist_code,
"custCode": cust_code,
"weight": weight,
"waybillNo": waybill_no,
},
)
def check_important_address(self, waybill_no: str, addr: str) -> dict:
"""检查重要地址。"""
return self._post(
"/pickup-web/a/postwaybill/importantaddress",
data={"waybillNo": waybill_no, "queryAddr": addr},
)
def check_sensitive_customers(self, mobile_phones: str, waybill_no: str) -> dict:
"""检查敏感客户。"""
return self._post(
"/pickup-web/a/postwaybillargs/findsensitivecustomers",
data={"mobilePhone": mobile_phones, "waybillNo": waybill_no},
)
# ── 杂项查询 ──────────────────────────────────────────────
def query_ecommerce(self, query: str = "B") -> list:
"""查询电商平台列表。"""
return self._post(
"/pickup-web/a/postwaybill/queryecommerce",
data={"query": query},
)
def query_package_sort(self) -> list:
"""查询包装分类。"""
return self._post("/pickup-web/a/common/querypackagesort")
def query_deliver_date(self) -> dict:
"""查询可投递日期。"""
return self._post("/pickup-web/a/postwaybillargs/deliverdate")
def query_reason_temp_storage(self) -> dict:
"""查询暂存原因列表。"""
return self._post("/pickup-web/a/postwaybill/queryreasonfortemporarystorage")
def check_customer_warning(self) -> dict:
"""检查客户预警。"""
return self._post("/pickup-web/a/common/customerwarning")
def check_sheet_warning(self) -> dict:
"""检查面单预警。"""
return self._post("/pickup-web/a/postwaybill/sheetWarning")
# ── 提交 ──────────────────────────────────────────────────
def submit(self, form_data: dict[str, str]) -> dict:
"""提交揽收表单。返回 {"resCode": "", "resMess": "收寄成功", ...}。"""
return self._post(
"/pickup-web/a/postwaybill/isnotetosave",
data=form_data,
)
+10
View File
@@ -0,0 +1,10 @@
[project]
name = "pickup-auto"
version = "2.0.0"
description = "Add your description here"
requires-python = ">=3.10"
dependencies = [
"openpyxl>=3.1.5",
"requests>=2.31.0",
"pyinstaller>=6.20.0",
]
BIN
View File
Binary file not shown.
+783
View File
@@ -0,0 +1,783 @@
{
"name": "pickup-token-bridge",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pickup-token-bridge",
"version": "1.0.0",
"dependencies": {
"jsdom": "^24.0.0"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/cssstyle/node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"license": "MIT"
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"license": "MIT"
},
"node_modules/jsdom": {
"version": "24.1.3",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz",
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
"license": "MIT",
"dependencies": {
"cssstyle": "^4.0.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.4.3",
"form-data": "^4.0.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.5",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.12",
"parse5": "^7.1.2",
"rrweb-cssom": "^0.7.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.4",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^2.11.2"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nwsapi": {
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"license": "MIT"
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/rrweb-cssom": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/ws": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "pickup-token-bridge",
"version": "1.0.0",
"private": true,
"dependencies": {
"jsdom": "^24.0.0"
}
}
+117
View File
@@ -0,0 +1,117 @@
/**
* Token Bridge — HTTP 服务器模式。
* 在 jsdom 中运行 $_ts 安全框架,计算 c93l5Fya token。
*
* 接口:
* GET /ping → {"ok":true}
* POST /init → body: {html, scripts, baseUrl} → {"ok":true, "nsd":...}
* POST /token → body: {method, path} → {"token":"..."}
*/
const { JSDOM } = require('jsdom');
const http = require('http');
let dom = null;
let window = null;
let _cdLen = 0;
let _nsd = null;
function initialize(html, scripts, baseUrl) {
dom = new JSDOM(html, {
url: baseUrl,
runScripts: 'dangerously',
pretendToBeVisual: true,
});
window = dom.window;
_cdLen = (window.$_ts && window.$_ts.cd) ? window.$_ts.cd.length : 0;
_nsd = (window.$_ts && window.$_ts.nsd) ? window.$_ts.nsd : null;
// 拦截 XMLHttpRequest.prototype.open —— security JS 会包装它
const OriginalXHR = window.XMLHttpRequest;
OriginalXHR.prototype.open = function (method, url) {
this.__ts_method = method;
this.__ts_url = url;
};
// 注入 security JS
for (const script of scripts) {
try {
const el = window.document.createElement('script');
el.textContent = script.content;
window.document.head.appendChild(el);
} catch (_) {}
}
// 验证
const testXhr = new OriginalXHR();
testXhr.open('GET', '/_ts_test_');
const captured = testXhr.__ts_url || '';
if (!captured.includes('c93l5Fya=')) {
throw new Error(
`$_ts init failed: cd_len=${_cdLen}, nsd=${_nsd}, captured=${captured.substring(0, 80)}`
);
}
return { ok: true, nsd: _nsd };
}
function computeToken(method, path) {
if (!window) throw new Error('not initialized');
process.stderr.write(JSON.stringify({trace: 'computeToken', method, path}) + '\n');
const OriginalXHR = window.XMLHttpRequest;
process.stderr.write(JSON.stringify({trace: 'XHR class', hasXHR: !!OriginalXHR}) + '\n');
const xhr = new OriginalXHR();
process.stderr.write(JSON.stringify({trace: 'xhr created'}) + '\n');
xhr.open(method || 'GET', path);
process.stderr.write(JSON.stringify({trace: 'open called', url: (xhr.__ts_url||'').substring(0,100)}) + '\n');
const url = xhr.__ts_url || '';
const idx = url.indexOf('c93l5Fya=');
if (idx < 0) throw new Error(`Token not found for ${path}, url=${url.substring(0,80)}`);
return url.substring(idx + 'c93l5Fya='.length);
}
// ─── HTTP Server ────────────────────────────────────────────
function readBody(req) {
return new Promise((resolve) => {
let data = '';
req.on('data', c => data += c);
req.on('end', () => resolve(data));
});
}
function json(res, data, status = 200) {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
const server = http.createServer(async (req, res) => {
try {
if (req.method === 'GET' && req.url === '/ping') {
json(res, { ok: true });
} else if (req.method === 'POST' && req.url === '/init') {
const body = await readBody(req);
const { html, scripts, baseUrl } = JSON.parse(body);
const result = initialize(html, scripts, baseUrl);
json(res, result);
} else if (req.method === 'POST' && req.url === '/token') {
const body = await readBody(req);
const { method, path } = JSON.parse(body);
const token = computeToken(method, path);
json(res, { token });
} else {
json(res, { error: 'not found' }, 404);
}
} catch (e) {
process.stderr.write(JSON.stringify({ error: e.message }) + '\n');
json(res, { error: e.message }, 500);
}
});
const port = process.argv[2] || 0;
server.listen(port, '127.0.0.1', () => {
const addr = server.address();
process.stderr.write(JSON.stringify({ ready: true, port: addr.port }) + '\n');
});
Generated
+286
View File
@@ -0,0 +1,286 @@
version = 1
revision = 3
requires-python = ">=3.10"
[[package]]
name = "altgraph"
version = "0.17.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
]
[[package]]
name = "certifi"
version = "2026.4.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" },
{ url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" },
{ url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" },
{ url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" },
{ url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" },
{ url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" },
{ url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" },
{ url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" },
{ url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" },
{ url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" },
{ url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" },
{ url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" },
{ url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" },
{ url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" },
{ url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" },
{ url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" },
{ url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" },
{ url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" },
{ url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" },
{ url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" },
{ url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" },
{ url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" },
{ url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" },
{ url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" },
{ url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" },
{ url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" },
{ url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" },
{ url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" },
{ url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" },
{ url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" },
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
]
[[package]]
name = "et-xmlfile"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
]
[[package]]
name = "idna"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
name = "macholib"
version = "1.16.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
]
[[package]]
name = "openpyxl"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "et-xmlfile" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pefile"
version = "2024.8.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
]
[[package]]
name = "pickup-auto"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "openpyxl" },
{ name = "pyinstaller" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [
{ name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pyinstaller", specifier = ">=6.20.0" },
{ name = "requests", specifier = ">=2.31.0" },
]
[[package]]
name = "pyinstaller"
version = "6.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "macholib", marker = "sys_platform == 'darwin'" },
{ name = "packaging" },
{ name = "pefile", marker = "sys_platform == 'win32'" },
{ name = "pyinstaller-hooks-contrib" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/60/d03d52e6690d4e9caf333dcd14550cde634ce6c118b3bc8fa3112c3186fd/pyinstaller-6.20.0.tar.gz", hash = "sha256:95c5c7e03d5d61e9dfb8ef259c699cf492bb1041beb6dbe83696608cec07347a", size = 4048728, upload-time = "2026-04-22T20:59:36.96Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/e4/e228d6d1bbb7fd62dc660a8fb202a583b023d3a3624ca95d1a9290ee4d6a/pyinstaller-6.20.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:bf3be4e1284ee78ddccba5e29f99443a12a7b4673168288ffc4c9d38c6f7b90e", size = 1047642, upload-time = "2026-04-22T20:58:32.006Z" },
{ url = "https://files.pythonhosted.org/packages/ce/bd/afb631bcb3f9040efebd4f6d067f0828b51710818f69fb41a2d4b7787f52/pyinstaller-6.20.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72ae9c1fdea134afa791f58bdc9a1934d5c7609753c111e0026bfc272b32b712", size = 742494, upload-time = "2026-04-22T20:58:36.285Z" },
{ url = "https://files.pythonhosted.org/packages/76/08/0729a5bac14754150e5d83b39d87d842eb42b0bffcaa03dbad6252e23a39/pyinstaller-6.20.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1031bcc307f3fbeffd4e162723e64d46dbf591c82dd0997413afb2a07328b941", size = 754191, upload-time = "2026-04-22T20:58:40.603Z" },
{ url = "https://files.pythonhosted.org/packages/e6/82/bc0ee4c7b97db1958eb651e0da9fb1e672e5ae53ca8867fd97701de52906/pyinstaller-6.20.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:8df3b3f347659fa2562d8d193a98ad4600133b8b8d07c268df89e4154376750e", size = 751902, upload-time = "2026-04-22T20:58:44.7Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e7/770002d6aaa54173881cb2c49bb195ba67b97bf39bac1cdf320f28401629/pyinstaller-6.20.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b0d3cc9dd8120d448459bd3880a12e2f9774c51443af49047801446377999a59", size = 748634, upload-time = "2026-04-22T20:58:48.579Z" },
{ url = "https://files.pythonhosted.org/packages/fe/db/68ba1fccb71278b2124fb90b37b7c8c0bc4c1173fba45b94466df3d9cb7f/pyinstaller-6.20.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03696bb6350177c6bc23bcaf78e71a33c4a89b6754dd90d1be2f318e978c918b", size = 748490, upload-time = "2026-04-22T20:58:52.749Z" },
{ url = "https://files.pythonhosted.org/packages/03/0f/ac77ffa996a56be3d5c8f85734a007f8347240691657f9704e7de2527fa3/pyinstaller-6.20.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:6357f1699f6af84f37e7367f031d4f68abdba65543b83990c9e8f5a4cebed0b7", size = 747650, upload-time = "2026-04-22T20:58:57.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/56/1ee91c3a2bc10ca1f36da10a6fd55ff7efc4dec367171eb25992a827874f/pyinstaller-6.20.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0ab39c690abad26ba148e8f664f0478acc82a733997f4f22e757774832802da9", size = 747413, upload-time = "2026-04-22T20:59:01.174Z" },
{ url = "https://files.pythonhosted.org/packages/d7/55/ae264339996953c4cdf9d89d916a0a8fa26a83cf917a742fff8b9d5f3fe8/pyinstaller-6.20.0-py3-none-win32.whl", hash = "sha256:9a7637e8e44b4387b13667fdcaac86ab6b29c446c16d34d8401539b81838759c", size = 1331584, upload-time = "2026-04-22T20:59:07.201Z" },
{ url = "https://files.pythonhosted.org/packages/76/8c/300f57578882cce259bfb5ae56fda3b69caa3fe9df40a176c719920ea6e2/pyinstaller-6.20.0-py3-none-win_amd64.whl", hash = "sha256:d588844e890ee80c4365867f98146636e1849bbca8e4284bbf0c809aff0f161a", size = 1391851, upload-time = "2026-04-22T20:59:14.024Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ea/b2f8e1642aecda78c0b75c7321f708e49e10bb3c00dd4f148c40761a1527/pyinstaller-6.20.0-py3-none-win_arm64.whl", hash = "sha256:bd53282c0a73e5c95573e1ddc8e5d564d4932bec91efbaed4dc5fdff9c2ae7f2", size = 1332259, upload-time = "2026-04-22T20:59:20.509Z" },
]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2026.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/67/f4452d68793fb15beba4f19ef39a38a8822f0da7452b503c400d5a21f5c1/pyinstaller_hooks_contrib-2026.5.tar.gz", hash = "sha256:f066dfca8f7c45ff6336c9cf9fe25b4e48bfeb322a1aa24faaedfb8a8d1b0b08", size = 173689, upload-time = "2026-05-04T22:36:55.124Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/5c/fd465d11da4d12b50d7eb5d2ee2ceb780d8d049dbb489f3828d131e387af/pyinstaller_hooks_contrib-2026.5-py3-none-any.whl", hash = "sha256:ea1535783fbdac4626351709e83f3ea80b681d3a4745763ebb407b5e27342eb9", size = 457314, upload-time = "2026-05-04T22:36:53.598Z" },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
[[package]]
name = "requests"
version = "2.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" },
]
[[package]]
name = "setuptools"
version = "82.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]