智谱 GLM Coding 抢购机器人实战

在 AI 大模型火热的当下,智谱 AI 的 GLM Coding 套餐因为性价比高经常被抢空。本文分享我开发的 ZipuRobot 抢购机器人,从 0 到 1 实现自动监控库存、自动抢购的完整过程。

前言

智谱 AI 的 GLM Coding 套餐(Lite/Pro/Max)一直热销,尤其是 Pro 版本经常在补货后几分钟内售罄。作为一名程序员,与其每次准点守在电脑前刷新页面,不如用技术手段解决这个问题。

于是我开发了 ZipuRobot——一个基于 Python + Playwright 的智谱 GLM Coding 抢购机器人。

项目介绍

ZipuRobot 是一个自动化抢购脚本,核心目标是:

  • 自动监控库存:实时检测目标套餐是否有货
  • 自动完成抢购:检测到有货后自动点击购买
  • 静默登录:通过 Cookie 实现免登录启动
  • 定时任务:支持设置定时运行

当前版本已经过实战验证,能够稳定完成「启动 → 登录 → 监控」的完整流程。

技术架构

技术栈

技术 用途
Python 3.10+ 主语言
Playwright 浏览器自动化
asyncio 异步任务调度
unittest 单元测试

核心模块

1
2
3
4
5
6
7
8
9
10
11
12
zhipuglmbot/
├── main.py # 主入口,包含 ZhipuRobot 类
├── src/
│ ├── config.py # 配置管理
│ ├── cookie_manager.py # Cookie 管理
│ ├── inventory_monitor.py # 库存监控
│ ├── purchase_bot.py # 抢购执行
│ ├── scheduler.py # 定时任务
│ └── logger.py # 日志模块
├── config.json # 配置文件
├── cookie.json # 登录 Cookie
└── logs/app.log # 运行日志

核心实现

1. 主入口与初始化

主程序入口在 main.py,核心类是 ZhipuRobot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# main.py
class ZhipuRobot:
"""智谱 AI 抢购机器人。"""

PRODUCT_URL = "https://bigmodel.cn/glm-coding"
LOGIN_WARNING_THRESHOLD = 4
LOGIN_WAIT_TIMEOUT_MS = 400
LOGIN_WAIT_POLLING_MS = 100

def __init__(self, config_path: str = "config.json", verbose: bool = False):
self.config = Config(config_path)
self.logger = setup_logger("zipurobot", "DEBUG" if verbose else "INFO")
self.cookie_manager = CookieManager()
# ... 初始化浏览器相关变量

async def init_browser(self):
"""初始化浏览器并应用本地 Cookie。"""
from playwright.async_api import async_playwright

self.playwright = await async_playwright().start()
self.browser = await self.playwright.chromium.launch(
headless=self.config.get("monitor.headless", False)
)
self.context = await self.browser.new_context(
viewport={"width": 1440, "height": 900},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
)
self.page = await self.context.new_page()

# 加载并应用 Cookie
self.cookie_manager.load_cookie()
await self.cookie_manager.apply_cookie(self.page)

2. 配置管理

配置管理通过 Config 类实现,支持点号分隔的键访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# src/config.py
class Config:
"""配置管理"""

def __init__(self, config_path: str = None):
self.config_path = Path(config_path)
self._data: dict = {}
self.load()

def get(self, key: str, default: Any = None) -> Any:
"""获取配置值,支持点号分隔的键"""
keys = key.split('.')
value = self._data
for k in keys:
if isinstance(value, dict):
value = value.get(k)
else:
return default
if value is None:
return default
return value

配置示例 config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"target": {
"plan": "pro",
"cycle": "quarterly"
},
"monitor": {
"headless": false,
"timeout": 300
},
"retry": {
"max_attempts": 3,
"base_interval": 5
}
}

Cookie 管理负责登录态的持久化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# src/cookie_manager.py
class CookieManager:
"""Cookie 管理"""

def __init__(self, cookie_path: str = None):
self.cookie_path = Path(cookie_path)
self._cookies: List[Dict] = []

def load_cookie(self) -> List[Dict]:
"""加载 Cookie"""
if self.cookie_path.exists():
with open(self.cookie_path, 'r', encoding='utf-8') as f:
self._cookies = json.load(f)
return self._cookies

