SKU 通用流程:history 差分产出 shelf_events(价/有货,unknown≠售罄), shops trust 分进 tab-caches/黑市面板;礼品卡面值假差标 noise; ensure-shops --record-outcome 回填我方履约。设计见 manhuang-procurement-radar.md。
1367 lines
61 KiB
Python
Executable file
1367 lines
61 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
"""定点探针:AI 账号/订阅 + 相邻额度 + 注册基建 比价盘(P0/P1/P2)。
|
||
|
||
策略(非 RSS):
|
||
- 每 SKU 最多 1 个 GGSel 搜索探针(控时)
|
||
- 官方 MSRP 作对照(无灰市命中时展示锚价)
|
||
- SuperGrok 保留类目页 + Plati 已知 item
|
||
写入:~/.cache/manhuang/quotes/ai-accounts.json
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
import time
|
||
import urllib.error
|
||
import urllib.parse
|
||
import urllib.request
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
UA = "joy-manhuang-quote-probe/1.3 (+local-monitor; scheduled-cache)"
|
||
CACHE_DIR = Path.home() / ".cache" / "manhuang" / "quotes"
|
||
OUT = CACHE_DIR / "ai-accounts.json"
|
||
COMMUNITY = CACHE_DIR / "community-prices.json"
|
||
HISTORY = CACHE_DIR / "history"
|
||
TTL_S = 3 * 3600 # 非 --force 时,缓存未过期则直接复用
|
||
SLEEP_S = 0.25
|
||
FETCH_TIMEOUT = 16
|
||
|
||
|
||
def _ggsel(query: str, title_filter: str, channel: str = "GGSel") -> dict[str, Any]:
|
||
q = urllib.parse.quote(query)
|
||
return {
|
||
"channel_id": "ggsel",
|
||
"channel": channel,
|
||
"kind": "marketplace",
|
||
"url": f"https://ggsel.net/en/catalog?query={q}",
|
||
"parser": "ggsel_catalog",
|
||
"title_filter": title_filter,
|
||
}
|
||
|
||
|
||
def _ggsel_path(path: str, title_filter: str, channel: str = "GGSel") -> dict[str, Any]:
|
||
"""固定类目路径(比 search query 更稳)。path 如 chatgpt / claude-ai / cursor。"""
|
||
path = path.strip("/")
|
||
return {
|
||
"channel_id": "ggsel",
|
||
"channel": channel,
|
||
"kind": "marketplace",
|
||
"url": f"https://ggsel.net/en/catalog/{path}",
|
||
"parser": "ggsel_catalog",
|
||
"title_filter": title_filter,
|
||
}
|
||
|
||
|
||
def _funpay_lot(lot_id: str, channel: str = "FunPay") -> dict[str, Any]:
|
||
"""FunPay 分区 lot 页(如 Claude 3173 / ChatGPT 3559)。"""
|
||
lid = str(lot_id).strip("/")
|
||
return {
|
||
"channel_id": "funpay",
|
||
"channel": channel,
|
||
"kind": "marketplace",
|
||
"url": f"https://funpay.com/en/lots/{lid}/",
|
||
"parser": "funpay_lots",
|
||
"lot_id": lid,
|
||
}
|
||
|
||
|
||
def _sku(
|
||
sid: str,
|
||
label: str,
|
||
category: str,
|
||
*,
|
||
msrp: float | None,
|
||
msrp_note: str,
|
||
msrp_url: str,
|
||
unit: str = "mo",
|
||
query: str | None = None,
|
||
title_filter: str | None = None,
|
||
probes: list[dict[str, Any]] | None = None,
|
||
monitor: str = "",
|
||
decide: str = "",
|
||
tier: str = "p0",
|
||
) -> dict[str, Any]:
|
||
official = None
|
||
if msrp is not None:
|
||
official = {
|
||
"price": float(msrp),
|
||
"currency": "USD",
|
||
"unit": unit,
|
||
"note": msrp_note,
|
||
"url": msrp_url,
|
||
}
|
||
prs = list(probes or [])
|
||
if query and title_filter and not prs:
|
||
prs = [_ggsel(query, title_filter)]
|
||
elif query and title_filter:
|
||
# keep explicit probes; also ensure at least one ggsel if none marketplace
|
||
if not any(p.get("kind") == "marketplace" for p in prs):
|
||
prs.append(_ggsel(query, title_filter))
|
||
return {
|
||
"id": sid,
|
||
"label": label,
|
||
"category": category,
|
||
"tier": tier,
|
||
"official_msrp": official,
|
||
"probes": prs,
|
||
"monitor": monitor or f"{label} 灰市最低 vs 官方锚",
|
||
"decide": decide or "能自开不买号;灰市需验货",
|
||
}
|
||
|
||
|
||
# ── P0 AI 服务/账号 ──────────────────────────────────────────
|
||
# ── P1 AI 相邻额度/算力 ──────────────────────────────────────
|
||
# ── P2 注册基建 ──────────────────────────────────────────────
|
||
SKUS: list[dict[str, Any]] = [
|
||
# —— 已有核心 ——
|
||
{
|
||
"id": "supergrok",
|
||
"label": "SuperGrok 账号/订阅",
|
||
"category": "account",
|
||
"tier": "p0",
|
||
"official_msrp": {
|
||
"price": 30.0, "currency": "USD", "unit": "mo",
|
||
"note": "xAI SuperGrok 档常见标价", "url": "https://x.ai/grok",
|
||
},
|
||
"probes": [
|
||
{"channel_id": "ggsel", "channel": "GGSel", "kind": "marketplace",
|
||
"url": "https://ggsel.net/en/catalog/grok-accounts", "parser": "ggsel_catalog"},
|
||
{"channel_id": "ggsel", "channel": "GGSel Grok", "kind": "marketplace",
|
||
"url": "https://ggsel.net/en/catalog/grok", "parser": "ggsel_catalog"},
|
||
{"channel_id": "plati-market", "channel": "Plati.Market", "kind": "marketplace",
|
||
"url": "https://plati.market/itm/ai-grok-super-grok-account-subscription-1-3-6-12-months/5816980?lang=en-US",
|
||
"parser": "plati_item"},
|
||
_funpay_lot("3175"), # Grok Subscription
|
||
_funpay_lot("3174"), # Grok Accounts
|
||
],
|
||
"monitor": "GGSel/Plati/FunPay SuperGrok vs 官方月费",
|
||
"decide": "灰市号风险高;紧急才买",
|
||
},
|
||
_sku("openai", "OpenAI / ChatGPT Plus", "account", tier="p0",
|
||
msrp=20.0, msrp_note="ChatGPT Plus $20/mo", msrp_url="https://openai.com/chatgpt/pricing/",
|
||
probes=[
|
||
_ggsel("chatgpt plus", r"chatgpt|gpt.?plus|openai.?plus|chat.?gpt"),
|
||
_ggsel_path("chatgpt", r"chatgpt|gpt|openai|plus"),
|
||
_ggsel("chatgpt account", r"chatgpt|openai|gpt"),
|
||
_funpay_lot("3559"), # ChatGPT Subscription
|
||
_funpay_lot("1355"), # ChatGPT Accounts
|
||
],
|
||
decide="共享号易封;能自开 Plus 不买号"),
|
||
_sku("chatgpt_team", "ChatGPT Team / Business", "account", tier="p0",
|
||
msrp=25.0, msrp_note="Team 约 $25–30/席/月", msrp_url="https://openai.com/chatgpt/team/",
|
||
probes=[
|
||
_ggsel("chatgpt team", r"chatgpt|team|business|workspace"),
|
||
_ggsel("chatgpt business", r"chatgpt|business|team"),
|
||
],
|
||
decide="席位拆卖多假;企业自开"),
|
||
_sku("openai_api", "OpenAI API 额度/Key", "api_credit", tier="p0",
|
||
msrp=None, msrp_note="按量计费(对照用无固定月费)", msrp_url="https://openai.com/api/pricing/",
|
||
unit="credit",
|
||
probes=[
|
||
_ggsel("openai api key", r"openai|api.?key|gpt.?key|chatgpt.?api"),
|
||
_ggsel("chatgpt api", r"api|key|token|credit|gpt"),
|
||
],
|
||
decide="卡密/共享 key 风险极高;优先官方充值"),
|
||
_sku("claude", "Claude Pro / Max", "account", tier="p0",
|
||
msrp=20.0, msrp_note="Claude Pro $20/mo", msrp_url="https://claude.ai/pricing",
|
||
probes=[
|
||
_ggsel("claude pro", r"claude|anthropic"),
|
||
_ggsel_path("claude", r"claude|anthropic|pro|max"),
|
||
_ggsel("claude ai", r"claude|anthropic"),
|
||
_funpay_lot("3173"), # Claude Subscription
|
||
_funpay_lot("3172"), # Claude Accounts
|
||
],
|
||
decide="优先官方 Pro"),
|
||
_sku("claude_team", "Claude Team", "account", tier="p0",
|
||
msrp=30.0, msrp_note="Team 席位量级对照", msrp_url="https://claude.ai/pricing",
|
||
query="claude team", title_filter=r"claude|team|anthropic|workspace",
|
||
decide="团队席自开;灰市共享勿用生产"),
|
||
_sku("claude_api", "Claude / Anthropic API", "api_credit", tier="p0",
|
||
msrp=None, msrp_note="按量", msrp_url="https://www.anthropic.com/pricing",
|
||
unit="credit", query="claude api key", title_filter=r"claude|anthropic|api\s*key",
|
||
decide="官方 Console 充值优先"),
|
||
_sku("gemini", "Gemini Advanced / AI Pro", "account", tier="p0",
|
||
msrp=19.99, msrp_note="Google AI Pro ≈ $19.99/mo", msrp_url="https://gemini.google/subscriptions/",
|
||
probes=[
|
||
_ggsel("gemini advanced", r"gemini|google.?ai|advanced|ai.?pro"),
|
||
_ggsel("google one ai", r"gemini|google|ai.?pro|one"),
|
||
],
|
||
decide="常绑 Google One;注意区服"),
|
||
_sku("gemini_api", "Gemini API / AI Studio", "api_credit", tier="p0",
|
||
msrp=None, msrp_note="免费档+按量", msrp_url="https://ai.google.dev/pricing",
|
||
unit="credit", query="gemini api key", title_filter=r"gemini|google\s*ai|studio|api",
|
||
decide="免费档够用则不买;付费走官方"),
|
||
_sku("cursor", "Cursor Pro", "subscription", tier="p0",
|
||
msrp=20.0, msrp_note="Cursor Pro $20/mo", msrp_url="https://cursor.com/pricing",
|
||
probes=[
|
||
_ggsel("cursor pro", r"cursor"),
|
||
_ggsel_path("cursor", r"cursor|pro|ide"),
|
||
_ggsel("cursor ai", r"cursor"),
|
||
],
|
||
decide="编码刚需;灰市共享易封号"),
|
||
_sku("cursor_biz", "Cursor Business", "subscription", tier="p0",
|
||
msrp=40.0, msrp_note="Business 档量级对照", msrp_url="https://cursor.com/pricing",
|
||
query="cursor business", title_filter=r"cursor|business|team",
|
||
decide="团队走官方 Business"),
|
||
_sku("copilot", "GitHub Copilot Pro", "subscription", tier="p0",
|
||
msrp=10.0, msrp_note="Copilot Pro $10/mo · Business $19", msrp_url="https://github.com/features/copilot#pricing",
|
||
query="github copilot", title_filter=r"copilot|github",
|
||
decide="教育/开源免费资格先查;否则官方"),
|
||
_sku("windsurf", "Windsurf / Cascade", "subscription", tier="p0",
|
||
msrp=15.0, msrp_note="订阅档量级对照", msrp_url="https://windsurf.com/pricing",
|
||
query="windsurf", title_filter=r"windsurf|cascade|codeium",
|
||
decide="与 Cursor 比性价比后自开"),
|
||
_sku("perplexity", "Perplexity Pro", "subscription", tier="p0",
|
||
msrp=20.0, msrp_note="Pro $20/mo", msrp_url="https://www.perplexity.ai/pro",
|
||
query="perplexity pro", title_filter=r"perplexity|pro",
|
||
decide="检索刚需可自开"),
|
||
_sku("midjourney", "Midjourney", "subscription", tier="p0",
|
||
msrp=10.0, msrp_note="Basic $10 起", msrp_url="https://www.midjourney.com/account",
|
||
query="midjourney", title_filter=r"midjourney|mj\b",
|
||
decide="共享号多;订阅自开更稳"),
|
||
_sku("runway", "Runway", "subscription", tier="p0",
|
||
msrp=15.0, msrp_note="Standard 档量级", msrp_url="https://runwayml.com/pricing",
|
||
query="runway ml", title_filter=r"runway",
|
||
decide="视频生成贵;按项目自开"),
|
||
_sku("kling", "Kling / 可灵", "subscription", tier="p0",
|
||
msrp=10.0, msrp_note="国内档位对照(美元约)", msrp_url="https://klingai.com/",
|
||
query="kling ai", title_filter=r"kling|可灵|kuaishou",
|
||
decide="区服与支付方式影响价"),
|
||
_sku("xai_api", "xAI / Grok API 额度", "api_credit", tier="p0",
|
||
msrp=None, msrp_note="按量", msrp_url="https://x.ai/api",
|
||
unit="credit", query="grok api", title_filter=r"grok|xai|x\.ai|api",
|
||
decide="与 SuperGrok 账号拆开监控"),
|
||
_sku("openrouter", "OpenRouter 余额", "api_credit", tier="p0",
|
||
msrp=None, msrp_note="预充值按量", msrp_url="https://openrouter.ai/credits",
|
||
unit="credit",
|
||
probes=[
|
||
_ggsel("openrouter", r"open.?router"),
|
||
# community overlay 会补 token 指数价
|
||
],
|
||
decide="统一多模型采购优选官方充值;价看 GitHub 周更表"),
|
||
_sku("poe", "Poe 订阅", "subscription", tier="p0",
|
||
msrp=19.99, msrp_note="Poe 订阅档", msrp_url="https://poe.com/",
|
||
query="poe subscription", title_filter=r"\bpoe\b|quora",
|
||
decide="多模型入口;灰市点券慎买"),
|
||
_sku("notion_ai", "Notion AI", "subscription", tier="p0",
|
||
msrp=10.0, msrp_note="Notion AI add-on 量级", msrp_url="https://www.notion.so/pricing",
|
||
query="notion ai", title_filter=r"notion",
|
||
decide="绑 Workspace;自开"),
|
||
_sku("ms_copilot", "Microsoft Copilot 365", "subscription", tier="p0",
|
||
msrp=30.0, msrp_note="Copilot for M365 量级/用户", msrp_url="https://www.microsoft.com/microsoft-365/copilot",
|
||
query="microsoft copilot", title_filter=r"copilot|microsoft\s*365|office",
|
||
decide="企业协议走官方"),
|
||
_sku("v0", "v0 / Lovable / Bolt 类", "subscription", tier="p0",
|
||
msrp=20.0, msrp_note="生成式前端订阅量级", msrp_url="https://v0.dev/pricing",
|
||
query="v0 dev", title_filter=r"\bv0\b|lovable|bolt\.new|boltai",
|
||
decide="按项目用量选官方"),
|
||
_sku("clinepass", "ClinePass", "subscription", tier="p0",
|
||
msrp=9.99, msrp_note="$9.99/mo 开源权重额度", msrp_url="https://cline.bot/cline-pass",
|
||
probes=[{"channel_id": "official-msrp", "channel": "官方 ClinePass", "kind": "msrp",
|
||
"url": "https://cline.bot/cline-pass", "parser": "official_clinepass"},
|
||
_ggsel("clinepass", r"cline\s*pass|clinepass|cline")],
|
||
decide="默认官方"),
|
||
_sku("opencode_go", "OpenCode Go", "subscription", tier="p0",
|
||
msrp=10.0, msrp_note="首月 $5 → $10/mo;集群主采 Plati(Google 号+卖家代付 Stripe)",
|
||
msrp_url="https://opencode.ai/go",
|
||
probes=[
|
||
{"channel_id": "official-msrp", "channel": "官方 Go", "kind": "msrp",
|
||
"url": "https://opencode.ai/go", "parser": "official_go"},
|
||
# 自动比价:GGSel;人工主源 Plati(parser 仅 plati_item 需具体商品 URL,搜索页另议)
|
||
_ggsel("opencode go", r"opencode|go\s*\$|open.?code"),
|
||
],
|
||
decide="主采 Plati(号+代付)→ prep/post-pay → opencode-cc;GGSel 仅交叉价;见 opencode-go-fleet-runbook §10"),
|
||
_sku("ollama_cloud", "Ollama Cloud Pro", "subscription", tier="p0",
|
||
msrp=20.0, msrp_note="Pro $20 · Max $100", msrp_url="https://ollama.com/pricing",
|
||
probes=[{"channel_id": "official-msrp", "channel": "官方 Pricing", "kind": "msrp",
|
||
"url": "https://ollama.com/pricing", "parser": "official_ollama"},
|
||
_ggsel("ollama cloud", r"ollama")],
|
||
decide="Pro 自开更稳"),
|
||
|
||
# —— P1 AI 相邻 ——
|
||
_sku("newapi_relay", "中转站/NewAPI 额度", "relay", tier="p1",
|
||
msrp=None, msrp_note="各站充值无统一 MSRP;token 指数见 community", msrp_url="",
|
||
unit="credit",
|
||
probes=[
|
||
_ggsel("chatgpt api", r"api|gpt|claude|中转|newapi|oneapi|key|token"),
|
||
_ggsel("claude api", r"claude|api|key|anthropic"),
|
||
],
|
||
decide="看延迟/纯血/风控;小额试充;价表见 awesome-ai-api-proxy"),
|
||
_sku("groq", "Groq 额度", "api_credit", tier="p1",
|
||
msrp=None, msrp_note="按量", msrp_url="https://groq.com/pricing/",
|
||
unit="credit", query="groq api", title_filter=r"groq",
|
||
decide="官方免费档+付费"),
|
||
_sku("together", "Together / Fireworks", "api_credit", tier="p1",
|
||
msrp=None, msrp_note="按量", msrp_url="https://www.together.ai/pricing",
|
||
unit="credit", query="together ai", title_filter=r"together|fireworks",
|
||
decide="开源权重托管走官方"),
|
||
_sku("hf_pro", "HuggingFace Pro", "subscription", tier="p1",
|
||
msrp=9.0, msrp_note="Pro $9/mo 量级", msrp_url="https://huggingface.co/pricing",
|
||
query="huggingface pro", title_filter=r"hugging\s*face|hf\s*pro",
|
||
decide="需要私有/加速再开"),
|
||
_sku("replicate", "Replicate / fal.ai", "api_credit", tier="p1",
|
||
msrp=None, msrp_note="按量", msrp_url="https://replicate.com/pricing",
|
||
unit="credit", query="replicate api", title_filter=r"replicate|fal\.ai|falai",
|
||
decide="生图视频 API 官方充值"),
|
||
_sku("elevenlabs", "ElevenLabs", "subscription", tier="p1",
|
||
msrp=5.0, msrp_note="Starter 档起", msrp_url="https://elevenlabs.io/pricing",
|
||
query="elevenlabs", title_filter=r"eleven|11labs|elevenlabs",
|
||
decide="语音合成;共享号风险"),
|
||
_sku("suno", "Suno / Udio", "subscription", tier="p1",
|
||
msrp=10.0, msrp_note="Pro 档量级", msrp_url="https://suno.com/",
|
||
query="suno ai", title_filter=r"suno|udio",
|
||
decide="音乐生成订阅"),
|
||
_sku("firefly", "Adobe Firefly / Canva AI", "subscription", tier="p1",
|
||
msrp=10.0, msrp_note="捆绑 Creative/Canva 套餐", msrp_url="https://www.adobe.com/products/firefly.html",
|
||
query="adobe firefly", title_filter=r"firefly|adobe|canva",
|
||
decide="设计侧捆绑账号"),
|
||
_sku("gpu_cloud", "GPU 云时 (RunPod/Vast/AutoDL)", "compute", tier="p1",
|
||
msrp=0.4, msrp_note="≈$0.2–0.8/h 消费卡量级(对照)", msrp_url="https://www.runpod.io/pricing",
|
||
unit="hour", query="runpod", title_filter=r"runpod|vast\.ai|autodl|gpu",
|
||
decide="比小时价+网络;勿买来路不明面板"),
|
||
|
||
# —— P2 注册基建 ——
|
||
_sku("sms_otp", "接码 OTP (5sim 类)", "infra", tier="p2",
|
||
msrp=0.15, msrp_note="≈$0.05–0.5/条 国别差异大", msrp_url="https://5sim.net/",
|
||
unit="sms", query="sms receive", title_filter=r"sms|otp|接码|virtual\s*number|5sim",
|
||
decide="按国别比单价;拒 exit-scam 平台"),
|
||
_sku("bulk_mail", "成品邮箱/老号", "infra", tier="p2",
|
||
msrp=0.5, msrp_note="≈$0.1–2/个 视平台", msrp_url="",
|
||
unit="account", query="gmail account", title_filter=r"gmail|outlook|email|mail\s*account|hotmail",
|
||
decide="注册前置;优先自产注册机"),
|
||
_sku("residential_proxy", "住宅代理", "infra", tier="p2",
|
||
msrp=5.0, msrp_note="≈$/GB 或月套餐量级", msrp_url="",
|
||
unit="gb", query="residential proxy", title_filter=r"residential|proxy|socks|isp",
|
||
decide="看出口纯净度;小额试"),
|
||
_sku("captcha_solve", "打码 (2Captcha 类)", "infra", tier="p2",
|
||
msrp=1.0, msrp_note="≈$/千次量级", msrp_url="https://2captcha.com/",
|
||
unit="1k", query="2captcha", title_filter=r"captcha|2captcha|anticaptcha|rucaptcha",
|
||
decide="注册基建成本;官方 API"),
|
||
_sku("antidetect", "指纹浏览器席位", "infra", tier="p2",
|
||
msrp=10.0, msrp_note="≈$5–30/席/月", msrp_url="",
|
||
unit="mo", query="antidetect browser", title_filter=r"antidetect|gologin|multilogin|ads\s*power|dolphin",
|
||
decide="按席位月费;能少开少开"),
|
||
_sku("giftcard_us", "美区 App Store / iTunes 卡", "infra", tier="p2",
|
||
msrp=100.0, msrp_note="面值 $100 对照折扣率", msrp_url="",
|
||
unit="face", query="itunes gift card usa",
|
||
# 必须 gift card + 美区/USD;排除 TR/账号
|
||
title_filter=r"(gift\s*card|iTunes).{0,40}(USA|US\b|USD)|Apple iTunes Gift Card.{0,20}USD",
|
||
decide="开 Plus/Claude 支付;看折扣率(条目价≠面值时看标题面额)"),
|
||
|
||
# —— 包 A:支付开号(高杠杆)——
|
||
_sku("giftcard_tr", "土区 Apple / Google 礼品卡", "pay", tier="pay",
|
||
msrp=100.0, msrp_note="面值对照(常用于低价开 AI)", msrp_url="",
|
||
unit="face", query="turkey itunes gift",
|
||
title_filter=r"(gift\s*card|iTunes|Google\s*Play).{0,40}(Turkey|Türkiye|TRY|TL\b)|Turkey.{0,20}(gift|iTunes)",
|
||
decide="区服开订阅常用;算折扣+封区风险"),
|
||
_sku("giftcard_hk", "港区 Apple / Google 礼品卡", "pay", tier="pay",
|
||
msrp=100.0, msrp_note="面值对照", msrp_url="",
|
||
unit="face", query="hong kong itunes gift card",
|
||
title_filter=r"(gift\s*card|iTunes|Google\s*Play).{0,40}(Hong\s*Kong|HK\b)|Hong\s*Kong.{0,20}(gift|iTunes)",
|
||
decide="港区支付通道;对照美区折扣"),
|
||
_sku("virtual_card", "虚拟卡开卡/充值损耗", "pay", tier="pay",
|
||
msrp=5.0, msrp_note="开卡费+充值损耗量级 $/次或月", msrp_url="",
|
||
unit="mo",
|
||
probes=[
|
||
_ggsel("virtual debit card", r"virtual\s*(debit|credit)?\s*card|prepaid\s*card|virtual\s*visa"),
|
||
_ggsel("prepaid visa card", r"prepaid|virtual\s*visa|virtual\s*mastercard|debit\s*card"),
|
||
],
|
||
decide="决定自开是否划算;监控费率勿囤来路不明卡"),
|
||
_sku("sms_us", "接码 · 美国号", "pay", tier="pay",
|
||
msrp=0.3, msrp_note="≈$/条 US", msrp_url="https://5sim.net/",
|
||
unit="sms", query="usa sms receive", title_filter=r"usa|us\b|united\s*states|sms|otp",
|
||
decide="OpenAI/Claude 注册常用;比成功率"),
|
||
_sku("sms_nl", "接码 · 荷兰号", "pay", tier="pay",
|
||
msrp=0.25, msrp_note="≈$/条 NL", msrp_url="https://5sim.net/",
|
||
unit="sms", query="netherlands sms", title_filter=r"netherlands|nl\b|dutch|sms",
|
||
decide="欧区注册备选"),
|
||
_sku("sms_id", "接码 · 印尼号", "pay", tier="pay",
|
||
msrp=0.08, msrp_note="≈$/条 ID 便宜但拒收多", msrp_url="https://5sim.net/",
|
||
unit="sms", query="indonesia sms", title_filter=r"indonesia|id\b|sms",
|
||
decide="便宜≠能过;小额试成功率"),
|
||
_sku("edu_perk", "教育认证 / 学生通道", "pay", tier="pay",
|
||
msrp=0.0, msrp_note="官方学生免费/折扣(Copilot/Cursor 等)", msrp_url="https://education.github.com/",
|
||
unit="mo", query="student email edu", title_filter=r"edu|student|university|教育",
|
||
decide="有资格走官方教育;灰市 .edu 高风险"),
|
||
|
||
# —— 包 B:调用/算力专线 ——
|
||
_sku("codex_quota", "Codex / ChatGPT 编程额度", "api_credit", tier="call",
|
||
msrp=20.0, msrp_note="与 Plus 拆开的编程额度对照", msrp_url="https://openai.com/chatgpt/pricing/",
|
||
unit="mo",
|
||
probes=[
|
||
_ggsel("chatgpt codex", r"codex|chatgpt|o1|o3|programming|plus"),
|
||
_ggsel("chatgpt plus", r"chatgpt|plus|openai|gpt"),
|
||
_ggsel_path("chatgpt", r"chatgpt|codex|plus|openai"),
|
||
_funpay_lot("3559"),
|
||
_funpay_lot("1356"), # Other / API 向
|
||
],
|
||
decide="号池 dead 时优先看此项;共享号易封"),
|
||
_sku("chatgpt_pro", "ChatGPT Pro 高档", "account", tier="call",
|
||
msrp=200.0, msrp_note="Pro 高档量级对照(若在售)", msrp_url="https://openai.com/chatgpt/pricing/",
|
||
unit="mo",
|
||
probes=[
|
||
_ggsel("chatgpt pro", r"chatgpt.?pro|gpt.?pro|pro.?200|\$200"),
|
||
_ggsel("chatgpt plus", r"chatgpt|plus|pro|openai"),
|
||
_funpay_lot("3559"),
|
||
],
|
||
decide="重度才考虑;灰市高仿多"),
|
||
_sku("claude_max", "Claude Max 高档", "account", tier="call",
|
||
msrp=200.0, msrp_note="Max 高档量级对照", msrp_url="https://claude.ai/pricing",
|
||
unit="mo",
|
||
probes=[
|
||
_ggsel("claude max", r"claude.?max|anthropic.?max|claude"),
|
||
_ggsel_path("claude", r"claude|max|pro|anthropic"),
|
||
_funpay_lot("3173"),
|
||
],
|
||
decide="编码重度;优先官方"),
|
||
_sku("supergrok_heavy", "SuperGrok Heavy", "account", tier="call",
|
||
msrp=300.0, msrp_note="SuperGrok Heavy 高档量级对照", msrp_url="https://x.ai/pricing",
|
||
unit="mo", query="super grok heavy", title_filter=r"super\s*grok\s*heavy|grok\s*heavy|heavy",
|
||
decide="最高档锚;灰市假多,优先官方"),
|
||
_sku("google_ai_ultra", "Google AI Ultra", "account", tier="call",
|
||
msrp=249.99, msrp_note="Google AI Ultra 高档量级对照", msrp_url="https://one.google.com/ai",
|
||
unit="mo", query="google ai ultra", title_filter=r"google\s*ai\s*ultra|ai\s*ultra|gemini\s*ultra",
|
||
decide="Google One AI Ultra 锚;区服价差大"),
|
||
_sku("deepseek", "DeepSeek 官方/渠道额度", "api_credit", tier="call",
|
||
msrp=None, msrp_note="按量 RMB/USD", msrp_url="https://platform.deepseek.com/",
|
||
unit="credit", query="deepseek api", title_filter=r"deepseek|深度求索",
|
||
decide="国内主路径;官方充值优先"),
|
||
_sku("siliconflow", "硅基流动额度", "api_credit", tier="call",
|
||
msrp=None, msrp_note="按量充值", msrp_url="https://siliconflow.cn/",
|
||
unit="credit", query="siliconflow", title_filter=r"silicon|硅基|siliconflow",
|
||
decide="开源权重托管;官方/代理差价"),
|
||
_sku("zhipu", "智谱 GLM 额度", "api_credit", tier="call",
|
||
msrp=None, msrp_note="按量", msrp_url="https://open.bigmodel.cn/",
|
||
unit="credit",
|
||
probes=[
|
||
_ggsel("zhipu glm api", r"zhipu|智谱|glm|bigmodel"),
|
||
_funpay_lot("4400"), # GLM Accounts
|
||
],
|
||
decide="国内 API;发票场景走官方"),
|
||
_sku("relay_premium", "中转站·高并发/纯血档", "relay", tier="call",
|
||
msrp=None, msrp_note="各站无统一价;token 指数见 community-prices", msrp_url="",
|
||
unit="credit",
|
||
probes=[
|
||
_ggsel("chatgpt api premium", r"api|gpt|claude|premium|pure|中转|key"),
|
||
_ggsel("openai api", r"openai|api|key|gpt"),
|
||
],
|
||
decide="小额试延迟/风控;与 OpenRouter + awesome 价表对照"),
|
||
_sku("gpu_4090", "GPU 4090 云时", "compute", tier="call",
|
||
msrp=0.35, msrp_note="≈$0.2–0.5/h 对照", msrp_url="https://www.runpod.io/pricing",
|
||
unit="hour", query="4090 gpu cloud", title_filter=r"4090|rtx",
|
||
decide="训推性价比首选消费卡"),
|
||
_sku("gpu_h100", "GPU H100 云时", "compute", tier="call",
|
||
msrp=2.5, msrp_note="≈$1.5–4/h 对照", msrp_url="https://www.runpod.io/pricing",
|
||
unit="hour", query="h100 gpu", title_filter=r"h100|hopper",
|
||
decide="大模型才上;对比区域与磁盘"),
|
||
]
|
||
|
||
|
||
def _now() -> str:
|
||
return datetime.now(timezone.utc).isoformat()
|
||
|
||
|
||
def _fetch(url: str, timeout: int = FETCH_TIMEOUT) -> tuple[int, str]:
|
||
if not url:
|
||
return 0, ""
|
||
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"})
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||
return r.status, r.read().decode("utf-8", "replace")
|
||
except urllib.error.HTTPError as e:
|
||
try:
|
||
body = e.read().decode("utf-8", "replace")
|
||
except Exception:
|
||
body = ""
|
||
return e.code, body
|
||
except Exception as e:
|
||
return 0, str(e)[:200]
|
||
|
||
|
||
def _parse_money_list(text: str) -> list[float]:
|
||
out = []
|
||
for m in re.finditer(r"(\d+)[,.](\d{1,2})\s*\$", text):
|
||
try:
|
||
v = float(f"{m.group(1)}.{m.group(2)}")
|
||
except ValueError:
|
||
continue
|
||
if 0.01 <= v <= 500:
|
||
out.append(v)
|
||
for m in re.finditer(r"\$\s*(\d+)(?:\.(\d{1,2}))?", text):
|
||
try:
|
||
v = float(m.group(1) + ("." + m.group(2) if m.group(2) else ""))
|
||
except ValueError:
|
||
continue
|
||
if 0.01 <= v <= 500:
|
||
out.append(v)
|
||
return out
|
||
|
||
|
||
|
||
def parse_face_from_title(title: str) -> dict[str, Any]:
|
||
"""从礼品卡标题抽面值区间。返回 face_min/max/currency/kind=fixed|range。"""
|
||
s = title or ""
|
||
out: dict[str, Any] = {"face_min": None, "face_max": None, "face_currency": None, "face_kind": None}
|
||
m = re.search(r"(\d+)\s*[-–~到至]\s*(\d+)\s*(USD|US\$|\$)", s, re.I)
|
||
if m:
|
||
a, b = float(m.group(1)), float(m.group(2))
|
||
out.update(face_min=min(a, b), face_max=max(a, b), face_currency="USD",
|
||
face_kind="range" if a != b else "fixed")
|
||
return out
|
||
m = re.search(r"(?:USD|US\$|\$)\s*(\d+)(?:\s*[-–~]\s*(\d+))?", s, re.I)
|
||
if m:
|
||
a = float(m.group(1))
|
||
b = float(m.group(2)) if m.group(2) else a
|
||
out.update(face_min=min(a, b), face_max=max(a, b), face_currency="USD",
|
||
face_kind="range" if a != b else "fixed")
|
||
return out
|
||
m = re.search(r"(\d+)\s*(USD|US\$)\b", s, re.I)
|
||
if m:
|
||
a = float(m.group(1))
|
||
out.update(face_min=a, face_max=a, face_currency="USD", face_kind="fixed")
|
||
return out
|
||
m = re.search(r"(\d+)\s*[-–~]\s*(\d+)\s*(TRY|TL)\b", s, re.I)
|
||
if m:
|
||
a, b = float(m.group(1)), float(m.group(2))
|
||
out.update(face_min=min(a, b), face_max=max(a, b), face_currency="TRY",
|
||
face_kind="range" if a != b else "fixed")
|
||
return out
|
||
m = re.search(r"(\d+)\s*(TRY|TL)\b", s, re.I)
|
||
if m:
|
||
a = float(m.group(1))
|
||
out.update(face_min=a, face_max=a, face_currency="TRY", face_kind="fixed")
|
||
return out
|
||
m = re.search(r"(\d+)\s*(HKD|HK\$)", s, re.I)
|
||
if m:
|
||
a = float(m.group(1))
|
||
out.update(face_min=a, face_max=a, face_currency="HKD", face_kind="fixed")
|
||
return out
|
||
return out
|
||
|
||
|
||
# 粗汇率:仅用于折扣估计(非财务)
|
||
_FACE_FX = {"USD": 1.0, "TRY": 0.029, "TL": 0.029, "HKD": 0.128}
|
||
|
||
|
||
def face_discount_fields(title: str, price_usd: float) -> dict[str, Any]:
|
||
"""listing 价 + 标题面值 → 折扣估计(诚实:range 给 at_min/at_max)。"""
|
||
face = parse_face_from_title(title)
|
||
out = dict(face)
|
||
out["listing_usd"] = price_usd
|
||
out["discount"] = None
|
||
out["discount_at_min"] = None
|
||
out["discount_at_max"] = None
|
||
out["discount_note"] = None
|
||
ccy = face.get("face_currency")
|
||
fx = _FACE_FX.get(str(ccy or "").upper()) or _FACE_FX.get(str(ccy or ""))
|
||
if not fx or not face.get("face_min") or price_usd is None or price_usd <= 0:
|
||
out["discount_note"] = "no_face"
|
||
return out
|
||
fmin = float(face["face_min"]) * fx
|
||
fmax = float(face["face_max"]) * fx
|
||
if fmin <= 0 or fmax <= 0:
|
||
out["discount_note"] = "bad_face"
|
||
return out
|
||
dmin = round(price_usd / fmin, 4)
|
||
dmax = round(price_usd / fmax, 4)
|
||
out["discount_at_min"] = dmin
|
||
out["discount_at_max"] = dmax
|
||
if face.get("face_kind") == "fixed":
|
||
out["discount"] = dmin
|
||
out["discount_note"] = "fixed_face"
|
||
else:
|
||
out["discount"] = None
|
||
out["discount_note"] = "multi_denom_listing_min_price"
|
||
return out
|
||
|
||
|
||
def _ggsel_title_ok(blob: str, title_filter: str | None) -> bool:
|
||
"""弱匹配硬化:filter 命中,且拒绝纯礼品卡/游戏 noise(除非 filter 明确要卡)。"""
|
||
if not title_filter:
|
||
return True
|
||
if not re.search(title_filter, blob, re.I):
|
||
return False
|
||
# 若 filter 不是礼品卡向,丢掉明显礼品卡/充值卡误伤
|
||
if not re.search(r"gift|itunes|card|礼品|itunes", title_filter, re.I):
|
||
if re.search(r"gift\s*card|itunes|steam\s*wallet|roblox|pubg|valorant", blob, re.I):
|
||
# 仍允许 blob 同时含目标品牌
|
||
if not re.search(r"chatgpt|claude|cursor|gemini|openai|grok|copilot|midjourney", blob, re.I):
|
||
return False
|
||
return True
|
||
|
||
|
||
def parse_ggsel_catalog(html: str, title_filter: str | None = None) -> list[dict]:
|
||
"""从 GGSel 目录 HTML 抽 product + 价(启发式)。
|
||
|
||
模式 C:SSR 内嵌 id_goods JSON 的 price_wme(礼品卡页几乎无 $ 明文,必须走此路)。
|
||
模式 A/B:旧 product slug + 邻近 $。
|
||
"""
|
||
quotes = []
|
||
gift_mode = bool(title_filter and re.search(
|
||
r"gift|itunes|card|礼品|app\s*store|virtual\s*card|debit|prepaid|visa",
|
||
title_filter, re.I,
|
||
))
|
||
price_lo = 0.15 if gift_mode else 0.5
|
||
|
||
# 模式 C:嵌入式商品 JSON(name + price_wme ≈ USD)
|
||
# 结构:{"id_goods":N, ... "name":"...", ... "price_wme":"1.81"...}
|
||
for m in re.finditer(r'\{"id_goods":(\d+),', html):
|
||
chunk = html[m.start(): m.start() + 2800]
|
||
nm = re.search(r'"name":"((?:[^"\\]|\\.){5,180})"', chunk)
|
||
pr = re.search(r'"price_wme":"?([0-9.]+)"?', chunk)
|
||
ur = re.search(r'"url":"([^"]+)"', chunk)
|
||
if not (nm and pr):
|
||
continue
|
||
try:
|
||
# unescape minimal
|
||
title = bytes(nm.group(1), "utf-8").decode("unicode_escape", errors="replace")
|
||
except Exception:
|
||
title = nm.group(1)
|
||
title = title.replace("\\/", "/").replace('\\"', '"')
|
||
try:
|
||
price = float(pr.group(1))
|
||
except ValueError:
|
||
continue
|
||
blob = f"{title} {ur.group(1) if ur else ''}"
|
||
if not _ggsel_title_ok(blob, title_filter):
|
||
continue
|
||
if not (price_lo <= price <= 500):
|
||
continue
|
||
# 礼品卡:必须标题含 gift card / iTunes 码;拒 Apple ID 账号与游戏礼包
|
||
if gift_mode and not re.search(r"virtual\s*card|debit|prepaid|visa|mastercard", title_filter or "", re.I):
|
||
if not re.search(r"gift\s*card|iTunes\s*Gift|Google\s*Play\s*Gift", title, re.I):
|
||
continue
|
||
if re.search(r"\bApple ID\b|personal account|steam gift|roblox|psn account|Nexus Mods", title, re.I):
|
||
continue
|
||
slug = (ur.group(1) if ur else "").lstrip("/")
|
||
pid = m.group(1)
|
||
if slug.startswith("http"):
|
||
url = slug
|
||
elif slug:
|
||
url = f"https://ggsel.net/en/catalog/product/{slug}"
|
||
else:
|
||
url = f"https://ggsel.net/en/catalog/product/{pid}"
|
||
quotes.append({
|
||
"title": title[:100],
|
||
"price": price,
|
||
"currency": "USD",
|
||
"url": url,
|
||
"product_id": pid,
|
||
"confidence": 0.72,
|
||
"price_field": "price_wme",
|
||
})
|
||
|
||
# 模式 A:product slug + 邻近美元
|
||
for m in re.finditer(
|
||
r'/en/catalog/product/([a-z0-9-]+)-(\d+)[^$]{0,500}?(\d+)[,.](\d+)\s*\$',
|
||
html, re.I | re.S,
|
||
):
|
||
slug, pid, a, b = m.group(1), m.group(2), m.group(3), m.group(4)
|
||
title = slug.replace("-", " ")
|
||
blob = f"{title} {slug}"
|
||
if not _ggsel_title_ok(blob, title_filter):
|
||
continue
|
||
price = float(f"{a}.{b}")
|
||
if not (price_lo <= price <= 500):
|
||
continue
|
||
quotes.append({
|
||
"title": title[:80],
|
||
"price": price,
|
||
"currency": "USD",
|
||
"url": f"https://ggsel.net/en/catalog/product/{slug}-{pid}",
|
||
"product_id": pid,
|
||
"confidence": 0.75,
|
||
})
|
||
# 模式 B:仅 slug(后扫价)— 扩大召回,仍强制 title_filter
|
||
if len(quotes) < 3 and title_filter:
|
||
for m in re.finditer(r'/en/catalog/product/([a-z0-9-]+)-(\d+)', html, re.I):
|
||
slug, pid = m.group(1), m.group(2)
|
||
title = slug.replace("-", " ")
|
||
blob = f"{title} {slug}"
|
||
if not _ggsel_title_ok(blob, title_filter):
|
||
continue
|
||
# 在匹配位置后 800 字符内找价
|
||
start = m.end()
|
||
window = html[start:start + 800]
|
||
moneys = _parse_money_list(window)
|
||
moneys = [p for p in moneys if price_lo <= p <= 500]
|
||
# 也扫 price_wme 在邻近
|
||
for wm in re.finditer(r'"price_wme":"?([0-9.]+)"?', window):
|
||
try:
|
||
moneys.append(float(wm.group(1)))
|
||
except ValueError:
|
||
pass
|
||
moneys = [p for p in moneys if price_lo <= p <= 500]
|
||
if not moneys:
|
||
continue
|
||
price = min(moneys)
|
||
quotes.append({
|
||
"title": title[:80],
|
||
"price": price,
|
||
"currency": "USD",
|
||
"url": f"https://ggsel.net/en/catalog/product/{slug}-{pid}",
|
||
"product_id": pid,
|
||
"confidence": 0.55,
|
||
})
|
||
# 无 title 命中时:绝不拿整页最低价(会污染成 $0.25 万能价)
|
||
# 仅在未设 filter 时允许 catalog 全局最低
|
||
if not quotes and not title_filter:
|
||
for price in sorted(set(_parse_money_list(html)))[:5]:
|
||
if price < 0.5:
|
||
continue
|
||
quotes.append({
|
||
"title": "catalog_min", "price": price, "currency": "USD",
|
||
"url": None, "product_id": None, "confidence": 0.3,
|
||
})
|
||
out, seen = [], set()
|
||
for q in sorted(quotes, key=lambda x: x["price"]):
|
||
k = (q.get("product_id") or q["price"], q["price"])
|
||
if k in seen:
|
||
continue
|
||
seen.add(k)
|
||
try:
|
||
fd = face_discount_fields(str(q.get("title") or ""), float(q["price"]))
|
||
q.update({k: v for k, v in fd.items() if v is not None or k.startswith("discount")})
|
||
except Exception:
|
||
pass
|
||
out.append(q)
|
||
return out[:12]
|
||
|
||
|
||
def parse_plati_item(html: str, url: str) -> list[dict]:
|
||
m = re.search(r'itemprop=["\']price["\'][^>]*content=["\']([\d.]+)["\']', html, re.I)
|
||
if not m:
|
||
m = re.search(r'content=["\']([\d.]+)["\'][^>]*itemprop=["\']price["\']', html, re.I)
|
||
if m:
|
||
return [{"title": "plati_item", "price": float(m.group(1)), "currency": "USD", "url": url, "product_id": None}]
|
||
moneys = _parse_money_list(html)
|
||
if moneys:
|
||
return [{"title": "plati_min", "price": min(moneys), "currency": "USD", "url": url, "product_id": None}]
|
||
return []
|
||
|
||
|
||
# 粗略 EUR→USD(面板统一 USD;精确汇率非目标)
|
||
_EUR_USD = 1.08
|
||
|
||
|
||
def parse_funpay_lots(html: str, url: str) -> list[dict]:
|
||
"""FunPay lot 列表页:抽 € / $ 标价,转 USD 后取合理最低。"""
|
||
quotes: list[dict] = []
|
||
# € prices
|
||
for m in re.finditer(
|
||
r'<div>(\d+)[.,](\d{2})\s*<span class="unit">€</span>',
|
||
html, re.I,
|
||
):
|
||
try:
|
||
eur = float(f"{m.group(1)}.{m.group(2)}")
|
||
except ValueError:
|
||
continue
|
||
if not (1.0 <= eur <= 400):
|
||
continue
|
||
usd = round(eur * _EUR_USD, 2)
|
||
quotes.append({
|
||
"title": "funpay_eur",
|
||
"price": usd,
|
||
"currency": "USD",
|
||
"price_orig": eur,
|
||
"currency_orig": "EUR",
|
||
"url": url,
|
||
"product_id": None,
|
||
"confidence": 0.7,
|
||
})
|
||
# $ prices
|
||
for m in re.finditer(
|
||
r'<div>(\d+)[.,](\d{2})\s*<span class="unit">\$</span>',
|
||
html, re.I,
|
||
):
|
||
try:
|
||
usd = float(f"{m.group(1)}.{m.group(2)}")
|
||
except ValueError:
|
||
continue
|
||
if not (1.0 <= usd <= 400):
|
||
continue
|
||
quotes.append({
|
||
"title": "funpay_usd",
|
||
"price": usd,
|
||
"currency": "USD",
|
||
"url": url,
|
||
"product_id": None,
|
||
"confidence": 0.75,
|
||
})
|
||
# 丢掉邀请试用类极低价(< $3 且同页有更高价时)
|
||
if not quotes:
|
||
return []
|
||
quotes.sort(key=lambda x: x["price"])
|
||
# 若最低 < 3 且存在 ≥8 的价,跳过试用噪声
|
||
solid = [q for q in quotes if q["price"] >= 8.0]
|
||
if solid and quotes[0]["price"] < 3.0:
|
||
quotes = solid
|
||
out, seen = [], set()
|
||
for q in quotes:
|
||
k = q["price"]
|
||
if k in seen:
|
||
continue
|
||
seen.add(k)
|
||
out.append(q)
|
||
return out[:12]
|
||
|
||
|
||
def parse_official_go(html: str, url: str) -> list[dict]:
|
||
found = _parse_money_list(html.replace("/month", " $").replace("/mo", " $"))
|
||
for m in re.finditer(r"\$\s*(\d+)\s*/\s*month", html, re.I):
|
||
found.append(float(m.group(1)))
|
||
for m in re.finditer(r"\$\s*(\d+)\s+for your first month", html, re.I):
|
||
found.append(float(m.group(1)))
|
||
if not found:
|
||
return [{"title": "official", "price": 10.0, "currency": "USD", "url": url, "product_id": None, "note": "fallback"}]
|
||
price = 10.0 if 10.0 in found else min(x for x in found if x >= 5)
|
||
return [{"title": "official_go", "price": price, "currency": "USD", "url": url, "unit": "mo", "product_id": None}]
|
||
|
||
|
||
def parse_official_ollama(html: str, url: str) -> list[dict]:
|
||
out = []
|
||
if re.search(r"Pro[\s\S]{0,80}\$\s*20", html, re.I) or "$20" in html:
|
||
out.append({"title": "Pro", "price": 20.0, "currency": "USD", "url": url, "unit": "mo", "product_id": None})
|
||
if re.search(r"Max[\s\S]{0,80}\$\s*100", html, re.I) or "$100" in html:
|
||
out.append({"title": "Max", "price": 100.0, "currency": "USD", "url": url, "unit": "mo", "product_id": None})
|
||
if not out:
|
||
out = [{"title": "Pro", "price": 20.0, "currency": "USD", "url": url, "unit": "mo", "product_id": None, "note": "fallback"}]
|
||
return out
|
||
|
||
|
||
def parse_official_clinepass(html: str, url: str) -> list[dict]:
|
||
found = _parse_money_list(html)
|
||
for m in re.finditer(r"\$\s*(\d+(?:\.\d{1,2})?)\s*/\s*month", html, re.I):
|
||
try:
|
||
found.append(float(m.group(1)))
|
||
except ValueError:
|
||
pass
|
||
recurring = [x for x in found if 8.0 <= x <= 15.0]
|
||
price = min(recurring) if recurring else (min(found) if found else 9.99)
|
||
return [{"title": "clinepass", "price": price, "currency": "USD", "url": url, "unit": "mo", "product_id": None}]
|
||
|
||
|
||
PARSERS = {
|
||
"ggsel_catalog": lambda html, url, **kw: parse_ggsel_catalog(html, kw.get("title_filter")),
|
||
"plati_item": lambda html, url, **kw: parse_plati_item(html, url),
|
||
"funpay_lots": lambda html, url, **kw: parse_funpay_lots(html, url),
|
||
"official_go": lambda html, url, **kw: parse_official_go(html, url),
|
||
"official_msrp": lambda html, url, **kw: [],
|
||
"official_ollama": lambda html, url, **kw: parse_official_ollama(html, url),
|
||
"official_clinepass": lambda html, url, **kw: parse_official_clinepass(html, url),
|
||
}
|
||
|
||
|
||
def _load_cached() -> dict | None:
|
||
if not OUT.is_file():
|
||
return None
|
||
try:
|
||
return json.loads(OUT.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
return None
|
||
|
||
|
||
def _cache_age_s(data: dict) -> float | None:
|
||
ts = data.get("ts") or ""
|
||
if not ts:
|
||
return None
|
||
try:
|
||
# 2026-07-10T19:38:23.190232+00:00
|
||
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||
return max(0.0, (datetime.now(timezone.utc) - dt).total_seconds())
|
||
except ValueError:
|
||
return None
|
||
|
||
|
||
def _history_best_entry(s: dict) -> dict:
|
||
"""单 SKU history 行:价 + 渠道 + availability(unknown≠sold_out)。"""
|
||
best = s.get("best") if isinstance(s.get("best"), dict) else {}
|
||
bm = s.get("best_market") if isinstance(s.get("best_market"), dict) else {}
|
||
price = best.get("price") if best.get("price") is not None else bm.get("price")
|
||
ch = best.get("channel") or bm.get("channel")
|
||
av = (
|
||
s.get("availability")
|
||
or best.get("availability")
|
||
or best.get("av")
|
||
or s.get("av")
|
||
)
|
||
entry: dict[str, Any] = {
|
||
"p": price,
|
||
"ch": ch,
|
||
"tier": s.get("tier"),
|
||
}
|
||
if av is not None:
|
||
entry["av"] = av
|
||
elif price is not None:
|
||
entry["av"] = "in_stock"
|
||
else:
|
||
sources = s.get("sources") or []
|
||
if sources and all(
|
||
isinstance(x, dict) and (x.get("ok") is False or x.get("error"))
|
||
for x in sources
|
||
):
|
||
entry["av"] = "unknown"
|
||
entry["probe_ok"] = False
|
||
else:
|
||
entry["av"] = "unknown"
|
||
seller = best.get("seller_id") or s.get("seller_id")
|
||
if seller:
|
||
entry["seller"] = seller
|
||
return entry
|
||
|
||
|
||
def _append_history(result: dict) -> None:
|
||
"""每次探针写一行摘要,供趋势/审计(自己渠道,不上云)。"""
|
||
try:
|
||
HISTORY.mkdir(parents=True, exist_ok=True)
|
||
day = datetime.now(timezone.utc).strftime("%Y%m%d")
|
||
path = HISTORY / f"{day}.jsonl"
|
||
best_map: dict[str, Any] = {}
|
||
for s in result.get("skus") or []:
|
||
if not isinstance(s, dict) or not s.get("id"):
|
||
continue
|
||
best_map[str(s.get("id"))] = _history_best_entry(s)
|
||
row = {
|
||
"ts": result.get("ts"),
|
||
"n": len(result.get("skus") or []),
|
||
"counts": result.get("counts"),
|
||
"best": best_map,
|
||
}
|
||
with path.open("a", encoding="utf-8") as f:
|
||
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
def probe_all(force: bool = False, merge: bool = True, subset: bool = False) -> dict:
|
||
"""拉取 SKUS(可能是子集),默认与本地缓存按 id 合并,保证轮询不全量抹掉。"""
|
||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||
prev = _load_cached() or {}
|
||
|
||
# 全量且缓存新鲜:直接复用(非 force)
|
||
if not force and not subset and prev.get("skus"):
|
||
age = _cache_age_s(prev)
|
||
if age is not None and age < TTL_S:
|
||
out = dict(prev)
|
||
out["cache_hit"] = True
|
||
out["cache_age_s"] = int(age)
|
||
return out
|
||
|
||
skus_out = []
|
||
for spec in SKUS:
|
||
sources = []
|
||
for pr in spec.get("probes") or []:
|
||
parser = pr.get("parser") or ""
|
||
url = pr.get("url") or ""
|
||
# 无 URL 的静态 MSRP 探针
|
||
if parser == "official_msrp" and not url:
|
||
msrp0 = (spec.get("official_msrp") or {})
|
||
if msrp0.get("price") is not None:
|
||
sources.append({
|
||
"channel_id": "official-msrp",
|
||
"channel": "官方",
|
||
"kind": "msrp",
|
||
"url": msrp0.get("url") or "",
|
||
"http": 200,
|
||
"ok": True,
|
||
"quotes": [],
|
||
"price": msrp0["price"],
|
||
"currency": msrp0.get("currency") or "USD",
|
||
"title": "msrp",
|
||
"link": msrp0.get("url"),
|
||
"error": None,
|
||
})
|
||
continue
|
||
|
||
code, body = _fetch(url)
|
||
# official 页允许短 body
|
||
min_len = 200 if str(parser).startswith("official") else 500
|
||
rec = {
|
||
"channel_id": pr.get("channel_id"),
|
||
"channel": pr.get("channel"),
|
||
"kind": pr.get("kind"),
|
||
"url": url,
|
||
"http": code,
|
||
"ok": 200 <= int(code) < 400 and len(body) > min_len,
|
||
"quotes": [],
|
||
"error": None,
|
||
}
|
||
if not rec["ok"]:
|
||
rec["error"] = f"http={code} len={len(body)}"
|
||
sources.append(rec)
|
||
time.sleep(SLEEP_S)
|
||
continue
|
||
fn = PARSERS.get(parser)
|
||
try:
|
||
quotes = fn(body, url, title_filter=pr.get("title_filter")) if fn else []
|
||
except Exception as e:
|
||
quotes = []
|
||
rec["error"] = str(e)[:80]
|
||
rec["quotes"] = quotes
|
||
if quotes:
|
||
rec["price"] = quotes[0]["price"]
|
||
rec["currency"] = quotes[0].get("currency") or "USD"
|
||
rec["title"] = quotes[0].get("title")
|
||
rec["link"] = quotes[0].get("url") or url
|
||
for fk in ("face_min", "face_max", "face_currency", "face_kind",
|
||
"discount", "discount_at_min", "discount_at_max", "discount_note"):
|
||
if quotes[0].get(fk) is not None:
|
||
rec[fk] = quotes[0].get(fk)
|
||
sources.append(rec)
|
||
time.sleep(SLEEP_S)
|
||
|
||
msrp = dict(spec.get("official_msrp") or {}) if spec.get("official_msrp") else {}
|
||
msrp_src = [s for s in sources if s.get("kind") == "msrp" and s.get("price") is not None]
|
||
if msrp_src and (not msrp.get("price") or msrp.get("price") == 0):
|
||
msrp["price"] = msrp_src[0]["price"]
|
||
msrp["currency"] = msrp_src[0].get("currency") or "USD"
|
||
if msrp_src:
|
||
msrp.setdefault("url", msrp_src[0].get("url"))
|
||
|
||
def _market_sane(s: dict) -> bool:
|
||
"""丢掉离谱灰市价(整页污染 / 试用价误匹配)。"""
|
||
if s.get("kind") == "msrp" or s.get("price") is None:
|
||
return False
|
||
# relay_index 不是「账号货架价」,单独标记,不进 best_market 账号比较
|
||
if s.get("kind") == "relay_index":
|
||
return False
|
||
p = float(s["price"])
|
||
if p <= 0:
|
||
return False
|
||
unit = (msrp.get("unit") if msrp else None) or "mo"
|
||
mprice = msrp.get("price") if msrp else None
|
||
conf = s.get("confidence")
|
||
if conf is not None:
|
||
try:
|
||
if float(conf) < 0.45:
|
||
return False
|
||
except (TypeError, ValueError):
|
||
pass
|
||
# 订阅类:丢掉整页污染的几毛钱价;真·灰市号可到 $1(如 SuperGrok)
|
||
if unit in ("mo",) and mprice and float(mprice) >= 5:
|
||
if p < 0.5:
|
||
return False
|
||
# < 官方 2% 且 < $1:基本是误匹配
|
||
if p < float(mprice) * 0.02 and p < 1.0:
|
||
return False
|
||
# 超高档(≥$100 MSRP)灰市价 < $5 几乎必假
|
||
if float(mprice) >= 100 and p < 5.0:
|
||
return False
|
||
# 礼品卡面值对照:灰市常是「多面值条目最低价」或区服 TL 卡,可远低于 face MSRP
|
||
if unit == "face":
|
||
if p < 0.15:
|
||
return False
|
||
# 置信度太低不进 best
|
||
if conf is not None:
|
||
try:
|
||
if float(conf) < 0.5:
|
||
return False
|
||
except (TypeError, ValueError):
|
||
pass
|
||
return True
|
||
# 非短信/按量:拒绝纯噪声
|
||
if unit not in ("sms", "1k", "hour", "gb", "account", "credit", "face") and p < 0.2:
|
||
return False
|
||
return True
|
||
|
||
market_prices = [s for s in sources if _market_sane(s)]
|
||
best_m = min(market_prices, key=lambda x: float(x["price"])) if market_prices else None
|
||
best = best_m
|
||
if not best and msrp.get("price") is not None:
|
||
best = {
|
||
"channel": "官方",
|
||
"channel_id": "official-msrp",
|
||
"price": msrp.get("price"),
|
||
"currency": msrp.get("currency") or "USD",
|
||
"link": msrp.get("url"),
|
||
"url": msrp.get("url"),
|
||
"title": "msrp",
|
||
"kind": "msrp",
|
||
}
|
||
# 无 MSRP 且无灰市:best 空,UI 显示「—」
|
||
quotes_ready = bool(market_prices) or bool(msrp.get("price") is not None)
|
||
skus_out.append({
|
||
"id": spec["id"],
|
||
"label": spec["label"],
|
||
"category": spec["category"],
|
||
"tier": spec.get("tier") or "p0",
|
||
"official_msrp": msrp or None,
|
||
"sources": sources,
|
||
"source_n": len(sources),
|
||
"quotes_ready": quotes_ready,
|
||
"best": {
|
||
"channel": best.get("channel"),
|
||
"channel_id": best.get("channel_id"),
|
||
"price": best.get("price"),
|
||
"currency": best.get("currency") or "USD",
|
||
"url": best.get("link") or best.get("url"),
|
||
"title": best.get("title"),
|
||
"kind": best.get("kind"),
|
||
"face_min": best.get("face_min"),
|
||
"face_max": best.get("face_max"),
|
||
"face_currency": best.get("face_currency"),
|
||
"face_kind": best.get("face_kind"),
|
||
"discount": best.get("discount"),
|
||
"discount_at_min": best.get("discount_at_min"),
|
||
"discount_at_max": best.get("discount_at_max"),
|
||
"discount_note": best.get("discount_note"),
|
||
} if best else None,
|
||
"best_market": {
|
||
"channel": best_m.get("channel"),
|
||
"price": best_m.get("price"),
|
||
"currency": best_m.get("currency") or "USD",
|
||
"url": best_m.get("link") or best_m.get("url"),
|
||
"face_min": best_m.get("face_min"),
|
||
"face_max": best_m.get("face_max"),
|
||
"face_currency": best_m.get("face_currency"),
|
||
"discount": best_m.get("discount"),
|
||
"discount_at_min": best_m.get("discount_at_min"),
|
||
"discount_at_max": best_m.get("discount_at_max"),
|
||
"discount_note": best_m.get("discount_note"),
|
||
} if best_m else None,
|
||
"spread": (
|
||
round(float(best_m["price"]) / float(msrp["price"]), 3)
|
||
if best_m and msrp and msrp.get("price")
|
||
else None
|
||
),
|
||
"monitor": spec.get("monitor"),
|
||
"decide": spec.get("decide"),
|
||
"hot": True,
|
||
"demand_link": [],
|
||
})
|
||
# 灰市价 ≫ MSRP:可能多月/捆绑/假货,勿当「单月更贵」误读
|
||
row = skus_out[-1]
|
||
try:
|
||
bm_p = (row.get("best_market") or {}).get("price")
|
||
ms_p = (row.get("official_msrp") or {}).get("price")
|
||
if bm_p is not None and ms_p is not None and float(ms_p) > 0:
|
||
ratio = float(bm_p) / float(ms_p)
|
||
row["spread"] = round(ratio, 3)
|
||
if ratio >= 1.5:
|
||
row["price_flag"] = "possible_bundle"
|
||
row["price_flag_note"] = (
|
||
f"灰市 {bm_p} ≥ MSRP×1.5({ms_p})→ 可能多月/捆绑/席位包,勿当单月溢价"
|
||
)
|
||
elif ratio <= 0.15 and float(ms_p) >= 10:
|
||
row["price_flag"] = "suspiciously_cheap"
|
||
row["price_flag_note"] = (
|
||
f"灰市 {bm_p} ≤ MSRP×0.15 → 高仿/试用/误匹配风险"
|
||
)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
# 与旧缓存合并:子集探针不丢其它 tier
|
||
if merge and prev.get("skus"):
|
||
by_id = {s.get("id"): s for s in prev["skus"] if isinstance(s, dict) and s.get("id")}
|
||
for s in skus_out:
|
||
by_id[s["id"]] = s
|
||
# 稳定顺序:按当前全量定义序,未知的挂尾
|
||
order = []
|
||
try:
|
||
# 重新读模块级全表顺序困难;按 prev 序 + 新 id
|
||
seen = set()
|
||
for s in prev["skus"]:
|
||
i = s.get("id")
|
||
if i in by_id and i not in seen:
|
||
order.append(by_id[i]); seen.add(i)
|
||
for s in skus_out:
|
||
i = s.get("id")
|
||
if i not in seen:
|
||
order.append(s); seen.add(i)
|
||
skus_out = order
|
||
except Exception:
|
||
skus_out = list(by_id.values())
|
||
|
||
# 合并 community-prices overlay(5sim / awesome 中转指数 / openrouter)
|
||
_apply_community_overlay(skus_out)
|
||
|
||
by_tier: dict[str, int] = {}
|
||
market_n = 0
|
||
for s in skus_out:
|
||
t = s.get("tier") or "p0"
|
||
by_tier[t] = by_tier.get(t, 0) + 1
|
||
if (s.get("best_market") or {}).get("price") is not None:
|
||
market_n += 1
|
||
|
||
result = {
|
||
"ts": _now(),
|
||
"ok": True,
|
||
"skus": skus_out,
|
||
"strategy": "sku_probe_local_cache+community",
|
||
"cache_hit": False,
|
||
"probed_n": len(SKUS),
|
||
"counts": {
|
||
"skus": len(skus_out),
|
||
"market_n": market_n,
|
||
**{f"tier_{k}": v for k, v in by_tier.items()},
|
||
},
|
||
"note": "本地缓存探针:P0/P1/P2+pay+call;GGSel+community(5sim/awesome-proxy/OR);history jsonl",
|
||
"paths": {
|
||
"quotes": str(OUT),
|
||
"community": str(COMMUNITY),
|
||
"history": str(HISTORY),
|
||
},
|
||
}
|
||
OUT.write_text(json.dumps(result, ensure_ascii=False, indent=2))
|
||
_append_history(result)
|
||
return result
|
||
|
||
|
||
def _apply_community_overlay(skus_out: list[dict[str, Any]]) -> None:
|
||
"""把 community-prices.json 的 sku_overlay 并入 SKU(不覆盖已有 marketplace 实价)。"""
|
||
if not COMMUNITY.is_file():
|
||
return
|
||
try:
|
||
data = json.loads(COMMUNITY.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
return
|
||
overlay = data.get("sku_overlay") if isinstance(data, dict) else None
|
||
if not isinstance(overlay, dict):
|
||
return
|
||
by_id = {s.get("id"): s for s in skus_out if isinstance(s, dict)}
|
||
for sid, o in overlay.items():
|
||
if not isinstance(o, dict) or o.get("price") is None:
|
||
continue
|
||
s = by_id.get(sid)
|
||
if not s:
|
||
continue
|
||
s["community"] = o
|
||
kind = o.get("kind") or "community"
|
||
# sms 等 marketplace 类:可作 best_market
|
||
if kind == "marketplace" and not (s.get("best_market") or {}).get("price"):
|
||
s["best_market"] = {
|
||
"channel": o.get("channel"),
|
||
"price": o.get("price"),
|
||
"currency": o.get("currency") or "USD",
|
||
"url": o.get("url"),
|
||
"kind": kind,
|
||
"unit": o.get("unit"),
|
||
"source": "community",
|
||
}
|
||
if not s.get("best") or (s.get("best") or {}).get("kind") == "msrp":
|
||
s["best"] = {
|
||
"channel": o.get("channel"),
|
||
"channel_id": o.get("channel_id"),
|
||
"price": o.get("price"),
|
||
"currency": o.get("currency") or "USD",
|
||
"url": o.get("url"),
|
||
"title": o.get("title"),
|
||
"kind": kind,
|
||
}
|
||
s["quotes_ready"] = True
|
||
# relay_index:挂 side 字段,不假装成账号店价
|
||
if kind == "relay_index":
|
||
s["relay_index"] = o
|
||
if not s.get("best") and o.get("price") is not None:
|
||
s["best"] = {
|
||
"channel": o.get("channel"),
|
||
"channel_id": o.get("channel_id"),
|
||
"price": o.get("price"),
|
||
"currency": o.get("currency") or "USD",
|
||
"url": o.get("url"),
|
||
"title": o.get("title") or "relay_index",
|
||
"kind": "relay_index",
|
||
"unit": o.get("unit"),
|
||
}
|
||
s["quotes_ready"] = True
|
||
|
||
|
||
# 供应链 demand 相关 SKU:轮询时每次必打(合并进子集)
|
||
DEMAND_SKU_IDS: set[str] = {
|
||
"codex_quota", "openai", "chatgpt_pro", "chatgpt_team", "claude", "claude_max",
|
||
"cursor", "relay_premium", "newapi_relay", "openrouter", "opencode_go",
|
||
"sms_otp", "sms_us", "sms_nl", "sms_id", "bulk_mail", "captcha_solve",
|
||
"supergrok", "giftcard_us", "giftcard_tr", "giftcard_hk", "virtual_card",
|
||
"residential_proxy",
|
||
}
|
||
_DEMAND_REQ = Path.home() / ".cache" / "manhuang" / "quotes" / ".demand-probe.request"
|
||
|
||
|
||
def _gap_skus_from_request() -> set[str]:
|
||
"""读 demand-first 请求旗上的 gap_skus(collect 写入)。"""
|
||
if not _DEMAND_REQ.is_file():
|
||
return set()
|
||
try:
|
||
data = json.loads(_DEMAND_REQ.read_text(encoding="utf-8"))
|
||
out: set[str] = set()
|
||
for sid in data.get("gap_skus") or []:
|
||
if sid:
|
||
out.add(str(sid))
|
||
# 也并入 priced 相关 id,保证刷新已有价
|
||
for p in data.get("priced_samples") or []:
|
||
if isinstance(p, dict) and p.get("id"):
|
||
out.add(str(p["id"]))
|
||
return out
|
||
except (OSError, json.JSONDecodeError, TypeError):
|
||
return set()
|
||
|
||
|
||
def main() -> int:
|
||
import argparse
|
||
ap = argparse.ArgumentParser(description="蛮荒比价探针 → ~/.cache/manhuang/quotes/")
|
||
ap.add_argument("--force", action="store_true", help="忽略 TTL,强制重拉")
|
||
ap.add_argument("--tier", action="append", help="only p0/p1/p2/pay/call (repeatable)")
|
||
ap.add_argument("--rotate", action="store_true",
|
||
help="按时段轮询:偶时 p0+pay,奇时 p1+p2+call;始终并入 demand SKU")
|
||
ap.add_argument("--demand-first", action="store_true",
|
||
help="只探针 DEMAND_SKU_IDS + request 旗 gap_skus(最快,适合危机时)")
|
||
ap.add_argument("--ids", action="append", help="只探针指定 id(可重复)")
|
||
ap.add_argument("--no-merge", action="store_true", help="不与旧缓存合并(全量覆盖)")
|
||
args = ap.parse_args()
|
||
global SKUS
|
||
full = list(SKUS)
|
||
if args.demand_first:
|
||
allow = set(DEMAND_SKU_IDS) | _gap_skus_from_request()
|
||
# --ids 可再收窄或追加
|
||
if args.ids:
|
||
allow |= set(args.ids)
|
||
SKUS = [s for s in full if s.get("id") in allow]
|
||
subset = True
|
||
print(f"demand-first allow={len(allow)} match={len(SKUS)} ids={sorted(allow)[:20]}")
|
||
elif args.ids:
|
||
allow_ids = set(args.ids)
|
||
SKUS = [s for s in full if s.get("id") in allow_ids]
|
||
subset = True
|
||
else:
|
||
if args.rotate and not args.tier:
|
||
h = datetime.now(timezone.utc).hour
|
||
if h % 2 == 0:
|
||
args.tier = ["p0", "pay"]
|
||
else:
|
||
args.tier = ["p1", "p2", "call"]
|
||
subset = False
|
||
if args.tier:
|
||
allow = {t.lower() for t in args.tier}
|
||
base = [s for s in full if (s.get("tier") or "p0") in allow]
|
||
# demand SKU 始终并入本轮(供应链优先)
|
||
seen = {s.get("id") for s in base}
|
||
for s in full:
|
||
if s.get("id") in DEMAND_SKU_IDS and s.get("id") not in seen:
|
||
base.append(s)
|
||
seen.add(s.get("id"))
|
||
SKUS = base
|
||
subset = True
|
||
r = probe_all(force=args.force, merge=not args.no_merge, subset=subset)
|
||
hit = "CACHE" if r.get("cache_hit") else "FRESH"
|
||
print(
|
||
f"wrote {OUT} n={len(r.get('skus') or [])} probed={r.get('probed_n')} "
|
||
f"[{hit}] tiers={r.get('counts')}"
|
||
)
|
||
# 只打印本轮探针的 id(subset 时用 SKUS 列表)
|
||
probed_ids = {s.get("id") for s in SKUS}
|
||
for s in r.get("skus") or []:
|
||
if subset and s.get("id") not in probed_ids:
|
||
continue
|
||
b = s.get("best") or {}
|
||
m = s.get("official_msrp") or {}
|
||
print(
|
||
f"- [{s.get('tier')}] {s['id']}: best={b.get('price')} @{b.get('channel')} "
|
||
f"| msrp={m.get('price')} | mkt_ok={sum(1 for x in s.get('sources') or [] if x.get('kind')!='msrp' and x.get('price') is not None)}"
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|