From 4c8ab4b916547e43395e3da49c41fbc8df5ed2c2 Mon Sep 17 00:00:00 2001 From: Abner <22141172+Silentely@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:21:05 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20docs:=20=E8=A7=84=E8=8C=83=20Sim?= =?UTF-8?q?yo=20=E5=AE=A2=E6=88=B7=E7=AB=AF=E4=B8=8E=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将注释与测试描述改为中性技术说明,仅保留接口路径与配置语义。 --- env.example | 5 ++-- netlify.toml | 4 +-- server.js | 8 +++--- src/simyo/js/modules/api-config.js | 21 ++++++--------- src/simyo/js/modules/auth-handler.js | 2 +- src/simyo/js/modules/client-identity.js | 26 +++++++++---------- src/simyo/js/modules/device-change-handler.js | 10 +++---- src/simyo/js/modules/esim-service.js | 2 +- src/simyo/simyo_proxy_server.js | 2 +- tests/simyo/api-config.test.js | 20 +++++++------- tests/simyo/auth-handler.test.js | 4 +-- tests/simyo/device-change-handler.test.js | 2 +- tests/test_simyo_esim.html | 2 +- 13 files changed, 50 insertions(+), 58 deletions(-) diff --git a/env.example b/env.example index 542b415..2521172 100644 --- a/env.example +++ b/env.example @@ -39,10 +39,9 @@ ACCESS_KEY= # Simyo 代理客户端令牌 # ⚠️ 启用 Simyo 代理时必填;server.js 不再提供默认回退 SIMYO_CLIENT_TOKEN= -# 可选:固定 X-Device-ID(UUID,官方 App 每个请求必带;未设则进程内自动生成) +# 可选:固定 X-Device-ID(UUID;未设则进程内自动生成) # SIMYO_DEVICE_ID= -# 可选:覆盖默认客户端身份(默认对齐 iOS 4.28.0 / iOS 18.2 / iPhone12,8 抓包) -# 与 src/simyo/js/modules/client-identity.js 保持同步 +# 可选:覆盖默认客户端身份(默认见 client-identity.js) # SIMYO_CLIENT_VERSION=4.28.0 # SIMYO_CLIENT_PLATFORM=ios # SIMYO_IOS_VERSION=18.2 diff --git a/netlify.toml b/netlify.toml index 72794a7..ec54556 100644 --- a/netlify.toml +++ b/netlify.toml @@ -35,7 +35,7 @@ # API代理重定向(用于解决CORS问题) # 通用 Simyo API 代理 (webapi - 新版API) -# User-Agent 覆盖为官方 iOS 4.28.0 eSIM 更换抓包值(浏览器无法可靠设置 UA) +# 代理层覆盖 User-Agent(浏览器无法可靠设置 UA) # 与 src/simyo/js/modules/client-identity.js 保持同步 [[redirects]] from = "/api/simyo/*" @@ -44,7 +44,7 @@ force = true headers = {X-Forwarded-Host = "appapi.simyo.nl", User-Agent = "MijnSimyoFT/4.28.0 (iOS 18.2; iPhone12,8)"} -# 保留旧版 simyoapi 路径的兼容性(如果需要) +# 旧版 simyoapi 路径兼容 [[redirects]] from = "/api/simyo/legacy/*" to = "https://appapi.simyo.nl/simyoapi/api/v1/:splat" diff --git a/server.js b/server.js index 5e7e530..0fac3e8 100644 --- a/server.js +++ b/server.js @@ -29,7 +29,7 @@ const { parseOrigins, isAllowedOrigin: _isAllowedOrigin, resolveCorsOrigin: _res const origins = parseOrigins(process.env.ALLOWED_ORIGIN); const isAllowedOrigin = (origin) => _isAllowedOrigin(origin, origins); const getCorsOrigin = (origin) => _resolveCorsOrigin(origin, origins); -// 与 src/simyo/js/modules/client-identity.js 保持同步(官方 iOS 4.28.0 eSIM 更换 HAR) +// 与 src/simyo/js/modules/client-identity.js 保持同步 const DEFAULT_SIMYO_CLIENT_PLATFORM = 'ios'; const DEFAULT_SIMYO_CLIENT_VERSION = '4.28.0'; const DEFAULT_SIMYO_IOS_VERSION = '18.2'; @@ -190,7 +190,7 @@ app.use('/api/simyo/*', (req, res) => { const [pathPart, queryPart] = req.originalUrl.replace(/^\/api\/simyo/, '').split('?'); const proxyPath = pathPart || '/'; const queryString = queryPart ? `?${queryPart}` : ''; - // 与官方 App / Netlify 代理一致,走 webapi(simyoapi 为旧路径) + // 本地代理走 webapi(simyoapi 为旧路径) const targetUrl = `https://appapi.simyo.nl/webapi/api/v1${proxyPath}${queryString}`; Logger.log(`[Simyo Proxy] ${req.method} ${req.path} -> ${targetUrl}`); @@ -212,8 +212,8 @@ app.use('/api/simyo/*', (req, res) => { }); } - // 代理请求:强制官方 App 身份头(浏览器 UA 不可靠,禁止透传) - // X-Device-ID 为 Simyo 4.28+ 必填,优先使用前端持久化 ID + // 代理请求:强制使用客户端身份头(浏览器 UA 不可靠,禁止透传) + // X-Device-ID 必填,优先使用前端持久化 ID const axios = require('axios'); const config = { method: req.method.toLowerCase(), diff --git a/src/simyo/js/modules/api-config.js b/src/simyo/js/modules/api-config.js index bfd7842..db3d08f 100644 --- a/src/simyo/js/modules/api-config.js +++ b/src/simyo/js/modules/api-config.js @@ -1,12 +1,11 @@ /** - * Simyo API配置模块 - * 定义所有API端点和请求配置 + * Simyo API 配置模块 + * 定义端点、请求头与响应解析 * - * 请求头字段对齐官方 Mijn Simyo iOS 抓包: - * X-Client-Token / X-Client-Platform / X-Client-Version / X-Device-ID / User-Agent - * 版本与 UA 见 client-identity.js(单一事实来源) + * 请求头:X-Client-Token / Platform / Version / X-Device-ID / User-Agent + * 版本与 UA 见 client-identity.js * - * eSIM 更换主路径(HAR 2026-07-22,仅 EMAIL): + * eSIM 更换主路径(仅 EMAIL): * POST /sessions → GET /settings/simcard → POST /settings/simcard * → POST /esim/verify-code → GET /esim/get-by-customer */ @@ -19,7 +18,7 @@ import { simyoClientIdentity } from './client-identity.js'; const DEVICE_ID_STORAGE_KEY = 'simyo_device_id'; /** - * Simyo客户端配置(与官方 App 抓包一致,源自 client-identity) + * Simyo 客户端配置(源自 client-identity) */ export const simyoConfig = { clientToken: simyoClientIdentity.clientToken, @@ -87,7 +86,7 @@ export function getOrCreateDeviceId() { } /** - * API端点配置(对齐官方 webapi 路径) + * API 端点配置(webapi 路径) */ export function getApiEndpoints() { const isNetlify = isNetlifyEnvironment(); @@ -227,7 +226,7 @@ export function mapSimyoErrorMessage(status, data) { /** * 判断 Simyo result 体是否表示业务成功 - * 官方登录/查询常无 result.success,仅有 sessionToken / eSimStatus 等字段 + * 登录/状态查询响应可能不含 result.success,而以 sessionToken、eSimStatus 等字段为准 * @param {object} result * @returns {boolean} */ @@ -241,19 +240,15 @@ export function isSimyoResultSuccessful(result) { if (result.success === true) { return true; } - // 登录 if (result.sessionToken) { return true; } - // 取 eSIM if (result.activationCode) { return true; } - // simcard 状态查询 if (result.eSimStatus || result.canCreateESim || result.canCreateSimcard) { return true; } - // 申请更换:reason + remainingNumberOfTries if (result.reason != null || result.remainingNumberOfTries != null) { return true; } diff --git a/src/simyo/js/modules/auth-handler.js b/src/simyo/js/modules/auth-handler.js index 0f98fed..2253d96 100644 --- a/src/simyo/js/modules/auth-handler.js +++ b/src/simyo/js/modules/auth-handler.js @@ -11,7 +11,7 @@ import Logger from '../../../js/modules/logger.js'; /** * 判断登录返回的 mfaStatus 是否允许继续业务 - * 官方 HAR:DISABLED_BY_CUSTOMER 表示客户关闭 MFA + * DISABLED_BY_CUSTOMER 等表示无需再完成 MFA * @param {string} mfaStatus * @returns {boolean} */ diff --git a/src/simyo/js/modules/client-identity.js b/src/simyo/js/modules/client-identity.js index 95876bc..effd732 100644 --- a/src/simyo/js/modules/client-identity.js +++ b/src/simyo/js/modules/client-identity.js @@ -1,13 +1,11 @@ /** - * Simyo 官方 App 客户端身份(单一事实来源) + * Simyo 客户端身份配置(单一事实来源) * - * 对齐 Mijn Simyo iOS 抓包(2026-07-22 eSIM 更换 HAR): - * - App 版本: 4.28.0 - * - API User-Agent: MijnSimyoFT/4.28.0 (iOS 18.2; iPhone12,8) + * 字段说明: + * - clientVersion: X-Client-Version + * - userAgent: MijnSimyoFT/{version} (iOS {ios}; {model}) * (版本号与括号之间为两个空格) - * - 平台: ios - * - 设备型号: iPhone12,8 - * - 系统版本: 18.2 + * - platform: X-Client-Platform(ios) * * 修改此处后请同步: * - server.js 中的 DEFAULT_SIMYO_* 常量 @@ -16,24 +14,24 @@ * - env.example 注释示例 */ -/** 官方 App 版本号(X-Client-Version) */ +/** X-Client-Version */ export const SIMYO_CLIENT_VERSION = '4.28.0'; -/** 官方平台(X-Client-Platform) */ +/** X-Client-Platform */ export const SIMYO_CLIENT_PLATFORM = 'ios'; -/** 官方客户端 Token(X-Client-Token) */ +/** X-Client-Token */ export const SIMYO_CLIENT_TOKEN = 'e77b7e2f43db41bb95b17a2a11581a38'; -/** 抓包中的 iOS 系统版本 */ +/** User-Agent 中的 iOS 系统版本 */ export const SIMYO_IOS_VERSION = '18.2'; -/** 抓包中的设备型号(identifier) */ +/** User-Agent 中的设备型号 */ export const SIMYO_DEVICE_MODEL = 'iPhone12,8'; /** - * appapi 请求使用的 User-Agent - * 注意:版本号后必须保留两个空格,与官方 Flutter 客户端一致 + * 发往 appapi 的 User-Agent + * 版本号后必须保留两个空格 */ export const SIMYO_USER_AGENT = `MijnSimyoFT/${SIMYO_CLIENT_VERSION} (iOS ${SIMYO_IOS_VERSION}; ${SIMYO_DEVICE_MODEL})`; diff --git a/src/simyo/js/modules/device-change-handler.js b/src/simyo/js/modules/device-change-handler.js index 53bd7a9..79ff75d 100644 --- a/src/simyo/js/modules/device-change-handler.js +++ b/src/simyo/js/modules/device-change-handler.js @@ -1,6 +1,6 @@ /** - * Simyo设备更换处理模块 - * 对齐官方 App HAR(仅 EMAIL 验证): + * Simyo 设备更换处理模块 + * 固定 EMAIL 验证流程: * GET settings/simcard → POST settings/simcard(EMAIL) → POST esim/verify-code */ @@ -10,7 +10,7 @@ import { validateVerificationCode } from './utils.js'; import { t } from '../../../js/modules/i18n.js'; import Logger from '../../../js/modules/logger.js'; -/** 官方 eSimStatus 状态 */ +/** eSimStatus 业务状态 */ export const ESIM_STATUS = { START_REQUEST: 'ESIM_START_REQUEST', WAITING_FOR_VALIDATION_CODE: 'ESIM_REQUEST_WAITING_FOR_VALIDATION_CODE', @@ -52,7 +52,7 @@ export class DeviceChangeHandler { } /** - * 请求设备更换(固定 EMAIL,对齐官方 body) + * 请求设备更换(固定 EMAIL) * 会先 GET 状态:已在等待验证码 / 可下载时避免重复下单 * @returns {Promise} */ @@ -107,7 +107,7 @@ export class DeviceChangeHandler { throw new Error(data.message || t('simyo.device.applyFailed')); } - // 官方:success:true + reason Available | AlreadyOrderedSimcardEsim + // success:true + reason Available | AlreadyOrderedSimcardEsim if (data.result.success === false) { throw new Error(data.message || data.result.reason || t('simyo.device.applyFailed')); } diff --git a/src/simyo/js/modules/esim-service.js b/src/simyo/js/modules/esim-service.js index 995fa9a..194a6ef 100644 --- a/src/simyo/js/modules/esim-service.js +++ b/src/simyo/js/modules/esim-service.js @@ -33,7 +33,7 @@ export class EsimService { const data = await handleApiResponse(response); - // 官方 HAR:{ result: { activationCode, success: true, errorCode: 0 } } + // 成功响应含 activationCode if (!data.result || !data.result.activationCode) { throw new Error(data.message || t('simyo.esim.errors.notFound')); } diff --git a/src/simyo/simyo_proxy_server.js b/src/simyo/simyo_proxy_server.js index 2520ad7..f5cbe21 100644 --- a/src/simyo/simyo_proxy_server.js +++ b/src/simyo/simyo_proxy_server.js @@ -33,7 +33,7 @@ function isAllowedTarget(urlStr) { } } -// Simyo API配置(与 client-identity.js 同步:iOS 4.28.0 / iOS 18.2 / iPhone12,8) +// Simyo API 配置(与 client-identity.js 同步) const crypto = require('crypto'); const SIMYO_CLIENT_VERSION = process.env.SIMYO_CLIENT_VERSION || '4.28.0'; const SIMYO_IOS_VERSION = process.env.SIMYO_IOS_VERSION || '18.2'; diff --git a/tests/simyo/api-config.test.js b/tests/simyo/api-config.test.js index a977f3d..c6df3a6 100644 --- a/tests/simyo/api-config.test.js +++ b/tests/simyo/api-config.test.js @@ -1,7 +1,7 @@ 'use strict'; /** - * Simyo api-config / client-identity:请求头与设备身份(对齐官方 App 抓包) + * Simyo api-config / client-identity:请求头与设备身份 */ import { @@ -22,18 +22,18 @@ import { const UUID_RE = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/; /** 官方抓包 UA:版本号后恰好两个空格 */ -const OFFICIAL_UA = 'MijnSimyoFT/4.28.0 (iOS 18.2; iPhone12,8)'; +const EXPECTED_UA = 'MijnSimyoFT/4.28.0 (iOS 18.2; iPhone12,8)'; describe('Simyo client-identity', () => { - it('版本 / 设备 / UA 应与 4.28.0 eSIM 更换抓包一致', () => { + it('版本 / 设备 / UA 应与 client-identity 常量一致', () => { expect(SIMYO_CLIENT_VERSION).toBe('4.28.0'); expect(SIMYO_CLIENT_PLATFORM).toBe('ios'); expect(SIMYO_IOS_VERSION).toBe('18.2'); expect(SIMYO_DEVICE_MODEL).toBe('iPhone12,8'); - expect(SIMYO_USER_AGENT).toBe(OFFICIAL_UA); - // 版本号与括号之间必须是两个空格(不是一个) + expect(SIMYO_USER_AGENT).toBe(EXPECTED_UA); + // 版本号与括号之间必须是两个空格 expect(SIMYO_USER_AGENT).toMatch(/^MijnSimyoFT\/4\.28\.0 {2}\(iOS 18\.2; iPhone12,8\)$/); - expect(simyoClientIdentity.userAgent).toBe(OFFICIAL_UA); + expect(simyoClientIdentity.userAgent).toBe(EXPECTED_UA); }); }); @@ -51,13 +51,13 @@ describe('Simyo api-config headers', () => { expect(localStorage.getItem('simyo_device_id')).toBe(id1); }); - it('createHeaders 必须包含官方抓包中的核心头', () => { + it('createHeaders 必须包含核心身份头', () => { const headers = createHeaders(false); expect(headers['X-Client-Token']).toBe(simyoConfig.clientToken); expect(headers['X-Client-Platform']).toBe(SIMYO_CLIENT_PLATFORM); expect(headers['X-Client-Version']).toBe(SIMYO_CLIENT_VERSION); expect(headers['X-Device-ID']).toMatch(UUID_RE); - expect(headers['User-Agent']).toBe(OFFICIAL_UA); + expect(headers['User-Agent']).toBe(EXPECTED_UA); expect(headers['User-Agent']).toBe(simyoConfig.userAgent); expect(headers['Content-Type']).toBe('application/json'); expect(headers.Accept).toBe('application/json'); @@ -68,7 +68,7 @@ describe('Simyo api-config headers', () => { const headers = createHeaders(true, 'sess-token-1'); expect(headers['X-Session-Token']).toBe('sess-token-1'); expect(headers['X-Device-ID']).toMatch(UUID_RE); - expect(headers['User-Agent']).toBe(OFFICIAL_UA); + expect(headers['User-Agent']).toBe(EXPECTED_UA); }); }); @@ -106,7 +106,7 @@ describe('Simyo mapSimyoErrorMessage / handleApiResponse', () => { await expect(handleApiResponse(response)).rejects.toThrow(/升级|upgrade|客户端|client version|refresh|刷新/i); }); - it('handleApiResponse 官方登录体(无 result.success)应视为成功', async () => { + it('handleApiResponse 登录体无 result.success 但有 sessionToken 应视为成功', async () => { const response = { ok: true, status: 200, diff --git a/tests/simyo/auth-handler.test.js b/tests/simyo/auth-handler.test.js index 8c6a028..243e85d 100644 --- a/tests/simyo/auth-handler.test.js +++ b/tests/simyo/auth-handler.test.js @@ -24,7 +24,7 @@ describe('Simyo AuthHandler', () => { }); it('登录成功应写入 token/手机号,且不保留 password', async () => { - // 官方 HAR 形态:无顶层 success,result 内无 success 字段 + // 登录响应:无顶层 success,result 内无 success 字段 global.fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => 'application/json' }, @@ -83,7 +83,7 @@ describe('Simyo AuthHandler', () => { expect(stateManager.get('sessionToken')).toBe(''); }); - it('isMfaDisabledOrComplete 识别官方 DISABLED_BY_CUSTOMER', () => { + it('isMfaDisabledOrComplete 识别 DISABLED_BY_CUSTOMER', () => { expect(isMfaDisabledOrComplete('DISABLED_BY_CUSTOMER')).toBe(true); expect(isMfaDisabledOrComplete('REQUIRED')).toBe(false); expect(isMfaDisabledOrComplete('')).toBe(true); diff --git a/tests/simyo/device-change-handler.test.js b/tests/simyo/device-change-handler.test.js index 4347925..bbd247c 100644 --- a/tests/simyo/device-change-handler.test.js +++ b/tests/simyo/device-change-handler.test.js @@ -1,5 +1,5 @@ /** - * Simyo 设备更换处理模块集成测试(对齐 HAR 状态机,仅 EMAIL) + * Simyo 设备更换处理模块集成测试(EMAIL 状态机) */ import { deviceChangeHandler, ESIM_STATUS } from '../../src/simyo/js/modules/device-change-handler.js'; diff --git a/tests/test_simyo_esim.html b/tests/test_simyo_esim.html index 251ecfe..4e7bc54 100644 --- a/tests/test_simyo_esim.html +++ b/tests/test_simyo_esim.html @@ -540,7 +540,7 @@ 'X-Client-Platform': 'ios', 'X-Client-Version': '4.28.0', 'X-Device-ID': 'E766D17B-BDF5-43B8-8807-35E43BA129E2', - // 官方 eSIM 更换抓包:版本号后两个空格 + iOS 18.2 + iPhone12,8 + // 版本号后两个空格;与 client-identity 默认 UA 一致 'User-Agent': 'MijnSimyoFT/4.28.0 (iOS 18.2; iPhone12,8)', 'Content-Type': 'application/json', 'Accept': 'application/json',