def save_cookie(self, cookies: List[Dict]):
"""保存 Cookie"""
self.cookie_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.cookie_path, 'w', encoding='utf-8') as f:
json.dump(cookies, f, indent=2, ensure_ascii=False)
self._cookies = cookies

async def apply_cookie(self, page):
"""应用 Cookie 到页面"""
if not self._cookies:
self.load_cookie()
if self._cookies:
await page.context.add_cookies(self._cookies)

4. 登录态识别

登录态识别是抢购的第一步,最初使用了不稳定的特征,后来根据实测改为更可靠的信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# main.py - 登录检测核心逻辑
async def login_check(self) -> bool:
"""验证登录状态,并在登录成功后持久化 Cookie。"""
await self.page.goto(self.PRODUCT_URL, wait_until="domcontentloaded")

self.logger.info("正在验证登录状态...")
retry_count = 0
max_retries = 120

# 登录信号检测脚本
login_signal_script = """
() => {
const text = document.body.innerText || '';
const signals = {
exitText: text.includes('退出') || text.includes('Logout'),
profileText: text.includes('个人中心'),
userProfileName: !!document.querySelector('.user-profile-name, [class*=username]'),
loginRegister: text.includes('登录 / 注册'),
myPlan: text.includes('我的编程套餐'), // 登录后顶部会出现
apiKey: text.includes('API Key') // 登录后可见
};
const isLoggedIn = !signals.loginRegister && (
signals.exitText ||
signals.profileText ||
signals.userProfileName ||
signals.myPlan ||
signals.apiKey
);
return isLoggedIn ? signals : false;
}
"""

while retry_count < max_retries:
try:
handle = await self.page.wait_for_function(
login_signal_script,
timeout=self.LOGIN_WAIT_TIMEOUT_MS,
polling=self.LOGIN_WAIT_POLLING_MS,
)
signals = await handle.json_value()
self.logger.info(f"登录成功验证 (信号: {signals}),正在同步 Cookie...")
# 登录成功后保存最新 Cookie
self.cookie_manager.save_cookie(await self.context.cookies())
return True
except PlaywrightTimeoutError:
retry_count += 1
if retry_count == self.LOGIN_WARNING_THRESHOLD:
self.logger.warning("未检测到登录,请在弹出的浏览器中手动扫码/登录!")

return False

[!tip] 关键发现
经过多次实测,发现最稳定的登录信号是「页面顶部出现 我的编程套餐API Key」,而不是传统的「退出」按钮或个人中心入口。

5. 库存监控

库存监控是核心功能,采用双层策略:

  • 快速 DOM 检查:每轮循环快速检查按钮状态
  • 自适应刷新:根据超时时间动态调整页面刷新间隔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# src/inventory_monitor.py
class InventoryMonitor:
"""库存监控"""

BUY_BUTTON_SELECTOR = ".buy-btn.el-button--primary"
PLAN_INDEX = {
"lite": 0,
"pro": 1,
"max": 2,
}

@staticmethod
def get_reload_interval_ms(elapsed_seconds: float, timeout_seconds: int) -> int:
"""根据监控进度返回自适应刷新间隔。"""
if timeout_seconds <= 0:
return 1000

progress = elapsed_seconds / timeout_seconds
if progress >= 0.9: # 临近超时,加快刷新
return 1000
if progress >= 0.6: # 中期
return 3000
return 5000 # 初期

async def check_stock(self, plan: str = "pro") -> bool:
"""精准检测目标方案的库存状态"""
try:
return await self.page.evaluate(f"""
(targetPlan) => {{
const buttons = Array.from(document.querySelectorAll("{self.BUY_BUTTON_SELECTOR}"));
const indexMap = {{ lite: 0, pro: 1, max: 2 }};
const index = indexMap[targetPlan.toLowerCase()] ?? indexMap.pro;
const btn = buttons[index];
if (!btn) return false;
return !btn.classList.contains('is-disabled') && !btn.disabled;
}}
""", plan)
except Exception as e:
self.logger.error(f"检查库存失败: {{e}}")
return False

async def wait_for_stock(self, plan: str = "pro", timeout: int = 300, check_interval_ms: int = 500) -> bool:
"""补货监控循环"""
self.logger.info(f"🚀 启动【{plan}】精准监控模式 (频率: {check_interval_ms}ms)")
start_time = asyncio.get_event_loop().time()
last_reload_at = start_time

