范围说明:本文件只列客户提的业务需求,不含框架代码 / 调试工具 / 基础设施。 架构决策、DebugLogger、WSL2/Plan E、mitmproxy 接入等内容不属于本文档范围。
同步基线:
tasks/stubs.py(13 任务定义)、launcher/navigate.py(HTTP 导航)、login/box_996.py(HTTP 登录)、client.py(高层编排)。
从用户输入凭证 + 区服名,到一个能挂机的传奇新号——这一整条链上的所有业务动作。
┌────────────────────────────────────────────────────────────────────┐
│ 用户输入层 │
│ (a) 账号+密码 (b) 手机号+短信码 (c) 区服名 = "玖扬复古176" │
└────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌──────────────────────────┐
│ A1 账号登录 │ │ A2 短信登录 │ │ B1 查区服 (按名称) │
│ 拿 JWT token │ │ 拿 JWT token │ │ 拿 server_id │
└───────┬───────┘ └───────┬───────┘ └────────────┬─────────────┘
│ │ │
└─────────┬────────┘ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ users 表入库 │ │ B2 拉当日新区列表 │
│ (token 持久化) │ │ 拿 zones[] + open_time │
└─────────┬────────┘ └────────────┬─────────────┘
│ │
│ ┌─────────────┘
│ ▼
│ ┌──────────────────────────┐
│ │ B3 选最新可玩区 │
│ │ 按 open_time + status │
│ └────────────┬─────────────┘
│ │
└──────────┬───────────┘
▼
┌──────────────────────────┐
│ C1 调启动接口 │
│ 拿 host/port/game_token │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ C2 等 cq2.exe 启动就绪 │
│ TCP 端口可连接 │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ D 13 个新号流程任务 │
│ T01 创建角色 → T13 活动 │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ E 持久化、状态、断点 │
│ 写 runs / task_logs 表 │
└──────────────────────────┘
| 组 | 业务含义 | 包含 | 客户价值 |
|---|---|---|---|
| A | 账号认证 | A1 密码登录 / A2 短信登录 | 用户能登进去 |
| B | 区服检索 | B1 查区服 / B2 拉新区 / B3 选最新区 | 知道今天能玩哪个区 |
| C | 启动游戏 | C1 调启动 / C2 等就绪 | cq2.exe 真的起来 |
| D | 新号流程 | T01 ~ T13(13 任务) | 一个能挂机的新号 |
| E | 持久化与编排 | E1 端到端 / E2 断点恢复 | 任务可恢复、可审计 |
| 项 | 内容 |
|---|---|
| 业务输入 | username: str(例 longwu988)+ password: str |
| 业务产出 | user_token: str(JWT) |
| 调用 | POST https://api.jiyuhz.cn/auth/v3/login/safe/byPwd |
| 异常 | code=101000 → 走极验(GeetestSolver)重试其他 code → 抛 Box996Error |
| 存储 | users.token、users.token_expires_at |
| 实现位置 | login/box_996.py::Box996Login.login_with_password |
| 当前状态 | ✅ 已实现 |
| 依赖 | .env 有 CQ2_ACCOUNT / CQ2_PASSWORD |
| 项 | 内容 |
|---|---|
| 业务输入 | phone: str(13800138000)+ sms_code: str(人工输入或自动接收) |
| 业务产出 | user_token: str(JWT) |
| 调用 | 1. POST /auth/v3/login/sms/send(触发发码)2. POST /auth/v3/login/safe/bySms(提交登录) |
| 异常 | 同 A1 |
| 存储 | users.phone、users.token |
| 实现位置 | login/box_996.py(待补全) |
| 当前状态 | ❌ 未实现(client.py 抛 NotImplementedError) |
| 依赖 | 短信码来源:手动输入 / 第三方接码平台(业务待确认) |
| 项 | 内容 |
|---|---|
| 业务输入 | server_name: str(当前固定 "玖扬复古176",未来可能有多个) |
| 业务产出 | ServerGroup:{server_id, group_id, engin_id} |
| 调用 | GET https://list-box.dhsf.xqhuyu.com/box_serverlist/{gameid}.txt |
| 业务规则 | 在 list_name 字段中模糊匹配;支持多区服共存(每个区服一份记录) |
| 存储 | servers 表(server_name UNIQUE) |
| 实现位置 | launcher/navigate.py::find_server |
| 当前状态 | ✅ 已实现 |
| 依赖 | 已知 gameid = 206923 |
| 项 | 内容 |
|---|---|
| 业务输入 | server_id: int(来自 B1) |
| 业务产出 | Zone[],每条含:zone_id, zone_namehost, portopen_time: datetime(今天开服时间)status: "open" \| "full" \| "closed" \| "maintenance" |
| 调用 | GET https://pc-api.996hezi.com/box/v1/getTodayOpen?game_id={gameid} |
| 业务规则 | 1. 每个区服开服时间点不同("玖扬复古176"是 00:00/07:00/10:00/12:00/14:00/16:00/19:00/20:30/21:30)2. 每天必须重新拉取(开服时间点可能调整) 3. 按 open_time 升序排序 |
| 存储 | zones 表(带 fetched_at,避免用过期数据) |
| 实现位置 | login/box_996.py::Box996Login.get_today_open(部分:open_time 字段未解析入库) |
| 当前状态 | 🟡 部分实现 |
| 依赖 | T19(消息注册表)后能完整解析 section 字段(如 " 19:00") |
| 项 | 内容 |
|---|---|
| 业务输入 | server_name: str |
| 业务产出 | 单个 Zone(zone_id, host, port, open_time) |
| 业务规则 | 1. 调 B2 拿当天所有 zone 2. 过滤 status=open 且 open_time <= now() 的3. 若多个 open,取 open_time 最新的(用户想玩最新的区)4. 全部 closed/maintenance → 报错 NoZoneAvailable5. 若当前时间 < 最早 open_time → 等待到那个时间再继续 |
| 存储 | 读 zones,不写 |
| 实现位置 | client.py::LauncherFacade.run_until_enter_game(逻辑不对:当前取 servers[-1],没有 B3 逻辑) |
| 当前状态 | ❌ 未实现 |
| 依赖 | B2 完成 |
| 项 | 内容 |
|---|---|
| 业务输入 | zone_id: str(来自 B3)+ user_token: str(来自 A) |
| 业务产出 | GameLaunchInfo:{host, port, game_token, env_data} |
| 调用 | POST https://user-api.996hezi.com/userApi/v1/Game/launch |
| 存储 | 创建 runs 表记录(status="running",current_task=NULL) |
| 实现位置 | launcher/navigate.py::launch_game |
| 当前状态 | 🟡 已实现,但未写 runs 表 |
| 依赖 | A1/A2(拿 token)+ B3(拿 zone) |
| 项 | 内容 |
|---|---|
| 业务输入 | C1 的 host, port |
| 业务产出 | 可连接的游戏服务器 |
| 业务规则 | 1. 轮询 host:port TCP 可连2. 超时 60s 3. 成功 → 启动 TCP session,准备 T01 |
| 实现位置 | launcher/navigate.py::wait_for_game_ready |
| 当前状态 | ✅ 已实现 |
| 依赖 | C1 |
来源:
src/cq2_sdk/tasks/stubs.py。 当前状态:🟡 全部 stub(只返回TaskResult(success=True, elapsed=0.0),不发任何协议消息)。
| ID | 名称 | 业务说明 | 关联消息(猜测) |
|---|---|---|---|
| T01 | Create role | 创建游戏角色 | CS_CREATE_ROLE → SC_ROLE_INFO |
| T02 | Dismiss hint | 关闭新手教学弹窗 | CS_DISMISS_HINT |
| T03 | Guide + drink | 走完引导 + 喝酒接任务 | CS_NPC_TALK + CS_USE_ITEM |
| T04 | Get hoe | 获取锄头(接工具任务) | CS_QUEST_ACCEPT |
| T05 | Mining | 挖矿(采集任务) | CS_USE_HOE + 多次 CS_MINE |
| T06 | Wait 20 min | 等 20 分钟 + 走到新手村 | CS_MOVE_PATH + sleep(1200) |
| T07 | Reach L10 + kill 10 | 升 10 级 + 洞穴杀 10 只怪 | CS_ATTACK × N + SC_LEVEL_UP |
| T08 | Kill 10 in mine cave | 矿洞杀 10 只怪 | CS_MOVE_TO + CS_ATTACK × 10 |
| T09 | Kill 30 in centipede cave | 蜈蚣洞杀 30 只怪 | CS_MOVE_TO + CS_ATTACK × 30 |
| T10 | Shen tong activation | 神通激活(解锁技能) | CS_SKILL_UNLOCK |
| T11 | Five elements | 五行激活 + 领奖 | CS_FIVE_ELEMENTS_ACTIVATE + CS_QUEST_CLAIM |
| T12 | Tp to Woma + AFK | 传送到沃玛 + 挂机 | CS_TELEPORT + 进入 AFK 循环 |
| T13 | Activity (SSR-driven) | 活动(规则从服务端拉) | SSR-driven,运行时确定 |
实现前置依赖:
| 项 | 内容 |
|---|---|
| 业务入口 | async def run_full_flow(account, server_name) -> RunResult |
| 职责 | A → B → C → D 全链调用 |
| 产出 | 完整新号 + 完整 run 记录 |
| 实现位置 | client.py::Cq2Client(架构在,缺业务实现) |
| 当前状态 | 🟡 部分 |
| 依赖 | A1+A2+B1+B2+B3+C1+C2+T01-T13 全部 |
| 项 | 内容 |
|---|---|
| 职责 | 1. 每完成 1 个任务 → 写 task_logs 表 + 更新 runs.current_task2. 任务失败 → 写错误 + 写 snapshot.json 断点3. 支持从任意任务恢复( resume_from_run(run_id, from_task)) |
| 存储 | runs 表(执行状态)+ task_logs 表(每步结果)+ snapshot.json(任务级断点) |
| 实现位置 | utils/snapshot.py(JSON 快照有,DB 表缺) |
| 当前状态 | 🟡 部分 |
| 依赖 | 数据库先建好 |
DB 选型:使用外部 MySQL 8(你提供的测试环境),不用 SQLite。 原因:① 外部服务器已经有 996cq 数据库;② 多进程/多机访问方便;③ 跟生产一致。
| 项 | 值 |
|---|---|
| host | 47.99.164.66 |
| port | 3306 |
| user | root |
| password | hn02le.34lkdLKD |
| database | 996cq |
| charset | utf8mb4(支持 emoji 和完整中文) |
| engine | InnoDB(事务 + 外键) |
SDK 里通过环境变量读取(不要在代码里硬编码):
CQ2_DB_HOST=47.99.164.66
CQ2_DB_PORT=3306
CQ2_DB_USER=root
CQ2_DB_PASSWORD=hn02le.34lkdLKD
CQ2_DB_NAME=996cq
✅ 2026-09-16 已验证凭证可用(pymysql 1.4.6 测试):
- TCP 可达,server version:
5.7.36user@host:root@%(远程 root 权限已开)- 当前 0 张表(空库),需要执行 §8.2 的 DDL 建表
- 注:之前我用自己手写的 MySQL 协议测报 1045,是 salt 解析 bug(少拿了 7 字节),用真库 pymysql 重测就过了
⚠️ 安全提醒:
.env已经在.gitignore里,不会被 commit- 本节明文记录在文档里是为了方便查阅——如果你把文档发给别人/截图/上传 wiki,先把这节抹掉
- 生产环境务必换密码 + 改 user(不要 root)+ 改 host(不要公网直连)
- 长期方案:考虑用 git-crypt / Vault / 环境变量注入,不要让密码进任何 git 历史
✅ 2026-09-16 已通过
scripts/init_db.py初始化:5 张表全部创建成功(users / servers / zones / runs / task_logs),MySQL 5.7.36,库996cq。 重跑:python scripts/init_db.py(idempotent,CREATE TABLE IF NOT EXISTS)。 删表重建:python scripts/init_db.py --drop(会清空数据)。 只检查:python scripts/init_db.py --check。
-- 任务组 A:账号
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) UNIQUE, -- 账号登录用
phone VARCHAR(32) UNIQUE, -- 短信登录用
password_hash VARCHAR(255), -- 加密存(暂不实现)
token TEXT, -- JWT
token_expires_at TIMESTAMP NULL,
last_login_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 任务组 B:区服
CREATE TABLE servers (
id INT PRIMARY KEY AUTO_INCREMENT,
server_name VARCHAR(128) UNIQUE, -- "玖扬复古176"
server_id INT, -- 996box 的 serverid
game_id INT DEFAULT 206923,
first_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE zones (
id INT PRIMARY KEY AUTO_INCREMENT,
server_id INT NOT NULL,
zone_id VARCHAR(64),
zone_name VARCHAR(128),
host VARCHAR(128),
port INT,
open_time TIMESTAMP, -- 今天该区的开服时间
status VARCHAR(32), -- open/full/closed/maintenance
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_zone (server_id, zone_id, open_time),
FOREIGN KEY (server_id) REFERENCES servers(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 任务组 C+D+E:执行记录
CREATE TABLE runs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
server_id INT NOT NULL,
zone_id INT NOT NULL,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
current_task VARCHAR(8), -- t01..t13, NULL = 完成
status VARCHAR(32), -- running/success/failed
snapshot_path VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (server_id) REFERENCES servers(id),
FOREIGN KEY (zone_id) REFERENCES zones(id),
INDEX idx_user_status (user_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE task_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
run_id INT NOT NULL,
task_id VARCHAR(8), -- t01..t13
success TINYINT(1), -- 0/1(MySQL 没有原生 BOOLEAN)
error TEXT,
elapsed_seconds DOUBLE,
started_at TIMESTAMP NULL, -- 显式 NULL:MySQL 5.7 要求非首列 TIMESTAMP 必须标
finished_at TIMESTAMP NULL,
FOREIGN KEY (run_id) REFERENCES runs(id),
INDEX idx_run_task (run_id, task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
踩坑笔记:MySQL 5.7 严格模式下,非首列的 TIMESTAMP 必须显式
NULL或DEFAULT,否则报1067 Invalid default value。本 schema 已按规则修过。
当前 pyproject.toml 没有 MySQL 驱动,需要加一个:
| 驱动 | 异步 | 安装难度 | 推荐 |
|---|---|---|---|
pymysql |
❌(配合 asyncio.to_thread) |
极简 | ⭐⭐⭐ 首选:纯 Python,Windows 一行装 |
aiomysql |
✅ | 中 | ⭐⭐ 等 SDK 全异步迁移后再切 |
mysqlclient |
❌ | 难(要 C 编译) | 不推荐(Windows 编译痛苦) |
推荐加 pymysql>=1.1:
# pyproject.toml dependencies 新增
"pymysql>=1.1.0",
后续会写一个 cq2_sdk/store.py 封装所有表操作(UserStore、ServerStore、ZoneStore、RunStore)。
# 账号
async def register_or_login_password(username, password) -> User
async def request_sms_code(phone) -> None
async def login_with_sms(phone, sms_code) -> User
# 区服
async def discover_today_zones(server_name="玖扬复古176") -> list[Zone]
async def select_latest_zone(server_name="玖扬复古176") -> Zone
# 端到端
async def run_full_flow(account: User, server_name: str = "玖扬复古176") -> RunResult
async def resume_run(run_id: int, from_task: str | None = None) -> RunResult
# 状态查询
async def get_run(run_id: int) -> Run
async def list_runs(user_id: int, limit: int = 20) -> list[Run]
┌──── A1 密码登录 ────┐
│ │
用户 ────┤ ├──► user_token
│ │
└──── A2 短信登录 ────┘
│
▼
users 表入库
│
▼ user_token
┌──── B1 查区服 ────────► server_id
│ │
│ └── B2 拉新区 ──────► zones[]
│ │
│ └── B3 选最新区 ────► zone
│ │
└──────────────┬──────────────────────┘
│
▼
┌──── C1 启动游戏 ─────► host/port
│ (创建 runs 记录)
│
└── C2 等就绪 ──────────► TCP session 可连
│
▼
┌──────────── 13 任务流程 ────────────┐
│ T01 → T02 → ... → T06 → T07 → ... │
│ → T12 → T13 │
└────────────────┬────────────────────┘
│
▼
┌──── E2 持久化 ────────┐
│ 写 task_logs │
│ 写 snapshot.json │
│ 更新 runs.status │
└───────────────────────┘
关键路径(最关键的 3 个瓶颈):
| 任务 | 状态 | 阻塞点 |
|---|---|---|
| A1 密码登录 | ⚠️ 代码 OK,极验风控(32401) | 已被 A2 取代 |
| A2 短信登录 | ✅ 协议已破解 + 真 JWT 落库(30 天有效,phoneOptionInfo 验证过) | — |
| B1 查区服 | ✅ 真跑(servers.id=8) | — |
| B2 拉新区 | ✅ 真跑(9 区,14426762-770) | — |
| B3 选最新区 | ✅ 真跑(选中 16:00 区 14426767) | — |
| C1 启动游戏 | ✅ 真跑(run_id=6,start_info → env_dumps/run_6_env.json) | — |
| C2 等 TCP 就绪 | ✅ 真跑(gfgame:12000,0.1s 连通) | — |
| D 13 任务(T01-T13) | 🟡 全部 stub | T17 + T19 |
| E1 编排 | ❌ | — |
| E2 持久化 | 🟡 | task_logs 已有 Store,缺 snapshot.json 整合 |
| 底层依赖 | ||
| 数据库(MySQL + schema) | ✅ 5 表 + 真数据 | — |
| T17 TCP 登录 | ❌ | D 全部 |
| T19 消息注册表 | 🟡 | D 全部 |
业务任务完成度(2026-09-16 晚):HTTP 链 A→B→C 7 项全部真跑贯通(含真 token + 真 9 区 + 真 run 记录 + 真 TCP 连通);剩 D(13 任务 stub)+ E1/E2 + T17/T19 两个硬骨头。
按"价值/难度"比和依赖关系:
| 步骤 | 任务 | 价值 | 难度 | 阻塞 |
|---|---|---|---|---|
| 1 | 选数据库(SQLite)+ 写 schema | 极高 | 低 | E 全部 |
| 2 | 写 UserStore / RunStore(CRUD) |
极高 | 低 | A、E |
| 3 | 写 ServerStore / ZoneStore(B1/B2/B3 闭环) |
极高 | 低 | B1、B2、B3 |
| 4 | 写 TaskLogStore(fold 3,13 任务落库 + 断点恢复) |
极高 | 低 | E2、D 全部 |
| 5 | A2 短信登录补全 | 中 | 低 | — |
| 6 | B2 解析 open_time 入库 |
中 | 低 | B3 |
| 7 | B3 选最新区逻辑 | 中 | 低 | E1 |
| 8 | C1 落库(创建 runs) | 中 | 低 | E1 |
| 9 | E1 串成 run_full_flow |
极高 | 中 | — |
| 10 | T19 消息注册表(解析 Lua) | 极高 | 中 | D 全部 |
| 11 | T17 TCP 登录(Frida hook) | 极高 | 高 | D 全部 |
| 12 | T01-T06 真实实现 | 极高 | 中 | T40 |
| 13 | T07-T13 真实实现 | 高 | 中 | T40 |
| 14 | T40 端到端验收 | 极高 | — | 全部 |
✅ 2026-09-16 进展(晚):SMS 登录协议破解 + HTTP 全链真数据跑通
- 用户用 Fiddler 抓了
手机号短信验证登录过程.saz,逆向出真实协议:
- 发码
POST user-api.996hezi.com/v1/UserInfo/get_phone_codebody{phone,event:1,type:login}+ 极验重发带_captcha_(base64 geetest JSON)- 登录
POST api.996hezi.com/auth/v3/login/byPhoneCodebody{phone,code,check_account:1}- token 验证
GET api.996hezi.com/user/v1/phoneOptionInfo(带 token 头)- 真 JWT 已从抓包提取并落 MySQL(longwu988,有效期至 2026-10-16,
phoneOptionInfo活体验证通过)getTodayOpenhost 修正:pc-api.996hezi.com(404)→api.996hezi.com(活,9 区)- B2 过滤逻辑修正:srvlist 的组 serverid(14301060)≠ 逐区 serverid(14426762+),改按 game 过滤
- B1→B2→B3 真数据全链跑通:玖扬复古176 → 9 个今日新区(14426762-770)→ B3 选中 16:00 区(14426767)
- 修复:
update_token曾错放进 RunStore(已挪回 UserStore + 回归测试);996box 系域名 SSL 证书链问题(verify=False)- ⚠️ C1/C2 未真跑:enter_game_client 会触发 cq2.exe 启动 → 反作弊 → OpenCode 崩溃(原始问题)。须从 cq2play 用户会话跑
- 工具沉淀:
scripts/parse_saz.py(Fiddler 抓包解析器)、scripts/seed_token_from_saz.py(从抓包提取 token 落库)、scripts/probe_today_open.py(端点活探)✅ 2026-09-16 进展:步骤 1+2+3+4 完成(Store 层 100% 完成)
- 数据库:MySQL 5.7.36
47.99.164.66:996cq,5 张表已建(init_db.py一键管理)- Store 层 fold 1:
UserStore+RunStore✅- Store 层 fold 2:
ServerStore+ZoneStore(含get_latest_openable实现 B3、mark_missing_closed)✅- Store 层 fold 3:
TaskLogStore(含completed_task_ids方便 E2 断点恢复)✅- 总计:5 个 Store 类,36 个 store 单测,3 个 e2e demo,全量回归 218 PASS
- 新文件:
src/cq2_sdk/db_models.py(5 个 dataclass)src/cq2_sdk/store.py(5 个 Store + get_connection/connection helpers)tests/unit/test_store.py(36 个单测)scripts/init_db.py(建表工具)scripts/demo_user_run_store.py(fold 1 demo)scripts/demo_server_zone_store.py(fold 2 demo)scripts/demo_task_log_store.py(fold 3 demo)- 业务代码还没接:E1 编排、A2 短信、T17 TCP、T01-T13 真实实现
这些必须客户拍板才能往下做:
| # | 问题 | 影响 |
|---|---|---|
| Q1 | 短信码怎么获取?手动输 / 接码平台 / 邮件? | A2 实现 |
| Q2 | 区服名是否会变(多区服)?怎么管理? | B1 扩展性 |
| Q3 | 任务失败时是否自动重试?最多几次? | E1 重试策略 |
| Q4 | T13 活动的"SSR 拉规则"具体是什么协议? | T13 实现 |
| Q5 | 是否需要并发跑多账号? | E1 架构 |
| Q6 | 用户 token 过期怎么处理?续签 / 重新登录? | A 流程 |
| Q7 | runs 记录保留多久?要不要定期清理? | DB 维护 |