/** * 直连 RunForge 切换跑区(与 Worker lepao.setZone 一致),用于 token 探活,不经过 runpy。 */ const axios = require('axios') const { URLSearchParams } = require('url') const db = require('../DataBase/db') const { dataEncrypt, dataDecrypt, dataSign } = require('./Crypto') const { getWebVpnCookieHeader, invalidateWebVpnCookie, isProbablyVpnLoginHtml, isWebVpnUnifiedAuthCredentialFailure, markLepaoUnifiedAuthFailed } = require('../../lib/Lepao/webvpnCookie') const BASE_URL = 'https://lepao.ctbu.edu.cn/v3/api.php' const DEFAULT_UA = 'Mozilla/5.0 (Linux; Android 16; 2211133C Build/BP2A.250605.031.A3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 MMWEBSDK/20250202 MMWEBID/1020 wxwork/5.0.6.66174 MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android Luggage/3.0.2.95ef3f83 NetType/WIFI Language/zh_CN ABI/arm64' const runZoneMap = { 兰花湖校区跑区: 2, 主校区北跑区: 3, 主校区南跑区: 5, 重庆工商大学茶园校区: 6 } function lepaoTimestamp() { return Number((Date.now() / 1000).toFixed(3)) } /** * @param {object} p * @param {string} p.uid * @param {string} p.token * @param {string|number} p.school_id * @param {string} p.student_num * @param {number} [p.random_id=1] path_data.id,需存在且 run_zone_name 可映射 * @param {string} [p.userAgent] * @param {string} [p.create_user] 绑定用户 uuid,用于取 WebVPN Cookie(教务登录名与学号 student_num 一致) * @returns {Promise} RunForge 响应体(与 Worker request 解密后形态一致) */ async function probeSetZone(p) { const { uid, token, school_id, student_num, random_id = 1, userAgent, create_user: createUser } = p const record = await db.query('SELECT run_zone_name FROM path_data WHERE id = ?', [random_id]) if (!record || record.length === 0) { throw new Error('跑区不存在') } const runZoneId = runZoneMap[record[0].run_zone_name] if (!runZoneId) throw new Error('跑区不存在') const raw = { uid, token, school_id, term_id: 0, course_id: 0, class_id: 0, student_num, card_id: student_num, timestamp: lepaoTimestamp(), version: 1, nonce: String(Math.floor(Math.random() * 900000 + 100000)), ostype: 5, run_zone_id: String(runZoneId) } raw.sign = dataSign(raw) const form = new URLSearchParams() form.append('ostype', '5') form.append('data', dataEncrypt(JSON.stringify(raw))) let webvpnCookie if (createUser) { try { webvpnCookie = await getWebVpnCookieHeader(createUser, student_num, { skipPostWebVpnLepaoSync: true }) } catch (e) { if (isWebVpnUnifiedAuthCredentialFailure(e)) { try { await markLepaoUnifiedAuthFailed(student_num, createUser, student_num) } catch (_) { /* ignore */ } } throw new Error(e.message || 'WebVPN 登录失败') } } const buildHeaders = () => ({ 'Content-Type': 'application/x-www-form-urlencoded', Accept: '*/*', 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', 'User-Agent': userAgent || DEFAULT_UA, Referer: 'https://servicewechat.com/wxf94c4ddb63d87ede/32/page-frame.html', charset: 'utf-8', ...(webvpnCookie ? { Cookie: webvpnCookie } : {}) }) const postOnce = () => axios.post(`${BASE_URL}/Run/setRunZone`, form, { headers: buildHeaders(), timeout: 20000, proxy: false, responseType: 'text', transformResponse: [(b) => b] }) let res = await postOnce() let result try { result = JSON.parse(res.data) } catch { result = res.data } if ( typeof result === 'string' && isProbablyVpnLoginHtml(result) && createUser ) { await invalidateWebVpnCookie(createUser, student_num) try { webvpnCookie = await getWebVpnCookieHeader(createUser, student_num, { skipCache: true, skipPostWebVpnLepaoSync: true }) } catch (vpnRefreshErr) { if (isWebVpnUnifiedAuthCredentialFailure(vpnRefreshErr)) { try { await markLepaoUnifiedAuthFailed(student_num, createUser, student_num) } catch (_) { /* ignore */ } } throw new Error(vpnRefreshErr.message || 'WebVPN 登录失败') } res = await postOnce() try { result = JSON.parse(res.data) } catch { result = res.data } } if (result?.data && result?.is_encrypt === 1) { result = { ...result, data: JSON.parse(dataDecrypt(result.data)) } } return result } function isProbeSetZoneOk(result) { if (!result) return false const hasData = result.data != null && result.data !== '' if (!hasData) return false if (Number(result.status) === 1) return true const cd = Number(result.code) if (cd === 1 || cd === 200) return true return false } function getProbeFailMessage(result) { if (!result) return '' return ( result.info || result.msg || result.message || (typeof result.data === 'object' && result.data ? result.data.info || result.data.msg || result.data.message : '') || '' ) } module.exports = { probeSetZone, isProbeSetZoneOk, getProbeFailMessage, BASE_URL }