while True:
now = asyncio.get_event_loop().time()
elapsed = now - start_time
# 自适应刷新间隔
reload_interval_ms = self.get_reload_interval_ms(elapsed, timeout)

# 定时刷新页面获取最新状态
if now - last_reload_at >= reload_interval_ms / 1000.0:
await self.page.reload(wait_until="domcontentloaded")
last_reload_at = asyncio.get_event_loop().time()

# 检查是否有货
if await self.check_stock(plan):
self.logger.info(f"🎯 监测到【{plan}】补货完成!")
return True

if elapsed > timeout:
return False
await asyncio.sleep(check_interval_ms / 1000.0)

6. 主执行流程

完整的抢购流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# main.py - 主流程
async def run(self) -> bool:
"""执行主流程。"""
self.logger.info("=" * 50)
self.logger.info("开始执行抢购任务")
self.logger.info("=" * 50)

try:
# 1. 初始化浏览器
await self.init_browser()

# 2. 检查登录态
if not await self.login_check():
self.logger.error("登录超时,任务终止")
return False

# 3. 获取目标套餐配置
target_plan = self.config.get("target.plan", "pro")
monitor = InventoryMonitor(self.page, self.logger)

# 4. 进入库存监控
self.logger.info(f"开始进入库存监控状态 (方案: {target_plan})...")
stock_start = asyncio.get_event_loop().time()
if not await monitor.wait_for_stock(
plan=target_plan,
timeout=self.config.get("monitor.timeout", 300),
check_interval_ms=300,
):
self.logger.warning("监控结束,未检测到补货")
return False

# 5. 检测到有货,点击购买
self.logger.info(f"发现库存!准备下单...")
if not await monitor.click_buy_button(plan=target_plan):
self.logger.error(f"点击 {target_plan} 订阅按钮失败")
return False

# 6. 执行购买流程
purchase_bot = PurchaseBot(self.page, self.logger, self.config)
if await purchase_bot.execute_purchase():
self.logger.info("抢购流程执行完成!请在浏览器中确认支付结果。")
return True

self.logger.error("购买流程中途失败")
return False
finally:
await self.close_browser()

使用教程

环境准备

1
2
3
4
5
6
7
8
9
# 克隆项目
git clone <repo-url>
cd zhipuglmbot

# 安装依赖
pip install -r requirements.txt

# 安装 Playwright 浏览器
playwright install chromium

配置说明

编辑 config.json

1
2
3
4
5
6
7
8
9
10
{
"target": {
"plan": "pro", // lite | pro | max
"cycle": "quarterly" // monthly | quarterly | yearly
},
"monitor": {
"headless": false, // 是否无头运行
"timeout": 300 // 超时时间(秒)
}
}

运行方式

1
2
3
4
5
6
7
# 正常运行
python main.py

# 调试模式(显示浏览器)
python main.py -v

# 首次运行会自动打开浏览器,扫码登录后会自动保存 Cookie

套餐定位

页面上有 3 个购买按钮(Lite/Pro/Max),通过固定索引定位:

索引 套餐 选择器
0 Lite .buy-btn.el-button--primary (第1个)
1 Pro .buy-btn.el-button--primary (第2个)
2 Max .buy-btn.el-button--primary (第3个)

经验总结

踩过的坑

  1. Cookie 过期:智谱的 Cookie 会定期失效,需要定时刷新
  2. 页面结构变化:购买按钮的选择器可能随页面更新变化,需要定期维护
  3. 弹窗结构不稳定:真实购买弹窗的 DOM 结构难以稳定获取,建议补货时手动抓取
  4. 登录信号误判:最初使用「退出」「个人中心」作为登录信号,实测发现不稳定,改为「我的编程套餐」「API Key」更可靠

注意事项

  • 请确保 Cookie 有效,支付时可能需要手动确认
  • 抢购成功后请及时完成支付
  • 本工具仅供学习交流,请遵守网站服务条款

下一步计划

  1. 在真实补货时抓取购买弹窗 DOM
  2. 校准周期选择器和最终确认按钮
  3. 完成一次完整的实战抢购验证

如果你对这个项目感兴趣,欢迎在评论区交流讨论!


相关文档:[[2026-03-14-zhipurobot-usage]] [[2026-03-14-zhipurobot-status]]