business-tasks.md 26 KB

业务任务总览(Business Tasks Overview)

范围说明:本文件只列客户提的业务需求,不含框架代码 / 调试工具 / 基础设施。 架构决策、DebugLogger、WSL2/Plan E、mitmproxy 接入等内容不属于本文档范围。

同步基线:tasks/stubs.py(13 任务定义)、launcher/navigate.py(HTTP 导航)、 login/box_996.py(HTTP 登录)、client.py(高层编排)。


0. 一句话

从用户输入凭证 + 区服名,到一个能挂机的传奇新号——这一整条链上的所有业务动作。

1. 业务流程全景

┌────────────────────────────────────────────────────────────────────┐
│ 用户输入层                                                           │
│  (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 表   │
                  └──────────────────────────┘

2. 业务任务分组

组 业务含义 包含 客户价值
A 账号认证 A1 密码登录 / A2 短信登录 用户能登进去
B 区服检索 B1 查区服 / B2 拉新区 / B3 选最新区 知道今天能玩哪个区
C 启动游戏 C1 调启动 / C2 等就绪 cq2.exe 真的起来
D 新号流程 T01 ~ T13(13 任务) 一个能挂机的新号
E 持久化与编排 E1 端到端 / E2 断点恢复 任务可恢复、可审计

3. 任务组 A:账号认证

A1. 账号密码登录

项 内容
业务输入 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

A2. 手机号短信登录

项 内容
业务输入 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)
依赖 短信码来源:手动输入 / 第三方接码平台(业务待确认)

4. 任务组 B:区服检索

B1. 按名称查找区服

项 内容
业务输入 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

B2. 获取当天新区列表

项 内容
业务输入 server_id: int(来自 B1)
业务产出 Zone[],每条含:
 zone_id, zone_name
 host, port
 open_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")

B3. 选择最新可玩区

项 内容
业务输入 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 → 报错 NoZoneAvailable
5. 若当前时间 < 最早 open_time → 等待到那个时间再继续
存储 读 zones,不写
实现位置 client.py::LauncherFacade.run_until_enter_game(逻辑不对:当前取 servers[-1],没有 B3 逻辑)
当前状态 ❌ 未实现
依赖 B2 完成

5. 任务组 C:启动游戏

C1. 调用启动接口

项 内容
业务输入 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)

C2. 等待 cq2.exe 启动 + TCP 连接

项 内容
业务输入 C1 的 host, port
业务产出 可连接的游戏服务器
业务规则 1. 轮询 host:port TCP 可连
2. 超时 60s
3. 成功 → 启动 TCP session,准备 T01
实现位置 launcher/navigate.py::wait_for_game_ready
当前状态 ✅ 已实现
依赖 C1

6. 任务组 D:13 个新号流程

来源: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,运行时确定

实现前置依赖:

  • T17(TCP 3 步登录)—— 所有任务的"能发消息"前提
  • T19(消息注册表)—— 每个任务要查表才知道发什么消息

7. 任务组 E:编排与持久化

E1. 端到端编排

项 内容
业务入口 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 全部

E2. 状态持久化与断点恢复

项 内容
职责 1. 每完成 1 个任务 → 写 task_logs 表 + 更新 runs.current_task
2. 任务失败 → 写错误 + 写 snapshot.json 断点
3. 支持从任意任务恢复(resume_from_run(run_id, from_task))
存储 runs 表(执行状态)+ task_logs 表(每步结果)+ snapshot.json(任务级断点)
实现位置 utils/snapshot.py(JSON 快照有,DB 表缺)
当前状态 🟡 部分
依赖 数据库先建好

8. 数据模型(MySQL 设计)

DB 选型:使用外部 MySQL 8(你提供的测试环境),不用 SQLite。 原因:① 外部服务器已经有 996cq 数据库;② 多进程/多机访问方便;③ 跟生产一致。

8.1 测试环境连接信息

项 值
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.36
  • user@host: root@%(远程 root 权限已开)
  • 当前 0 张表(空库),需要执行 §8.2 的 DDL 建表
  • 注:之前我用自己手写的 MySQL 协议测报 1045,是 salt 解析 bug(少拿了 7 字节),用真库 pymysql 重测就过了

⚠️ 安全提醒:

  • .env 已经在 .gitignore 里,不会被 commit
  • 本节明文记录在文档里是为了方便查阅——如果你把文档发给别人/截图/上传 wiki,先把这节抹掉
  • 生产环境务必换密码 + 改 user(不要 root)+ 改 host(不要公网直连)
  • 长期方案:考虑用 git-crypt / Vault / 环境变量注入,不要让密码进任何 git 历史

8.2 表结构

✅ 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 已按规则修过。

8.3 Python 驱动选型

当前 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)。

9. 用户视角的 API(最终交付形态)

# 账号
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]

10. 任务依赖图(业务视角)

        ┌──── 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 个瓶颈):

  1. A1/A2 → 没有 token 啥都做不了
  2. B2 → 不知道今天能玩啥区
  3. T17(TCP 登录)+ T19(消息注册表) → 不破这俩,T01-T13 一个都跑不了

11. 当前进度

任务 状态 阻塞点
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 两个硬骨头。

12. 推荐实现顺序

按"价值/难度"比和依赖关系:

步骤 任务 价值 难度 阻塞
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_code body {phone,event:1,type:login} + 极验重发带 _captcha_(base64 geetest JSON)
    • 登录 POST api.996hezi.com/auth/v3/login/byPhoneCode body {phone,code,check_account:1}
    • token 验证 GET api.996hezi.com/user/v1/phoneOptionInfo(带 token 头)
  • 真 JWT 已从抓包提取并落 MySQL(longwu988,有效期至 2026-10-16,phoneOptionInfo 活体验证通过)
  • getTodayOpen host 修正: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 真实实现

13. 待客户确认的业务问题

这些必须客户拍板才能往下做:

# 问题 影响
Q1 短信码怎么获取?手动输 / 接码平台 / 邮件? A2 实现
Q2 区服名是否会变(多区服)?怎么管理? B1 扩展性
Q3 任务失败时是否自动重试?最多几次? E1 重试策略
Q4 T13 活动的"SSR 拉规则"具体是什么协议? T13 实现
Q5 是否需要并发跑多账号? E1 架构
Q6 用户 token 过期怎么处理?续签 / 重新登录? A 流程
Q7 runs 记录保留多久?要不要定期清理? DB 维护