diff --git a/AppleStoreAPI.js b/AppleStoreAPI.js new file mode 100644 index 00000000..068799c8 --- /dev/null +++ b/AppleStoreAPI.js @@ -0,0 +1,1087 @@ +// 模块导入工具类 +class $ { + static async imports(...input) { + return await Promise.all(input.map(i => this.import(...i))); + } + + static async import(...args) { + const url = this.#cdn(args.pop()); + const rule = args[0]; + + try { + const module = await import(url); + const exp = "default" in module ? module.default : module; + const expName = typeof exp.name === "string" ? exp.name : "default"; + this.#mountFunction(rule, exp, expName); + console.log(`✅ ${url} 加载成功`); + } catch (error) { + console.log(`❌ 模块加载失败: ${url}`, error); + throw error; + } + } + static #cdn(path) { + const { host } = new URL(path); + if (!host.includes("github")) return path; + return path + .replace(host, `fastly.jsdelivr.net/gh`) + .replace(/refs.+(?=main)/, "") + .replace(/\/blob\//, "@") + .replace(/\/main/, "@main") + .replace(/\/master/, "@master"); + } + static #mountFunction(rule, exp, expName, depth = 0) { + if (typeof exp === "object" && depth === 0) { + if (typeof rule === "string" && rule.includes("* as")) + return (this[rule.split(" ").at(-1)] = exp); + + Object.entries(exp).forEach(([k, v]) => { + this.#mountFunction(rule, v, k, depth + 1); + }); + } else if (!rule) { + this[expName] = exp; + } else if (typeof rule === "string") { + this[rule.split(" ").at(-1)] = exp; + } else if (typeof rule === "function") { + const result = rule({ name: expName, fn: exp }); + result.stop || (this[result.name ?? expName] = result.fn ?? exp); + } else if (Array.isArray(rule)) { + rule.forEach(n => { + if (n.includes(expName)) this[n.split(" ").at(-1)] = exp; + }); + } + } +} +// LRU缓存类 +class LRUCache { + #cache; + constructor(capacity, cache) { + this.capacity = capacity; + this.#cache = new Map(cache || []); + } + + has(key) { + return this.#cache.has(key); + } + + get(key) { + if (!this.#cache.has(key)) return; + const value = this.#cache.get(key); + this.#cache.delete(key); + this.#cache.set(key, value); + return value; + } + + put(key, value) { + if (this.#cache.has(key)) { + this.#cache.delete(key); + } else if (this.#cache.size >= this.capacity) { + this.#cache.delete(this.#cache.keys().next().value); + } + + this.#cache.set(key, value); + } + + toArray() { + return [...this.#cache.entries()]; + } +} +// 自定义错误类 +class CustomError extends Error { + constructor(...args) { + super(args.pop()); + if (args[0]) this.name = args[0] + "Error"; + } +} +// 生成虚拟GUID·缓存Mac地址 +const getMAc = key => { + const generateHexPair = () => + Math.floor(Math.random() * 256) + .toString(16) + .padStart(2, "0"); + + let uniqueId = $.cache.get(key); + + if (!uniqueId) { + uniqueId = Array.from({ length: 6 }, generateHexPair) + .join("") + .toUpperCase(); + + $.cache.set(key, uniqueId); + } + + return uniqueId; +}; +// 计算容量 +const formatSize = (size, unit = "B") => { + const units = ["B", "KB", "MB", "GB", "TB", "PB"]; + const currentIndex = Math.max(0, units.indexOf(unit.toUpperCase())); + let bytes = size * 1024 ** currentIndex; + let unitIndex = 0; + while (bytes >= 1024 && unitIndex < units.length - 1) { + bytes /= 1024; + unitIndex++; + } + const formattedSize = + bytes < 10 + ? bytes.toFixed(2) + : bytes < 100 + ? bytes.toFixed(1) + : Math.round(bytes).toString(); + return `${formattedSize} ${units[unitIndex]}`; +}; + +//今天的日期 +const today = new Date().toISOString().split("T")[0]; + +// 共享状态·全局配置 +const sharedState = { + GUID: "AppleMac", + LOGIN_KEY: "AppleLogin", + VERSION_KEY: "AppVersions", + MAX_APP_CACHE: 50, + CONCURRENCY_CONFIG: { + concurrencyLimit: 5, + maxRetry: 2, + waitTime: 0.5, + }, +}; + +/** + * 第三方服务抽象基类 + * 提供通用的接口发现和数据获取能力 + * @description 为所有第三方服务提供统一的基础功能 + */ +const ThirdPartyService = class { + /** + * 抽象属性:子类必须实现的类型标识 + * @returns {string} 服务类型标识 + */ + static get type() { + throw new Error( + `抽象属性 type 定义接口类型 必须在子类 ${this.name} 中实现` + ); + } + + /** + * 获取所有可用的第三方接口列表 + * 接口命名规范:_get + InterfacesName + type + * @description 返回当前支持的所有第三方接口标识符 + * @param {number} [limit=Number.MAX_SAFE_INTEGER] - 限制返回的接口数量,默认返回所有接口 + * @returns {Array} - 可用的第三方接口标识符数组 + */ + static getAvailableInterfaces(limit = Number.MAX_SAFE_INTEGER) { + const methods = Object.getOwnPropertyNames(this); + const excludeMethod = `_getApp${this.type}List`; + const regex = new RegExp(`^_get(.+)${this.type}$`); + + // 筛选符合模式的私有方法名并提取接口名称 + return methods + .flatMap(method => { + if ( + method.startsWith("_get") && + method.endsWith(this.type) && + method !== excludeMethod + ) { + return method.replace(regex, "$1"); + } + return []; + }) + .slice(0, limit); + } + + /** + * 动态搜索并调用第三方接口 + * @description 根据接口名称动态调用对应的私有方法 + * @param {string} selset - 接口名称 (如 "Timbrd", "Bilin") + * @param {...any} args - 传递给具体方法的参数 + * @returns {Promise} - 接口调用结果 + */ + static async searchInterface(selset, ...args) { + const methodName = `_get${ + selset.charAt(0).toUpperCase() + selset.slice(1) + }${this.type}`; + + if (this[methodName]) { + return await this[methodName](...args); + } else { + throw new Error(`第三方接口 ${selset} 暂未实现`); + } + } + + /** + * 通用第三方数据获取方法 + * @param {string|onject} req - 请求URL|请求对象 + * @param {string} id - 应用ID或查询参数 + * @param {Function} dataExtractor - 数据提取函数 + * @returns {Promise} - 格式化后的数据信息 + */ + static async _fetchThirdPartyData(req, id, dataExtractor) { + try { + const { body } = await $.http(req, 8); + const data = dataExtractor(body); + + return { + appId: id, + data, + total: data.length, + }; + } catch (error) { + throw new Error(`${req?.url ?? req} 接口请求失败: ${error.message}`); + } + } +}; + +/** + * 版本查询服务 + * 专门处理应用版本相关查询 + * 重写基类抽象属性 type,返回 "Versions" + * 接口命名规范:_get + InterfacesName + type + * @description 继承自 ThirdPartyService,提供应用版本查询功能 + */ +const VersionService = class extends ThirdPartyService { + static type = "Versions"; + + /** + * 获取应用版本列表 + * @description 根据应用ID和选择的第三方接口获取应用版本列表 + * @param {string} id - 应用ID + * @param {string} selset - 选择的第三方接口标识 + * @returns {Promise<[number, number][]>>} - 应用版本列表信息,每个元素为 [版本ID, 版本号] + */ + static async getAppVersionList(id, selset) { + return await this.searchInterface(selset, id); + } + + /** + * 并发获取应用版本列表 + * @description 并发调用所有可用的版本接口,返回第一个成功的结果 + * @param {string} id - 应用ID + * @param {number} [num=Number.MAX_SAFE_INTEGER] - 限制返回的接口数量,默认返回所有接口 + * @returns {Promise} - 第一个成功的版本接口调用结果 + * @throws {Error} - 当所有接口都失败时抛出错误 + */ + static async concurrentGetVersionList(id, num = Number.MAX_SAFE_INTEGER) { + const availableInterfaces = this.getAvailableInterfaces(num); + + if (availableInterfaces.length === 0) { + throw new Error(`没有可用的版本接口`); + } + + return Promise.any( + availableInterfaces.map(interfaceName => + this.getAppVersionList(id, interfaceName) + ) + ); + } + + /** + * 通过 timbrd 接口获取应用版本列表(私有方法) + * @param {string} id - 应用ID + * @returns {Promise} - 应用版本列表 + */ + static async _getTimbrdVersions(id) { + const url = `https://api.timbrd.com/apple/app-version/index.php?id=${id}`; + return this._fetchThirdPartyData(url, id, body => + JSON.parse(body) + .reverse() + .map(({ external_identifier, bundle_version }) => [ + external_identifier, + bundle_version, + today, + ]) + ); + } + + /** + * 通过 bilin 接口获取应用版本列表(私有方法) + * @param {string} id - 应用ID + * @returns {Promise} - 应用版本列表 + */ + static async _getBilinVersions(id) { + const url = `https://apis.bilin.eu.org/history/${id}`; + return this._fetchThirdPartyData(url, id, body => + body.data.map(({ external_identifier, bundle_version }) => [ + external_identifier, + bundle_version, + today, + ]) + ); + } +}; + +// 认证服务·登录Apple账号 +const AuthService = class { + /** + * 登录Apple账号 + * 缓存登录响应 + * @description 登录Apple账号,获取登录响应数据 + * @param {Object} op - 登录参数 + * @param {string} op.appleId - Apple账号 + * @param {string} op.password - Apple账号密码 + * @param {string} [op.code] - 验证码,登录时需要提供 + * @returns {Promise} - 登录成功后的响应数据 + */ + static async #login({ appleId, password, code }) { + const dataJson = { + attempt: code ? 2 : 4, + createSession: "true", + guid: getMAc(sharedState.GUID), + rmp: 0, + why: "signIn", + appleId, + password: `${password}${code ?? ""}`, + }; + const body = $.plist.build(dataJson); + const url = `https://auth.itunes.apple.com/auth/v1/native/fast?guid=${dataJson.guid}`; + const resp = await $.http.post({ url, body, timeout: 6 }); + const parsedResp = $.plist.parse(resp.body); + + this.validate(parsedResp); + $.log("✅登录成功", parsedResp?.accountInfo); + const cacheLoginResp = JSON.parse( + $.cache.get(sharedState.LOGIN_KEY) || "{}" + ); + + const { "set-cookie": Cookie } = resp.headers; + const storeFront = resp.headers["x-set-apple-store-front"]?.split("-")?.[0]; + Object.assign(cacheLoginResp, parsedResp, { password, Cookie, storeFront }); + $.cache.setJson(sharedState.LOGIN_KEY, cacheLoginResp); + return { ...parsedResp, storeFront }; + } + + /** + * 加载缓存登录响应 + * @description 从缓存中加载登录响应,如果不存在则进行登录并缓存 + * @param {Object} op - 登录参数 + * @param {string} op.appleId - Apple账号 当登录账号与缓存账号不一致时,会切换账号重新登录 + * @returns {Promise} - 登录成功后的响应数据 + */ + static async login(op) { + const loginResp = JSON.parse($.cache.get(sharedState.LOGIN_KEY) || null); + + if (op && !loginResp) return await this.#login(op); + + if (op && op.appleId !== loginResp.accountInfo?.appleId) { + $.log("登录账号与缓存账号不一致, 切换账号登录"); + $.cache.remove(sharedState.LOGIN_KEY); + return await this.#login(op); + } + + if (op && op.password !== loginResp.password) { + $.log("密码变化,尝试重新登陆"); + return await this.#login(op); + } + + this.validate(loginResp); + op && $.log("✅已登录", loginResp.accountInfo); + return loginResp; + } + + /** + * 刷新登录Cookie + * @description 刷新当前登录的Cookie,延长登录有效期 + * @returns {Promise} - 刷新成功后的响应数据 + */ + static async refreshCookie() { + const { accountInfo = {}, password } = JSON.parse( + $.cache.get(sharedState.LOGIN_KEY) || "{}" + ); + const { appleId } = accountInfo; + if (!appleId || !password) { + throw new CustomError("Login", "❌未登录,刷新Cookie失败,请重新登录"); + } + return await this.#login({ appleId, password }); + } + /** + * 重置登录状态和缓存数据 + * @description 清除登录相关的缓存数据和GUID缓存 + * @returns {Object} - 重置结果信息 + */ + static reset() { + try { + // 清除登录缓存 + $.cache.remove(sharedState.LOGIN_KEY); + // 清除GUID缓存(MAC地址) + $.cache.remove(sharedState.GUID); + + $.log("✅重置成功,已清除登录缓存和GUID缓存"); + + return { + success: true, + message: "重置成功,已清除登录信息和GUID缓存", + clearedKeys: [sharedState.LOGIN_KEY, sharedState.GUID], + }; + } catch (error) { + throw new CustomError("Reset", `❌重置失败: ${error.message}`); + } + } + + /** + * 验证登录响应 + * @param {Object} loginResp - 登录响应数据 + * @throws {Error} - 如果登录响应无效,抛出错误 + */ + static validate(loginResp) { + if (!loginResp) throw new CustomError("Login", "❌未登录, 请先登录"); + + if (!loginResp.accountInfo && !loginResp.customerMessage) { + throw new CustomError("Login", "❌缓存数据异常, 请重新登陆"); + } + + if (Object.hasOwn(loginResp, "failureType")) { + const { failureType, customerMessage } = loginResp; + throw new CustomError("Login", [ + "❌登录失败", + failureType, + customerMessage, + ]); + } + + return true; + } +}; + +// 商店服务·下载·购买 +const StoreService = class { + /** + * 搜索应用 + * @param {string} term - 搜索关键词 + * @param {number} [limit=10] - 返回结果数量限制,默认10个 + * @param {string} [country='CN'] - 搜索的国家/地区,默认中国 + * @returns {Promise} - 搜索结果数组 + */ + static async searchApps({ + term, + limit = 10, + country = "CN", + entity = "software", + }) { + // 构建搜索 URL + const searchUrl = new URL("https://itunes.apple.com/search"); + searchUrl.searchParams.set("term", term.trim()); + searchUrl.searchParams.set("country", country.toLowerCase()); + searchUrl.searchParams.set("entity", entity); + searchUrl.searchParams.set("explicit", "yes"); + searchUrl.searchParams.set("limit", limit.toString()); + + // 发送请求 + const { body } = await $.http(searchUrl.toString(), 8); + return body; + } + + /** + * 获取APP信息 + * 如果未购买应用 会尝试购买应用 购买失败会抛出错误 + * @param {number} salableAdamId - 应用的Adam ID + * @param {number} [externalVersionId] - 应用的外部版本 ID,可选参数,不传则返回最新版本 + * @returns {Promise} - 应用信息 + */ + static async getAppInfo(salableAdamId, externalVersionId) { + const { dsPersonId, Cookie } = await this.getValidatedAuth(); + + const dataJson = { + creditDisplay: "", + guid: getMAc(sharedState.GUID), + salableAdamId, + externalVersionId, + }; + const resp = await $.http.post({ + url: `https://p25-buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct?guid=${dataJson.guid}`, + body: $.plist.build(dataJson), + timeout: 6, + headers: { + Cookie, + "X-Dsid": dsPersonId, + "iCloud-DSID": dsPersonId, + }, + }); + + const appInfo = $.plist.parse(resp.body); + try { + this.validateAppInfo(appInfo); + return await this.formatAppInfo(appInfo); + } catch (error) { + if ( + error.name === "AppInfoError" && + error.message.includes("2042") && + error.message.includes("2034") + ) { + await AuthService.refreshCookie(); + return await this.getAppInfo(salableAdamId, externalVersionId); + } + + if (error.name === "AppInfoError" && error.message.includes("9610")) { + await this.purchaseApp(salableAdamId); + return await this.getAppInfo(salableAdamId, externalVersionId); + } + + throw error; + } + } + + /** + * 获取验证后的登录响应 + * @description 从缓存中获取登录响应并验证其有效性 + * @throws {Error} - 如果登录响应无效,抛出错误 + * @returns {Object} - 登录成功后的响应数据 + */ + static async getValidatedAuth() { + return await AuthService.login(); + } + + /** + * 校验应用信息 + * @param {Object} appInfo - 应用信息 + * @throws {Error} - 如果应用信息无效,抛出错误 + */ + static validateAppInfo(appInfo) { + if (!appInfo) throw new CustomError("AppInfo", "❌应用信息为空"); + if (Object.hasOwn(appInfo, "failureType")) { + const { failureType, customerMessage } = appInfo; + throw new CustomError("AppInfo", [ + "❌获取应用信息失败", + failureType, + customerMessage, + ]); + } + if (!appInfo?.songList?.length) { + throw new CustomError("AppInfo", "❌这个版本号的应用信息为空"); + } + + return true; + } + + /** + * 批量查询应用版本号 + * @param {number} salableAdamId - 要查询的应用ID + * @param {number} startVersionId - 查询的版本ID 默认为最新版本ID + * @returns {Promise>} - 版本信息列表 + */ + static async getVersions({ salableAdamId, startVersionId }) { + // 初始化 从缓存中获取应用版本列表 如果缓存中不存在则从请求获取 + const { versionList } = await this.getAppVersionCache( + salableAdamId, + startVersionId + ); + + return { + data: versionList, + total: versionList.length, + }; + } + + /** + * 获取应用版本缓存 + * 合并三方接口数据 + * @description 从缓存中获取应用版本列表,如果不存在则从请求获取 + * @param {number} salableAdamId - 应用的Adam ID + * @param {number} startVersionId - 应用的外部版本 ID,可选参数,不传则返回最新版本 + * @returns {cachedVersionsAll} - 所有app版本列表,缓存中不存在则返回空数组 + * @returns {versionList} - 当前应用版本列表(Map 类型,键为外部版本ID,值为null或者版本标识符),缓存中不存在则拉取请求返回版本列表 + */ + static async getAppVersionCache(salableAdamId, startVersionId) { + const cachedVersionsAll = new LRUCache( + sharedState.MAX_APP_CACHE, + JSON.parse($.cache.get(sharedState.VERSION_KEY) ?? "[]") + ); + + //如果缓存中不存在该应用的版本列表 或者缓存事件过期,则从请求获取 官方,三方接口数据 + if ( + !cachedVersionsAll.has(salableAdamId) || + !cachedVersionsAll.get(salableAdamId)[0].includes(today) + ) { + const [processedVersions, legacyVersions] = await Promise.all([ + this.#processVersionIdList(salableAdamId, startVersionId), + VersionService.concurrentGetVersionList(salableAdamId).catch( + ({ errors = [], error }) => { + $.log(...errors, error); + return { total: 0, data: [] }; + } + ), + ]); + + if (processedVersions.length >= legacyVersions.total) { + // 合并数据源 + processedVersions.forEach(p => { + const legacy = legacyVersions.data.find(i => i[0] === p[0]); + if (legacy && p[1] === "????") p[1] = legacy[1]; + }); + cachedVersionsAll.put(salableAdamId, processedVersions); + } else { + cachedVersionsAll.put(salableAdamId, legacyVersions.data); + } + } + + $.cache.set( + sharedState.VERSION_KEY, + JSON.stringify(cachedVersionsAll.toArray()) + ); + + return { + cachedVersionsAll, + versionList: cachedVersionsAll.get(salableAdamId), + }; + } + + /** + * 初始化版本ID列表,确保至少包含一个版本ID + * @param {number} salableAdamId - 应用的Adam ID + * @param {string} startVersionId - 应用的外部版本 ID,可选参数,不传则返回最新版本 + * @returns {Array} - 格式化后的版本ID数组,每个元素为[id, null]格式 + */ + static async #processVersionIdList(salableAdamId, startVersionId) { + const { externalVersionIdList, externalVersionId, displayVersion } = + await this.getAppInfo(salableAdamId, startVersionId); + + if (!externalVersionIdList.length) { + return [[externalVersionId, displayVersion]]; + } + + return externalVersionIdList.reverse().map(id => [id, "????", today]); + } + + /** + * 购买应用 + * @param {number} salableAdamId - 应用的Adam ID + * @returns {Promise} - 购买成功的应用软件ID + * @throws {CustomError} - 如果购买失败,抛出错误 + */ + static async purchaseApp(salableAdamId) { + const { dsPersonId, passwordToken, storeFront, Cookie } = + await AuthService.refreshCookie(); + + const dataJson = { + appExtVrsId: "0", + buyWithoutAuthorization: "true", + guid: getMAc(sharedState.GUID), + hasAskedToFulfillPreorder: "true", + hasDoneAgeCheck: "true", + price: "0", + pricingParameters: "STDQ", + productType: "C", + salableAdamId, + }; + const body = $.plist.build(dataJson); + const url = + "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/buyProduct"; + const headers = { + Cookie, + "X-Token": passwordToken, + "X-Dsid": dsPersonId, + "iCloud-DSID": dsPersonId, + "X-Apple-Store-Front": storeFront, + }; + const resp = await $.http.post({ url, body, headers }, 6); + const { failureType, customerMessage, jingleDocType } = $.plist.parse( + resp.body + ); + + switch (failureType) { + case "5002": + throw new CustomError("buy", "[发生未知错误] 已购买过"); + case "2040": + throw new CustomError("buy", "[购买失败] 已购买过,已下架了"); + case "2059": + throw new CustomError("buy", "[购买失败] 未买过,已下架,地区未上架"); + case "1010": + throw new CustomError("buy", "[无效 Store] 该地区未上架"); + case "2034": + throw new CustomError("buy", "[未登录到 iTunes Store] CK过期"); + case "2042": + throw new CustomError("buy", "[未登录到 iTunes Store] CK为空或者过期"); + case "2019": + throw new CustomError("buy", "[购买失败] 无法直接购买付费软件"); + case "9610": + throw new CustomError("buy", "[未找到许可] 没购买过或应用ID错误"); + default: + if (failureType || failureType === "") + throw new CustomError("buy", `[购买失败] ${customerMessage}`); + } + + if (jingleDocType) { + $.log("购买成功", "软件ID:", salableAdamId); + return salableAdamId; + } + } + + /** + * 格式化应用信息 + * @param {Object} appInfo - 应用信息对象 + * @property {string} name - 应用名称 + * @property {string} appId - 应用唯一标识符 + * @property {string} url - 应用下载链接 + * @property {string} sinf - 应用授权信息 + * @property {string} bundleId - 应用包标识符 + * @property {string} displayVersion - 用户可见版本号 + * @property {string} buildVersion - 内部构建版本号 + * @property {number} externalVersionId - 外部版本标识符 + * @property {Array} externalVersionIdList - 外部版本标识符列表 + * @property {number} fileSize - 文件大小 + * @property {Object} metadata - iTunesMetadata.plist 文件内容 + * @property {string} currency - 货币单位 + */ + static async formatAppInfo(appInfo) { + const { + metrics: { currency }, + } = appInfo; + + const { + songId: appId, + URL: url, + "artwork-urls": { + default: { url: icon }, + }, + sinfs: [{ sinf }], + "asset-info": { "file-size": fileSize }, + metadata, + } = appInfo.songList[0]; + + const { + bundleDisplayName: name, + softwareVersionBundleId: bundleId, + bundleShortVersionString: displayVersion, + bundleVersion: buildVersion, + softwareVersionExternalIdentifier: externalVersionId, + softwareVersionExternalIdentifiers: externalVersionIdList, + rating: { label: minimumOsVersion }, + } = metadata; + + const { + accountInfo: { appleId }, + } = await this.getValidatedAuth(); + + Object.assign(metadata, { appleId }); + + // 调试信息输出; + //$.log("应用名称:", name); + // $.log("软件下载链接:", url); + // $.log("应用图标:", icon); + // $.log("应用ID:", appId); + // $.log("软件授权信息:", sinf); + // $.log("应用包标识符:", bundleId); + // $.log("用户版本号:", displayVersion); + // $.log("内部版本号:", buildVersion); + // $.log("版本标识符:", externalVersionId); + // $.log("版本标识符列表:", externalVersionIdList); + // $.log("文件大小:", fileSize); + // $.log("iTunesMetadata.plist 文件", metadata); + // $.log("货币:", currency); + // $.log("最低支持系统版本:", minimumOsVersion); + return { + name, + appId, + url, + icon, + sinf, + bundleId, + displayVersion, + buildVersion, + externalVersionId, + externalVersionIdList, + fileSize, + metadata: $.plist.build(metadata), + minimumOsVersion: minimumOsVersion.replace("+", ""), + currency, + }; + } +}; + +/** + * 统一响应格式处理器 + * @param {boolean} success - 是否成功 + * @param {any} data - 响应数据 + * @param {string} error - 错误信息 + * @returns {Object} 统一格式的响应对象 + */ +const createResponse = (success, data = null, error = null) => ({ + success, + data, + error, + timestamp: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), +}); + +/** + * 参数验证工具函数 + * @param {boolean} condition - 验证条件 + * @param {string} message - 错误信息 + * @throws {Error} 当条件不满足时抛出错误 + */ +const validate = (condition, message) => { + if (!condition) { + const error = new Error(message); + error.status = 400; + throw error; + } +}; + +// 主函数 +const main = async () => { + try { + await $.imports( + //["* as plist", "https://esm.sh/plist"], + [ + "* as plist", + "https://raw.githubusercontent.com/xiaobailian67/Surge/refs/heads/main/plist-complete.js", + ], + [ + "express", + "https://raw.githubusercontent.com/xiaobailian67/Surge/refs/heads/main/SimpleExpressBeta.js", + ], + [ + ({ name, fn }) => ({ name: name.slice(1), fn }), + "https://raw.githubusercontent.com/xiaobailian67/Surge/refs/heads/main/utils.js", + ] + ); + + $.http.useReq(req => { + Object.assign(req.headers, { + "User-Agent": + "Configurator/2.15 (Macintosh; OS X 11.0.0; 16G29) AppleWebKit/2603.3.8", + "Content-Type": "application/x-www-form-urlencoded", + }); + return req; + }); + + $.http.useRes(res => { + res.headers = Object.fromEntries( + Object.entries(res.headers).map(([k, v]) => [k.toLowerCase(), v]) + ); + return res; + }); + + const app = new $.express($request); + + // 添加中间件 + app.use($.express.json()); + app.use($.express.logger()); + + // 根路径 - API 信息 + app.get("/", (req, res, next) => { + const data = { + name: "Apple Store API", + version: "1.0.0", + description: "苹果商店应用信息和购买接口", + endpoints: { + "POST /auth/login": { + description: "用户登录", + body: { + appleId: "Apple账号 (必需)", + password: "Apple账号密码 (必需)", + code: "验证码 (可选,二次验证时需要)", + }, + }, + "POST /auth/refresh": { + description: "刷新 token", + body: "无参数", + }, + "POST /auth/reset": { + description: "重置登录状态和GUID缓存", + body: "无参数", + }, + "GET /apps/:id": { + description: "获取应用信息(含下载地址)", + params: { + id: "应用ID (必需)", + }, + query: { + appVerId: "应用版本ID (可选,不传则返回最新版本)", + }, + }, + "GET /apps/:id/versions": { + description: "官方获取应用历史版本,已合并三方数据", + params: { + id: "应用ID (必需)", + }, + query: { + appVerId: "起始版本ID (可选,不传则从最新版本开始)", + }, + }, + "GET /apps/:id/versions/legacy": { + description: "第三方应用历史版本", + params: { + id: "应用ID (必需)", + }, + query: { + selset: + "第三方接口名称 (可选,默认 '并发返回最快的',可选值:'Timbrd'|'Bilin')", + }, + }, + "POST /apps/:id/purchase": { + description: "购买应用", + params: { + id: "应用ID (必需)", + }, + body: "无参数", + }, + "GET /apps/search/:term": { + description: "搜索应用", + params: { + term: "搜索关键词 (必需)", + }, + query: { + limit: "返回结果数量 (可选,默认10,范围1-20)", + country: "搜索的国家/地区 (可选,默认'CN')", + }, + }, + }, + responseFormat: { + success: { + success: true, + data: "响应数据", + message: "成功信息 (可选)", + }, + error: { + success: false, + data: null, + message: "错误信息", + }, + }, + }; + res.json(createResponse(true, data)); + }); + + // 登录接口 + app.post("/auth/login", async (req, res, next) => { + const { appleId, password, code } = req.body; + validate(appleId && password, "缺少必要参数: appleId 和 password"); + + const result = await AuthService.login({ appleId, password, code }); + const data = { + message: "登录成功", + loginData: result, + }; + res.json(createResponse(true, data)); + }); + + // 刷新Cookie接口 + app.post("/auth/refresh", async (req, res, next) => { + await AuthService.refreshCookie(); + const data = { + message: "Cookie 刷新成功", + }; + res.json(createResponse(true, data)); + }); + + // 重置登录状态和缓存接口 + app.post("/auth/reset", async (req, res, next) => { + const result = AuthService.reset(); + res.json(createResponse(true, result)); + }); + + // 获取应用信息接口 - 可用于下载APP + app.get("/apps/:id", async (req, res, next) => { + const { id } = req.params; + const { appVerId } = req.query; + + validate(!isNaN(id), "无效的应用 ID"); + + const appInfo = await StoreService.getAppInfo(parseInt(id), appVerId); + const data = { + appId: id, + appInfo: appInfo, + }; + res.json(createResponse(true, data)); + }); + + // 官方获取应用历史版本信息接口 + app.get("/apps/:id/versions", async (req, res, next) => { + const { id } = req.params; + const { appVerId } = req.query; + + validate(!isNaN(id), "无效的应用 ID"); + + const versions = await StoreService.getVersions({ + salableAdamId: parseInt(id), + startVersionId: appVerId ? parseInt(appVerId) : undefined, + }); + + const data = { + appId: id, + ...versions, + appVerId, + }; + res.json(createResponse(true, data)); + }); + + // 三方获取应用历史版本信息接口 + app.get("/apps/:id/versions/legacy", async (req, res, next) => { + const { id } = req.params; + const { selset } = req.query; + + validate(!isNaN(id), "无效的应用 ID"); + + const data = selset + ? await VersionService.getAppVersionList(id, selset) + : await VersionService.concurrentGetVersionList(id).catch( + ({ errors = [], error }) => { + throw errors.length ? errors.map(e => e.message) : error; + } + ); + + res.json(createResponse(true, data)); + }); + + // 购买APP接口 + app.post("/apps/:id/purchase", async (req, res, next) => { + const { id } = req.params; + + validate(!isNaN(id), "无效的应用 ID"); + + const result = await StoreService.purchaseApp(id); + const data = { + appId: id, + message: "购买请求已提交", + purchaseResult: result, + }; + res.json(createResponse(true, data)); + }); + + // 搜索APP接口 + app.get("/apps/search/:term", async (req, res, next) => { + const { term } = req.params; + const { limit = 10, country } = req.query; + + // // 参数验证 + validate(term, "缺少必要参数: term"); + validate(limit > 0 && limit <= 20, "结果数量限制必须在1-20之间"); + + const searchResult = await StoreService.searchApps({ + term, + country, + limit: parseInt(limit), + }); + + const data = { + searchTerm: term, + explicit: true, + ...searchResult, + }; + res.json(createResponse(true, data)); + }); + + // 添加错误处理中间件 + app.use((err, req, res, next) => { + $.log("API Error:", err); + + res + .status(err.status || 500) + .json(createResponse(false, null, err.message || "未知错误")); + }); + + const response = await app.run(); + + if ($.env("Qx")) { + $done({...response, status: `HTTP/1.1 ${response.status} OK`}) + } else { + $done({ response }); + } + + } catch (error) { + console.log(error.toString()); + console.log(error.stack); + $done(); + } +}; + +main(); + diff --git a/Auto_join_TF.js b/Auto_join_TF 2.js similarity index 100% rename from Auto_join_TF.js rename to Auto_join_TF 2.js diff --git a/IPA Tool.scripting b/IPA Tool.scripting new file mode 100644 index 00000000..c09fab12 Binary files /dev/null and b/IPA Tool.scripting differ diff --git a/JSBox/AI keyboard.js b/JSBox/AI keyboard.js new file mode 100644 index 00000000..3905550c --- /dev/null +++ b/JSBox/AI keyboard.js @@ -0,0 +1,994 @@ +/* + +AI键盘 修改自@Neurogram + • 支持编辑工具 + • 支持附加或覆盖生成结果的提示 + • 支持自定义角色 + • 支持提示模板 + • 支持多轮对话 + • 支持显示提示的长度 + • 支持显示使用的 Token 提醒 + • 支持按压"助手"切换 Ai 模型 + • 支持按压"翻译文本"切换目标语言 + • 支持长按任意普通健代替 Ai 回复加发送(对话模式除外) + • 支持连点三次切换"开喷、吐槽"模式,开喷模式支持单击或按压开启单发或连发模式(判断连按间隔0.3s) + +教程:点击这里查看手册 https://neurogram.notion.site/ChatGPT-Keyboard-af8f7c74bc5c47989259393c953b8017 + +*/ + +// --- AI 选填配置区 --- + +const ai_configs = { + "Grok": { + api_keys: ["YOUR_GROK_API_KEY_1", "YOUR_GROK_API_KEY_2"],// Grok Token + proxy_urls: ["https://api.milltea.com"],// 代理地址 + models: ["grok-3-fast-beta", "mixtral-8x7b-32768"],// 模型 + api_endpoint_template: "{proxy_url}/v1/chat/completions", + type: "openai_compatible" + }, + "ChatGPT": { + api_keys: ["YOUR_CHATGPT_API_KEY_1", "YOUR_CHATGPT_API_KEY_2"],// ChatGPT + proxy_urls: ["https://api.openai.com", "YOUR_CHATGPT_PROXY_URL"], + models: ["gpt-4o", "gpt-3.5-turbo"], + api_endpoint_template: "{proxy_url}/v1/chat/completions", + type: "openai_compatible" + }, + "DeepSeek": { + api_keys: ["YOUR_DEEPSEEK_API_KEY_1"],// DeepSeek + proxy_urls: ["https://api.deepseek.com"], + models: ["deepseek-chat", "deepseek-coder"], + api_endpoint_template: "{proxy_url}/v1/chat/completions", + type: "openai_compatible" + }, + "Gemini": { + api_keys: ["YOUR_GEMINI_API_KEY_1"],// Gemini + proxy_urls: ["https://generativelanguage.googleapis.com"], + models: ["gemini-1.5-pro-latest", "gemini-pro"], + api_endpoint_template: "{proxy_url}/v1beta/models/{model}:generateContent?key={api_key}", + type: "gemini" + } +}; + +// --- UI 布局配置区 --- + +const usage_toast = true // 是否开启使用量显示 +const keyboard_sound = true // 是否开启键盘声音 +const keyboard_vibrate = 0 // -1:无振动, 0~2: 振动强度 +const edit_tool_columns = 5 // 编辑工具默认列数 +const chatgpt_role_columns = 3 // Ai角色默认列数 +$keyboard.barHidden = true // 是否隐藏JSBox键盘底部工具栏 +const heartbeat = 1 // -1: 无回复等待反馈, 0~2: 心跳强度 +const heartbeat_interval = 1.2 // 心跳间隔(秒) + +// --- 其他配置 不懂勿动 --- + +function getAdaptiveLayoutParams() { + const screenWidthPt = $device.info.screen.width; + const minScreenWidth = 320; + const maxScreenWidth = 450; + + function interpolateValue(currentWidth, minWidth, maxWidth, minValue, maxValue) { + if (currentWidth <= minWidth) return minValue; + if (currentWidth >= maxWidth) return maxValue; + const ratio = (currentWidth - minWidth) / (maxWidth - minWidth); + return minValue + ratio * (maxValue - minValue); + } + + let spacing = interpolateValue(screenWidthPt, minScreenWidth, maxScreenWidth, 4, 7); + + let buttonFontSize = interpolateValue(screenWidthPt, minScreenWidth, maxScreenWidth, 12, 15); + + let footerFontSize = interpolateValue(screenWidthPt, minScreenWidth, maxScreenWidth, 9, 12); + let footerHeight = interpolateValue(screenWidthPt, minScreenWidth, maxScreenWidth, 18, 24); + + let totalHeight = interpolateValue(screenWidthPt, minScreenWidth, maxScreenWidth, 220, 295); + + spacing = Math.round(spacing); + buttonFontSize = Math.round(buttonFontSize); + footerFontSize = Math.round(footerFontSize); + footerHeight = Math.round(footerHeight); + totalHeight = Math.round(totalHeight); + + const numKeyRows = 5; + + let keyHeight = (totalHeight - footerHeight - (numKeyRows + 1) * spacing) / numKeyRows; + keyHeight = Math.round(keyHeight); + + if (keyHeight <= 0) { + keyHeight = Math.max(buttonFontSize + 10, 35); + } + + return { + spacing: spacing, + keyHeight: keyHeight, + totalHeight: totalHeight, + buttonFontSize: buttonFontSize, + footerFontSize: footerFontSize, + footerHeight: footerHeight + }; +} + +const adaptiveParams = getAdaptiveLayoutParams(); + +const user_gesture = { + tap: 1, + long_press: 0 +} + +const role_data = { + "助手": ["", "你是一个热心且乐于助人的Ai助手,提供帮助和建议。", ""], + "续写": ["", "用相同语言继续创作或完成内容。"], + "翻译文本": ["将所给内容翻译成指定语言。", ""], + "总结": ["", "用相同语言总结内容,提炼出关键信息。"], + "润色": ["", "用相同语言对内容进行润色或优化。"], + "百度搜索": ["", ""], + "扩展": ["", "你是一名高级网络工程师兼自动化脚本专家,精通 Surge、JSBox、JavaScript 和 API 调用,且具有极强的逻辑分析与优化能力。请从专业技术视角出发,基于以下内容,进行详细推演、拓展、优化或修复建议,以利于高效实现目标功能:\n\n{USER_CONTENT}"], + "吐槽": ["", "使用相同语言启动强烈的怼人模式,进行尖锐的反击讽刺与吐槽。"], + "谷歌搜索": ["", ""] +}; + +const translateTargets = { + "en": { name: "英语", prompt: "Translate the following text to English (American English preferably, if not specified otherwise)." }, + "zh-Hans": { name: "中文", prompt: "将以下文本翻译成中文(简体)。" }, + "ja": { name: "日语", prompt: "将以下文本翻译成日语。" }, + "th": { name: "泰语", prompt: "将以下文本翻译成泰语。" }, + "hxw": { name: "火星文", prompt: "将以下文本转换成火星文风格,请使用网络上流行的、非主流的、有趣的字符或表达方式。" } +}; +const PREF_TRANSLATE_TARGET_KEY = "current_translate_target_key_v4"; +let currentSelectedTranslateTargetKey = $cache.get(PREF_TRANSLATE_TARGET_KEY) || "zh-Hans"; + +const edit_tool = { + "Start": "arrow.left.to.line", + "Left": "arrow.left", + "Right": "arrow.right", + "End": "arrow.right.to.line", + "Return": "return", + "Copy": "doc.on.doc", + "Paste": "doc.on.clipboard", + "Cut": "scissors", + "Empty": "trash", + "Dismiss": "keyboard.chevron.compact.down" +} +const PREF_CURRENT_AI_SERVICE = "current_ai_service_name_v3"; +const PREF_AI_CONFIG_INDICES = "current_ai_config_indices_v1"; + +let current_ai_service_name = $cache.get(PREF_CURRENT_AI_SERVICE) || Object.keys(ai_configs)[0]; +if (!ai_configs[current_ai_service_name]) { + current_ai_service_name = Object.keys(ai_configs)[0]; +} + +let current_config_indices = $cache.get(PREF_AI_CONFIG_INDICES) || {}; +Object.keys(ai_configs).forEach(serviceName => { + if (!current_config_indices[serviceName]) { + current_config_indices[serviceName] = { key_idx: 0, proxy_idx: 0, model_idx: 0 }; + } +}); + +function getCurrentAiConfig() { + const service_config = ai_configs[current_ai_service_name]; + const indices = current_config_indices[current_ai_service_name]; + + if (!service_config) { + $ui.error(`AI 服务 "${current_ai_service_name}" 未配置.`); + current_ai_service_name = Object.keys(ai_configs)[0]; + $cache.set(PREF_CURRENT_AI_SERVICE, current_ai_service_name); + if (!current_config_indices[current_ai_service_name]) { + current_config_indices[current_ai_service_name] = { key_idx: 0, proxy_idx: 0, model_idx: 0 }; + $cache.set(PREF_AI_CONFIG_INDICES, current_config_indices); + } + return getCurrentAiConfig(); + } + + const api_key = service_config.api_keys[indices.key_idx % service_config.api_keys.length]; + const proxy_url_base = service_config.proxy_urls[indices.proxy_idx % service_config.proxy_urls.length]; + const model = service_config.models[indices.model_idx % service_config.models.length]; + + let api_url = service_config.api_endpoint_template + .replace("{proxy_url}", proxy_url_base) + .replace("{model}", model); + + if (service_config.type === "gemini") { + api_url = api_url.replace("{api_key}", api_key); + } + + return { + name: current_ai_service_name, + api_key: api_key, + model: model, + api_url: api_url, + type: service_config.type, + raw_proxy_url: proxy_url_base + }; +} + +const edit_tool_amount = Object.keys(edit_tool).length +let dialogue = $cache.get("dialogue") +let multi_turn = false +if (dialogue) multi_turn = dialogue.mode +$app.theme = "auto" +let generating = false +let timer = "" +let generating_icon = 0 +let trollTimer = null +let sprayButtonTapCount = 0 + +const spray_mode_cache_key = "chatgpt_keyboard_spray_mode_v1"; +let sprayButtonMode = $cache.get(spray_mode_cache_key) || "吐槽"; + +let lastSprayButtonTapTime = 0 +const tripleTapInterval = 500 +let sprayActionTimeoutId = null; +const sprayActionDelay = 300; + + +const firstRoleName = Object.keys(role_data)[0]; + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +async function get_content_for_new_buttons() { + let inputText = await get_content(0); + let trimmedInputText = (inputText || "").trim(); + + if (trimmedInputText) { + return trimmedInputText; + } + + let clipboardText = ($clipboard.text || "").trim(); + if (clipboardText) { + return clipboardText; + } + return ""; +} + +const view = { + props: { + title: "AI keyboard", + navBarHidden: $app.env == $env.keyboard, + pageSheet: $app.env == $env.keyboard, + }, + views: [{ + type: "matrix", + props: { + spacing: adaptiveParams.spacing, + bgcolor: $color("clear"), + data: dataPush(Object.keys(edit_tool).concat(Object.keys(role_data))), + template: { + props: {}, + views: [{ + type: "button", + props: { + id: "button", + radius: 10, + titleColor: $color("black", "white"), + tintColor: $color("black", "white"), + font: $font(adaptiveParams.buttonFontSize) + }, + layout: $layout.fill, + events: { + tapped: async function (sender, indexPath, data) { + if (trollTimer) { + clearInterval(trollTimer); + trollTimer = null; + } + + const buttonOriginalKey = sender.info.originalKey; + + if (buttonOriginalKey === "吐槽") { + const currentTime = Date.now(); + + if (sprayActionTimeoutId) { + clearTimeout(sprayActionTimeoutId); + sprayActionTimeoutId = null; + } + + if (currentTime - lastSprayButtonTapTime < tripleTapInterval) { + sprayButtonTapCount++; + } else { + sprayButtonTapCount = 1; + } + lastSprayButtonTapTime = currentTime; + + if (sprayButtonTapCount >= 3) { + sprayButtonMode = (sprayButtonMode === "开喷") ? "吐槽" : "开喷"; + sender.title = sprayButtonMode; + sender.bgcolor = (sprayButtonMode === "开喷") ? $color("#FFF0F0", "#806B6B") : $color("#FFFFFF", "#6B6B6B"); + + $ui.toast(`已切换至"${sprayButtonMode}"模式`); + $cache.set(spray_mode_cache_key, sprayButtonMode); + + sprayButtonTapCount = 0; + lastSprayButtonTapTime = 0; + + if (keyboard_sound) $keyboard.playInputClick(); + if (keyboard_vibrate != -1) $device.taptic(keyboard_vibrate); + return; + } + + if (sprayButtonTapCount === 1) { + const modeWhenClicked = sprayButtonMode; + sprayActionTimeoutId = setTimeout(() => { + if (sprayButtonMode === modeWhenClicked && sprayButtonTapCount === 1) { + if (modeWhenClicked === "开喷") { + if (keyboard_sound) $keyboard.playInputClick(); + if (keyboard_vibrate != -1) $device.taptic(keyboard_vibrate); + fetchTextAndSend(); + } else { + if (keyboard_sound) $keyboard.playInputClick(); + if (keyboard_vibrate != -1) $device.taptic(keyboard_vibrate); + handler(sender, "tap"); + } + sprayButtonTapCount = 0; + lastSprayButtonTapTime = 0; + } + sprayActionTimeoutId = null; + }, sprayActionDelay); + } + } else if (buttonOriginalKey === "百度搜索") { + sprayButtonTapCount = 0; lastSprayButtonTapTime = 0; if (sprayActionTimeoutId) { clearTimeout(sprayActionTimeoutId); sprayActionTimeoutId = null; } + if (keyboard_sound) $keyboard.playInputClick(); + if (keyboard_vibrate != -1) $device.taptic(keyboard_vibrate); + await performSearch("baidu"); + } else if (buttonOriginalKey === "谷歌搜索") { + sprayButtonTapCount = 0; lastSprayButtonTapTime = 0; if (sprayActionTimeoutId) { clearTimeout(sprayActionTimeoutId); sprayActionTimeoutId = null; } + if (keyboard_sound) $keyboard.playInputClick(); + if (keyboard_vibrate != -1) $device.taptic(keyboard_vibrate); + await performSearch("google"); + } else { + sprayButtonTapCount = 0; + lastSprayButtonTapTime = 0; + if (sprayActionTimeoutId) { + clearTimeout(sprayActionTimeoutId); + sprayActionTimeoutId = null; + } + handler(sender, "tap"); + } + }, + longPressed: function (info, indexPath, data) { + if (trollTimer) { + clearInterval(trollTimer); + trollTimer = null; + } + if (sprayActionTimeoutId) { + clearTimeout(sprayActionTimeoutId); + sprayActionTimeoutId = null; + } + sprayButtonTapCount = 0; + lastSprayButtonTapTime = 0; + + const buttonOriginalKey = info.sender.info.originalKey; + const isMainAssistantButton = (buttonOriginalKey === firstRoleName && !info.sender.info.action); + + if (isMainAssistantButton) { + const availableAIs = Object.keys(ai_configs); + $ui.menu({ + items: availableAIs.map(aiName => `${aiName}${aiName === current_ai_service_name ? " \u23CE" : ""}`), + handler: function(title, idx) { + const selectedAiName = availableAIs[idx]; + if (selectedAiName !== current_ai_service_name) { + current_ai_service_name = selectedAiName; + $cache.set(PREF_CURRENT_AI_SERVICE, current_ai_service_name); + if (!current_config_indices[current_ai_service_name]) { + current_config_indices[current_ai_service_name] = { key_idx: 0, proxy_idx: 0, model_idx: 0 }; + $cache.set(PREF_AI_CONFIG_INDICES, current_config_indices); + } + $ui.toast(`已切换到 ${current_ai_service_name}`); + updateFooterTitle(); + } + } + }); + return; + } + + if (buttonOriginalKey === "翻译文本") { + const menuItems = Object.keys(translateTargets).map(key => { + return translateTargets[key].name + (key === currentSelectedTranslateTargetKey ? " \u23CE" : ""); + }); + $ui.menu({ + items: menuItems, + handler: function(selectedName, idx) { + const selectedKey = Object.keys(translateTargets)[idx]; + if (selectedKey !== currentSelectedTranslateTargetKey) { + currentSelectedTranslateTargetKey = selectedKey; + $cache.set(PREF_TRANSLATE_TARGET_KEY, currentSelectedTranslateTargetKey); + $ui.toast(`翻译目标已设为: ${translateTargets[selectedKey].name}`); + } + } + }); + return; + } + + if (buttonOriginalKey === "吐槽" && sprayButtonMode === "开喷") { + if (keyboard_sound) $keyboard.playInputClick(); + if (keyboard_vibrate != -1) $device.taptic(keyboard_vibrate); + if (trollTimer) { + clearInterval(trollTimer); + trollTimer = null; + } + trollTimer = setInterval(() => { + fetchTextAndSend(); + }, 1000); + $ui.toast("长按连续开喷中,再次点击停止"); + } else { + handler(info.sender, "long_press"); + } + } + } + }] + }, + footer: { + type: "button", + props: { + id: "footer", + height: adaptiveParams.footerHeight, + title: `JSBox'Ai (${current_ai_service_name})`, + titleColor: $color("#AAAAAA"), + bgcolor: $color("clear"), + symbol: multi_turn ? "bubble.left.and.bubble.right" : "bubble.left", + tintColor: $color("#AAAAAA"), + align: $align.center, + font: $font(adaptiveParams.footerFontSize) + }, + events: { + tapped: async (sender) => { + if (trollTimer) { clearInterval(trollTimer); trollTimer = null; } + if (sprayActionTimeoutId) { clearTimeout(sprayActionTimeoutId); sprayActionTimeoutId = null; } + sprayButtonTapCount = 0; lastSprayButtonTapTime = 0; + + const popover = $ui.popover({ + sourceView: sender, + sourceRect: sender.bounds, + directions: $popoverDirection.any, + size: $size(320, 200), + views: [ + { + type: "scroll", + layout: function (make, view) { + make.edges.insets($insets(10, 10, 10, 10)) + }, + views: [{ + type: "label", + props: { + text: await get_content(1), + font: $font(15), + lines: 0 + }, + layout: function (make, view) { + make.width.equalTo(300) + }, + events: { + tapped: () => { + popover.dismiss() + } + } + }] + } + ] + }) + }, + longPressed: function (info) { + if (trollTimer) { clearInterval(trollTimer); trollTimer = null; } + if (sprayActionTimeoutId) { clearTimeout(sprayActionTimeoutId); sprayActionTimeoutId = null; } + sprayButtonTapCount = 0; lastSprayButtonTapTime = 0; + + multi_turn = multi_turn ? false : true + set_bubble() + $ui.toast("对话模式" + (multi_turn ? " 开" : " 关")) + $cache.set("dialogue", { mode: multi_turn }) + } + } + } + }, + layout: $layout.fill, + events: { + itemSize: function (sender, indexPath) { + const availableWidth = sender.frame.width > 0 ? sender.frame.width : $device.info.screen.width; + let keyboard_columns = indexPath.item < edit_tool_amount ? edit_tool_columns : chatgpt_role_columns; + let item_width; + if (keyboard_columns === chatgpt_role_columns) { + item_width = Math.floor((availableWidth - (keyboard_columns + 1) * adaptiveParams.spacing - 1) / keyboard_columns); + } else { + item_width = (availableWidth - (keyboard_columns + 1) * adaptiveParams.spacing) / keyboard_columns; + } + return $size(item_width, adaptiveParams.keyHeight); + } + + } + }], + layout: (make, view) => { + make.width.equalTo(view.super) + if (adaptiveParams.totalHeight && adaptiveParams.totalHeight > 0){ + make.height.equalTo(adaptiveParams.totalHeight) + } else { + if ($app.env === $env.keyboard && $keyboard.height > 0) { + make.height.equalTo($keyboard.height) + } else { + make.height.equalTo(view.super) + } + } + } +} + +function dataPush(data) { + let key_title = []; + for (let i = 0; i < data.length; i++) { + const configName = data[i]; + let displayTitle = configName; + let displayBgColor = $color("#FFFFFF", "#6B6B6B"); + + if (i < edit_tool_amount) { + displayTitle = ""; + } else { + if (configName === "吐槽") { + displayTitle = sprayButtonMode; + if (sprayButtonMode === "开喷") { + displayBgColor = $color("#FFF0F0", "#806B6B"); + } else { + displayBgColor = $color("#FFFFFF", "#6B6B6B"); + } + } + } + + key_title.push({ + button: { + title: displayTitle, + symbol: i < edit_tool_amount ? edit_tool[configName] : "", + info: { action: i < edit_tool_amount ? configName : "" , originalKey: configName }, + bgcolor: displayBgColor + } + }); + } + return key_title; +} + +function handler(sender, gesture) { + if (keyboard_sound) $keyboard.playInputClick() + if (keyboard_vibrate != -1) $device.taptic(keyboard_vibrate) + if ($app.env != $env.keyboard) return $ui.warning("请在键盘内运行") + if (sender.info.action) return edit(sender.info.action, gesture) + gpt(sender.info.originalKey || sender.title, gesture) +} + +async function performSearch(engine) { + if (generating) return $ui.warning("请等待当前任务完成"); + let query = await get_content_for_new_buttons(); + + if (!query || query.trim() === "") { + return $ui.warning("请输入或粘贴搜索内容"); + } + generating = true; + let searchUrl = ""; + const encodedQuery = encodeURIComponent(query); + + if (engine === "baidu") { + searchUrl = `https://www.baidu.com/s?wd=${encodedQuery}`; + } else if (engine === "google") { + searchUrl = `https://www.google.com/search?q=${encodedQuery}`; + } else { + generating = false; + return $ui.error("未知的搜索引擎"); + } + $app.openURL(searchUrl); + $delay(0.5, () => generating = false); +} + +async function edit(action, gesture) { + let before = $keyboard.textBeforeInput ? $keyboard.textBeforeInput.length : 0 + let after = $keyboard.textAfterInput ? $keyboard.textAfterInput.length : 0 + + if (action == "Start") return $keyboard.moveCursor(-before) + if (action == "Left") return $keyboard.moveCursor(-1) + if (action == "Right") return $keyboard.moveCursor(1) + if (action == "End") return $keyboard.moveCursor(after) + if (action == "Return") return $keyboard.insert("\n") + if (action == "Paste") return $keyboard.insert($clipboard.text || "") + if (action == "Dismiss") return gesture == "tap" ? $app.close() : $keyboard.dismiss() + if (action == "Empty" && gesture == "tap") return $keyboard.delete() + + let content = await get_content(0) + if (action != "Empty") $clipboard.text = content + + if (action == "Copy") return $ui.success("完成") + + if (action == "Cut" || action == "Empty") { + if (!$keyboard.selectedText) { + $keyboard.moveCursor(after) + delete_content(content.length) + } + if ($keyboard.selectedText) $keyboard.delete() + } +} + +async function gpt(role, gesture) { + if (generating) return $ui.warning("正在生成中"); + + if (role === "吐槽" && sprayButtonMode === "开喷") { + return; + } + + let user_content; + + let translation_source_info = { + from_selection: false, + original_all_text_length_to_delete: 0, + is_translation_from_input_field: false + }; + + if (role === "翻译文本") { + const raw_selected_text = $keyboard.selectedText; + const raw_all_text = await $keyboard.getAllText(); + + user_content = await get_content_for_new_buttons(); + + if (raw_selected_text && raw_selected_text.trim() === user_content) { + translation_source_info.from_selection = true; + translation_source_info.is_translation_from_input_field = true; + } else if (!raw_selected_text && raw_all_text && raw_all_text.trim() === user_content) { + translation_source_info.original_all_text_length_to_delete = raw_all_text.length; + translation_source_info.is_translation_from_input_field = true; + } + } else { + user_content = await get_content(0); + } + + + if (!user_content && !multi_turn && role !== "翻译文本") { + const nonTranslateRolesRequireContent = ["助手", "续写", "总结", "润色", "扩展", "吐槽"]; + if (nonTranslateRolesRequireContent.includes(role)) { + return $ui.warning("未找到提示"); + } + } + if (role === "翻译文本" && (!user_content || user_content.trim() === "")) { + return $ui.warning("请输入或粘贴需要翻译的内容"); + } + + generating = true; + + let messages = []; + const systemMarker = "⚙️ 系统:\n"; + const userMarker = "👨‍💻 用户:\n"; + const assistantMarker = "🤖 助手:\n"; + const endMarker = "🔚"; + + if (multi_turn) { + let currentFullText = user_content.trim(); + const sysPromptRegexText = `^\\s*${escapeRegExp(systemMarker)}([^${escapeRegExp(endMarker)}]*)(${escapeRegExp(endMarker)})?`; + const sysPromptRegex = new RegExp(sysPromptRegexText, "m"); + const sysMatch = currentFullText.match(sysPromptRegex); + let systemContentProvided = false; + + if (sysMatch) { + const systemPromptText = sysMatch[1] ? sysMatch[1].trim() : ""; + if (systemPromptText && systemPromptText !== "-") { + messages.push({ role: "system", content: systemPromptText }); + } + systemContentProvided = true; + currentFullText = currentFullText.substring(sysMatch[0].length).trim(); + } + + const turnRegexText = `(?:${escapeRegExp(userMarker)}|${escapeRegExp(assistantMarker)})([^${escapeRegExp(endMarker)}]*)(${escapeRegExp(endMarker)})`; + const turnRegex = new RegExp(turnRegexText, "g"); + let match; + let lastTurnEndIndex = 0; + let tempTextForTurns = currentFullText; + + while ((match = turnRegex.exec(tempTextForTurns)) !== null) { + const markerText = match[0].startsWith(userMarker) ? userMarker : assistantMarker; + const content = match[1] ? match[1].trim() : ""; + if (content) { + messages.push({ role: markerText === userMarker ? "user" : "assistant", content }); + } + lastTurnEndIndex = match.index + match[0].length; + } + + let currentUserNewInput = tempTextForTurns.substring(lastTurnEndIndex).trim(); + if (currentUserNewInput.startsWith(userMarker)) { + currentUserNewInput = currentUserNewInput.substring(userMarker.length).trim(); + } else if (currentUserNewInput.startsWith(assistantMarker)) { + currentUserNewInput = ""; + } + + if (!systemContentProvided && messages.length === 0 && currentUserNewInput) { + messages.push({ role: "user", content: currentUserNewInput }); + $keyboard.delete(); + $keyboard.insert(`\n${systemMarker}-${endMarker}\n\n${userMarker}${currentUserNewInput}`); + } else if (currentUserNewInput) { + messages.push({ role: "user", content: currentUserNewInput }); + } + + const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null; + const hasUserMessageWithContent = messages.some(m => m.role === "user" && m.content && m.content.trim() !== ""); + + const needsWarningCheck = !hasUserMessageWithContent || (lastMsg && lastMsg.role !== "user") || (lastMsg && !lastMsg.content); + const isAllowedEmptyInputCaseInMultiTurn = messages.length === 0 && systemContentProvided && !currentUserNewInput; + + if (needsWarningCheck && !isAllowedEmptyInputCaseInMultiTurn) { + $ui.warning("请输入对话内容"); + generating = false; + return; + } + } else { + if (role === "翻译文本") { + const targetLangConfig = translateTargets[currentSelectedTranslateTargetKey]; + if (targetLangConfig) { + messages.push({ "role": "system", "content": targetLangConfig.prompt }); + } else { + messages.push({ "role": "system", "content": "Translate the text."}); + } + messages.push({ "role": "user", "content": user_content }); + } else { + if (!user_gesture[gesture]) { + $keyboard.moveCursor(1); + $keyboard.insert("\n"); + } + if (user_gesture[gesture] && !$keyboard.selectedText) { + delete_content(user_content.length); + } + + if (role_data[role] && role_data[role][0]) { + messages.push({ "role": "system", "content": role_data[role][0] }); + } + let preset_prompt = role_data[role] ? role_data[role][1] : ""; + + if (preset_prompt && !preset_prompt.match(/{USER_CONTENT}/)) user_content = preset_prompt + "\n" + user_content; + if (preset_prompt && preset_prompt.match(/{USER_CONTENT}/)) user_content = preset_prompt.replace(/{USER_CONTENT}/g, user_content); + messages.push({ "role": "user", "content": user_content }); + } + } + + if (messages.length === 0 || (messages.length === 1 && messages[0].role === 'system' && (!messages[0].content || messages[0].content.trim()==='-'))) { + $ui.warning("请输入有效的用户指令。"); + generating = false; + return; + } + + if (heartbeat != -1) { + timer = $timer.schedule({ + interval: heartbeat_interval, + handler: async () => { + $device.taptic(heartbeat); + if($("footer")) $("footer").symbol = "ellipsis.bubble.fill"; + await $wait(0.2); + $device.taptic(heartbeat); + if($("footer")) $("footer").symbol = "ellipsis.bubble"; + } + }); + } else { + timer = $timer.schedule({ + interval: heartbeat_interval / 2, + handler: async () => { + generating_icon = generating_icon ? 0 : 1; + if($("footer")) $("footer").symbol = generating_icon ? "ellipsis.bubble.fill" : "ellipsis.bubble"; + } + }); + } + + const current_ai = getCurrentAiConfig(); + let request_body; + let request_headers = { "Content-Type": "application/json" }; + + if (current_ai.type === "openai_compatible") { + request_headers["Authorization"] = `Bearer ${current_ai.api_key}`; + request_body = { + "model": current_ai.model, + "messages": messages.filter(m => m.content && m.content.trim() !== "") + }; + } else if (current_ai.type === "gemini") { + let gemini_messages = []; + let system_instruction_gemini = null; + for (const msg of messages) { + if (!msg.content || msg.content.trim() === "") continue; + + if (msg.role === "system") { + if (!system_instruction_gemini) { + system_instruction_gemini = { parts: [{ text: msg.content }] }; + } else { + system_instruction_gemini.parts[0].text += "\n" + msg.content; + } + continue; + } + gemini_messages.push({ + role: msg.role === "assistant" ? "model" : "user", + parts: [{ text: msg.content }] + }); + } + request_body = { "contents": gemini_messages }; + if (system_instruction_gemini) { + request_body.systemInstruction = system_instruction_gemini; + } + if (gemini_messages.length === 0) { + $ui.warning("Gemini 需要有效的用户输入。"); + generating = false; + if(timer) timer.invalidate(); + set_bubble(); + return; + } + } else { + $ui.error(`不支持的 AI 类型: ${current_ai.type}`); + generating = false; + if(timer) timer.invalidate(); + set_bubble(); + return; + } + + let response; + try { + response = await $http.post({ + url: current_ai.api_url, + header: request_headers, + body: request_body + }); + } catch (err) { + console.error("API Request Error:", err); + $ui.error(`请求失败: ${err.message || '未知网络错误'}`); + if(timer) timer.invalidate(); + set_bubble(); + generating = false; + generating_icon = 0; + return; + } + + if(timer) timer.invalidate(); + set_bubble(); + generating = false; + generating_icon = 0; + + let response_text = ""; + let error_message = ""; + + if (response.data) { + if (current_ai.type === "openai_compatible") { + if (response.data.error) error_message = response.data.error.message; + else if (response.data.choices && response.data.choices[0] && response.data.choices[0].message) { + response_text = response.data.choices[0].message.content; + } else error_message = "OpenAI 兼容 API 返回结构无效"; + + if (!error_message && usage_toast && response.data.usage) { + let usage = response.data.usage; + $ui.toast(`${current_ai.name} 用量: P${usage.prompt_tokens} + C${usage.completion_tokens} = T${usage.total_tokens}`); + } else if (!error_message && usage_toast) { + $ui.toast(`${current_ai.name} 完成`); + } + } else if (current_ai.type === "gemini") { + if (response.data.error) error_message = `Gemini API 错误: ${response.data.error.message}`; + else if (response.data.candidates && response.data.candidates[0] && response.data.candidates[0].content && response.data.candidates[0].content.parts && response.data.candidates[0].content.parts[0]) { + response_text = response.data.candidates[0].content.parts[0].text; + } else if (response.data.promptFeedback && response.data.promptFeedback.blockReason) { + error_message = `内容被 Gemini 阻止: ${response.data.promptFeedback.blockReason}`; + } else { + console.error("Gemini API 返回无效结构:", JSON.stringify(response.data)); + error_message = "Gemini API 返回无效结构"; + } + if (!error_message && usage_toast) { + $ui.toast(`${current_ai.name} 完成`); + } + } + } else if (response.error) { + error_message = `请求错误: ${response.error.localizedDescription || "未知错误"}`; + } else { + error_message = "请求失败,未收到有效数据。"; + } + + if (error_message) { + $ui.error(error_message); + return; + } + + + if (!multi_turn) { + if (role === "翻译文本") { + if (translation_source_info.is_translation_from_input_field) { + if (translation_source_info.from_selection) { + $keyboard.delete(); + } else if (translation_source_info.original_all_text_length_to_delete > 0) { + await delete_content(translation_source_info.original_all_text_length_to_delete); + } + } + $keyboard.insert(response_text); + } else { + $keyboard.insert(response_text); + } + } else { + const textToInsert = `\n${assistantMarker}${response_text.trim()}${endMarker}\n\n${userMarker}`; + $keyboard.insert(textToInsert); + } + +} + +async function get_content(length) { + let content = $keyboard.selectedText || await $keyboard.getAllText() + if (length) content = `长度: ${content.replace(/(⚙️ 系统|👨‍💻 用户|🤖 助手):\n|🔚/g, "").replace(/\n+/g, "\n").length}\n\n${content}` + return content +} + +function delete_content(times) { + for (let i = 0; i < times; i++) { + $keyboard.delete() + } +} + +function set_bubble() { + const footer = $("footer"); + if (footer) { + footer.symbol = multi_turn ? "bubble.left.and.bubble.right" : "bubble.left" + } +} + +function updateFooterTitle() { + const footer = $("footer"); + if (footer) { + footer.title = `JSBox'Ai (${current_ai_service_name})`; + } +} + +async function fetchTextAndSend() { + $http.get({ + url: "https://yyapi.a1aa.cn/api.php?level=max",//开喷接口 + handler: async function(resp) { + if (resp.error) { + $ui.error("获取文本失败: " + resp.error.message); + if (timer) timer.invalidate(); + set_bubble(); + return; + } + var text = resp.data; + $keyboard.insert(text); + $keyboard.send(); + if (heartbeat != -1) { + $device.taptic(heartbeat); + } + const footer = $("footer"); + if (footer) { + footer.symbol = "paperplane.fill"; + await $wait(0.5); + } + set_bubble(); + } + }); +} + +function initializeKeyboard() { + if (adaptiveParams.totalHeight && adaptiveParams.totalHeight > 0 && $app.env === $env.keyboard) { + $keyboard.height = adaptiveParams.totalHeight; + } + + if ($app.env === $env.keyboard) { + $ui.render({ props: { navBarHidden: true } }); + $delay(0, () => { + const mainView = $ui.create(view); + $ui.controller.view = mainView; + mainView.layout(view.layout); + + const tucaoButtonIdentifierInInit = "吐槽"; + const allButtonKeysForInit = Object.keys(edit_tool).concat(Object.keys(role_data)); + const tucaoInitIndex = allButtonKeysForInit.indexOf(tucaoButtonIdentifierInInit); + + if (tucaoInitIndex !== -1) { + let matrixView; + if (mainView.views && mainView.views[0] && mainView.views[0].type === "matrix") { + matrixView = mainView.views[0]; + } else { + matrixView = mainView.get("matrix"); + } + + if (matrixView) { + const buttonCellView = matrixView.cell($indexPath(0, tucaoInitIndex)); + if (buttonCellView) { + const btnToUpdate = buttonCellView.get("button"); + if (btnToUpdate) { + btnToUpdate.title = sprayButtonMode; + btnToUpdate.bgcolor = (sprayButtonMode === "开喷") ? $color("#FFF0F0", "#806B6B") : $color("#FFFFFF", "#6B6B6B"); + } + } + } + } + }); + } else { + $ui.render(view); + } +} + +initializeKeyboard(); + diff --git a/JSBox/ChatGPT.js b/JSBox/ChatGPT.js new file mode 100644 index 00000000..ac9a59c1 --- /dev/null +++ b/JSBox/ChatGPT.js @@ -0,0 +1,363 @@ +/* + +ChatGPT 键盘 by Neurogram + • 支持编辑工具 + • 支持附加或覆盖生成结果的提示 + • 支持自定义角色 + • 支持提示模板 + • 支持多轮对话 + • 支持显示提示的长度 + • 支持显示使用的 Token 提醒 + +教程:点击这里查看手册 https://neurogram.notion.site/ChatGPT-Keyboard-af8f7c74bc5c47989259393c953b8017 + +*/ +const api_key = " " // 填写 key,可选的第三方代理地址 237行 +const model = "gpt-4" +const user_gesture = { // Generated results: 0: auto-wrap 1: overwrite selected/all prompts + tap: 1, + long_press: 0 +} +const usage_toast = true // 显示使用量 + +const keyboard_sound = true // 是否开启键盘声音 +const keyboard_vibrate = 0 // -1: 无振动, 0~2: 振动强度 +const edit_tool_columns = 5 // 编辑工具默认列数 +const chatgpt_role_columns = 3 // ChatGPT 角色默认列数 +const keyboard_spacing = 6 // 按键间隔 +const keyboard_height = 41 // 按键高度 +const keyboard_total_height = 265 //键盘总高度 0为系统默认 +$keyboard.barHidden = true //是否隐藏JSBox 键盘底部工具栏 + +const heartbeat = 1 // -1: 无回复等待反馈, 0~2: 心跳强度 +const heartbeat_interval = 1.2 // 心跳间隔(秒) + +const role_data = { // "Role Name": ["System Content", "Prompts Template"] + "助手": ["你是一个热心且乐于助人的Ai助手,提供帮助和建议。", ""], + "续写": ["", "用相同语言继续创作或完成内容。"], + "译为中文": ["将所给内容翻译成中文。", ""], + "总结": ["", "用相同语言总结内容,提炼出关键信息。"], + "润色": ["", "用相同语言对内容进行润色或优化。"], + "译为英文": ["将所给内容翻译成美式英语。", ""], + "扩展": ["", "你是一名高级网络工程师兼自动化脚本专家,精通 Surge、JSBox、JavaScript 和 API 调用,且具有极强的逻辑分析与优化能力。请从专业技术视角出发,基于以下内容,进行详细推演、拓展、优化或修复建议,以利于高效实现目标功能:\n\n{USER_CONTENT}"], + "吐槽": ["", "使用相同语言启动强烈的怼人模式,进行尖锐的反击讽刺与谩骂。"], + "译为日文": ["将所给内容翻译成日语。", ""] +} +const edit_tool = { + "Start": "arrow.left.to.line", + "Left": "arrow.left", + "Right": "arrow.right", + "End": "arrow.right.to.line", + "Return": "return", + "Copy": "doc.on.doc", + "Paste": "doc.on.clipboard", + "Cut": "scissors", + "Empty": "trash", + "Dismiss": "keyboard.chevron.compact.down" +} +const edit_tool_amount = Object.keys(edit_tool).length +let dialogue = $cache.get("dialogue") +let multi_turn = false +if (dialogue) multi_turn = dialogue.mode +$app.theme = "auto" +const view = { props: { + title: "ChatGPT", + navBarHidden: $app.env == $env.keyboard, + pageSheet: $app.env == $env.keyboard, + }, + views: [{ + type: "matrix", + props: { + spacing: keyboard_spacing, + bgcolor: $color("clear"), + data: dataPush(Object.keys(edit_tool).concat(Object.keys(role_data))), + template: { + props: {}, + views: [{ + type: "button", + props: { + id: "button", + radius: 10, + titleColor: $color("black", "white"), + tintColor: $color("black", "white"), + bgcolor: $color("#FFFFFF", "#6B6B6B"), + font: $font(14) //按键文字大小 + + }, + layout: $layout.fill, + events: { + tapped: function (sender, indexPath, data) { + handler(sender, "tap") + }, + longPressed: function (info, indexPath, data) { + handler(info.sender, "long_press") + } + } + }] + }, + footer: { + type: "button", + props: { + id: "footer", + height: 20, + title: " JSBox'ChatGPT 键盘", + titleColor: $color("#AAAAAA"), + bgcolor: $color("clear"), + symbol: multi_turn ? "bubble.left.and.bubble.right" : "bubble.left", + tintColor: $color("#AAAAAA"), + + align: $align.center, + font: $font(10) + }, + events: { + tapped: async (sender) => { + const popover = $ui.popover({ + sourceView: sender, + sourceRect: sender.bounds, + directions: $popoverDirection.any, + size: $size(320, 200), + views: [ + { + type: "scroll", + layout: function (make, view) { + make.edges.insets($insets(10, 10, 10, 10)) + }, + views: [{ + type: "label", + props: { + text: await get_content(1), + font: $font(15), + lines: 0 + }, + layout: function (make, view) { + make.width.equalTo(300) + }, + events: { + tapped: () => { + popover.dismiss() + } + } + }] + } + ] + }) + }, + longPressed: function (info) { + multi_turn = multi_turn ? false : true + + set_bubble() + $ui.toast("对话模式" + (multi_turn ? " 开" : " 关")) + $cache.set("dialogue", { mode: multi_turn }) + } + } + } + }, + layout: $layout.fill, + events: { + itemSize: function (sender, indexPath) { + let keyboard_columns = indexPath.item < edit_tool_amount ? edit_tool_columns : chatgpt_role_columns + return $size(($device.info.screen.width - (keyboard_columns + 1) * keyboard_spacing) / keyboard_columns, keyboard_height); + } + } + }], + layout: (make, view) => { + make.width.equalTo(view.super) + if (keyboard_total_height){ + make.height.equalTo(keyboard_total_height) + } else { + make.height.equalTo(view.super) + } + } +} +if ($app.env === $env.keyboard) { + $ui.render({ props: { navBarHidden: true } }) + $delay(0, () => { + $ui.controller.view = $ui.create(view) + $ui.controller.view.layout(view.layout) + }) +} else { + $ui.render(view) +} + +function dataPush(data) { + let key_title = [] + for (let i = 0; i < data.length; i++) { + key_title.push({ + button: { + title: i < edit_tool_amount ? "" : data[i], + symbol: i < edit_tool_amount ? edit_tool[data[i]] : "", + info: { action: i < edit_tool_amount ? data[i] : "" } + } + }) + } + return key_title +} + +function handler(sender, gesture) { + if (keyboard_sound) $keyboard.playInputClick() + if (keyboard_vibrate != -1) $device.taptic(keyboard_vibrate) + if ($app.env != $env.keyboard) return $ui.warning("请在键盘内运行") + if (sender.info.action) return edit(sender.info.action, gesture) + gpt(sender.title, gesture) +} + +async function edit(action, gesture) { + + let before = $keyboard.textBeforeInput ? $keyboard.textBeforeInput.length : 0 + let after = $keyboard.textAfterInput ? $keyboard.textAfterInput.length : 0 + + if (action == "Start") return $keyboard.moveCursor(-before) + if (action == "Left") return $keyboard.moveCursor(-1) + if (action == "Right") return $keyboard.moveCursor(1) + if (action == "End") return $keyboard.moveCursor(after) + if (action == "Return") return $keyboard.insert("\n") + if (action == "Paste") return $keyboard.insert($clipboard.text || "") + if (action == "Dismiss") return gesture == "tap" ? $app.close() : $keyboard.dismiss() + if (action == "Empty" && gesture == "tap") return $keyboard.delete() + + let content = await get_content(0) + if (action != "Empty") $clipboard.text = content + + if (action == "Copy") return $ui.success("完成") + + if (action == "Cut" || action == "Empty") { + if (!$keyboard.selectedText) { + $keyboard.moveCursor(after) + delete_content(content.length) + } + if ($keyboard.selectedText) $keyboard.delete() + } + +} + +let generating = false +let timer = "" +let generating_icon = 0 + +const openai_proxy_url = "https://api.nio.gs"; // 可选的代理地址,留空或注释掉以禁用代理 + +async function gpt(role, gesture) { + if (generating) return $ui.warning("正在生成中"); + let user_content = await get_content(0); + if (!user_content && !multi_turn) return $ui.warning("未找到提示"); + generating = true; + + let messages = []; + + if (multi_turn) { + + if ($keyboard.selectedText) $keyboard.moveCursor(1) + + if (!user_content.match(/⚙️ 系统:[^🔚]+/)) { + $ui.warning("未找到对话") + $keyboard.insert(`\n⚙️ 系统:\n${role_data[role][0] || "-"}🔚\n\n👨‍💻 用户:\n`) + generating = false + return + } + + let contents = user_content.match(/(👨‍💻 用户|🤖 助手):\n([^🔚]+)/g) + + if (contents) { + for (let i in contents) { + if (contents[i].match(/👨‍💻 用户:\n([^🔚]+)/)) messages.push({ "role": "user", "content": contents[i].match(/👨‍💻 用户:\n([^🔚]+)/)[1] }) + if (contents[i].match(/🤖 助手:\n([^🔚]+)/)) messages.push({ "role": "assistant", "content": contents[i].match(/🤖 助手:\n([^🔚]+)/)[1] }) + } + } + + if (!contents || messages[messages.length - 1].role != "user") { + $ui.warning("未找到内容") + generating = false + return + } + + let system_content = user_content.match(/⚙️ 系统:\n([^🔚]+)/)[1] + if (system_content != "-") messages = [{ "role": "system", "content": system_content }].concat(messages) + } + + if (!multi_turn) { + if (!user_gesture[gesture]) { + $keyboard.moveCursor(1) + $keyboard.insert("\n") + } + + if (user_gesture[gesture] && !$keyboard.selectedText) delete_content(user_content.length) + + if (role_data[role][0]) messages.push({ "role": "system", "content": role_data[role][0] }) + + let preset_prompt = role_data[role][1] + if (preset_prompt && !preset_prompt.match(/{USER_CONTENT}/)) user_content = preset_prompt + "\n" + user_content + if (preset_prompt && preset_prompt.match(/{USER_CONTENT}/)) user_content = preset_prompt.replace(/{USER_CONTENT}/g, user_content) + + messages.push({ "role": "user", "content": user_content }) + } + + if (heartbeat != -1) { + timer = $timer.schedule({ + interval: heartbeat_interval, + handler: async () => { + $device.taptic(heartbeat) + $("footer").symbol = "ellipsis.bubble.fill" + await $wait(0.2) + $device.taptic(heartbeat) + $("footer").symbol = "ellipsis.bubble" + } + }) + } + + if (heartbeat == -1) { + timer = $timer.schedule({ + interval: heartbeat_interval / 2, + handler: async () => { + if (generating_icon) { + generating_icon = 0 + $("footer").symbol = "ellipsis.bubble" + } else { + generating_icon = 1 + $("footer").symbol = "ellipsis.bubble.fill" + } + } + }) + } + let api_url = openai_proxy_url ? openai_proxy_url + "/v1/chat/completions" : "https://api.openai.com/v1/chat/completions"; + + let openai = await $http.post({ + url: api_url, + header: { + "Content-Type": "application/json", + "Authorization": `Bearer ${api_key}` + }, + body: { + "model": model, + "messages": messages + } + }) + + timer.invalidate() + set_bubble() + generating = false + generating_icon = 0 + if (openai.data.error) return $ui.error(openai.data.error.message) + + if (!multi_turn) $keyboard.insert(openai.data.choices[0].message.content) + if (multi_turn) $keyboard.insert(`🔚\n\n🤖 助手:\n${openai.data.choices[0].message.content}🔚\n\n👨‍💻 用户:\n`) + + if (!usage_toast) return + let usage = openai.data.usage + $ui.toast(`用量: P${usage.prompt_tokens} + C${usage.completion_tokens} = T${usage.total_tokens}`) +} + +async function get_content(length) { + let content = $keyboard.selectedText || await $keyboard.getAllText() + if (length) content = `长度: ${content.replace(/(⚙️ 系统|👨‍💻 用户|🤖 助手):\n|🔚/g, "").replace(/\n+/g, "\n").length}\n\n${content}` + return content +} + +function delete_content(times) { + for (let i = 0; i < times; i++) { + $keyboard.delete() + } +} + +function set_bubble() { + $("footer").symbol = multi_turn ? "bubble.left.and.bubble.right" : "bubble.left" +} \ No newline at end of file diff --git a/MmmCK.js b/MmmCK.js new file mode 100644 index 00000000..ac7dfe28 --- /dev/null +++ b/MmmCK.js @@ -0,0 +1,36 @@ +const $prs = { + get: this.$prefs?.valueForKey ?? $persistentStore.read, + getJson: (key) => JSON.parse($prs.get(key), null, 4), + set: (key, value) => + (this.$prefs?.setValueForKey ?? $persistentStore.write)(value, key), + setJson: (key, obj) => $prs.set(key, JSON.stringify(obj)), +}; + +const $msg = (...a) => { + const { $open, $copy, $media, ...r } = typeof a.at(-1) === "object" && a.pop(); + const [t = "", s = "", b = ""] = a; + (this.$notify ??= $notification.post)(t, s, b, { + action: $copy ? "clipboard" : "open-url", + text: $copy, + "update-pasteboard": $copy, + clipboard: $copy, + "open-url": $open, + openUrl: $open, + url: $open, + mediaUrl: $media, + "media-url": $media, + ...r, + }); +}; + +const params = new URLSearchParams($request.body); +const devId = params.get('c_mmbDevId'); + +if (devId) { + $msg('CK获取成功', devId); + $prs.set('慢慢买CK', devId); +} else { + $msg('CK获取失败', 'c_mmbDevId参数不存在,请重新再试', $request.body); +} + +$done({}); \ No newline at end of file diff --git a/Scriptable/ChinaUnicom_Multi.js b/Scriptable/ChinaUnicom_Multi.js new file mode 100644 index 00000000..df5ce28a --- /dev/null +++ b/Scriptable/ChinaUnicom_Multi.js @@ -0,0 +1,1708 @@ +/* + * @author: 脑瓜 + * @feedback https://t.me/Scriptable_CN + * telegram: @anker1209 + * version: 2.6.10 + * update: 2026/01/16 + * 原创UI,修改套用请注明来源 + * * 使用说明: + * 1. 获取 Cookie 脚本,点击首页流量获取。 https://raw.githubusercontent.com/dompling/Script/master/10010/index.js + * 2. 运行 ChinaUnicom_Multi 脚本,进入【账户设置】手动填写或点击【代理缓存】从持久化数据读取后代理缓存到指定账户。 + * 3. 最多支持5个账户,获取多个 Cookie 需要删除重装 App。 + * 4. 在桌面添加小组件,【Parameter/参数】一栏填写: + * - 填 1 或不填:显示账户 1 + * - 填 2:显示账户 2 + * - 填 3:显示账户 3 … +*/ + +if (typeof require === 'undefined') require = importModule; +const {DmYY, Runing} = require('./DmYY'); + +class Widget extends DmYY { + constructor(arg) { + super(arg); + this.name = '中国联通'; + this.en = 'ChinaUnicom_2024'; + this.logo = 'https://raw.githubusercontent.com/anker1209/icon/main/zglt-big.png'; + this.smallLogo = 'https://raw.githubusercontent.com/anker1209/icon/main/zglt.png'; + this.Run(); + } + + version = '2.6.10'; + + cookie = ''; + gradient = false; + flowColorHex = '#12A6E4'; + voiceColorHex = '#F86527'; + + currIndex = '1'; + + ringStackSize = 65; + ringTextSize = 14; + feeTextSize = 21; + textSize = 13; + smallPadding = 12; + padding = 10; + logoScale = 0.24; + SCALE = 1; + + canvSize = 178; + canvWidth = 18; + canvRadius = 80; + + widgetStyle = '1'; + customFlowLimit = 0; + customVoiceLimit = 0; + + format = (str) => { + return parseInt(str) >= 10 ? str : `0${str}`; + }; + + date = new Date(); + arrUpdateTime = [ + this.format(this.date.getMonth() + 1), + this.format(this.date.getDate()), + this.format(this.date.getHours()), + this.format(this.date.getMinutes()), + ]; + + refreshUpdateTime(date) { + this.arrUpdateTime = [ + this.format(date.getMonth() + 1), + this.format(date.getDate()), + this.format(date.getHours()), + this.format(date.getMinutes()), + ]; + }; + + fee = { + title: '话费剩余', + icon: 'antenna.radiowaves.left.and.right', + number: '0', + iconColor: new Color('#F86527'), + unit: '元', + en: '¥', + }; + + flow = { + percent: 0, + title: '已用流量', + number: '0', + unit: 'MB', + en: 'MB', + icon: 'antenna.radiowaves.left.and.right', + iconColor: new Color('#1AB6F8'), + FGColor: new Color(this.flowColorHex), + BGColor: new Color(this.flowColorHex, 0.2), + colors: [], + }; + + voice = { + percent: 0, + title: '语音剩余', + number: '0', + unit: '分钟', + en: 'MIN', + icon: 'phone.badge.waveform.fill', + iconColor: new Color('#30D15B'), + FGColor: new Color(this.voiceColorHex), + BGColor: new Color(this.voiceColorHex, 0.2), + colors: [], + }; + + point = { + title: '剩余积分', + number: 0, + unit: '', + icon: 'tag.fill', + iconColor: new Color('fc6d6d'), + } + + gradientColor(colors, step) { + var startRGB = this.colorToRgb(colors[0]), + startR = startRGB[0], + startG = startRGB[1], + startB = startRGB[2]; + + var endRGB = this.colorToRgb(colors[1]), + endR = endRGB[0], + endG = endRGB[1], + endB = endRGB[2]; + + var sR = (endR - startR) / step, + sG = (endG - startG) / step, + sB = (endB - startB) / step; + + var colorArr = []; + for (var i = 0;i < step; i++) { + var hex = this.colorToHex('rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')'); + colorArr.push(hex); + } + return colorArr; + }; + + colorToRgb(sColor) { + var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; + var sColor = sColor.toLowerCase(); + if (sColor && reg.test(sColor)) { + if (sColor.length === 4) { + var sColorNew = "#"; + for (var i = 1; i < 4; i += 1) { + sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)); + } + sColor = sColorNew; + } + var sColorChange = []; + for (var i = 1; i < 7; i += 2) { + sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2))); + } + return sColorChange; + } else { + return sColor; + } + }; + + colorToHex(rgb) { + var _this = rgb; + var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; + if (/^(rgb|RGB)/.test(_this)) { + var aColor = _this.replace(/(?:\(|\)|rgb|RGB)*/g,"").split(","); + var strHex = "#"; + for (var i = 0; i < aColor.length; i++) { + var hex = Number(aColor[i]).toString(16); + hex = hex.length < 2 ? 0 + '' + hex : hex; + if (hex === "0") { + hex += hex; + } + strHex += hex; + } + if (strHex.length !== 7) { + strHex = _this; + } + return strHex; + } else if (reg.test(_this)) { + var aNum = _this.replace(/#/,"").split(""); + if (aNum.length === 6) { + return _this; + } else if (aNum.length === 3) { + var numHex = "#"; + for (var i = 0; i < aNum.length; i+=1) { + numHex += (aNum[i] + aNum[i]); + } + return numHex; + } + } else { + return _this; + } + }; + + init = async () => { + try { + const scale = this.getWidgetScaleFactor(); + this.SCALE = this.settings.SCALE || scale; + + const { + step1, + step2, + logoColor, + flowIconColor, + voiceIconColor, + gradient, + builtInColor, + previewAccount + } = this.settings; + + let param = args.widgetParameter ? args.widgetParameter.toString() : (previewAccount || '1'); + if (!['1','2','3','4','5'].includes(param)) param = '1'; + + this.currIndex = param; + + this.cookie = this.settings[`cookie${param}`]; + this.widgetStyle = this.settings[`widgetStyle${param}`] || '1'; + this.customFlowLimit = this.settings[`flow${param}`]; + this.customVoiceLimit = this.settings[`voice${param}`]; + + if (!this.cookie) { + console.warn(`⚠️ 账户 ${param} 未设置 Cookie,请进入脚本设置。`); + } + + this.gradient = gradient === 'true'; + + if (builtInColor === 'true') { + const [feeColor, flowColor, voiceColor] = this.getIconColorSet(); + this.fee.iconColor = new Color(feeColor); + this.flow.iconColor = new Color(flowColor); + this.voice.iconColor = new Color(voiceColor); + } else { + this.fee.iconColor = logoColor ? new Color(logoColor) : this.fee.iconColor; + this.flow.iconColor = flowIconColor ? new Color(flowIconColor) : this.flow.iconColor; + this.voice.iconColor = voiceIconColor ? new Color(voiceIconColor) : this.voice.iconColor; + } + + this.flowColorHex = step1 || this.flowColorHex; + this.voiceColorHex = step2 || this.voiceColorHex; + this.flow.BGColor = new Color(this.flowColorHex, 0.2); + this.voice.BGColor = new Color(this.voiceColorHex, 0.2); + this.flow.FGColor = new Color(this.flowColorHex); + this.voice.FGColor = new Color(this.voiceColorHex); + + const sizeSettings = [ + 'ringStackSize', + 'ringTextSize', + 'feeTextSize', + 'textSize', + 'smallPadding', + 'padding', + ]; + + for (const key of sizeSettings) { + this[key] = this.settings[key] ? parseFloat(this.settings[key]) : this[key]; + this[key] = this[key] * this.SCALE; + } + + if (this.gradient) { + this.flow.colors = this.arrColor(); + this.voice.colors = this.arrColor(); + this.flow.BGColor = new Color(this.flow.colors[1], 0.2); + this.voice.BGColor = new Color(this.voice.colors[1], 0.2); + this.flow.FGColor = this.gradientColor(this.flow.colors, 360); + this.voice.FGColor = this.gradientColor(this.voice.colors, 360); + this.flowColorHex = this.flow.colors[1]; + this.voiceColorHex = this.voice.colors[1]; + } + + } catch (e) { + console.error(e); + } + + await this.getData(); + }; + + cleanCookieStr(str) { + return String(str) + .split(";") + .map((i) => { + return i + .trim() + .replace( + /(^Domain\s*?=\s*?.+?$|^Path\s*?=\s*?.+?\s*?(,\s*|$))/gi, + "" + ); + }) + .filter((i) => i) + .join("; "); + }; + + maskName(str) { + if (!str) return "未知"; + if (str.length <= 1) return "*"; + return "*" + str.substring(1); + }; + + maskMobile(str) { + if (!str) return "未知"; + return str.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); + }; + + async getData() { + if (!this.cookie) { + console.log(`❌ 账户[${this.currIndex}] 未配置 Cookie`); + return; + } + + const fm = FileManager.local(); + const cacheDir = fm.joinPath(fm.documentsDirectory(), "ChinaUnicom_Cache"); + const cachePath = fm.joinPath(cacheDir, `account_${this.currIndex}.json`); + + if (!fm.fileExists(cacheDir)) fm.createDirectory(cacheDir, true); + + let userInfo = null; + let useCache = false; + + let settingTime = 30; + if (this.settings.refreshAfterDate) { + settingTime = parseInt(this.settings.refreshAfterDate); + } + + if (fm.fileExists(cachePath)) { + const modified = fm.modificationDate(cachePath); + const diff = (new Date() - modified) / (1000 * 60); + + if (diff < settingTime) { + console.log(`\n🟠 联通数据:缓存 ${diff.toFixed(0)} 分钟前 (设置: ${settingTime}分)`); + console.log(`🟠 联通数据:读取缓存`); + try { + userInfo = JSON.parse(fm.readString(cachePath)); + useCache = true; + this.refreshUpdateTime(modified); + } catch (e) { + console.log(`⚠️ 缓存损坏,刷新数据`); + } + } else { + console.log(`\n🔵 联通数据:缓存已过期 (${diff.toFixed(0)} > ${settingTime}分)`); + } + } + + if (!useCache) { + const url= 'https://m.client.10010.com/mobileserviceimportant/home/queryUserInfoSeven?version=iphone_c@8.0200&desmobiel=&showType=0'; + try { + const req = new Request(url); + req.headers = {'cookie': this.cookie}; + const response = await req.loadJSON(); + + if (response && response.code === 'Y') { + console.log(`🟢 联通数据:网络请求成功`); + userInfo = response; + if (fm.fileExists(cachePath)) fm.remove(cachePath); + fm.writeString(cachePath, JSON.stringify(userInfo)); + this.refreshUpdateTime(new Date()); + } else { + console.log(`❌ 联通数据:请求失败 (Code: ${response ? response.code : 'unknown'})`); + if (fm.fileExists(cachePath)) { + console.log(`🟠 联通数据:服务异常,读取旧缓存`); + userInfo = JSON.parse(fm.readString(cachePath)); + this.refreshUpdateTime(fm.modificationDate(cachePath)); + } else { + throw `账户[${this.currIndex}] Cookie失效或服务器维护`; + } + } + } catch (e) { + console.log(`❌ 联通数据:网络错误 ${e}`); + if (fm.fileExists(cachePath)) { + console.log(`🟠 联通数据:网络异常,读取旧缓存`); + userInfo = JSON.parse(fm.readString(cachePath)); + this.refreshUpdateTime(fm.modificationDate(cachePath)); + } + } + } + + if (userInfo && userInfo.code === 'Y') { + try { + if (userInfo.data && userInfo.data.userInfo) { + const info = userInfo.data.userInfo; + const uName = this.maskName(info.userName); + const uMobile = this.maskMobile(info.userMobile); + console.log(`👤 用户${this.currIndex}信息: ${uName} | ${uMobile}`); + } + + let configured = []; + for(let i=1; i<=5; i++) { + if(this.settings[`cookie${i}`]) configured.push(i); + } + console.log(`📋 已配置账户: [${configured.join(',')}] (当前显示: ${this.currIndex})`); + + } catch (e) {} + + userInfo.data.dataList.forEach((item) => { + if (item.type === 'fee') { + if (item.unit ==='万元') { + this.fee.number = item.number * 10000; + } else { + this.fee.number = item.number; + this.fee.unit = item.unit; + } + this.fee.title = item.remainTitle; + } + if (item.type === 'flow') { + this.flow.number = item.number; + this.flow.unit = item.unit; + this.flow.en = item.unit; + this.flow.percent = item.persent ? (100 - item.persent).toFixed(2) : this.customFlowLimit ? ((this.flow.number / this.customFlowLimit) * 100).toFixed(2) : 100; + this.flow.title = item.remainTitle.replace(/通用/g, ""); + } + if (item.type === 'voice') { + this.voice.number = item.number; + this.voice.unit = item.unit; + this.voice.percent = item.persent ? (100 - item.persent).toFixed(2) : this.customVoiceLimit ? ((this.voice.number / this.customVoiceLimit) * 100).toFixed(2) : 100; + this.voice.title = item.remainTitle; + } + if (item.type === 'point') { + this.point.number = item.number; + this.point.title = item.remainTitle; + } + }); + } + }; + + async header(stack) { + const headerStack = stack.addStack(); + headerStack.addSpacer(); + const logo = headerStack.addImage(await this.$request.get(this.logo, 'IMG')); + logo.imageSize = new Size(415 * this.logoScale * this.SCALE, 125 * this.logoScale * this.SCALE); + headerStack.addSpacer(); + stack.addSpacer(); + + const feeStack = stack.addStack(); + feeStack.centerAlignContent(); + feeStack.addSpacer(); + const feeValue = feeStack.addText(`${this.fee.number}`); + this.unit(feeStack, '元', 5 * this.SCALE, this.widgetColor); + feeValue.font = Font.boldRoundedSystemFont(this.feeTextSize * this.SCALE); + feeValue.textColor = this.widgetColor; + feeStack.addSpacer(); + stack.addSpacer(); + }; + + textLayout(stack, data) { + const rowStack = stack.addStack(); + rowStack.centerAlignContent(); + const icon = SFSymbol.named(data.icon) || SFSymbol.named('phone.fill'); + icon.applyHeavyWeight(); + let iconElement = rowStack.addImage(icon.image); + iconElement.imageSize = new Size(this.textSize, this.textSize); + iconElement.tintColor = data.iconColor; + rowStack.addSpacer(4 * this.SCALE); + let title = rowStack.addText(data.title); + rowStack.addSpacer(); + let number = rowStack.addText(data.number + data.unit); + [title, number].map(t => t.textColor = this.widgetColor); + [title, number].map(t => t.font = Font.systemFont(this.textSize * this.SCALE)); + }; + + async setThirdWidget(widget) { + const amountStack = widget.addStack(); + amountStack.centerAlignContent(); + + const icon = await this.$request.get(this.smallLogo, 'IMG'); + + if (this.settings.builtInColor === 'true') { + const iconStack = amountStack.addStack(); + iconStack.setPadding(3 * this.SCALE, 3 * this.SCALE, 3 * this.SCALE, 3 * this.SCALE); + iconStack.backgroundColor = this.fee.iconColor; + iconStack.cornerRadius = 12 * this.SCALE; + const iconImage = iconStack.addImage(icon); + iconImage.imageSize = new Size(18 * this.SCALE, 18 * this.SCALE); + iconImage.tintColor = Color.white(); + } else { + const iconImage = amountStack.addImage(icon); + iconImage.imageSize = new Size(24 * this.SCALE, 24 * this.SCALE); + } + + amountStack.addSpacer(); + + const amountText = amountStack.addText(`${this.fee.number}`); + amountText.font = Font.boldRoundedSystemFont(24 * this.SCALE); + amountText.minimumScaleFactor = 0.5; + amountText.textColor = this.widgetColor; + this.unit(amountStack, '元', 7 * this.SCALE); + + widget.addSpacer(); + + const mainStack = widget.addStack(); + this.setRow(mainStack, this.flow, this.flowColorHex); + mainStack.addSpacer(); + this.setRow(mainStack, this.voice, this.voiceColorHex); + }; + + async setForthWidget(widget) { + const bodyStack = widget.addStack(); + bodyStack.cornerRadius = 14 * this.SCALE; + bodyStack.layoutVertically(); + const headerStack = bodyStack.addStack(); + headerStack.setPadding(8 * this.SCALE, 12 * this.SCALE, 0, 12 * this.SCALE); + headerStack.layoutVertically(); + const title = headerStack.addText(this.fee.title); + title.font = Font.systemFont(12 * this.SCALE); + title.textColor = this.widgetColor + title.textOpacity = 0.7; + const balanceStack = headerStack.addStack(); + const balanceText = balanceStack.addText(`${this.fee.number}`); + balanceText.minimumScaleFactor = 0.5; + balanceText.font = Font.boldRoundedSystemFont(22 * this.SCALE); + const color = this.widgetColor; + balanceText.textColor = color; + this.unit(balanceStack, '元', 5 * this.SCALE, color); + balanceStack.addSpacer(); + balanceStack.centerAlignContent(); + + const icon = await this.$request.get(this.smallLogo, 'IMG'); + + if (this.settings.builtInColor === 'true') { + const iconStack = balanceStack.addStack(); + iconStack.setPadding(3 * this.SCALE, 3 * this.SCALE, 3 * this.SCALE, 3 * this.SCALE); + iconStack.backgroundColor = this.fee.iconColor; + iconStack.cornerRadius = 12 * this.SCALE; + const iconImage = iconStack.addImage(icon); + iconImage.imageSize = new Size(18 * this.SCALE, 18 * this.SCALE); + iconImage.tintColor = Color.white(); + } else { + const iconImage = balanceStack.addImage(icon); + iconImage.imageSize = new Size(24 * this.SCALE, 24 * this.SCALE); + } + + + bodyStack.addSpacer(); + const mainStack = bodyStack.addStack(); + mainStack.setPadding(8 * this.SCALE, 12 * this.SCALE, 8 * this.SCALE, 12 * this.SCALE); + mainStack.cornerRadius = 14 * this.SCALE; + mainStack.backgroundColor = Color.dynamic(new Color("#E2E2E7", 0.3), new Color("#2C2C2F", 1)); + mainStack.layoutVertically(); + + this.setList(mainStack, this.flow); + mainStack.addSpacer(); + this.setList(mainStack, this.voice); + }; + + setList(stack, data) { + const rowStack = stack.addStack(); + rowStack.centerAlignContent(); + const lineStack = rowStack.addStack(); + lineStack.size = new Size(8 * this.SCALE, 30 * this.SCALE); + lineStack.cornerRadius = 4 * this.SCALE; + + lineStack.backgroundColor = data.iconColor; + + rowStack.addSpacer(10 * this.SCALE); + + const leftStack = rowStack.addStack(); + leftStack.layoutVertically(); + leftStack.addSpacer(2 * this.SCALE); + + const titleStack = leftStack.addStack(); + const title = titleStack.addText(data.title); + title.font = Font.systemFont(10 * this.SCALE); + title.textColor = this.widgetColor; + title.textOpacity = 0.5; + + const valueStack = leftStack.addStack(); + valueStack.centerAlignContent(); + const value = valueStack.addText(`${data.number}`); + value.font = Font.semiboldRoundedSystemFont(16 * this.SCALE); + value.textColor = this.widgetColor; + valueStack.addSpacer(); + + const unitStack = valueStack.addStack(); + unitStack.cornerRadius = 4 * this.SCALE; + unitStack.borderWidth = 1; + unitStack.borderColor = data.iconColor; + unitStack.setPadding(1, 3 * this.SCALE, 1, 3 * this.SCALE); + unitStack.size = new Size(30 * this.SCALE, 0) + unitStack.backgroundColor = Color.dynamic(data.iconColor, new Color(data.iconColor.hex, 0.3)); + const unit = unitStack.addText(data.en); + unit.font = Font.mediumRoundedSystemFont(10 * this.SCALE); + unit.textColor = Color.dynamic(Color.white(), data.iconColor); + }; + + setRow(stack, data, color) { + const stackWidth = 68 * this.SCALE; + const rowStack = stack.addStack(); + rowStack.layoutVertically(); + rowStack.size = new Size(stackWidth, 0); + const image = this.gaugeChart(data, color); + const imageStack = rowStack.addStack(); + imageStack.layoutVertically(); + imageStack.size = new Size(stackWidth, stackWidth); + imageStack.backgroundImage = image; + imageStack.addSpacer(); + const iconStack = imageStack.addStack(); + iconStack.addSpacer(); + const sfs = SFSymbol.named(data.icon) || SFSymbol.named('phone.fill'); + sfs.applyHeavyWeight(); + const icon = iconStack.addImage(sfs.image); + icon.imageSize = new Size(22 * this.SCALE, 22 * this.SCALE); + icon.tintColor = new Color(color); + iconStack.addSpacer(); + imageStack.addSpacer(8 * this.SCALE); + const unitStack = imageStack.addStack(); + unitStack.addSpacer(); + const innerStack = unitStack.addStack(); + innerStack.size = new Size(32 * this.SCALE, 0); + innerStack.setPadding(1, 1, 1, 1); + innerStack.backgroundColor = new Color(color); + innerStack.cornerRadius = 4 * this.SCALE; + const unit = innerStack.addText(data.en); + unit.font = Font.semiboldRoundedSystemFont(10 * this.SCALE); + unit.textColor = Color.white(); + unitStack.addSpacer(); + imageStack.addSpacer(4 * this.SCALE); + + const infoStack = rowStack.addStack(); + infoStack.cornerRadius = 12 * this.SCALE; + infoStack.layoutVertically(); + let gradient = new LinearGradient(); + gradient.colors = [new Color(color,0.1), new Color(color,0.01)]; + gradient.locations = [0, 1]; + gradient.startPoint = new Point(0, 0); + gradient.endPoint = new Point(0, 1); + infoStack.backgroundGradient = gradient; + + const valueStack = infoStack.addStack(); + valueStack.size = new Size(stackWidth, 0); + valueStack.setPadding(3 * this.SCALE, 0, 2 * this.SCALE, 0) + const value = valueStack.addText(`${data.number}`); + value.textColor = this.widgetColor; + value.font = Font.semiboldRoundedSystemFont(18 * this.SCALE); + value.centerAlignText(); + + const titleStack = infoStack.addStack(); + titleStack.addSpacer(); + const title = titleStack.addText(data.title); + title.font = Font.regularRoundedSystemFont(9 * this.SCALE); + title.textOpacity = 0.5; + titleStack.addSpacer(); + }; + + async small(stack, data, logo = false, en = false) { + const bg = new LinearGradient(); + bg.locations = [0, 1]; + bg.endPoint = new Point(1, 0) + bg.colors = [ + new Color(data.iconColor.hex, 0.1), + new Color(data.iconColor.hex, 0.03) + ]; + const rowStack = stack.addStack(); + rowStack.centerAlignContent(); + rowStack.setPadding(5, 8, 5, 8) + rowStack.backgroundGradient = bg; + rowStack.cornerRadius = 12; + const leftStack = rowStack.addStack(); + leftStack.layoutVertically(); + const titleStack = leftStack.addStack(); + const title = titleStack.addText(data.title); + const balanceStack = leftStack.addStack(); + balanceStack.centerAlignContent(); + const balanceUnit = en ? data.en : '' + const balance = balanceStack.addText(`${data.number} ${balanceUnit}`); + if (!en) this.addChineseUnit(balanceStack, data.unit, data.iconColor, 13 * this.SCALE); + balance.font = Font.semiboldRoundedSystemFont(16 * this.SCALE); + title.textOpacity = 0.5; + title.font = Font.mediumSystemFont(11 * this.SCALE); + [title, balance].map(t => t.textColor = data.iconColor); + rowStack.addSpacer(); + let iconImage; + if (logo) { + const icon = await this.$request.get(this.smallLogo, 'IMG'); + iconImage = rowStack.addImage(icon); + } else { + const icon = SFSymbol.named(data.icon) || SFSymbol.named('phone.fill'); + icon.applyHeavyWeight(); + iconImage = rowStack.addImage(icon.image); + }; + iconImage.imageSize = new Size(22 * this.SCALE, 22 * this.SCALE); + iconImage.tintColor = data.iconColor; + }; + + async smallCell(stack, data, logo = false, en = false) { + const bg = new LinearGradient(); + const padding = 6 * this.SCALE; + bg.locations = [0, 1]; + bg.endPoint = new Point(1, 0) + bg.colors = [ + new Color(data.iconColor.hex, 0.03), + new Color(data.iconColor.hex, 0.1) + ]; + const rowStack = stack.addStack(); + rowStack.setPadding(4, 4, 4, 4) + rowStack.backgroundGradient = bg; + rowStack.cornerRadius = 12; + const iconStack = rowStack.addStack(); + iconStack.backgroundColor = data.iconColor; + iconStack.setPadding(padding, padding, padding, padding); + iconStack.cornerRadius = 17 * this.SCALE; + let iconImage; + if (logo) { + const icon = await this.$request.get(this.smallLogo, 'IMG'); + iconImage = iconStack.addImage(icon); + } else { + const icon = SFSymbol.named(data.icon) || SFSymbol.named('phone.fill'); + icon.applyHeavyWeight(); + iconImage = iconStack.addImage(icon.image); + }; + iconImage.imageSize = new Size(22 * this.SCALE, 22 * this.SCALE); + iconImage.tintColor = new Color('FFFFFF'); + rowStack.addSpacer(15); + const rightStack = rowStack.addStack(); + rightStack.layoutVertically(); + const balanceStack = rightStack.addStack(); + balanceStack.centerAlignContent(); + const balanceUnit = en ? data.en : '' + const balance = balanceStack.addText(`${data.number} ${balanceUnit}`); + if (!en) this.addChineseUnit(balanceStack, data.unit, data.iconColor, 13 * this.SCALE); + balance.font = Font.semiboldRoundedSystemFont(16 * this.SCALE); + const titleStack = rightStack.addStack(); + const title = titleStack.addText(data.title); + title.centerAlignText(); + rowStack.addSpacer(); + title.textOpacity = 0.5; + title.font = Font.mediumSystemFont(11 * this.SCALE); + [title, balance].map(t => t.textColor = data.iconColor); + }; + + async mediumCell(canvas, stack, data, color, fee = false, percent) { + const bg = new LinearGradient(); + bg.locations = [0, 1]; + bg.colors = [ + new Color(color, 0.03), + new Color(color, 0.1) + ]; + const dataStack = stack.addStack(); + dataStack.backgroundGradient = bg; + dataStack.cornerRadius = 15; + dataStack.layoutVertically(); + dataStack.addSpacer(); + + const topStack = dataStack.addStack(); + topStack.addSpacer(); + await this.imageCell(canvas, topStack, data, fee, percent); + topStack.addSpacer(); + + if (fee) { + dataStack.addSpacer(5); + const updateStack = dataStack.addStack(); + updateStack.addSpacer(); + updateStack.centerAlignContent(); + const updataIcon = SFSymbol.named('arrow.2.circlepath'); + updataIcon.applyHeavyWeight(); + const updateImg = updateStack.addImage(updataIcon.image); + updateImg.tintColor = new Color(color, 0.6); + updateImg.imageSize = new Size(10, 10); + updateStack.addSpacer(3); + const updateText = updateStack.addText(`${this.arrUpdateTime[2]}:${this.arrUpdateTime[3]}`) + updateText.font = Font.mediumSystemFont(10); + updateText.textColor = new Color(color, 0.6); + updateStack.addSpacer(); + } + + dataStack.addSpacer(); + + const numberStack = dataStack.addStack(); + numberStack.addSpacer(); + const number = numberStack.addText(`${data.number} ${data.en}`); + number.font = Font.semiboldSystemFont(15); + numberStack.addSpacer(); + + dataStack.addSpacer(3); + + const titleStack = dataStack.addStack(); + titleStack.addSpacer(); + const title = titleStack.addText(data.title); + title.font = Font.mediumSystemFont(11); + title.textOpacity = 0.7; + titleStack.addSpacer(); + + dataStack.addSpacer(15); + [title, number].map(t => t.textColor = new Color(color)); + }; + + async imageCell(canvas, stack, data, fee, percent) { + const canvaStack = stack.addStack(); + canvaStack.layoutVertically(); + if (!fee) { + this.drawArc(canvas, data.percent * 3.6, data.FGColor, data.BGColor); + canvaStack.size = new Size(this.ringStackSize, this.ringStackSize); + canvaStack.backgroundImage = canvas.getImage(); + this.ringContent(canvaStack, data, percent); + } else { + canvaStack.addSpacer(10); + const smallLogo = await this.$request.get(this.smallLogo, 'IMG'); + const logoStack = canvaStack.addStack(); + logoStack.size = new Size(40, 40); + logoStack.backgroundImage = smallLogo; + } + }; + + ringContent(stack, data, percent = false) { + const rowIcon = stack.addStack(); + rowIcon.addSpacer(); + const icon = SFSymbol.named(data.icon) || SFSymbol.named('phone.fill'); + icon.applyHeavyWeight(); + const iconElement = rowIcon.addImage(icon.image); + iconElement.tintColor = this.gradient ? new Color(data.colors[1]) : data.FGColor; + iconElement.imageSize = new Size(12, 12); + iconElement.imageOpacity = 0.7; + rowIcon.addSpacer(); + + stack.addSpacer(1); + + const rowNumber = stack.addStack(); + rowNumber.addSpacer(); + const number = rowNumber.addText(percent ? `${data.percent}` : `${data.number}`); + number.font = percent ? Font.systemFont(this.ringTextSize - 2) : Font.mediumSystemFont(this.ringTextSize); + rowNumber.addSpacer(); + + const rowUnit = stack.addStack(); + rowUnit.addSpacer(); + const unit = rowUnit.addText(percent ? '%' : data.unit); + unit.font = Font.boldSystemFont(8); + unit.textOpacity = 0.5; + rowUnit.addSpacer(); + + if (percent) { + if (this.gradient) { + [unit, number].map(t => t.textColor = new Color(data.colors[1])); + } else { + [unit, number].map(t => t.textColor = data.FGColor); + } + } else { + [unit, number].map(t => t.textColor = this.widgetColor); + } + }; + + makeCanvas(w = this.canvSize, h = this.canvSize) { + const canvas = new DrawContext(); + canvas.opaque = false; + canvas.respectScreenScale = true; + canvas.size = new Size(w, h); + return canvas; + }; + + sinDeg(deg) { + return Math.sin((deg * Math.PI) / 180); + }; + + cosDeg(deg) { + return Math.cos((deg * Math.PI) / 180); + }; + + drawArc(canvas, deg, fillColor, strokeColor) { + let ctr = new Point(this.canvSize / 2, this.canvSize / 2); + let bgx = ctr.x - this.canvRadius; + let bgy = ctr.y - this.canvRadius; + let bgd = 2 * this.canvRadius; + let bgr = new Rect(bgx, bgy, bgd, bgd) + + canvas.setStrokeColor(strokeColor); + canvas.setLineWidth(this.canvWidth); + canvas.strokeEllipse(bgr); + + for (let t = 0; t < deg; t++) { + let rect_x = ctr.x + this.canvRadius * this.sinDeg(t) - this.canvWidth / 2; + let rect_y = ctr.y - this.canvRadius * this.cosDeg(t) - this.canvWidth / 2; + let rect_r = new Rect(rect_x, rect_y, this.canvWidth, this.canvWidth); + + if (this.gradient && Array.isArray(fillColor)) { + let idx = Math.floor(t); + if (idx >= fillColor.length) idx = fillColor.length - 1; + let c = fillColor[idx]; + if (typeof c === 'string') { + canvas.setFillColor(new Color(c)); + } else { + canvas.setFillColor(new Color("000000")); // 默认 fallback + } + } else { + canvas.setFillColor(fillColor); + } + + canvas.setStrokeColor(strokeColor) + canvas.fillEllipse(rect_r); + } + }; + + fillRect(drawing, x, y, width, height, cornerradio, color) { + let path = new Path(); + let rect = new Rect(x, y, width, height); + path.addRoundedRect(rect, cornerradio, cornerradio); + drawing.addPath(path); + drawing.setFillColor(color); + drawing.fillPath(); + }; + + progressBar(data) { + const W = 60, H = 9 , r = 4.5, h = 3; + const drawing = this.makeCanvas(W, H); + const progress = data.percent / 100 * W; + const circle = progress - 2 * r; + const fgColor = data.iconColor; + const bgColor = new Color(data.iconColor.hex, 0.3); + const pointerColor = data.iconColor; + this.fillRect(drawing, 0, (H - h) / 2, W, h, h / 2, bgColor); + this.fillRect(drawing, 0, (H - h) / 2, progress > W ? W : progress < r * 2 ? r * 2 : progress, h, h / 2, fgColor); + this.fillRect(drawing, circle > W - r * 2 ? W - r * 2 : circle < 0 ? 0 : circle, H / 2 - r, r * 2, r * 2, r, pointerColor); + return drawing.getImage(); + }; + + gaugeChart(data, color) { + const drawing = this.makeCanvas(); + const center = new Point(this.canvSize / 2, this.canvSize / 2); + const radius = this.canvSize / 2 - 10; + const circleRadius = 8; + const startBgAngle = (10 * Math.PI) / 12; + const endBgAngle = (26 * Math.PI) / 12; + const totalBgAngle = endBgAngle - startBgAngle; + const gapAngle = Math.PI / 80; + const fillColor = data.BGColor; + const lineWidth = circleRadius * 2; + let progress = data.percent / 100; + + this.drawLineArc(drawing, center, radius, startBgAngle, endBgAngle, 1, fillColor, lineWidth); + + this.drawHalfCircle(center.x + radius * Math.cos(startBgAngle), center.y + radius * Math.sin(startBgAngle), startBgAngle, circleRadius, drawing, fillColor, -1); + this.drawHalfCircle(center.x + radius * Math.cos(endBgAngle), center.y + radius * Math.sin(endBgAngle), endBgAngle, circleRadius, drawing, fillColor, 1); + + let totalProgressAngle = totalBgAngle * progress; + for (let i = 0; i < 240 * progress; i++) { + const t = i / 240; + const angle = startBgAngle + totalBgAngle * t; + const x = center.x + radius * Math.cos(angle); + const y = center.y + radius * Math.sin(angle); + + const circleRect = new Rect(x - circleRadius, y - circleRadius, circleRadius * 2, circleRadius * 2); + + if (this.gradient && Array.isArray(data.FGColor)) { + let idx = Math.floor(i); + if (idx >= data.FGColor.length) idx = data.FGColor.length - 1; + drawing.setFillColor(new Color(data.FGColor[idx])); + } else { + drawing.setFillColor(data.FGColor); + } + + drawing.fillEllipse(circleRect); + } + return drawing.getImage(); + }; + + drawHalfCircle(centerX, centerY, startAngle, circleRadius, context, fillColor, direction = 1) { + const halfCirclePath = new Path(); + const startX = centerX + circleRadius * Math.cos(startAngle); + const startY = centerY + circleRadius * Math.sin(startAngle); + halfCirclePath.move(new Point(startX, startY)); + + for (let i = 0; i <= 10; i++) { + const t = i / 10; + const angle = startAngle + direction * Math.PI * t; + const x = centerX + circleRadius * Math.cos(angle); + const y = centerY + circleRadius * Math.sin(angle); + halfCirclePath.addLine(new Point(x, y)); + } + + context.setFillColor(fillColor); + context.addPath(halfCirclePath); + context.fillPath(); + }; + + drawLineArc(context, center, radius, startAngle, endAngle, segments, fillColor, lineWidth, dir = 1) { + const path = new Path(); + const startX = center.x + radius * Math.cos(startAngle); + const startY = center.y + radius * Math.sin(startAngle); + path.move(new Point(startX, startY)); + + const steps = 100; + for (let i = 1; i <= steps; i++) { + const t = i / steps; + const angle = startAngle + (endAngle - startAngle) * t; + const x = center.x + radius * Math.cos(angle); + const y = center.y + radius * Math.sin(angle); + path.addLine(new Point(x, y)); + } + + context.setStrokeColor(fillColor); + context.setLineWidth(lineWidth); + context.addPath(path); + context.strokePath(); + }; + + addChineseUnit(stack, text, color, size) { + let textElement = stack.addText(text); + textElement.textColor = color; + textElement.font = Font.semiboldSystemFont(size); + return textElement; + }; + + unit(stack, text, spacer, color = this.widgetColor) { + stack.addSpacer(1); + const unitStack = stack.addStack(); + unitStack.layoutVertically(); + unitStack.addSpacer(spacer); + const unitTitle = unitStack.addText(text); + unitTitle.font = Font.semiboldRoundedSystemFont(10); + unitTitle.textColor = color; + }; + + arrColor() { + let colorArr = [ + ["#FFF000", "#E62490"], // 0. 亮黄色 → 粉红色 + ["#ABDCFF", "#0396FF"], // 1. 浅蓝色 → 靛蓝 + ["#FEB692", "#EA5455"], // 2. 桃橙色 → 红宝石 + ["#FEB692", "#EA5455"], // 3. 桃橙色 → 红宝石 + ["#CE9FFC", "#7367F0"], // 4. 淡紫色 → 紫蓝色 + ["#90F7EC", "#32CCBC"], // 5. 淡青色 → 青绿色 + ["#FFF6B7", "#F6416C"], // 6. 柔黄色 → 樱桃红 + ["#E2B0FF", "#9F44D3"], // 7. 淡紫色 → 深紫色 + ["#F97794", "#F072B6"], // 8. 玫瑰粉 → 紫粉色 + ["#FCCF31", "#F55555"], // 9. 金黄色 → 红橙色 + ["#5EFCE8", "#736EFE"], // 10. 薄荷绿 → 深紫蓝 + ["#FAD7A1", "#E96D71"], // 11. 浅杏色 → 粉红色 + ["#FFFF1C", "#00C3FF"], // 12. 亮黄色 → 电蓝色 + ["#FEC163", "#DE4313"], // 13. 浅橙色 → 深橙红 + ["#F6CEEC", "#D939CD"], // 14. 粉紫色 → 深紫粉 + ["#FDD819", "#E80505"], // 15. 柠檬黄 → 鲜红色 + ["#FFF3B0", "#CA26FF"], // 16. 米黄色 → 亮紫色 + ["#EECDA3", "#EF629F"], // 17. 杏仁色 → 粉红色 + ["#C2E59C", "#64B3F4"], // 18. 青柠绿 → 天蓝色 + ["#FFF886", "#F072B6"], // 19. 浅黄绿 → 紫粉色 + ["#F5CBFF", "#C346C2"], // 20. 浅紫粉 → 深紫粉 + ["#FFF720", "#3CD500"], // 21. 荧光黄 → 亮绿色 + ["#FFC371", "#FF5F6D"], // 22. 浅橙色 → 樱桃红 + ["#FFD3A5", "#FD6585"], // 23. 柔橙色 → 浅玫瑰红 + ["#C2FFD8", "#465EFB"], // 24. 淡薄荷绿 → 电蓝色 + ["#FFC600", "#FD6E6A"], // 25. 橙黄色 → 珊瑚粉 + ["#FFC600", "#FD6E6A"], // 26. 橙黄色 → 珊瑚粉 + ["#92FE9D", "#00C9FF"], // 27. 荧光绿 → 浅蓝色 + ["#FFDDE1", "#EE9CA7"], // 28. 淡粉红 → 珊瑚粉 + ["#F0FF00", "#58CFFB"], // 29. 亮黄绿 → 天蓝色 + ["#FFE985", "#FA742B"], // 30. 柔黄橙 → 深橙色 + ["#72EDF2", "#5151E5"], // 31. 浅蓝绿 → 紫蓝色 + ["#F6D242", "#FF52E5"], // 32. 金黄 → 粉紫色 + ["#F9D423", "#FF4E50"], // 33. 柠檬黄 → 亮红色 + ["#00EAFF", "#3C8CE7"], // 34. 电青色 → 海蓝色 + ["#FCFF00", "#FFA8A8"], // 35. 亮黄绿 → 浅粉红 + ["#FF96F9", "#C32BAC"], // 36. 亮粉紫 → 紫红色 + ["#FFDD94", "#FA897B"], // 37. 柔黄色 → 鲑鱼色 + ["#FFCC4B", "#FF7D58"], // 38. 柔金色 → 橙红色 + ["#D0E6A5", "#86E3CE"], // 39. 淡青绿 → 青绿色 + ["#E8E965", "#64C5C7"] // 50. 亮黄绿 → 浅青色 + ]; + let colors = colorArr[Math.floor(Math.random() * colorArr.length)]; + return colors; + }; + + getIconColorSet() { + const colors = [ + ["#1E81B0", "#FF5714", "#FF6347"], // 0. 深蓝色,亮橙色,番茄红 + ["#FF6347", "#32CD32", "#3CB371"], // 1. 番茄红,鲜绿色,海洋绿 + ["#FF8C00", "#4682B4", "#20B2AA"], // 2. 暗橙色,钢蓝色,浅海蓝 + ["#FF4500", "#00CED1", "#00BFFF"], // 3. 橙红色,深青色,深天蓝 + ["#DB7093", "#3CB371", "#FFA07A"], // 4. 草莓红,海洋绿,浅橙红 + ["#FF8C00", "#4682B4", "#20B2AA"], // 5. 暗橙色,钢蓝色,浅海蓝 + ["#FF7F50", "#4CAF50", "#1E90FF"], // 6. 珊瑚橙,鲜绿色,亮天蓝 + ["#FF4500", "#00CED1", "#1E90FF"], // 7. 橙红色,深青色,亮天蓝 + ["#FF4500", "#3CB371", "#FFA07A"], // 8. 橙红色,海洋绿,浅橙红 + ["#FF7F50", "#00A9A5", "#C41E3A"], // 9. 珊瑚橙,深青绿,红宝石红 + ["#2E8B57", "#FF6347", "#00BFFF"], // 10. 海绿色,番茄红,深天蓝 + ["#FF4500", "#008B8B", "#3CB371"], // 11. 橙红色,深青色,海洋绿 + ["#DC143C", "#00BFFF", "#F08080"], // 12. 猩红色,深天蓝,淡珊瑚红 + ["#20B2AA", "#FF8C00", "#32CD32"], // 13. 浅海蓝,暗橙色,鲜绿色 + ["#FF4500", "#66E579", "#00CED1"], // 14. 橙红色,萤光绿,深青色 + ["#DA70D6", "#5DB8E8", "#FF6347"], // 15. 兰花紫,天蓝色,番茄红 + ["#32CD32", "#F86527", "#00CED1"], // 16. 鲜绿色,夕阳橙,深青色 + ["#FF6347", "#00FA9A", "#20B2AA"], // 17. 番茄红,适中春绿,浅海蓝 + ["#FA8072", "#4682B4", "#3CB371"], // 18. 鲑鱼色,钢蓝色,海洋绿 + ["#5856CF", "#FF4500", "#00BFFF"], // 19. 淡紫蓝,橙红色,深天蓝 + ["#FF8C00", "#20B2AA", "#5856CF"], // 20. 暗橙色,浅海蓝,淡紫蓝 + ["#704CE4", "#20B2AA", "#FF8F8F"], // 21. 紫罗兰,浅海蓝,玫瑰粉 + ["#73DE00", "#48D1CC", "#FF6347"], // 22. 新生绿,松石绿,番茄红 + ["#DB7093", "#6495ED", "#FA8072"], // 23. 草莓红,矢车菊蓝,鲑鱼色 + ["#FFA07A", "#32CD32", "#1E90FF"], // 24. 浅橙红,鲜绿色,亮天蓝 + ["#00A9A5", "#FF4500", "#4682B4"], // 25. 深青绿,橙红色,钢蓝色 + ["#13C07E", "#00BCD4", "#FF6347"], // 26. 薄荷绿,青蓝色,番茄红 + ["#8BC34A", "#FF5722", "#3F51B5"], // 27. 黄绿色,烈焰橙,靛蓝 + ["#4CAF50", "#00BCD4", "#F44336"], // 28. 鲜绿色,青蓝色,火烈鸟红 + ["#3F51B5", "#009688", "#FF5722"], // 29. 靛蓝,青色,烈焰橙 + ["#B170FF", "#03A9F4", "#3CB371"], // 30. 丁香紫,亮天蓝,海洋绿 + ["#009688", "#8BC34A", "#FF6347"], // 31. 青色,黄绿色,番茄红 + ["#F44336", "#00BCD4", "#3CB371"], // 32. 火烈鸟红,青蓝色,海洋绿 + ["#FF4500", "#32CD32", "#3CB371"], // 33. 橙红色,鲜绿色,海洋绿 + ["#3CB371", "#FF9800", "#009688"], // 34. 海洋绿,橙色,青色 + ["#4CAF50", "#00BCD4", "#F44336"], // 35. 鲜绿色,青蓝色,火烈鸟红 + ["#FF5722", "#8BC34A", "#38B1B7"], // 36. 烈焰橙,黄绿色,猩红色 + ["#03A9F4", "#3CB371", "#FF788B"], // 37. 亮天蓝,海洋绿,珊瑚粉 + ["#FF5722", "#03A9F4", "#DB7093"], // 38. 烈焰橙,亮天蓝,草莓红 + ["#1E90FF", "#38B1B7", "#CD5C5C"], // 39. 亮天蓝,海洋蓝,印度红 + ["#FF6347", "#48D1CC", "#32CD32"], // 40. 番茄红,松石绿,鲜绿色 + ["#FF4500", "#73DE00", "#4682B4"], // 41. 橙红色,新生绿,钢蓝色 + ["#FF5722", "#8BC34A", "#00CED1"], // 42. 烈焰橙,黄绿色,深青色 + ["#FF4500", "#32CD32", "#4682B4"], // 43. 橙红色,鲜绿色,钢蓝色 + ["#8BC34A", "#F08080", "#00BFFF"], // 44. 黄绿色,淡珊瑚红,深天蓝 + ["#FF6F61", "#40E0D0", "#1E90FF"], // 45. 珊瑚红,松石绿,亮天蓝 + ["#00CED1", "#FF6347", "#4682B4"], // 46. 深青色,番茄红,钢蓝色 + ["#E57373", "#4DD0E1", "#81C784"], // 47. 浅红色,青绿,黄绿色 + ["#FF5722", "#8BC34A", "#FFD700"], // 48. 烈焰橙,黄绿色,金色 + ["#F08080", "#48D1CC", "#32CD32"], // 49. 珊瑚红,松石绿,鲜绿色 + ]; + const randomIndex = Math.floor(Math.random() * colors.length); + return colors[randomIndex]; + }; + + getWidgetScaleFactor() { + const referenceScreenSize = { width: 430, height: 932, widgetSize: 170 }; + const screenData = [ + { width: 440, height: 956, widgetSize: 170 }, + { width: 430, height: 932, widgetSize: 170 }, + { width: 428, height: 926, widgetSize: 170 }, + { width: 414, height: 896, widgetSize: 169 }, + { width: 414, height: 736, widgetSize: 159 }, + { width: 393, height: 852, widgetSize: 158 }, + { width: 390, height: 844, widgetSize: 158 }, + { width: 375, height: 812, widgetSize: 155 }, + { width: 375, height: 667, widgetSize: 148 }, + { width: 360, height: 780, widgetSize: 155 }, + { width: 320, height: 568, widgetSize: 141 } + ]; + + const deviceScreenWidth = Device.screenSize().width; + const deviceScreenHeight = Device.screenSize().height; + + const matchingScreen = screenData.find(screen => + (screen.width === deviceScreenWidth && screen.height === deviceScreenHeight) || + (screen.width === deviceScreenHeight && screen.height === deviceScreenWidth) + ); + + if (!matchingScreen) { + return 1; + }; + + const scaleFactor = matchingScreen.widgetSize / referenceScreenSize.widgetSize; + + return Math.floor(scaleFactor * 100) / 100; + }; + + async checkAndUpdateScript() { + const remoteScriptUrl = "https://raw.githubusercontent.com/githubdulong/Script/master/Scriptable/ChinaUnicom_Multi.js"; + const scriptName = Script.name() + '.js' + + console.log("正在检查更新...") + + try { + const request = new Request(remoteScriptUrl); + const newScriptContent = await request.loadString(); + + let versionPattern = /version\s*=\s*['"]([^'"]+)['"]/; + let match = newScriptContent.match(versionPattern); + + if (!match) { + console.log("未在远程代码中找到版本号"); + const alert = new Alert(); + alert.title = "检查失败"; + alert.message = "远程脚本格式可能不正确,未找到版本号。"; + alert.addAction("确定"); + await alert.present(); + return; + } + + const latestVersion = match[1]; + const isUpdateAvailable = this.version !== latestVersion; + + if (isUpdateAvailable) { + const alert = new Alert(); + alert.title = "检测到新版本"; + alert.message = `当前版本:${this.version}\n新版本:${latestVersion}\n是否更新?`; + alert.addAction("更新"); + alert.addCancelAction("取消"); + + const response = await alert.presentAlert(); + if (response === 0) { + const fm = FileManager[ + module.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local' + ](); + const scriptPath = fm.documentsDirectory() + `/${scriptName}`; + fm.writeString(scriptPath, newScriptContent); + + const successAlert = new Alert(); + successAlert.title = "更新成功"; + successAlert.message = "脚本已更新,请关闭本脚本后重新打开!"; + successAlert.addAction("确定"); + await successAlert.present(); + // this.reopenScript(); + } + } else { + const noUpdateAlert = new Alert(); + noUpdateAlert.title = "无需更新"; + noUpdateAlert.message = "当前已是最新版本。"; + noUpdateAlert.addAction("确定"); + await noUpdateAlert.present(); + } + } catch (e) { + console.error(e); + const alert = new Alert(); + alert.title = "更新出错"; + alert.message = "网络请求失败或地址错误:" + e.message; + alert.addAction("确定"); + await alert.present(); + } + }; + + renderSmall = async (w) => { + w.setPadding(this.smallPadding, this.smallPadding, this.smallPadding, this.smallPadding); + if (this.widgetStyle == "1") { + const bodyStack = w.addStack(); + bodyStack.layoutVertically(); + await this.small(bodyStack, this.fee, true); + bodyStack.addSpacer(); + await this.small(bodyStack, this.flow, false, true); + bodyStack.addSpacer(); + await this.small(bodyStack, this.voice); + } else if (this.widgetStyle == "2") { + const bodyStack = w.addStack(); + bodyStack.layoutVertically(); + await this.smallCell(bodyStack, this.fee, true); + bodyStack.addSpacer(); + await this.smallCell(bodyStack, this.flow, false, true); + bodyStack.addSpacer(); + await this.smallCell(bodyStack, this.voice); + } else if (this.widgetStyle == "3") { + const bodyStack = w.addStack(); + bodyStack.layoutVertically(); + await this.setThirdWidget(bodyStack); + } else if (this.widgetStyle == "4") { + const bodyStack = w.addStack(); + bodyStack.layoutVertically(); + await this.setForthWidget(bodyStack); + } else if (this.widgetStyle == "5") { + const bodyStack = w.addStack(); + bodyStack.layoutVertically(); + await this.header(bodyStack); + const canvas = this.makeCanvas(); + const ringStack = bodyStack.addStack(); + this.imageCell(canvas, ringStack, this.flow); + ringStack.addSpacer(); + this.imageCell(canvas, ringStack, this.voice); + } else { + const bodyStack = w.addStack(); + bodyStack.layoutVertically(); + await this.header(bodyStack); + this.textLayout(bodyStack, this.flow); + bodyStack.addSpacer(7); + this.textLayout(bodyStack, this.voice); + bodyStack.addSpacer(7); + this.textLayout(bodyStack, this.point); + } + return w; + }; + + renderMedium = async (w) => { + w.setPadding(this.padding, this.padding, this.padding, this.padding); + const canvas = this.makeCanvas(); + const bodyStack = w.addStack(); + await this.mediumCell(canvas, bodyStack, this.fee, 'd7000f', true); + bodyStack.addSpacer(this.padding); + await this.mediumCell(canvas, bodyStack, this.flow, this.flowColorHex, false, true); + bodyStack.addSpacer(this.padding); + await this.mediumCell(canvas, bodyStack, this.voice, this.voiceColorHex, false,true); + return w; + }; + + setColorConfig = async () => { + return this.renderAppView([ + { + title: '颜色配置', + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/gradient.png', + type: 'switch', + title: '渐变进度条', + desc: '', + val: 'gradient', + }, + ], + }, + { + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/step1.png', + type: 'color', + title: '流量进度条', + defaultValue: '#12A6E4', + desc: '', + val: 'step1', + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/step2.png', + type: 'color', + title: '语音进度条', + defaultValue: '#F86527', + desc: '', + val: 'step2', + }, + ], + }, + { + title: '颜色配置', + menu: [ + { + url: 'https://pic1.imgdb.cn/item/63315c1e16f2c2beb1a27363.png', + type: 'switch', + title: '内置图标颜色', + desc: '', + val: 'builtInColor', + }, + ], + }, + { + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/logoColor.png', + type: 'color', + title: 'LOGO图标颜色', + defaultValue: '#F86527', + desc: '', + val: 'logoColor', + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/flowIconColor.png', + type: 'color', + title: '流量图标颜色', + defaultValue: '#1AB6F8', + desc: '', + val: 'flowIconColor', + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/voiceIconColor.png', + type: 'color', + title: '语音图标颜色', + defaultValue: '#30D15B', + desc: '', + val: 'voiceIconColor', + }, + ], + }, + { + title: '重置颜色', + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/clear.png', + title: '重置颜色', + desc: '重置当前颜色配置', + name: 'reset', + val: 'reset', + onClick: () => { + const propertiesToDelete = ['gradient', 'step1', 'step2', 'inner1', 'inner2', 'logoColor', 'flowIconColor', 'voiceIconColor']; + propertiesToDelete.forEach(prop => { + delete this.settings[prop]; + }); + this.saveSettings(); + this.reopenScript(); + }, + }, + ], + }, + ]).catch((e) => { + console.log(e); + }); + }; + + setSizeConfig = async () => { + return this.renderAppView([ + { + title: '尺寸设置', + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/SCALE.png', + type: 'input', + title: '小组件缩放比例', + desc: '', + placeholder : '1', + val: 'SCALE', + }, + ], + }, + { + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/ringStackSize.png', + type: 'input', + title: '圆环大小', + placeholder : '65', + desc: '', + val: 'ringStackSize', + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/ringTextSize.png', + type: 'input', + title: '圆环中心文字大小', + placeholder : '14', + desc: '', + val: 'ringTextSize', + }, + ], + }, + { + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/feeTextSize.png', + type: 'input', + title: '话费文字大小', + placeholder : '21', + desc: '', + val: 'feeTextSize', + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/textSize.png', + type: 'input', + title: '文字模式下文字大小', + placeholder : '13', + desc: '', + val: 'textSize', + }, + ], + }, + { + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/smallPadding.png', + type: 'input', + title: '小尺寸组件边距', + placeholder : '13', + desc: '', + val: 'smallPadding', + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/padding.png', + type: 'input', + title: '中尺寸组件边距', + placeholder : '10', + desc: '', + val: 'padding', + }, + ], + }, + { + title: '重置尺寸', + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/clear.png', + title: '重置尺寸', + desc: '重置当前尺寸配置', + name: 'reset', + val: 'reset', + onClick: () => { + const propertiesToDelete = ['SCALE', 'ringStackSize', 'ringTextSize', 'feeTextSize', 'textSize', 'smallPadding', 'padding', ]; + propertiesToDelete.forEach(prop => { + delete this.settings[prop]; + }); + this.saveSettings(); + this.reopenScript(); + }, + }, + ], + }, + ]).catch((e) => { + console.log(e); + }); + }; + + getAccountMenu(index) { + return [ + { + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/enableName.png', + title: `账户 ${index} Cookie`, + type: 'input', + val: `cookie${index}`, + desc: '手动粘贴 Cookie' + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/widgetStyle.png', + type: 'select', + title: `账户 ${index} 组件样式`, + options: ['1', '2', '3', '4', '5', '6'], + val: `widgetStyle${index}`, + desc: '默认使用样式1' + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/flowIconColor.png', + type: 'input', + title: `账户 ${index} 自定流量`, + desc: 'GB (不填自动计算)', + val: `flow${index}`, + }, + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/voiceIconColor.png', + type: 'input', + title: `账户 ${index} 自定语音`, + desc: '分钟 (不填自动计算)', + val: `voice${index}`, + } + ] + } + ]; + } + + async handlerBoxJS() { + try { + const boxjsKey = '@YaYa_10010.cookie'; + const url = `http://boxjs.com/query/data/${boxjsKey}`; + const req = new Request(url); + const userCookie = await req.loadJSON(); + + if (!userCookie || !userCookie.val) { + const alert = new Alert(); + alert.title = "读取失败"; + alert.message = "未在 BoxJS 中找到相关数据,请检查 BoxJS 订阅及 Key 设置。"; + alert.addAction("确定"); + await alert.present(); + return; + } + + const cookieVal = userCookie.val; + + const alert = new Alert(); + alert.title = "BoxJS 读取成功"; + alert.message = "请选择要将 Cookie 存入哪个账户?"; + + for (let i = 1; i <= 5; i++) { + const hasData = this.settings[`cookie${i}`]; + const statusText = hasData ? " (覆盖)" : " (空)"; + alert.addAction(`账户 ${i}${statusText}`); + } + alert.addCancelAction("取消"); + + const index = await alert.presentSheet(); + if (index === -1) return; + + const targetAccount = index + 1; + this.settings[`cookie${targetAccount}`] = cookieVal; + this.saveSettings(); + + const successAlert = new Alert(); + successAlert.title = "保存成功"; + successAlert.message = `BoxJS 数据已存入账户 ${targetAccount},脚本将自动刷新。`; + successAlert.addAction("确定"); + await successAlert.present(); + + this.reopenScript(); + + } catch (e) { + console.log(e); + const alert = new Alert(); + alert.title = "错误"; + alert.message = "连接 BoxJS 失败,请确保 BoxJS 服务正常运行。"; + alert.addAction("确定"); + await alert.present(); + } + } + + Run() { + if (config.runsInApp) { + + let accountMenus = []; + for (let i = 1; i <= 5; i++) { + accountMenus = accountMenus.concat(this.getAccountMenu(i)); + } + + if (accountMenus.length > 0) { + accountMenus[0].title = '账户设置'; + } + + accountMenus.push({ + title: '重置账户', + menu: [{ + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/clear.png', + title: '重置账户', + desc: '清空所有账户设置与缓存', + name: 'reset', + val: 'reset', + onClick: () => { + for (let i = 1; i <= 5; i++) { + delete this.settings[`cookie${i}`]; + delete this.settings[`widgetStyle${i}`]; + delete this.settings[`flow${i}`]; + delete this.settings[`voice${i}`]; + } + this.saveSettings(); + this.reopenScript(); + } + }] + }); + + this.registerAction({ + title: '组件配置', + menu: [ + { + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/update.png', + type: 'input', + title: '脚本更新', + name: 'update', + onClick: async () => { + await this.checkAndUpdateScript(); + }, + }, + { + icon: { name: 'lineweight', color: '#a0d911' }, + type: 'select', + title: '账户预览', + options: ['1', '2', '3', '4', '5'], + val: 'previewAccount', + desc: '仅在 App 内运行脚本时生效' + } + ], + }); + + this.registerAction({ + title: '', + menu: [ + { + name: 'accounts', + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/enableName.png', + title: '账户设置', + type: 'input', + onClick: () => { + return this.renderAppView(accountMenus); + }, + }, + { + icon: { name: 'shippingbox', color: '#f7bb10' }, + title: '代理缓存', + val: 'boxjs', + onClick: async () => { + await this.handlerBoxJS(); + }, + } + ], + }); + + this.registerAction({ + title: '', + menu: [ + { + name: 'color', + url: 'https://pic1.imgdb.cn/item/63315c1e16f2c2beb1a27363.png', + title: '颜色配置', + type: 'input', + onClick: () => { + return this.setColorConfig(); + }, + }, + { + name: 'size', + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/size.png', + title: '尺寸设置', + type: 'input', + onClick: () => { + return this.setSizeConfig(); + }, + }, + ], + }); + + this.registerAction({ + title: '', + menu: [ + { + name: 'basic', + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/basic.png', + title: '基础功能', + type: 'input', + onClick: () => { + return this.setWidgetConfig(); + }, + }, + { + name: 'reload', + url: 'https://raw.githubusercontent.com/anker1209/Scriptable/main/icon/reload.png', + title: '重载组件', + type: 'input', + onClick: () => { + this.reopenScript(); + }, + }, + ], + }); + } + }; + + async render() { + await this.init(); + const widget = new ListWidget(); + await this.getWidgetBackgroundImage(widget); + if (this.widgetFamily === 'medium') { + return await this.renderMedium(widget); + } else if (this.widgetFamily === 'large') { + return await this.renderLarge(widget); + } else { + return await this.renderSmall(widget); + } + }; +} + +await Runing(Widget, args.widgetParameter, false); diff --git a/Surge/Github_Private.sgmodule b/Surge/Github_Private.sgmodule index 55943463..b8ef8eda 100644 --- a/Surge/Github_Private.sgmodule +++ b/Surge/Github_Private.sgmodule @@ -1,12 +1,16 @@ #!name=访问私库 -#!desc=允许用户访问自定义的 GitHub/Gist 私有仓库 +#!desc=允许用户访问 GitHub/Gist 私有仓库及语言请求 #!category=Third Party Module #!arguments=USERNAME:GITHUB_USERNAME,TOKEN:GITHUB_TOKEN #!arguments-desc=[参数设置]\nUSERNAME: 填入GitHub的用户名\nTOKEN: 填入GitHub生成的Token\n\n[Token获取方式]\n头像菜单 -> Settings -> Developer settings -> Personal access tokens -> Generate new token -> 在权限列表中找到并勾选 gist 然后提交生成 [Header Rewrite] +# 私库认证 http-request ^https?:\/\/(raw|gist)\.githubusercontent\.com\/{{{USERNAME}}} header-del Authorization http-request ^https?:\/\/(raw|gist)\.githubusercontent\.com\/{{{USERNAME}}} header-add Authorization "token {{{TOKEN}}}" +# 全局防 429 语言设置 +http-request (raw|gist).githubusercontent.com header-replace Accept-Language en-us + [MITM] hostname = %APPEND% raw.githubusercontent.com, gist.githubusercontent.com \ No newline at end of file diff --git a/Surge/Hub.sgmodule b/Surge/Hub.sgmodule index 8a1546bd..ec8331a4 100644 --- a/Surge/Hub.sgmodule +++ b/Surge/Hub.sgmodule @@ -1,16 +1,18 @@ #!name=解析转换 -#!desc=快速添加通用链接为Surge模块,依赖Script.hub +#!desc=快速添加通用链接为Surge格式,依赖Script.hub #!category=Third Party Module -#!arguments=SURGE_SUFFIX:Surge,STASH_SUFFIX:Stash,LOON_SUFFIX:Loon -#!arguments-desc=[参数设置]\n▪︎ SURGE_SUFFIX: 自定义为Surge转换链接的后戳参数;\n▪︎ STASH_SUFFIX: 自定义为Stash转换链接的后戳参数;\n▪︎ LOON_SUFFIX: 自定义为Loon转换链接的后戳参数;\n\n[模块说明]\n⓵ 模块依赖于https://script.hub环境运行,需提前配置;\n⓶ 模块限Surge使用,Loon或Stash需自行修改格式使用;\n⓷ 只需修改"SURGE_SUFFIX"参数即可,其他参数仅供预览; +#!arguments=SURGE_SUFFIX:Surge,STASH_SUFFIX:Stash,LOON_SUFFIX:Loon,DOMAIN_HOST:kelee.one,DOMAIN_REGEX:kelee\.one,JQ_ENABLED:true,FILE_SUFFIX:js|conf|snippet|plugin|list|yaml|lpx|lsr|ltx|lcf|txt|json +#!arguments-desc=[参数设置]\n▪︎ SURGE_SUFFIX: 自定义为Surge转换链接的后戳参数\n▪︎ STASH_SUFFIX: 自定义为Stash转换链接的后戳参数\n▪︎ LOON_SUFFIX: 自定义为Loon转换链接的后戳参数\n▪︎ DOMAIN_HOST: MITM的私有域名(多个用","分隔)\n▪︎ DOMAIN_REGEX: 正则匹配的私有域名(多个用"|"分隔)\n▪︎ JQ_ENABLED: 是否支持jq语法,(true启用/false关闭)\n▪︎ FILE_SUFFIX: 自定义匹配的文件后缀(多个用"|"分隔)\n\n[模块说明]\n⓵ 模块依赖于https://script.hub环境运行,需提前配置\n⓶ 模块限Surge使用,Loon或Stash需自行修改格式使用\n⓷ "SUFFIX"参数按 UA填写,"DOMAIN"参数按域名填写 [URL Rewrite] -# 转换格式为:Surge -^https:\/\/([a-zA-Z0-9.-]*(git|github|gitlab|gitee|gitcode|kelee)[a-zA-Z0-9.-]*)\/(.*)\/(.*\.(js|conf|snippet|plugin|list|yaml)).*({{{SURGE_SUFFIX}}})$ http://script.hub/file/_start_/https://$1/$3/$4/_end_/$4.sgmodule?type=surge-module&target=surge-module&del=true -# 转换格式为:Stash -^https:\/\/([a-zA-Z0-9.-]*(git|github|gitlab|gitee|gitcode|kelee)[a-zA-Z0-9.-]*)\/(.*)\/(.*\.(js|conf|snippet|plugin|list|yaml)).*({{{STASH_SUFFIX}}})$ http://script.hub/file/_start_/https://$1/$3/$4/_end_/$4.sgmodule?type=qx-rewrite&target=stash-stoverride&del=true -# 转换格式为:Loon -^https:\/\/([a-zA-Z0-9.-]*(git|github|gitlab|gitee|gitcode|kelee)[a-zA-Z0-9.-]*)\/(.*)\/(.*\.(js|conf|snippet|plugin|list|yaml)).*({{{LOON_SUFFIX}}})$ http://script.hub/file/_start_/https://$1/$3/$4/_end_/$4.sgmodule?type=loon-plugin&target=loon-plugin&del=true +# Surge 转换 +^https:\/\/((?:[a-zA-Z0-9.-]*(?:git|github|gitlab|gitee|gitcode)[a-zA-Z0-9.-]*|{{{DOMAIN_REGEX}}}))\/(.*)\/(.*\.({{{FILE_SUFFIX}}})).*({{{SURGE_SUFFIX}}})$ http://script.hub/file/_start_/https://$1/$2/$3/_end_/$3.sgmodule?type=surge-module&target=surge-module&del=true&jqEnabled={{{JQ_ENABLED}}} + +# Stash 转换 +^https:\/\/((?:[a-zA-Z0-9.-]*(?:git|github|gitlab|gitee|gitcode)[a-zA-Z0-9.-]*|{{{DOMAIN_REGEX}}}))\/(.*)\/(.*\.({{{FILE_SUFFIX}}})).*({{{STASH_SUFFIX}}})$ http://script.hub/file/_start_/https://$1/$2/$3/_end_/$3.sgmodule?type=qx-rewrite&target=stash-stoverride&del=true&jqEnabled={{{JQ_ENABLED}}} + +# Loon 转换 +^https:\/\/((?:[a-zA-Z0-9.-]*(?:git|github|gitlab|gitee|gitcode)[a-zA-Z0-9.-]*|{{{DOMAIN_REGEX}}}))\/(.*)\/(.*\.({{{FILE_SUFFIX}}})).*({{{LOON_SUFFIX}}})$ http://script.hub/file/_start_/https://$1/$2/$3/_end_/$3.sgmodule?type=loon-plugin&target=loon-plugin&del=true&jqEnabled={{{JQ_ENABLED}}} [MITM] -hostname = %APPEND% raw.githubusercontent.com, gist.github.com, gist.githubusercontent.com, gitlab.com, gitee.com, gitcode.net, github.*, kelee.one +hostname = %APPEND% raw.githubusercontent.com, gist.github.com, gist.githubusercontent.com, gitlab.com, gitee.com, gitcode.net, github.*, {{{DOMAIN_HOST}}} \ No newline at end of file diff --git a/Surge/JD_Helper.sgmodule b/Surge/JD_Helper.sgmodule new file mode 100644 index 00000000..6d218a82 --- /dev/null +++ b/Surge/JD_Helper.sgmodule @@ -0,0 +1,12 @@ +#!name=京东助手 +#!desc=京东App点击商品详情触发佣金返利和历史价格 +#!arguments=JD_UNION_ID:null,JD_POSITION_ID:null,JTT_APPID:null,JTT_APPKEY:null,ENGINE:auto,TIMEOUT:120,MMMCK_SCRIPT:慢慢买CK,DISABLE_NOTICE:true,THEME_TIME:7-19 +#!arguments-desc=[参数设置]\nJD_UNION_ID: 填写京东联盟ID\n ├ 获取方式:登录京东联盟官网https://union.jd.com/index\n └ 参考格式:12345678\nJD_POSITION_ID: 填写推广位ID\n ├ 获取方式:在京东联盟后台创建推广位\n └ 参考格式:1234567890\nJTT_APPID: 填写京推推AppID\n ├ 获取方式:登录京推推官网https://www.jingtuitui.com/user/login\n └ 参考格式:1234567890123456\nJTT_APPKEY: 填写京推推AppKey\n ├ 获取方式:登录京推推官网\n └ 参考格式:b123456ce90123456lk890126789012\nENGINE: 脚本执行引擎\n ├ auto:自动选择(默认值)\n ├ jsc:JavaScriptCore 引擎\n └ webview:WebView 引擎\nTIMEOUT: 脚本超时(单位:秒)\n ├ 120:脚本超时(默认值)\n └ 可自定义\nMMMCK_SCRIPT: 慢慢买 CK 脚本启停\n ├ 慢慢买CK:启用(默认值)\n └ 填入"#":获取后注释停用\nDISABLE_NOTICE: 是否启用转链及通知功能\n ├ true:启用转链与通知功能(默认)\n └ false:禁用转链与通知,仅显示比价图表\nTHEME_TIME: 自定义暗黑模式时间范围\n ├ 格式:起始小时-结束小时(默认 7~19为明亮模式)\n └ 示例:6-22 表示每天 22:00~6:00 为暗黑模式时间段 + +[Script] +京东助手 = type=http-response,pattern=^https:\/\/in\.m\.jd\.com\/product\/.+?\.html,requires-body=1,max-size=-1,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/jd_price1.js,argument=jd_union_id={{{JD_UNION_ID}}}&jd_position_id={{{JD_POSITION_ID}}}&jtt_appid={{{JTT_APPID}}}&jtt_appkey={{{JTT_APPKEY}}}&engine={{{ENGINE}}}&timeout={{{TIMEOUT}}}&disable_notice={{{DISABLE_NOTICE}}}&theme_time={{{THEME_TIME}}} + +{{{MMMCK_SCRIPT}}} = type=http-request,pattern=^https?:\/\/apapia-sqk-weblogic\.manmanbuy\.com/baoliao\/center\/menu,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/jd_price1.js,timeout=30 + +[MITM] +hostname = %APPEND% in.m.jd.com, lite-in.m.jd.com, apapia-sqk-weblogic.manmanbuy.com \ No newline at end of file diff --git a/Surge/ipaTool.sgmodule b/Surge/ipaTool.sgmodule new file mode 100644 index 00000000..d809a3b2 --- /dev/null +++ b/Surge/ipaTool.sgmodule @@ -0,0 +1,10 @@ +#!name=应用安装 +#!desc=App Store 低版本应用安装、已购下架应用安装 + +[Script] +应用接口 = type=http-request,pattern=https://apple-api.com,requires-body=1,max-size=-1,binary-body-mode=0,timeout=30,debug=1,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/AppleStoreAPI.js +应用安装 = type=http-request,pattern=https:\/\/xiaobai\.app,requires-body=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/installapp.js + +[MITM] +hostname = %APPEND% apple-api.com, xiaobai.app + diff --git a/Surge/jd_buy_helper.sgmodule b/Surge/jd_buy_helper.sgmodule index dda5ce64..462985c0 100644 --- a/Surge/jd_buy_helper.sgmodule +++ b/Surge/jd_buy_helper.sgmodule @@ -1,11 +1,13 @@ #!name=购物助手 #!desc=京东购物助手,注入跳转、佣金返利、历史价格 #!category=Third Party Module -#!arguments=JD_UNION_ID:京东联盟ID,JD_POSITION_ID:推广位ID,JTT_APPID:京推推AppID,JTT_APPKEY:京推推AppKey,BUY_HELPER_ZDM:true,BUY_HELPER_MMM:true,BUY_HELPER_GWD:true,BUY_HELPER_COPY:true,BUY_HELPER_LR:left -#!arguments-desc=[参数设置]\nJD_UNION_ID: 填写京东联盟ID\n ├ 获取方式:登录京东联盟官网https://union.jd.com/index\n └ 参考格式:12345678\nJD_POSITION_ID: 填写推广位ID\n ├ 获取方式:在京东联盟后台创建推广位\n └ 参考格式:1234567890\nJTT_APPID: 填写京推推AppID\n ├ 获取方式:登录京推推官网https://www.jingtuitui.com/user/login\n └ 参考格式:1234567890123456\nJTT_APPKEY: 填写京推推AppKey\n ├ 获取方式:登录京推推官网\n └ 参考格式:b123456ce90123456lk890126789012\nBUY_HELPER_ZDM: 值得买按钮\n ├ true:启用(默认值)\n └ false:关闭\nBUY_HELPER_MMM: 慢慢买按钮\n ├ true:启用(默认值)\n └ false:关闭\nBUY_HELPER_GWD: 购物党按钮\n ├ true:启用(默认值)\n └ false:关闭\nBUY_HELPER_COPY: 复制短链按钮\n ├ true:启用(默认值)\n └ false:关闭\nBUY_HELPER_LR: 按钮位置\n ├ left:左边(默认值)\n └ right:右边 +#!arguments=JD_UNION_ID:null,JD_POSITION_ID:null,JTT_APPID:null,JTT_APPKEY:null,BUY_HELPER_ZDM:true,BUY_HELPER_MMM:true,BUY_HELPER_GWD:true,BUY_HELPER_COPY:true,BUY_HELPER_LR:left,ENGINE:auto,TIMEOUT:120,MMMCK_SCRIPT:慢慢买CK +#!arguments-desc=[参数设置]\nJD_UNION_ID: 填写京东联盟ID\n ├ 获取方式:登录京东联盟官网https://union.jd.com/index\n └ 参考格式:12345678\nJD_POSITION_ID: 填写推广位ID\n ├ 获取方式:在京东联盟后台创建推广位\n └ 参考格式:1234567890\nJTT_APPID: 填写京推推AppID\n ├ 获取方式:登录京推推官网https://www.jingtuitui.com/user/login\n └ 参考格式:1234567890123456\nJTT_APPKEY: 填写京推推AppKey\n ├ 获取方式:登录京推推官网\n └ 参考格式:b123456ce90123456lk890126789012\nBUY_HELPER_ZDM: 值得买按钮\n ├ true:启用(默认值)\n └ false:关闭\nBUY_HELPER_MMM: 慢慢买按钮\n ├ true:启用(默认值)\n └ false:关闭\nBUY_HELPER_GWD: 购物党按钮\n ├ true:启用(默认值)\n └ false:关闭\nBUY_HELPER_COPY: 复制短链按钮\n ├ true:启用(默认值)\n └ false:关闭\nBUY_HELPER_LR: 按钮位置\n ├ left:左边(默认值)\n └ right:右边\nENGINE: 脚本执行引擎\n ├ auto:自动选择(默认值)\n ├ jsc:JavaScriptCore 引擎\n └ webview:WebView 引擎\nTIMEOUT: 脚本超时(单位:秒)\n ├ 120:默认超时(默认值)\n └ 可自定义\nMMMCK_SCRIPT: 慢慢买 CK 脚本启停\n ├ 慢慢买CK:启用(默认值)\n └ 填入"#":注释停用 [Script] -购物助手 = type=http-response,pattern=^https:\/\/in\.m\.jd\.com\/product\/.+?\.html,requires-body=1,max-size=-1,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/jd_buy_helper.js,argument=jd_union_id={{{JD_UNION_ID}}}&jd_position_id={{{JD_POSITION_ID}}}&jtt_appid={{{JTT_APPID}}}&jtt_appkey={{{JTT_APPKEY}}}&buy_helper_zdm={{{BUY_HELPER_ZDM}}}&buy_helper_mmm={{{BUY_HELPER_MMM}}}&buy_helper_gwd={{{BUY_HELPER_GWD}}}&buy_helper_copy={{{BUY_HELPER_COPY}}}&buy_helper_LR={{{BUY_HELPER_LR}}} +购物助手 = type=http-response,pattern=^https:\/\/in\.m\.jd\.com\/product\/.+?\.html,requires-body=1,max-size=-1,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/jd_buy_helper.js,argument=jd_union_id={{{JD_UNION_ID}}}&jd_position_id={{{JD_POSITION_ID}}}&jtt_appid={{{JTT_APPID}}}&jtt_appkey={{{JTT_APPKEY}}}&buy_helper_zdm={{{BUY_HELPER_ZDM}}}&buy_helper_mmm={{{BUY_HELPER_MMM}}}&buy_helper_gwd={{{BUY_HELPER_GWD}}}&buy_helper_copy={{{BUY_HELPER_COPY}}}&buy_helper_LR={{{BUY_HELPER_LR}}}&engine={{{ENGINE}}}&timeout={{{TIMEOUT}}} + +{{{MMMCK_SCRIPT}}} = type=http-request,pattern=^https?:\/\/apapia-sqk-weblogic\.manmanbuy\.com/baoliao\/center\/menu,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/MmmCK.js [MITM] -hostname = %APPEND% in.m.jd.com, lite-in.m.jd.com +hostname = %APPEND% in.m.jd.com, lite-in.m.jd.com, apapia-sqk-weblogic.manmanbuy.com \ No newline at end of file diff --git a/Surge/jd_price.sgmodule b/Surge/jd_price.sgmodule new file mode 100644 index 00000000..7f62daf0 --- /dev/null +++ b/Surge/jd_price.sgmodule @@ -0,0 +1,12 @@ +#!name=京东比价 +#!desc=京东App历史价格(商品详情页面触发)折线图 +#!arguments=MMMCK_SCRIPT:慢慢买CK +#!arguments-desc=[参数设置]\n1. MMMCK_SCRIPT:慢慢买CK 脚本前往慢慢买App获取Cookie\n2.避免不必要的重写,禁用请设置为"#" + +[Script] +{{{MMMCK_SCRIPT}}} = type=http-request,pattern=^https?:\/\/apapia-sqk-weblogic\.manmanbuy\.com/baoliao\/center\/menu,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/MmmCK.js + +京东比价 = type=http-response,pattern=^https:\/\/in\.m\.jd\.com\/product\/graphext\/\d+\.html,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/jd_price.js,timeout=30 + +[MITM] +hostname = %APPEND% in.m.jd.com, apapia-sqk-weblogic.manmanbuy.com \ No newline at end of file diff --git a/Surge/jd_price2.sgmodule b/Surge/jd_price2.sgmodule new file mode 100644 index 00000000..30eece2c --- /dev/null +++ b/Surge/jd_price2.sgmodule @@ -0,0 +1,13 @@ +#!name=京东比价 +#!desc=京东App历史价格(商品详情页面触发)表格 +#!arguments=MMMCK_SCRIPT:慢慢买CK +#!arguments-desc=[参数设置]\n1. MMMCK_SCRIPT:慢慢买CK 脚本前往慢慢买App获取Cookie\n2.避免不必要的重写,禁用请设置为"#" + +[Script] +{{{MMMCK_SCRIPT}}} = type=http-request, pattern=^https?:\/\/apapia-sqk-weblogic\.manmanbuy\.com\/baoliao\/center\/menu$, script-path=https://raw.githubusercontent.com/wf021325/qx/master/js/jd_price.js, requires-body=true, max-size=-1, timeout=60 + +京东比价 = type=http-response, pattern=^https?:\/\/in\.m\.jd\.com\/product\/graphext\/\d+\.html, script-path=https://raw.githubusercontent.com/wf021325/qx/master/js/jd_price.js, requires-body=true, max-size=-1, timeout=60 + + +[MITM] +hostname = %APPEND% in.m.jd.com, apapia-sqk-weblogic.manmanbuy.com \ No newline at end of file diff --git a/WPS_checkin.js b/WPS_checkin 2.js similarity index 100% rename from WPS_checkin.js rename to WPS_checkin 2.js diff --git a/forward.js b/forward.js new file mode 100644 index 00000000..09ea19f6 --- /dev/null +++ b/forward.js @@ -0,0 +1,10 @@ +/* + +嘘,憋问 + +hostname = %APPEND% fluxapi.vvebo.vip + +forward = type=http-request, pattern=^https?:\/\/fluxapi\.vvebo\.vip\/v1\/purchase\/iap\/subscription$, script-path=forward.js, requires-body=true, max-size=-1, timeout=60 + +*/ +(()=>{var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};var e={exports:{}},s=function(t){if(t.__esModule)return t;var e=t.default;if("function"==typeof e){var s=function t(){return this instanceof t?Reflect.construct(e,arguments,this.constructor):e.apply(this,arguments)};s.prototype=e.prototype}else s={};return Object.defineProperty(s,"__esModule",{value:!0}),Object.keys(t).forEach((function(e){var r=Object.getOwnPropertyDescriptor(t,e);Object.defineProperty(s,e,r.get?r:{enumerable:!0,get:function(){return t[e]}})})),s}(Object.freeze({__proto__:null,default:{}}));e.exports=function(){var e=e||function(e,r){var i;if("undefined"!=typeof window&&window.crypto&&(i=window.crypto),"undefined"!=typeof self&&self.crypto&&(i=self.crypto),"undefined"!=typeof globalThis&&globalThis.crypto&&(i=globalThis.crypto),!i&&"undefined"!=typeof window&&window.msCrypto&&(i=window.msCrypto),!i&&void 0!==t&&t.crypto&&(i=t.crypto),!i)try{i=s}catch(t){}var n=function(){if(i){if("function"==typeof i.getRandomValues)try{return i.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof i.randomBytes)try{return i.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")},o=Object.create||function(){function t(){}return function(e){var s;return t.prototype=e,s=new t,t.prototype=null,s}}(),a={},c=a.lib={},h=c.Base={extend:function(t){var e=o(this);return t&&e.mixIn(t),e.hasOwnProperty("init")&&this.init!==e.init||(e.init=function(){e.$super.init.apply(this,arguments)}),e.init.prototype=e,e.$super=this,e},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var e in t)t.hasOwnProperty(e)&&(this[e]=t[e]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},u=c.WordArray=h.extend({init:function(t,e){t=this.words=t||[],this.sigBytes=null!=e?e:4*t.length},toString:function(t){return(t||l).stringify(this)},concat:function(t){var e=this.words,s=t.words,r=this.sigBytes,i=t.sigBytes;if(this.clamp(),r%4)for(var n=0;n>>2]>>>24-n%4*8&255;e[r+n>>>2]|=o<<24-(r+n)%4*8}else for(var a=0;a>>2]=s[a>>>2];return this.sigBytes+=i,this},clamp:function(){var t=this.words,s=this.sigBytes;t[s>>>2]&=4294967295<<32-s%4*8,t.length=e.ceil(s/4)},clone:function(){var t=h.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var e=[],s=0;s>>2]>>>24-i%4*8&255;r.push((n>>>4).toString(16)),r.push((15&n).toString(16))}return r.join("")},parse:function(t){for(var e=t.length,s=[],r=0;r>>3]|=parseInt(t.substr(r,2),16)<<24-r%8*4;return new u.init(s,e/2)}},f=d.Latin1={stringify:function(t){for(var e=t.words,s=t.sigBytes,r=[],i=0;i>>2]>>>24-i%4*8&255;r.push(String.fromCharCode(n))}return r.join("")},parse:function(t){for(var e=t.length,s=[],r=0;r>>2]|=(255&t.charCodeAt(r))<<24-r%4*8;return new u.init(s,e)}},p=d.Utf8={stringify:function(t){try{return decodeURIComponent(escape(f.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return f.parse(unescape(encodeURIComponent(t)))}},g=c.BufferedBlockAlgorithm=h.extend({reset:function(){this._data=new u.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=p.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(t){var s,r=this._data,i=r.words,n=r.sigBytes,o=this.blockSize,a=n/(4*o),c=(a=t?e.ceil(a):e.max((0|a)-this._minBufferSize,0))*o,h=e.min(4*c,n);if(c){for(var d=0;d>>31}var d=(r<<5|r>>>27)+c+o[h];d+=h<20?1518500249+(i&n|~i&a):h<40?1859775393+(i^n^a):h<60?(i&n|i&a|n&a)-1894007588:(i^n^a)-899497514,c=a,a=n,n=i<<30|i>>>2,i=r,r=d}s[0]=s[0]+r|0,s[1]=s[1]+i|0,s[2]=s[2]+n|0,s[3]=s[3]+a|0,s[4]=s[4]+c|0},_doFinalize:function(){var t=this._data,e=t.words,s=8*this._nDataBytes,r=8*t.sigBytes;return e[r>>>5]|=128<<24-r%32,e[14+(r+64>>>9<<4)]=Math.floor(s/4294967296),e[15+(r+64>>>9<<4)]=s,t.sigBytes=4*e.length,this._process(),this._hash},clone:function(){var t=i.clone.call(this);return t._hash=this._hash.clone(),t}}),e.SHA1=i._createHelper(a),e.HmacSHA1=i._createHmacHelper(a),t.SHA1;var e,s,r,i,n,o,a}(i)),d.exports),(l||(l=1,p.exports=(e=(t=i).lib.Base,s=t.enc.Utf8,void(t.algo.HMAC=e.extend({init:function(t,e){t=this._hasher=new t.init,"string"==typeof e&&(e=s.parse(e));var r=t.blockSize,i=4*r;e.sigBytes>i&&(e=t.finalize(e)),e.clamp();for(var n=this._oKey=e.clone(),o=this._iKey=e.clone(),a=n.words,c=o.words,h=0;h>>8^255&p^99,i[s]=p,n[p]=s;var g=t[s],y=t[g],v=t[y],m=257*t[p]^16843008*p;o[s]=m<<24|m>>>8,a[s]=m<<16|m>>>16,c[s]=m<<8|m>>>24,h[s]=m,m=16843009*v^65537*y^257*g^16843008*s,u[p]=m<<24|m>>>8,d[p]=m<<16|m>>>16,l[p]=m<<8|m>>>24,f[p]=m,s?(s=g^t[t[t[v^g]]],r^=t[t[r]]):s=r=1}}();var p=[0,1,2,4,8,16,32,64,128,27,54],g=r.AES=s.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var t=this._keyPriorReset=this._key,e=t.words,s=t.sigBytes/4,r=4*((this._nRounds=s+6)+1),n=this._keySchedule=[],o=0;o6&&o%s==4&&(h=i[h>>>24]<<24|i[h>>>16&255]<<16|i[h>>>8&255]<<8|i[255&h]):(h=i[(h=h<<8|h>>>24)>>>24]<<24|i[h>>>16&255]<<16|i[h>>>8&255]<<8|i[255&h],h^=p[o/s|0]<<24),n[o]=n[o-s]^h);for(var a=this._invKeySchedule=[],c=0;c>>24]]^d[i[h>>>16&255]]^l[i[h>>>8&255]]^f[i[255&h]]}}},encryptBlock:function(t,e){this._doCryptBlock(t,e,this._keySchedule,o,a,c,h,i)},decryptBlock:function(t,e){var s=t[e+1];t[e+1]=t[e+3],t[e+3]=s,this._doCryptBlock(t,e,this._invKeySchedule,u,d,l,f,n),s=t[e+1],t[e+1]=t[e+3],t[e+3]=s},_doCryptBlock:function(t,e,s,r,i,n,o,a){for(var c=this._nRounds,h=t[e]^s[0],u=t[e+1]^s[1],d=t[e+2]^s[2],l=t[e+3]^s[3],f=4,p=1;p>>24]^i[u>>>16&255]^n[d>>>8&255]^o[255&l]^s[f++],y=r[u>>>24]^i[d>>>16&255]^n[l>>>8&255]^o[255&h]^s[f++],v=r[d>>>24]^i[l>>>16&255]^n[h>>>8&255]^o[255&u]^s[f++],m=r[l>>>24]^i[h>>>16&255]^n[u>>>8&255]^o[255&d]^s[f++];h=g,u=y,d=v,l=m}g=(a[h>>>24]<<24|a[u>>>16&255]<<16|a[d>>>8&255]<<8|a[255&l])^s[f++],y=(a[u>>>24]<<24|a[d>>>16&255]<<16|a[l>>>8&255]<<8|a[255&h])^s[f++],v=(a[d>>>24]<<24|a[l>>>16&255]<<16|a[h>>>8&255]<<8|a[255&u])^s[f++],m=(a[l>>>24]<<24|a[h>>>16&255]<<16|a[u>>>8&255]<<8|a[255&d])^s[f++],t[e]=g,t[e+1]=y,t[e+2]=v,t[e+3]=m},keySize:8});e.AES=s._createHelper(g)})(),t.AES}(i,(r||(r=1,o.exports=function(t){return function(){var e=t,s=e.lib.WordArray;function r(t,e,r){for(var i=[],n=0,o=0;o>>6-o%4*2;i[n>>>2]|=a<<24-n%4*8,n++}return s.create(i,n)}e.enc.Base64={stringify:function(t){var e=t.words,s=t.sigBytes,r=this._map;t.clamp();for(var i=[],n=0;n>>2]>>>24-n%4*8&255)<<16|(e[n+1>>>2]>>>24-(n+1)%4*8&255)<<8|e[n+2>>>2]>>>24-(n+2)%4*8&255,a=0;a<4&&n+.75*a>>6*(3-a)&63));var c=r.charAt(64);if(c)for(;i.length%4;)i.push(c);return i.join("")},parse:function(t){var e=t.length,s=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var n=0;n>>24)|4278255360&(i<<24|i>>>8)}var n=this._hash.words,o=t[e+0],c=t[e+1],f=t[e+2],p=t[e+3],g=t[e+4],y=t[e+5],v=t[e+6],m=t[e+7],_=t[e+8],S=t[e+9],w=t[e+10],b=t[e+11],k=t[e+12],x=t[e+13],B=t[e+14],E=t[e+15],C=n[0],O=n[1],j=n[2],R=n[3];C=h(C,O,j,R,o,7,a[0]),R=h(R,C,O,j,c,12,a[1]),j=h(j,R,C,O,f,17,a[2]),O=h(O,j,R,C,p,22,a[3]),C=h(C,O,j,R,g,7,a[4]),R=h(R,C,O,j,y,12,a[5]),j=h(j,R,C,O,v,17,a[6]),O=h(O,j,R,C,m,22,a[7]),C=h(C,O,j,R,_,7,a[8]),R=h(R,C,O,j,S,12,a[9]),j=h(j,R,C,O,w,17,a[10]),O=h(O,j,R,C,b,22,a[11]),C=h(C,O,j,R,k,7,a[12]),R=h(R,C,O,j,x,12,a[13]),j=h(j,R,C,O,B,17,a[14]),C=u(C,O=h(O,j,R,C,E,22,a[15]),j,R,c,5,a[16]),R=u(R,C,O,j,v,9,a[17]),j=u(j,R,C,O,b,14,a[18]),O=u(O,j,R,C,o,20,a[19]),C=u(C,O,j,R,y,5,a[20]),R=u(R,C,O,j,w,9,a[21]),j=u(j,R,C,O,E,14,a[22]),O=u(O,j,R,C,g,20,a[23]),C=u(C,O,j,R,S,5,a[24]),R=u(R,C,O,j,B,9,a[25]),j=u(j,R,C,O,p,14,a[26]),O=u(O,j,R,C,_,20,a[27]),C=u(C,O,j,R,x,5,a[28]),R=u(R,C,O,j,f,9,a[29]),j=u(j,R,C,O,m,14,a[30]),C=d(C,O=u(O,j,R,C,k,20,a[31]),j,R,y,4,a[32]),R=d(R,C,O,j,_,11,a[33]),j=d(j,R,C,O,b,16,a[34]),O=d(O,j,R,C,B,23,a[35]),C=d(C,O,j,R,c,4,a[36]),R=d(R,C,O,j,g,11,a[37]),j=d(j,R,C,O,m,16,a[38]),O=d(O,j,R,C,w,23,a[39]),C=d(C,O,j,R,x,4,a[40]),R=d(R,C,O,j,o,11,a[41]),j=d(j,R,C,O,p,16,a[42]),O=d(O,j,R,C,v,23,a[43]),C=d(C,O,j,R,S,4,a[44]),R=d(R,C,O,j,k,11,a[45]),j=d(j,R,C,O,E,16,a[46]),C=l(C,O=d(O,j,R,C,f,23,a[47]),j,R,o,6,a[48]),R=l(R,C,O,j,m,10,a[49]),j=l(j,R,C,O,B,15,a[50]),O=l(O,j,R,C,y,21,a[51]),C=l(C,O,j,R,k,6,a[52]),R=l(R,C,O,j,p,10,a[53]),j=l(j,R,C,O,w,15,a[54]),O=l(O,j,R,C,c,21,a[55]),C=l(C,O,j,R,_,6,a[56]),R=l(R,C,O,j,E,10,a[57]),j=l(j,R,C,O,v,15,a[58]),O=l(O,j,R,C,x,21,a[59]),C=l(C,O,j,R,g,6,a[60]),R=l(R,C,O,j,b,10,a[61]),j=l(j,R,C,O,f,15,a[62]),O=l(O,j,R,C,S,21,a[63]),n[0]=n[0]+C|0,n[1]=n[1]+O|0,n[2]=n[2]+j|0,n[3]=n[3]+R|0},_doFinalize:function(){var t=this._data,s=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;s[i>>>5]|=128<<24-i%32;var n=e.floor(r/4294967296),o=r;s[15+(i+64>>>9<<4)]=16711935&(n<<8|n>>>24)|4278255360&(n<<24|n>>>8),s[14+(i+64>>>9<<4)]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),t.sigBytes=4*(s.length+1),this._process();for(var a=this._hash,c=a.words,h=0;h<4;h++){var u=c[h];c[h]=16711935&(u<<8|u>>>24)|4278255360&(u<<24|u>>>8)}return a},clone:function(){var t=n.clone.call(this);return t._hash=this._hash.clone(),t}});function h(t,e,s,r,i,n,o){var a=t+(e&s|~e&r)+i+o;return(a<>>32-n)+e}function u(t,e,s,r,i,n,o){var a=t+(e&r|s&~r)+i+o;return(a<>>32-n)+e}function d(t,e,s,r,i,n,o){var a=t+(e^s^r)+i+o;return(a<>>32-n)+e}function l(t,e,s,r,i,n,o){var a=t+(s^(e|~r))+i+o;return(a<>>32-n)+e}s.MD5=n._createHelper(c),s.HmacMD5=n._createHmacHelper(c)}(Math),t.MD5}(i)),c.exports),g(),y||(y=1,function(t){t.lib.Cipher||function(e){var s=t,r=s.lib,i=r.Base,n=r.WordArray,o=r.BufferedBlockAlgorithm,a=s.enc;a.Utf8;var c=a.Base64,h=s.algo.EvpKDF,u=r.Cipher=o.extend({cfg:i.extend(),createEncryptor:function(t,e){return this.create(this._ENC_XFORM_MODE,t,e)},createDecryptor:function(t,e){return this.create(this._DEC_XFORM_MODE,t,e)},init:function(t,e,s){this.cfg=this.cfg.extend(s),this._xformMode=t,this._key=e,this.reset()},reset:function(){o.reset.call(this),this._doReset()},process:function(t){return this._append(t),this._process()},finalize:function(t){return t&&this._append(t),this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(){function t(t){return"string"==typeof t?_:v}return function(e){return{encrypt:function(s,r,i){return t(r).encrypt(e,s,r,i)},decrypt:function(s,r,i){return t(r).decrypt(e,s,r,i)}}}}()});r.StreamCipher=u.extend({_doFinalize:function(){return this._process(!0)},blockSize:1});var d=s.mode={},l=r.BlockCipherMode=i.extend({createEncryptor:function(t,e){return this.Encryptor.create(t,e)},createDecryptor:function(t,e){return this.Decryptor.create(t,e)},init:function(t,e){this._cipher=t,this._iv=e}}),f=d.CBC=function(){var t=l.extend();function e(t,e,s){var r,i=this._iv;i?(r=i,this._iv=undefined):r=this._prevBlock;for(var n=0;n>>2];t.sigBytes-=e}};r.BlockCipher=u.extend({cfg:u.cfg.extend({mode:f,padding:p}),reset:function(){var t;u.reset.call(this);var e=this.cfg,s=e.iv,r=e.mode;this._xformMode==this._ENC_XFORM_MODE?t=r.createEncryptor:(t=r.createDecryptor,this._minBufferSize=1),this._mode&&this._mode.__creator==t?this._mode.init(this,s&&s.words):(this._mode=t.call(r,this,s&&s.words),this._mode.__creator=t)},_doProcessBlock:function(t,e){this._mode.processBlock(t,e)},_doFinalize:function(){var t,e=this.cfg.padding;return this._xformMode==this._ENC_XFORM_MODE?(e.pad(this._data,this.blockSize),t=this._process(!0)):(t=this._process(!0),e.unpad(t)),t},blockSize:4});var g=r.CipherParams=i.extend({init:function(t){this.mixIn(t)},toString:function(t){return(t||this.formatter).stringify(this)}}),y=(s.format={}).OpenSSL={stringify:function(t){var e=t.ciphertext,s=t.salt;return(s?n.create([1398893684,1701076831]).concat(s).concat(e):e).toString(c)},parse:function(t){var e,s=c.parse(t),r=s.words;return 1398893684==r[0]&&1701076831==r[1]&&(e=n.create(r.slice(2,4)),r.splice(0,4),s.sigBytes-=16),g.create({ciphertext:s,salt:e})}},v=r.SerializableCipher=i.extend({cfg:i.extend({format:y}),encrypt:function(t,e,s,r){r=this.cfg.extend(r);var i=t.createEncryptor(s,r),n=i.finalize(e),o=i.cfg;return g.create({ciphertext:n,key:s,iv:o.iv,algorithm:t,mode:o.mode,padding:o.padding,blockSize:t.blockSize,formatter:r.format})},decrypt:function(t,e,s,r){return r=this.cfg.extend(r),e=this._parse(e,r.format),t.createDecryptor(s,r).finalize(e.ciphertext)},_parse:function(t,e){return"string"==typeof t?e.parse(t,this):t}}),m=(s.kdf={}).OpenSSL={execute:function(t,e,s,r,i){if(r||(r=n.random(8)),i)o=h.create({keySize:e+s,hasher:i}).compute(t,r);else var o=h.create({keySize:e+s}).compute(t,r);var a=n.create(o.words.slice(e),4*s);return o.sigBytes=4*e,g.create({key:o,iv:a,salt:r})}},_=r.PasswordBasedCipher=v.extend({cfg:v.cfg.extend({kdf:m}),encrypt:function(t,e,s,r){var i=(r=this.cfg.extend(r)).kdf.execute(s,t.keySize,t.ivSize,r.salt,r.hasher);r.iv=i.iv;var n=v.encrypt.call(this,t,e,i.key,r);return n.mixIn(i),n},decrypt:function(t,e,s,r){r=this.cfg.extend(r),e=this._parse(e,r.format);var i=r.kdf.execute(s,t.keySize,t.ivSize,e.salt,r.hasher);return r.iv=i.iv,v.decrypt.call(this,t,e,i.key,r)}})}()}(i,g())));const v="75c4bc828b770497bfe74805724b72f4",m="73cfab358fe3aa0f";t.$decrypt=t=>{const e=n.enc.Utf8.parse(v),s=n.enc.Utf8.parse(m);return n.AES.decrypt(t,e,{iv:s,mode:n.mode.CBC,padding:n.pad.Pkcs7}).toString(n.enc.Utf8)},t.$encrypt=t=>{const e=n.enc.Utf8.parse(v),s=n.enc.Utf8.parse(m);return n.AES.encrypt(t,e,{iv:s,mode:n.mode.CBC,padding:n.pad.Pkcs7}).toString()}})();const $=new Env("ForWard"),Response={status:$.isQuanX()?"HTTP/1.1 200 OK":200,headers:{Server:"openresty",Date:(new Date).toUTCString(),"X-Powered-By":"Express","Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, OPTIONS, PUT, DELETE","Access-Control-Allow-Headers":"Content-Type, Authorization, X-Timestamp, X-Sign, X-Auth","Content-Type":"application/json; charset=utf-8"},body:""};function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;"POST"===e&&(s=this.post);const r=new Promise(((e,r)=>{s.call(this,t,((t,s,i)=>{t?r(t):e(s)}))}));return t.timeout?((t,e=1e3)=>Promise.race([t,new Promise(((t,s)=>{setTimeout((()=>{s(new Error("请求超时"))}),e)}))]))(r,t.timeout):r}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.logLevels={debug:0,info:1,warn:2,error:3},this.logLevelPrefixs={debug:"[DEBUG] ",info:"[INFO] ",warn:"[WARN] ",error:"[ERROR] "},this.logLevel="info",this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`🔔${this.name}, 开始!`)}getEnv(){return"undefined"!=typeof $environment&&$environment["surge-version"]?"Surge":"undefined"!=typeof $environment&&$environment["stash-version"]?"Stash":"undefined"!=typeof module&&module.exports?"Node.js":"undefined"!=typeof $task?"Quantumult X":"undefined"!=typeof $loon?"Loon":"undefined"!=typeof $rocket?"Shadowrocket":void 0}isNode(){return"Node.js"===this.getEnv()}isQuanX(){return"Quantumult X"===this.getEnv()}isSurge(){return"Surge"===this.getEnv()}isLoon(){return"Loon"===this.getEnv()}isShadowrocket(){return"Shadowrocket"===this.getEnv()}isStash(){return"Stash"===this.getEnv()}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null,...s){try{return JSON.stringify(t,...s)}catch{return e}}getjson(t,e){let s=e;if(this.getdata(t))try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise((e=>{this.get({url:t},((t,s,r)=>e(r)))}))}runScript(t,e){return new Promise((s=>{let r=this.getdata("@chavy_boxjs_userCfgs.httpapi");r=r?r.replace(/\n/g,"").trim():r;let i=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");i=i?1*i:20,i=e&&e.timeout?e.timeout:i;const[n,o]=r.split("@"),a={url:`http://${o}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:i},headers:{"X-Key":n,Accept:"*/*"},policy:"DIRECT",timeout:i};this.post(a,((t,e,r)=>s(r)))})).catch((t=>this.logErr(t)))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),r=!s&&this.fs.existsSync(e);if(!s&&!r)return{};{const r=s?t:e;try{return JSON.parse(this.fs.readFileSync(r))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),r=!s&&this.fs.existsSync(e),i=JSON.stringify(this.data);s?this.fs.writeFileSync(t,i):r?this.fs.writeFileSync(e,i):this.fs.writeFileSync(t,i)}}lodash_get(t,e,s){const r=e.replace(/\[(\d+)\]/g,".$1").split(".");let i=t;for(const t of r)if(i=Object(i)[t],void 0===i)return s;return i}lodash_set(t,e,s){return Object(t)!==t||(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce(((t,s,r)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[r+1])>>0==+e[r+1]?[]:{}),t)[e[e.length-1]]=s),t}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,r]=/^@(.*?)\.(.*?)$/.exec(t),i=s?this.getval(s):"";if(i)try{const t=JSON.parse(i);e=t?this.lodash_get(t,r,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,r,i]=/^@(.*?)\.(.*?)$/.exec(e),n=this.getval(r),o=r?"null"===n?null:n||"{}":"{}";try{const e=JSON.parse(o);this.lodash_set(e,i,t),s=this.setval(JSON.stringify(e),r)}catch(e){const n={};this.lodash_set(n,i,t),s=this.setval(JSON.stringify(n),r)}}else s=this.setval(t,e);return s}getval(t){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.read(t);case"Quantumult X":return $prefs.valueForKey(t);case"Node.js":return this.data=this.loaddata(),this.data[t];default:return this.data&&this.data[t]||null}}setval(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.write(t,e);case"Quantumult X":return $prefs.setValueForKey(t,e);case"Node.js":return this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0;default:return this.data&&this.data[e]||null}}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.cookie&&void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar)))}get(t,e=(()=>{})){switch(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"],delete t.headers["content-type"],delete t.headers["content-length"]),t.params&&(t.url+="?"+this.queryStr(t.params)),void 0===t.followRedirect||t.followRedirect||((this.isSurge()||this.isLoon())&&(t["auto-redirect"]=!1),this.isQuanX()&&(t.opts?t.opts.redirection=!1:t.opts={redirection:!1})),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,((t,s,r)=>{!t&&s&&(s.body=r,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,r)}));break;case"Quantumult X":this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then((t=>{const{statusCode:s,statusCode:r,headers:i,body:n,bodyBytes:o}=t;e(null,{status:s,statusCode:r,headers:i,body:n,bodyBytes:o},n,o)}),(t=>e(t&&t.error||"UndefinedError")));break;case"Node.js":let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",((t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}})).then((t=>{const{statusCode:r,statusCode:i,headers:n,rawBody:o}=t,a=s.decode(o,this.encoding);e(null,{status:r,statusCode:i,headers:n,rawBody:o,body:a},a)}),(t=>{const{message:r,response:i}=t;e(r,i,i&&s.decode(i.rawBody,this.encoding))}));break}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";switch(t.body&&t.headers&&!t.headers["Content-Type"]&&!t.headers["content-type"]&&(t.headers["content-type"]="application/x-www-form-urlencoded"),t.headers&&(delete t.headers["Content-Length"],delete t.headers["content-length"]),void 0===t.followRedirect||t.followRedirect||((this.isSurge()||this.isLoon())&&(t["auto-redirect"]=!1),this.isQuanX()&&(t.opts?t.opts.redirection=!1:t.opts={redirection:!1})),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,((t,s,r)=>{!t&&s&&(s.body=r,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,r)}));break;case"Quantumult X":t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then((t=>{const{statusCode:s,statusCode:r,headers:i,body:n,bodyBytes:o}=t;e(null,{status:s,statusCode:r,headers:i,body:n,bodyBytes:o},n,o)}),(t=>e(t&&t.error||"UndefinedError")));break;case"Node.js":let r=require("iconv-lite");this.initGotEnv(t);const{url:i,...n}=t;this.got[s](i,n).then((t=>{const{statusCode:s,statusCode:i,headers:n,rawBody:o}=t,a=r.decode(o,this.encoding);e(null,{status:s,statusCode:i,headers:n,rawBody:o,body:a},a)}),(t=>{const{message:s,response:i}=t;e(s,i,i&&r.decode(i.rawBody,this.encoding))}));break}}time(t,e=null){const s=e?new Date(e):new Date;let r={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in r)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?r[e]:("00"+r[e]).substr((""+r[e]).length)));return t}queryStr(t){let e="";for(const s in t){let r=t[s];null!=r&&""!==r&&("object"==typeof r&&(r=JSON.stringify(r)),e+=`${s}=${r}&`)}return e=e.substring(0,e.length-1),e}msg(e=t,s="",r="",i={}){const n=t=>{const{$open:e,$copy:s,$media:r,$mediaMime:i}=t;switch(typeof t){case void 0:return t;case"string":switch(this.getEnv()){case"Surge":case"Stash":default:return{url:t};case"Loon":case"Shadowrocket":return t;case"Quantumult X":return{"open-url":t};case"Node.js":return}case"object":switch(this.getEnv()){case"Surge":case"Stash":case"Shadowrocket":default:{const n={};let o=t.openUrl||t.url||t["open-url"]||e;o&&Object.assign(n,{action:"open-url",url:o});let a=t["update-pasteboard"]||t.updatePasteboard||s;if(a&&Object.assign(n,{action:"clipboard",text:a}),r){let t,e,s;if(r.startsWith("http"))t=r;else if(r.startsWith("data:")){const[t]=r.split(";"),[,i]=r.split(",");e=i,s=t.replace("data:","")}else e=r,s=(t=>{const e={JVBERi0:"application/pdf",R0lGODdh:"image/gif",R0lGODlh:"image/gif",iVBORw0KGgo:"image/png","/9j/":"image/jpg"};for(var s in e)if(0===t.indexOf(s))return e[s];return null})(r);Object.assign(n,{"media-url":t,"media-base64":e,"media-base64-mime":i??s})}return Object.assign(n,{"auto-dismiss":t["auto-dismiss"],sound:t.sound}),n}case"Loon":{const s={};let i=t.openUrl||t.url||t["open-url"]||e;i&&Object.assign(s,{openUrl:i});let n=t.mediaUrl||t["media-url"];return r?.startsWith("http")&&(n=r),n&&Object.assign(s,{mediaUrl:n}),s}case"Quantumult X":{const i={};let n=t["open-url"]||t.url||t.openUrl||e;n&&Object.assign(i,{"open-url":n});let o=t["media-url"]||t.mediaUrl;r?.startsWith("http")&&(o=r),o&&Object.assign(i,{"media-url":o});let a=t["update-pasteboard"]||t.updatePasteboard||s;return a&&Object.assign(i,{"update-pasteboard":a}),i}case"Node.js":return}default:return}};if(!this.isMute)switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:$notification.post(e,s,r,n(i));break;case"Quantumult X":$notify(e,s,r,n(i));break;case"Node.js":break}if(!this.isMuteLog){let t=["","==============📣系统通知📣=============="];t.push(e),s&&t.push(s),r&&t.push(r),this.logs=this.logs.concat(t)}}debug(...t){this.logLevels[this.logLevel]<=this.logLevels.debug&&t.length>0&&(this.logs=[...this.logs,...t])}info(...t){this.logLevels[this.logLevel]<=this.logLevels.info&&t.length>0&&(this.logs=[...this.logs,...t])}warn(...t){this.logLevels[this.logLevel]<=this.logLevels.warn&&t.length>0&&(this.logs=[...this.logs,...t])}error(...t){this.logLevels[this.logLevel]<=this.logLevels.error&&t.length>0&&(this.logs=[...this.logs,...t])}log(...t){t.length>0&&(this.logs=[...this.logs,...t])}logErr(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:this.log("",`❗️${this.name}, 错误!`,e,t);break;case"Node.js":this.log("",`❗️${this.name}, 错误!`,e,void 0!==t.message?t.message:t,t.stack);break}}wait(t){return new Promise((e=>setTimeout(e,t)))}done(t={}){const e=((new Date).getTime()-this.startTime)/1e3;switch(this.log("",`🔔${this.name}, 结束! 🕛 ${e} 秒`),this.log(),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:$done(t);break;case"Node.js":process.exit(1)}}}(t,e)}(async()=>{const t=Object.fromEntries(Object.entries($request.headers).map((([t,e])=>[t.toLowerCase(),e]))),{"x-auth-key":e,"x-timestamp":s}=t;Response.body=`"${$encrypt(JSON.stringify({status_code:200,success:!0,message:"Success",data:{isOEM:!0,isSubscribed:!0,expiresDate:null,originalTransactionId:"290002232734378",bindingType:"",bindingDatas:[],isEarlyBird:!1},randomKey:e,timestamp:Number(s)}))}"`,$.done($.isQuanX()?Response:{response:Response})})().catch((t=>$.logErr(t))); \ No newline at end of file diff --git a/installapp.js b/installapp.js new file mode 100644 index 00000000..4405af5c --- /dev/null +++ b/installapp.js @@ -0,0 +1,51 @@ +const { name,displayVersion,bundleId,fileName } = Object.fromEntries(new URL(decodeURIComponent(decodeURIComponent($request.url))).searchParams); + +const url = `http://localhost:8000/${fileName}`; + + +const body = ` + + + + items + + + assets + + + kind + software-package + url + ${url} + + + kind + display-image + needs-shine + + url + + + + metadata + + bundle-identifier + ${bundleId} + bundle-version + ${displayVersion} + kind + software + title + ${name} + + + + +` + + +if (this.$task) { + $done({body}) +} else { + $done({response: {body}}) +} diff --git a/jd_buy_helper 2.js b/jd_buy_helper 2.js new file mode 100644 index 00000000..d5173438 --- /dev/null +++ b/jd_buy_helper 2.js @@ -0,0 +1,415 @@ +/** + * 脚本名称:京东购物助手 + * 使用说明:进入APP商品详情页触发,支持京东下单返利和慢慢买、购物党和什么值得买跳转比价领券以及慢慢买接口历史价格。 + +[Script] +# > 京东购物助手 +购物助手 = type=http-response,pattern=^https:\/\/in\.m\.jd\.com\/product\/.+?\.html,requires-body=1,max-size=-1,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/jd_buy_helper.js +慢慢买CK = type=http-request,pattern=^https?:\/\/apapia-sqk-weblogic\.manmanbuy\.com/baoliao\/center\/menu,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/MmmCK.js +[MITM] +hostname = %APPEND% in.m.jd.com, lite-in.m.jd.com, apapia-sqk-weblogic.manmanbuy.com + + * 添加脚本后依赖BoxJs使用 https://raw.githubusercontent.com/FoKit/Scripts/main/boxjs/fokit.boxjs.json + + * Surge可填写模块参数使用 https://raw.githubusercontent.com/githubdulong/Script/master/Surge/jd_buy_helper.sgmodule + + */ + +const $ = new Env("购物助手"); +intCryptoJS(); +let html = $response.body; + +(async () => { + if (!html || !html.includes("")) { + $.log("找不到标签,结束执行"); + $.done({ body: html }); + return; + } + $.log("已开始执行"); + + // 初始化参数与环境变量 + await init_tools(); + + if ($.jd_unionId && $.jtt_appid && $.jtt_appkey) { + + await jingfenJingTuiTui(); + } else { + $.msg($.name, "", "请检查配置是否正确 ❌"); + $.done({ body: html }); + return; + } + + await get_price_comparison(); + + await notice(); + + await hook_html(); +})() + .catch((e) => { + $.log($.name, "", "出错了: " + e + "❌"); + }) + .finally(() => { + $.done({ body: html }); + }); + +/** 初始化:读取模块参数和本地存储参数,设置全局变量 */ +async function init_tools() { + $.log("初始化开始"); + + // 获取模块传入参数 + const args = typeof $argument !== "undefined" ? $argument : ""; + $.log(`传入的参数: ${args}`); + const argObj = Object.fromEntries( + args.split("&").map(item => item.split("=").map(decodeURIComponent)) + ); + const isEmpty = (val) => !val || val === "null"; + + // 参数优先级:模块参数 > BoxJs 本地存储 + $.jd_unionId = !isEmpty(argObj["jd_union_id"]) + ? argObj["jd_union_id"] + : $.getdata("jd_unionId") || ""; + $.jd_positionId = !isEmpty(argObj["jd_position_id"]) + ? argObj["jd_position_id"] + : $.getdata("jd_positionId") || ""; + $.jtt_appid = !isEmpty(argObj["jtt_appid"]) + ? argObj["jtt_appid"] + : $.getdata("jtt_appid") || ""; + $.jtt_appkey = !isEmpty(argObj["jtt_appkey"]) + ? argObj["jtt_appkey"] + : $.getdata("jtt_appkey") || ""; + + $.log(`jd_unionId: ${$.jd_unionId}`); + $.log(`jd_positionId: ${$.jd_positionId}`); + $.log(`jtt_appid: ${$.jtt_appid}`); + $.log(`jtt_appkey: ${$.jtt_appkey}`); + + $.button = []; + const helperConfig = { + zdm: argObj["buy_helper_zdm"] || $.getdata("buy_helper_zdm") || "true", + mmm: argObj["buy_helper_mmm"] || $.getdata("buy_helper_mmm") || "true", + gwd: argObj["buy_helper_gwd"] || $.getdata("buy_helper_gwd") || "false", + copy: argObj["buy_helper_copy"] || $.getdata("buy_helper_copy") || "true", + }; + if (helperConfig.zdm !== "false") $.button.push("smzdm"); + if (helperConfig.mmm !== "false") $.button.push("mmm"); + if (helperConfig.gwd !== "false") $.button.push("gwd"); + if (helperConfig.copy !== "false") $.button.push("copy"); + + $.buy_helper_LR = argObj["buy_helper_LR"] || $.getdata("buy_helper_LR") || "left"; + + let url = $request.url; + $.appType = url.includes("lite-in.m.jd.com") ? "jdtj" : "jd"; + $.sku = (url.match(/\/(\d+)\.html/) || [])[1] || ""; + $.shortUrl = `https://item.jd.com/${$.sku}.html`; + + $.log("初始化完成"); + $.log(`类型: ${$.appType}`); + $.log(`商品: ${$.sku}`); + $.log(`appId: ${$.jtt_appid}`); + $.log(`appkey: ${$.jtt_appkey}`); +} + +/** 京推推转链 */ +async function jingfenJingTuiTui() { + $.log("转链开始"); + return new Promise((resolve) => { + const options = { + url: `http://japi.jingtuitui.com/api/universal?appid=${$.jtt_appid}&appkey=${$.jtt_appkey}&v=v3&unionid=${$.jd_unionId}&positionid=${$.jd_positionId}&content=https://item.jd.com/${$.sku}.html`, + timeout: 100 * 1000, + headers: { "Content-Type": "application/json;charset=utf-8" }, + }; + $.get(options, (err, resp, data) => { + try { + if (err) { + $.log("京推推 universal 请求失败:" + $.toStr(err)); + } else { + data = JSON.parse(data); + if (data["return"] == 0) { + const linkData = data?.result?.link_date?.[0] || {}; + const { chain_link, goods_info } = linkData; + if (goods_info) { + const { skuName = chain_link, imageInfo, commissionInfo, priceInfo } = goods_info; + $.commissionShare = commissionInfo.commissionShare; + $.commission = commissionInfo.couponCommission; + $.price = priceInfo.lowestPrice; + $.skuName = skuName; + $.skuImg = imageInfo.imageList?.[0]?.url; + } + $.shortUrl = chain_link; + $.log("转链完成,短链地址:" + $.shortUrl); + } else { + $.log("转链返回异常:" + $.toStr(data)); + } + } + } catch (e) { + $.logErr(e, resp); + } finally { + resolve(); + } + }); + }); +} + +/** 获取慢慢买 CK 的封装 */ +const getmmCK = () => { + const ck = $.getdata("慢慢买CK"); + if (!ck) { + $.msg("未获取ck", "请先打开【慢慢买】APP--我的, 获取ck", ""); + } + return ck; +}; + +const $http = (op, t = 4) => { + const { promise, resolve, reject } = Promise.withResolvers(); + const HTTPError = (e, req, res) => + Object.assign(new Error(e), { + name: "HTTPError", + request: req, + response: res, + }); + + const handleRes = ({ bodyBytes, ...res }) => { + res.status ??= res.statusCode; + res.json = () => JSON.parse(res.body); + if (res.headers?.["binary-mode"] && bodyBytes) + res.body = new Uint8Array(bodyBytes); + + res.error || res.status < 200 || res.status > 307 + ? reject(HTTPError(res.error, op, res)) + : resolve(res); + }; + + const timer = setTimeout( + () => reject(HTTPError("timeout", op)), + op.$timeout ?? t * 1000 + ); + this.$httpClient?.[op.method || "get"](op, (error, resp, body) => { + handleRes({ error, ...resp, body }); + }); + this.$task?.fetch({ url: op, ...op }).then(handleRes, handleRes); + + return promise.finally(() => clearTimeout(timer)); +}; + +const getMMdata = (id) => { + const buildMultipart = (fields) => { + const boundary = + "----WebKitFormBoundary" + Math.random().toString(36).substr(2); + let body = ""; + + for (const [name, value] of Object.entries(fields)) { + body += `--${boundary}\r\n`; + body += `Content-Disposition: form-data; name="${name}"\r\n\r\n`; + body += `${value}\r\n`; + } + body += `--${boundary}--\r\n`; + + return { body, boundary }; + }; + + const shareBody = { + methodName: "trendJava", + spbh: `1|${id}`, + url: `https://item.jd.com/${id}.html`, + t: Date.now().toString(), + c_appver: "4.8.3.1", + c_mmbDevId: getmmCK(), + }; + + shareBody.token = md5( + encodeURIComponent( + "3E41D1331F5DDAFCD0A38FE2D52FF66F" + + jsonToCustomString(shareBody) + + "3E41D1331F5DDAFCD0A38FE2D52FF66F" + ) + ).toUpperCase(); + + const headers = { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + "User-Agent": + "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 - mmbWebBrowse - ios", + }; + + const reqShare = { + method: "post", + url: "https://apapia-history-weblogic.manmanbuy.com/app/share", + headers, + body: jsonToQueryString(shareBody), + }; + + return $http(reqShare) + .then((res) => { + const { msg, code, data } = res.json(); + if (code !== 2000) throw new Error(msg); + if (!data) throw new Error(`${reqShare.url}: 无效数据`); + + return new URL(data).searchParams; + }) + .then((params) => { + const fields = { + shareId: params.get("shareId"), + sign: params.get("sign"), + spbh: params.get("spbh"), + url: params.get("url"), + }; + + const { body, boundary } = buildMultipart(fields); + + const reqTrendData = { + method: "post", + url: "https://apapia-history-weblogic.manmanbuy.com/h5/share/trendData", + headers: { + "content-type": `multipart/form-data; boundary=${boundary}`, + }, + body, + }; + + return $http(reqTrendData); + }) + .then((res) => res.json()); +}; + +/** 获取比价信息 */ +async function get_price_comparison() { + try { + const data = await getMMdata($.sku); + if (data?.ok && data?.result?.priceRemark?.ListPriceDetail) { + const lowerItem = data?.result?.priceRemark?.ListPriceDetail.find(item => item.ShowName === "历史最低"); + if (lowerItem) { + const { extraPrice, Price, Difference, Date } = lowerItem; + $.Difference = Difference; + $.desc = `历史最低: ${Price || `¥${extraPrice}`} (${Date})`; + } else { + $.desc = "历史最低: 暂无"; + } + $.price = data?.recentlyZK?.currentprice || $.price; + $.skuName = data?.single?.title || $.skuName; + $.skuImg = data?.single?.smallpic || $.skuImg; + } else { + $.desc = "历史最低: 暂无"; + $.log("获取比价信息失败"); + } + } catch (e) { + $.logErr(e); + } +} + +/** 发送通知 */ +async function notice() { + $.log("发送通知"); + $.title = $.skuName || "商品信息"; + $.opts = { "auto-dismiss": 30 }; + + $.desc = $.desc || ""; + if (/u\.jd\.com/.test($.shortUrl)) { + $.desc += `\n预计返利: ¥${(($.price * $.commissionShare) / 100).toFixed(2)} ${$.commissionShare}%`; + $.desc += `\n当前到手: ¥${$.price}${$.Difference ? " " + $.Difference : ""}`; + + // 根据平台生成跳转链接 + if ($.appType === "jdtj") { + $.jumpUrl = `openjdlite://virtual?params=${encodeURIComponent( + '{"category":"jump","des":"m","url":"' + $.shortUrl + '"}' + )}`; + } else { + $.jumpUrl = `openApp.jdMobile://virtual?params=${encodeURIComponent( + '{"category":"jump","des":"m","sourceValue":"babel-act","sourceType":"babel","url":"' + $.shortUrl + '"}' + )}`; + } + $.opts["$open"] = $.jumpUrl; + } else { + $.desc += "\n预计返利: 暂无"; + $.log("无佣金商品"); + } + if ($.skuImg) $.opts["$media"] = $.skuImg; + if ($.isLoon() && $loon.split(" ")[1].split(".")[0] === "16") { + $.opts["$media"] = undefined; + } + $.msg($.title, $.subt, $.desc, $.opts); +} + +/** 注入 HTML */ +async function hook_html() { + $.log("开始注入html"); + const buttons = [ + { key: "mmm", icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/mmm.png" }, + { key: "smzdm", icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/zdm.png" }, + { key: "gwd", icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/gwd.png" }, + { key: "jf", icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/jf.png" }, + { key: "copy", icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/copy.png" } + ].filter(item => $.button.includes(item.key) && $.sku); + + + const hookContent = ` + +
+ ${buttons.map(item => ``).join("\n")} +
+ + `; + + html = html.replace(/<\/html>/, hookContent); + $.log("注入html完成"); + $.done({ body: html }); +} + + +function jsonToQueryString(jsonObject) {return Object.keys(jsonObject).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(jsonObject[key])}`).join('&');} + + +function jsonToCustomString(jsonObject){return Object.keys(jsonObject).filter(key=>jsonObject[key]!==''&&key.toLowerCase()!=='token').sort().map(key=>`${key.toUpperCase()}${jsonObject[key].toUpperCase()}`).join('');} + + +function intCryptoJS(){CryptoJS=function(t,r){var n;if("undefined"!=typeof window&&window.crypto&&(n=window.crypto),"undefined"!=typeof self&&self.crypto&&(n=self.crypto),"undefined"!=typeof globalThis&&globalThis.crypto&&(n=globalThis.crypto),!n&&"undefined"!=typeof window&&window.msCrypto&&(n=window.msCrypto),!n&&"undefined"!=typeof global&&global.crypto&&(n=global.crypto),!n&&"function"==typeof require)try{n=require("crypto")}catch(t){}var e=function(){if(n){if("function"==typeof n.getRandomValues)try{return n.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof n.randomBytes)try{return n.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")},i=Object.create||function(){function t(){}return function(r){var n;return t.prototype=r,n=new t,t.prototype=null,n}}(),o={},a=o.lib={},s=a.Base={extend:function(t){var r=i(this);return t&&r.mixIn(t),r.hasOwnProperty("init")&&this.init!==r.init||(r.init=function(){r.$super.init.apply(this,arguments)}),r.init.prototype=r,r.$super=this,r},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var r in t)t.hasOwnProperty(r)&&(this[r]=t[r]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},c=a.WordArray=s.extend({init:function(t,r){t=this.words=t||[],this.sigBytes=null!=r?r:4*t.length},toString:function(t){return(t||f).stringify(this)},concat:function(t){var r=this.words,n=t.words,e=this.sigBytes,i=t.sigBytes;if(this.clamp(),e%4)for(var o=0;o>>2]>>>24-o%4*8&255;r[e+o>>>2]|=a<<24-(e+o)%4*8}else for(var s=0;s>>2]=n[s>>>2];return this.sigBytes+=i,this},clamp:function(){var r=this.words,n=this.sigBytes;r[n>>>2]&=4294967295<<32-n%4*8,r.length=t.ceil(n/4)},clone:function(){var t=s.clone.call(this);return t.words=this.words.slice(0),t},random:function(r){var n,i=[],o=function(r){r=r;var n=987654321,e=4294967295;return function(){var i=((n=36969*(65535&n)+(n>>16)&e)<<16)+(r=18e3*(65535&r)+(r>>16)&e)&e;return i/=4294967296,(i+=.5)*(t.random()>.5?1:-1)}},a=!1;try{e(),a=!0}catch(t){}for(var s,u=0;u>>2]>>>24-i%4*8&255;e.push((o>>>4).toString(16)),e.push((15&o).toString(16))}return e.join("")},parse:function(t){for(var r=t.length,n=[],e=0;e>>3]|=parseInt(t.substr(e,2),16)<<24-e%8*4;return new c.init(n,r/2)}},h=u.Latin1={stringify:function(t){for(var r=t.words,n=t.sigBytes,e=[],i=0;i>>2]>>>24-i%4*8&255;e.push(String.fromCharCode(o))}return e.join("")},parse:function(t){for(var r=t.length,n=[],e=0;e>>2]|=(255&t.charCodeAt(e))<<24-e%4*8;return new c.init(n,r)}},p=u.Utf8={stringify:function(t){try{return decodeURIComponent(escape(h.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return h.parse(unescape(encodeURIComponent(t)))}},d=a.BufferedBlockAlgorithm=s.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=p.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(r){var n,e=this._data,i=e.words,o=e.sigBytes,a=this.blockSize,s=o/(4*a),u=(s=r?t.ceil(s):t.max((0|s)-this._minBufferSize,0))*a,f=t.min(4*u,o);if(u){for(var h=0;h>>24)|4278255360&(i<<24|i>>>8)}var o=this._hash.words,s=t[r+0],p=t[r+1],d=t[r+2],l=t[r+3],y=t[r+4],v=t[r+5],g=t[r+6],w=t[r+7],_=t[r+8],m=t[r+9],B=t[r+10],b=t[r+11],C=t[r+12],S=t[r+13],x=t[r+14],A=t[r+15],H=o[0],z=o[1],M=o[2],D=o[3];z=h(z=h(z=h(z=h(z=f(z=f(z=f(z=f(z=u(z=u(z=u(z=u(z=c(z=c(z=c(z=c(z,M=c(M,D=c(D,H=c(H,z,M,D,s,7,a[0]),z,M,p,12,a[1]),H,z,d,17,a[2]),D,H,l,22,a[3]),M=c(M,D=c(D,H=c(H,z,M,D,y,7,a[4]),z,M,v,12,a[5]),H,z,g,17,a[6]),D,H,w,22,a[7]),M=c(M,D=c(D,H=c(H,z,M,D,_,7,a[8]),z,M,m,12,a[9]),H,z,B,17,a[10]),D,H,b,22,a[11]),M=c(M,D=c(D,H=c(H,z,M,D,C,7,a[12]),z,M,S,12,a[13]),H,z,x,17,a[14]),D,H,A,22,a[15]),M=u(M,D=u(D,H=u(H,z,M,D,p,5,a[16]),z,M,g,9,a[17]),H,z,b,14,a[18]),D,H,s,20,a[19]),M=u(M,D=u(D,H=u(H,z,M,D,v,5,a[20]),z,M,B,9,a[21]),H,z,A,14,a[22]),D,H,y,20,a[23]),M=u(M,D=u(D,H=u(H,z,M,D,m,5,a[24]),z,M,x,9,a[25]),H,z,l,14,a[26]),D,H,_,20,a[27]),M=u(M,D=u(D,H=u(H,z,M,D,S,5,a[28]),z,M,d,9,a[29]),H,z,w,14,a[30]),D,H,C,20,a[31]),M=f(M,D=f(D,H=f(H,z,M,D,v,4,a[32]),z,M,_,11,a[33]),H,z,b,16,a[34]),D,H,x,23,a[35]),M=f(M,D=f(D,H=f(H,z,M,D,p,4,a[36]),z,M,y,11,a[37]),H,z,w,16,a[38]),D,H,B,23,a[39]),M=f(M,D=f(D,H=f(H,z,M,D,S,4,a[40]),z,M,s,11,a[41]),H,z,l,16,a[42]),D,H,g,23,a[43]),M=f(M,D=f(D,H=f(H,z,M,D,m,4,a[44]),z,M,C,11,a[45]),H,z,A,16,a[46]),D,H,d,23,a[47]),M=h(M,D=h(D,H=h(H,z,M,D,s,6,a[48]),z,M,w,10,a[49]),H,z,x,15,a[50]),D,H,v,21,a[51]),M=h(M,D=h(D,H=h(H,z,M,D,C,6,a[52]),z,M,l,10,a[53]),H,z,B,15,a[54]),D,H,p,21,a[55]),M=h(M,D=h(D,H=h(H,z,M,D,_,6,a[56]),z,M,A,10,a[57]),H,z,g,15,a[58]),D,H,S,21,a[59]),M=h(M,D=h(D,H=h(H,z,M,D,y,6,a[60]),z,M,b,10,a[61]),H,z,d,15,a[62]),D,H,m,21,a[63]),o[0]=o[0]+H|0,o[1]=o[1]+z|0,o[2]=o[2]+M|0,o[3]=o[3]+D|0},_doFinalize:function(){var r=this._data,n=r.words,e=8*this._nDataBytes,i=8*r.sigBytes;n[i>>>5]|=128<<24-i%32;var o=t.floor(e/4294967296),a=e;n[15+(i+64>>>9<<4)]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),n[14+(i+64>>>9<<4)]=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8),r.sigBytes=4*(n.length+1),this._process();for(var s=this._hash,c=s.words,u=0;u<4;u++){var f=c[u];c[u]=16711935&(f<<8|f>>>24)|4278255360&(f<<24|f>>>8)}return s},clone:function(){var t=i.clone.call(this);return t._hash=this._hash.clone(),t}});function c(t,r,n,e,i,o,a){var s=t+(r&n|~r&e)+i+a;return(s<>>32-o)+r}function u(t,r,n,e,i,o,a){var s=t+(r&e|n&~e)+i+a;return(s<>>32-o)+r}function f(t,r,n,e,i,o,a){var s=t+(r^n^e)+i+a;return(s<>>32-o)+r}function h(t,r,n,e,i,o,a){var s=t+(n^(r|~e))+i+a;return(s<>>32-o)+r}r.MD5=i._createHelper(s),r.HmacMD5=i._createHmacHelper(s)}(Math),function(){var t=CryptoJS,r=t.lib.WordArray;t.enc.Base64={stringify:function(t){var r=t.words,n=t.sigBytes,e=this._map;t.clamp();for(var i=[],o=0;o>>2]>>>24-o%4*8&255)<<16|(r[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|r[o+2>>>2]>>>24-(o+2)%4*8&255,s=0;s<4&&o+.75*s>>6*(3-s)&63));var c=e.charAt(64);if(c)for(;i.length%4;)i.push(c);return i.join("")},parse:function(t){var n=t.length,e=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var o=0;o>>6-a%4*2;i[o>>>2]|=(s|c)<<24-o%4*8,o++}return r.create(i,o)}(t,n,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}();}; +function md5(word){return CryptoJS.MD5(word).toString();} + + +// prettier-ignore +function Env(t, e) { class s { constructor(t) { this.env = t } send(t, e = "GET") { t = "string" == typeof t ? { url: t } : t; let s = this.get; return "POST" === e && (s = this.post), new Promise(((e, i) => { s.call(this, t, ((t, s, o) => { t ? i(t) : e(s) })) })) } get(t) { return this.send.call(this.env, t) } post(t) { return this.send.call(this.env, t, "POST") } } return new class { constructor(t, e) { this.logLevels = { debug: 0, info: 1, warn: 2, error: 3 }, this.logLevelPrefixs = { debug: "[DEBUG] ", info: "[INFO] ", warn: "[WARN] ", error: "[ERROR] " }, this.logLevel = "info", this.name = t, this.http = new s(this), this.data = null, this.dataFile = "box.dat", this.logs = [], this.isMute = !1, this.isNeedRewrite = !1, this.logSeparator = "\n", this.encoding = "utf-8", this.startTime = (new Date).getTime(), Object.assign(this, e), this.log("", `🔔${this.name}, 开始!`) } getEnv() { return "undefined" != typeof $environment && $environment["surge-version"] ? "Surge" : "undefined" != typeof $environment && $environment["stash-version"] ? "Stash" : "undefined" != typeof module && module.exports ? "Node.js" : "undefined" != typeof $task ? "Quantumult X" : "undefined" != typeof $loon ? "Loon" : "undefined" != typeof $rocket ? "Shadowrocket" : void 0 } isNode() { return "Node.js" === this.getEnv() } isQuanX() { return "Quantumult X" === this.getEnv() } isSurge() { return "Surge" === this.getEnv() } isLoon() { return "Loon" === this.getEnv() } isShadowrocket() { return "Shadowrocket" === this.getEnv() } isStash() { return "Stash" === this.getEnv() } toObj(t, e = null) { try { return JSON.parse(t) } catch { return e } } toStr(t, e = null, ...s) { try { return JSON.stringify(t, ...s) } catch { return e } } getjson(t, e) { let s = e; if (this.getdata(t)) try { s = JSON.parse(this.getdata(t)) } catch { } return s } setjson(t, e) { try { return this.setdata(JSON.stringify(t), e) } catch { return !1 } } getScript(t) { return new Promise((e => { this.get({ url: t }, ((t, s, i) => e(i))) })) } runScript(t, e) { return new Promise((s => { let i = this.getdata("@chavy_boxjs_userCfgs.httpapi"); i = i ? i.replace(/\n/g, "").trim() : i; let o = this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout"); o = o ? 1 * o : 20, o = e && e.timeout ? e.timeout : o; const [r, a] = i.split("@"), n = { url: `http://${a}/v1/scripting/evaluate`, body: { script_text: t, mock_type: "cron", timeout: o }, headers: { "X-Key": r, Accept: "*/*" }, timeout: o }; this.post(n, ((t, e, i) => s(i))) })).catch((t => this.logErr(t))) } loaddata() { if (!this.isNode()) return {}; { this.fs = this.fs ? this.fs : require("fs"), this.path = this.path ? this.path : require("path"); const t = this.path.resolve(this.dataFile), e = this.path.resolve(process.cwd(), this.dataFile), s = this.fs.existsSync(t), i = !s && this.fs.existsSync(e); if (!s && !i) return {}; { const i = s ? t : e; try { return JSON.parse(this.fs.readFileSync(i)) } catch (t) { return {} } } } } writedata() { if (this.isNode()) { this.fs = this.fs ? this.fs : require("fs"), this.path = this.path ? this.path : require("path"); const t = this.path.resolve(this.dataFile), e = this.path.resolve(process.cwd(), this.dataFile), s = this.fs.existsSync(t), i = !s && this.fs.existsSync(e), o = JSON.stringify(this.data); s ? this.fs.writeFileSync(t, o) : i ? this.fs.writeFileSync(e, o) : this.fs.writeFileSync(t, o) } } lodash_get(t, e, s) { const i = e.replace(/\[(\d+)\]/g, ".$1").split("."); let o = t; for (const t of i) if (o = Object(o)[t], void 0 === o) return s; return o } lodash_set(t, e, s) { return Object(t) !== t || (Array.isArray(e) || (e = e.toString().match(/[^.[\]]+/g) || []), e.slice(0, -1).reduce(((t, s, i) => Object(t[s]) === t[s] ? t[s] : t[s] = Math.abs(e[i + 1]) >> 0 == +e[i + 1] ? [] : {}), t)[e[e.length - 1]] = s), t } getdata(t) { let e = this.getval(t); if (/^@/.test(t)) { const [, s, i] = /^@(.*?)\.(.*?)$/.exec(t), o = s ? this.getval(s) : ""; if (o) try { const t = JSON.parse(o); e = t ? this.lodash_get(t, i, "") : e } catch (t) { e = "" } } return e } setdata(t, e) { let s = !1; if (/^@/.test(e)) { const [, i, o] = /^@(.*?)\.(.*?)$/.exec(e), r = this.getval(i), a = i ? "null" === r ? null : r || "{}" : "{}"; try { const e = JSON.parse(a); this.lodash_set(e, o, t), s = this.setval(JSON.stringify(e), i) } catch (e) { const r = {}; this.lodash_set(r, o, t), s = this.setval(JSON.stringify(r), i) } } else s = this.setval(t, e); return s } getval(t) { switch (this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": return $persistentStore.read(t); case "Quantumult X": return $prefs.valueForKey(t); case "Node.js": return this.data = this.loaddata(), this.data[t]; default: return this.data && this.data[t] || null } } setval(t, e) { switch (this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": return $persistentStore.write(t, e); case "Quantumult X": return $prefs.setValueForKey(t, e); case "Node.js": return this.data = this.loaddata(), this.data[e] = t, this.writedata(), !0; default: return this.data && this.data[e] || null } } initGotEnv(t) { this.got = this.got ? this.got : require("got"), this.cktough = this.cktough ? this.cktough : require("tough-cookie"), this.ckjar = this.ckjar ? this.ckjar : new this.cktough.CookieJar, t && (t.headers = t.headers ? t.headers : {}, t && (t.headers = t.headers ? t.headers : {}, void 0 === t.headers.cookie && void 0 === t.headers.Cookie && void 0 === t.cookieJar && (t.cookieJar = this.ckjar))) } get(t, e = (() => { })) { switch (t.headers && (delete t.headers["Content-Type"], delete t.headers["Content-Length"], delete t.headers["content-type"], delete t.headers["content-length"]), t.params && (t.url += "?" + this.queryStr(t.params)), void 0 === t.followRedirect || t.followRedirect || ((this.isSurge() || this.isLoon()) && (t["auto-redirect"] = !1), this.isQuanX() && (t.opts ? t.opts.redirection = !1 : t.opts = { redirection: !1 })), this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": default: this.isSurge() && this.isNeedRewrite && (t.headers = t.headers || {}, Object.assign(t.headers, { "X-Surge-Skip-Scripting": !1 })), $httpClient.get(t, ((t, s, i) => { !t && s && (s.body = i, s.statusCode = s.status ? s.status : s.statusCode, s.status = s.statusCode), e(t, s, i) })); break; case "Quantumult X": this.isNeedRewrite && (t.opts = t.opts || {}, Object.assign(t.opts, { hints: !1 })), $task.fetch(t).then((t => { const { statusCode: s, statusCode: i, headers: o, body: r, bodyBytes: a } = t; e(null, { status: s, statusCode: i, headers: o, body: r, bodyBytes: a }, r, a) }), (t => e(t && t.error || "UndefinedError"))); break; case "Node.js": let s = require("iconv-lite"); this.initGotEnv(t), this.got(t).on("redirect", ((t, e) => { try { if (t.headers["set-cookie"]) { const s = t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString(); s && this.ckjar.setCookieSync(s, null), e.cookieJar = this.ckjar } } catch (t) { this.logErr(t) } })).then((t => { const { statusCode: i, statusCode: o, headers: r, rawBody: a } = t, n = s.decode(a, this.encoding); e(null, { status: i, statusCode: o, headers: r, rawBody: a, body: n }, n) }), (t => { const { message: i, response: o } = t; e(i, o, o && s.decode(o.rawBody, this.encoding)) })); break } } post(t, e = (() => { })) { const s = t.method ? t.method.toLocaleLowerCase() : "post"; switch (t.body && t.headers && !t.headers["Content-Type"] && !t.headers["content-type"] && (t.headers["content-type"] = "application/x-www-form-urlencoded"), t.headers && (delete t.headers["Content-Length"], delete t.headers["content-length"]), void 0 === t.followRedirect || t.followRedirect || ((this.isSurge() || this.isLoon()) && (t["auto-redirect"] = !1), this.isQuanX() && (t.opts ? t.opts.redirection = !1 : t.opts = { redirection: !1 })), this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": default: this.isSurge() && this.isNeedRewrite && (t.headers = t.headers || {}, Object.assign(t.headers, { "X-Surge-Skip-Scripting": !1 })), $httpClient[s](t, ((t, s, i) => { !t && s && (s.body = i, s.statusCode = s.status ? s.status : s.statusCode, s.status = s.statusCode), e(t, s, i) })); break; case "Quantumult X": t.method = s, this.isNeedRewrite && (t.opts = t.opts || {}, Object.assign(t.opts, { hints: !1 })), $task.fetch(t).then((t => { const { statusCode: s, statusCode: i, headers: o, body: r, bodyBytes: a } = t; e(null, { status: s, statusCode: i, headers: o, body: r, bodyBytes: a }, r, a) }), (t => e(t && t.error || "UndefinedError"))); break; case "Node.js": let i = require("iconv-lite"); this.initGotEnv(t); const { url: o, ...r } = t; this.got[s](o, r).then((t => { const { statusCode: s, statusCode: o, headers: r, rawBody: a } = t, n = i.decode(a, this.encoding); e(null, { status: s, statusCode: o, headers: r, rawBody: a, body: n }, n) }), (t => { const { message: s, response: o } = t; e(s, o, o && i.decode(o.rawBody, this.encoding)) })); break } } time(t, e = null) { const s = e ? new Date(e) : new Date; let i = { "M+": s.getMonth() + 1, "d+": s.getDate(), "H+": s.getHours(), "m+": s.getMinutes(), "s+": s.getSeconds(), "q+": Math.floor((s.getMonth() + 3) / 3), S: s.getMilliseconds() }; /(y+)/.test(t) && (t = t.replace(RegExp.$1, (s.getFullYear() + "").substr(4 - RegExp.$1.length))); for (let e in i) new RegExp("(" + e + ")").test(t) && (t = t.replace(RegExp.$1, 1 == RegExp.$1.length ? i[e] : ("00" + i[e]).substr(("" + i[e]).length))); return t } queryStr(t) { let e = ""; for (const s in t) { let i = t[s]; null != i && "" !== i && ("object" == typeof i && (i = JSON.stringify(i)), e += `${s}=${i}&`) } return e = e.substring(0, e.length - 1), e } msg(e = t, s = "", i = "", o) { const r = t => { const { $open: e, $copy: s, $media: i, $mediaMime: o } = t; switch (typeof t) { case void 0: return t; case "string": switch (this.getEnv()) { case "Surge": case "Stash": default: return { url: t }; case "Loon": case "Shadowrocket": return t; case "Quantumult X": return { "open-url": t }; case "Node.js": return }case "object": switch (this.getEnv()) { case "Surge": case "Stash": case "Shadowrocket": default: { const r = {}; let a = t.openUrl || t.url || t["open-url"] || e; a && Object.assign(r, { action: "open-url", url: a }); let n = t["update-pasteboard"] || t.updatePasteboard || s; if (n && Object.assign(r, { action: "clipboard", text: n }), i) { let t, e, s; if (i.startsWith("http")) t = i; else if (i.startsWith("data:")) { const [t] = i.split(";"), [, o] = i.split(","); e = o, s = t.replace("data:", "") } else { e = i, s = (t => { const e = { JVBERi0: "application/pdf", R0lGODdh: "image/gif", R0lGODlh: "image/gif", iVBORw0KGgo: "image/png", "/9j/": "image/jpg" }; for (var s in e) if (0 === t.indexOf(s)) return e[s]; return null })(i) } Object.assign(r, { "media-url": t, "media-base64": e, "media-base64-mime": o ?? s }) } return Object.assign(r, { "auto-dismiss": t["auto-dismiss"], sound: t.sound }), r } case "Loon": { const s = {}; let i = t.openUrl || t.url || t["open-url"] || e; i && Object.assign(s, { openUrl: i }); let o = t.mediaUrl || t["media-url"]; return o && Object.assign(s, { mediaUrl: o }), s } case "Quantumult X": { const i = {}; let o = t["open-url"] || t.url || t.openUrl || e; o && Object.assign(i, { "open-url": o }); let r = t["media-url"] || t.mediaUrl; r && Object.assign(i, { "media-url": r }); let a = t["update-pasteboard"] || t.updatePasteboard || s; return a && Object.assign(i, { "update-pasteboard": a }), i } case "Node.js": return }default: return } }; if (!this.isMute) switch (this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": default: $notification.post(e, s, i, r(o)); break; case "Quantumult X": $notify(e, s, i, r(o)); break; case "Node.js": break }if (!this.isMuteLog) { let t = ["", "==============📣系统通知📣=============="]; t.push(e), s && t.push(s), i && t.push(i), console.log(t.join("\n")), this.logs = this.logs.concat(t) } } debug(...t) { this.logLevels[this.logLevel] <= this.logLevels.debug && (t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(`${this.logLevelPrefixs.debug}${t.map((t => t ?? String(t))).join(this.logSeparator)}`)) } info(...t) { this.logLevels[this.logLevel] <= this.logLevels.info && (t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(`${this.logLevelPrefixs.info}${t.map((t => t ?? String(t))).join(this.logSeparator)}`)) } warn(...t) { this.logLevels[this.logLevel] <= this.logLevels.warn && (t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(`${this.logLevelPrefixs.warn}${t.map((t => t ?? String(t))).join(this.logSeparator)}`)) } error(...t) { this.logLevels[this.logLevel] <= this.logLevels.error && (t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(`${this.logLevelPrefixs.error}${t.map((t => t ?? String(t))).join(this.logSeparator)}`)) } log(...t) { t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(t.map((t => t ?? String(t))).join(this.logSeparator)) } logErr(t, e) { switch (this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": case "Quantumult X": default: this.log("", `❗️${this.name}, 错误!`, e, t); break; case "Node.js": this.log("", `❗️${this.name}, 错误!`, e, void 0 !== t.message ? t.message : t, t.stack); break } } wait(t) { return new Promise((e => setTimeout(e, t))) } done(t = {}) { const e = ((new Date).getTime() - this.startTime) / 1e3; switch (this.log("", `🔔${this.name}, 结束! 🕛 ${e} 秒`), this.log(), this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": case "Quantumult X": default: $done(t); break; case "Node.js": process.exit(1) } } }(t, e) } diff --git a/jd_buy_helper.js b/jd_buy_helper.js deleted file mode 100644 index 4a59c5dd..00000000 --- a/jd_buy_helper.js +++ /dev/null @@ -1,409 +0,0 @@ -/** - * 脚本名称:京东购物助手 - * 使用说明:进入APP商品详情页触发,支持京东下单返利和慢慢买、购物党和什么值得买跳转比价领券以及慢慢买接口历史价格。 - -[Script] -# > 京东购物助手 -购物助手 = type=http-response,pattern=^https:\/\/in\.m\.jd\.com\/product\/.+?\.html,requires-body=1,max-size=-1,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/jd_buy_helper.js -[MITM] -hostname = %APPEND% in.m.jd.com, lite-in.m.jd.com - - * 添加脚本后依赖BoxJs使用 https://raw.githubusercontent.com/FoKit/Scripts/main/boxjs/fokit.boxjs.json - - * Surge可填写模块参数使用 https://raw.githubusercontent.com/githubdulong/Script/master/Surge/jd_buy_helper.sgmodule - - */ -const $ = new Env("购物助手"); -let html = $response.body; - -!(async () => { - if (!html || !html.includes("")) { - $.log("找不到标签,结束执行"); - $.done({ body: html }); - } else { - $.log("已开始执行"); - await init_tools(); // 初始化 - if ($.jd_unionId && $.jtt_appid && $.jtt_appkey) { - await jingfenJingTuiTui(); // 京推推转链 - } else { - $.msg($.name, ``, `请检查配置是否正确 ❌`); - $.done({ body: html }); - } - await get_price_comparison(); // 获取比价信息 - await notice(); // 通知 - await hook_html(); // 注入 - } -})() - .catch((e) => { - $.log($.name, ``, `出错了: ${e}❌`); - }) - .finally(() => { - $.done({ body: html }); - }); - -// 初始化 -async function init_tools() { - $.log("初始化开始"); - - // 从模块传入参数 - const args = typeof $argument !== 'undefined' ? $argument : ""; - $.log(`传入的参数: ${args}`); - const argObj = Object.fromEntries( - args.split("&").map((item) => item.split("=").map(decodeURIComponent)) - ); - - // 参数优先级:模块参数 > BoxJs 参数 - $.jd_unionId = argObj["jd_union_id"] || $.getdata("jd_unionId") || ""; - $.jd_positionId = argObj["jd_position_id"] || $.getdata("jd_positionId") || ""; - $.jtt_appid = argObj["jtt_appid"] || $.getdata("jtt_appid") || ""; - $.jtt_appkey = argObj["jtt_appkey"] || $.getdata("jtt_appkey") || ""; - - $.log(`jd_unionId: ${$.jd_unionId}`); - $.log(`jd_positionId: ${$.jd_positionId}`); - $.log(`jtt_appid: ${$.jtt_appid}`); - $.log(`jtt_appkey: ${$.jtt_appkey}`); - - $.button = []; - const helperConfig = { - zdm: argObj["buy_helper_zdm"] || $.getdata("buy_helper_zdm") || "true", - mmm: argObj["buy_helper_mmm"] || $.getdata("buy_helper_mmm") || "true", - gwd: argObj["buy_helper_gwd"] || $.getdata("buy_helper_gwd") || "false", - copy: argObj["buy_helper_copy"] || $.getdata("buy_helper_copy") || "true", - }; - - if (helperConfig.zdm !== "false") $.button.push("smzdm"); - if (helperConfig.mmm !== "false") $.button.push("mmm"); - if (helperConfig.gwd !== "false") $.button.push("gwd"); - if (helperConfig.copy !== "false") $.button.push("copy"); - - $.buy_helper_LR = argObj["buy_helper_LR"] || $.getdata("buy_helper_LR") || "left"; - - let url = $request.url; - $.appType = url.includes("lite-in.m.jd.com") ? "jdtj" : "jd"; - $.sku = url.match(/\/(\d+)\.html/)?.[1] || ""; - $.shortUrl = `https://item.jd.com/${$.sku}.html`; - - $.log("初始化完成"); - $.log(`类型: ${$.appType}`); - $.log(`商品: ${$.sku}`); - $.log(`appId: ${$.jtt_appid}`); - $.log(`appkey: ${$.jtt_appkey}`); -} - -// 京推推转链 -async function jingfenJingTuiTui() { - $.log('转链开始'); - return new Promise((resolve) => { - const options = { - url: `http://japi.jingtuitui.com/api/universal?appid=${$.jtt_appid}&appkey=${$.jtt_appkey}&v=v3&unionid=${$.jd_unionId}&positionid=${$.jd_positionId}&content=https://item.jd.com/${$.sku}.html`, - timeout: 100 * 1000, - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, - } - $.get(options, async (err, resp, data) => { - try { - if (err) { - $.log(`京推推 universal 请求失败:${$.toStr(err)}\n`); - } else { - data = JSON.parse(data); - if (data['return'] == 0) { - const { chain_link, goods_info } = data?.result?.link_date?.[0]; - const { skuName = chain_link, imageInfo, commissionInfo, priceInfo } = goods_info || {}; - $.commissionShare = commissionInfo.commissionShare; // 佣金比例 - $.commission = commissionInfo.couponCommission; // 券后佣金 - $.shortUrl = chain_link; // 二合一短链 - $.price = priceInfo.lowestPrice; // 商品原价 - $.skuName = skuName; // 商品名称 - $.skuImg = imageInfo.imageList[0].url; // 商品主图 - $.log(`短链地址 ${$.shortUrl}`); - $.log('转链完成'); - } else { - console.log($.toStr(data)); - } - } - } catch (e) { - $.logErr(e, resp); - } finally { - resolve(); - } - }) - }) -} - -// 获取比价信息 -async function get_price_comparison() { - return new Promise((resolve) => { - const options = { - url: "https://apapia-history.manmanbuy.com/ChromeWidgetServices/WidgetServices.ashx", - headers: { - "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", - "User-Agent": "iPhone/CFNetwork/Darwin" - }, - body: 'methodName=getHistoryTrend&p_url=' + encodeURIComponent(`https://item.m.jd.com/product/${$.sku}.html`) - }; - $.post(options, (error, response, data) => { - try { - data = JSON.parse(data); - if (data?.ok == 1 && data?.single && data?.PriceRemark?.ListPriceDetail) { - // 获取历史最低价和历史最低日期 - const ListPriceDetail = data?.PriceRemark?.ListPriceDetail; - - // 使用 find() 方法找到历史最低对象 - const lower_data = ListPriceDetail.find(item => item.ShowName == "历史最低"); - if (lower_data) { - const { extraPrice, Price, Difference, Date } = lower_data; // 提取最低价、价格差值和日期信息 - $.Difference = Difference; // 价格差值 - $.desc = `历史最低: ${Price || `¥${extraPrice}`} (${Date})`; // 确保 $.desc 已初始化 - } else { - $.desc = `历史最低: 暂无`; // 如果未找到历史最低价,初始化 $.desc - } - $.price = data?.recentlyZK?.currentprice || $.price; // 当前到手价 - $.skuName = data?.single?.title || $.skuName; // 商品标题 - $.skuImg = data?.single?.smallpic || $.skuImg; // 商品图片 - } else { - $.desc = `历史最低: 暂无`; // 如果获取比价信息失败,初始化 $.desc - $.log(`获取比价信息失败`); - } - } catch (e) { - $.logErr(e, response); - } finally { - resolve(); - } - }); - }); -} - -// 通知 -async function notice() { - $.log(`发送通知`); - $.title = $.skuName; - - // 定义 opts 对象 - $.opts = { - 'auto-dismiss': 30 // 30 秒自动删除通知 - }; - - // 确保 $.desc 已初始化 - if (!$.desc) { - $.desc = ""; // 如果 $.desc 未初始化,设置为空字符串 - } - - if (/u\.jd\.com/.test($.shortUrl)) { - $.desc += `\n预计返利: ¥${($.price * $.commissionShare / 100).toFixed(2)} ${$.commissionShare}%`; - $.desc += `\n当前到手: ¥${$.price}${$.Difference ? ` ${$.Difference}` : ``}`; - - // 生成跳转链接 - switch ($.appType) { - case "jdtj": - $.jumpUrl = `openjdlite://virtual?params=${encodeURIComponent( - `{"category":"jump","des":"m","url":"${$.shortUrl}"}` - )}`; - break; - default: - $.jumpUrl = `openApp.jdMobile://virtual?params=${encodeURIComponent( - `{"category":"jump","des":"m","sourceValue":"babel-act","sourceType":"babel","url":"${$.shortUrl}"}` - )}`; - break; - } - - // 添加跳转链接 - if ($.jumpUrl) $.opts['$open'] = $.jumpUrl; - - } else { - $.desc += `\n预计返利: 暂无`; - $.log(`无佣金商品`); - } - - // 添加媒体图片 - if ($.skuImg) $.opts['$media'] = $.skuImg; - - // 修复 Loon 在 iOS 16 带有媒体导致无法正常通知的 bug - if ($.isLoon()) { - $.opts = $loon.split(' ')[1].split('.')[0] === '16' ? { ...$.opts, '$media': undefined } : $.opts; - } - - // 发送通知 - $.msg($.title, $.subt, $.desc, $.opts); -} - -// 注入html -async function hook_html() { - $.log("开始注入html"); - - const buttons = [ - { - key: "mmm", - icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/mmm.png", - }, - { - key: "smzdm", - icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/zdm.png", - }, - { - key: "gwd", - icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/gwd.png", - }, - { - key: "jf", - icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/jf.png", - }, - { - key: "copy", - icon: "https://raw.githubusercontent.com/FoKit/Scripts/main/images/icon/copy.png", - }, - ].filter((item) => $.button.includes(item.key) && $.sku); - - $.hook = ` - - -
- ${buttons - .map((item) => { - return ``; - }) - .join(`\n`)} -
- - - `; - html = html.replace(/(<\/html>)/g, $.hook); - $.log("注入html完成"); - $.done({ body: html }); -} - - -// prettier-ignore -function Env(t, e) { class s { constructor(t) { this.env = t } send(t, e = "GET") { t = "string" == typeof t ? { url: t } : t; let s = this.get; return "POST" === e && (s = this.post), new Promise(((e, i) => { s.call(this, t, ((t, s, o) => { t ? i(t) : e(s) })) })) } get(t) { return this.send.call(this.env, t) } post(t) { return this.send.call(this.env, t, "POST") } } return new class { constructor(t, e) { this.logLevels = { debug: 0, info: 1, warn: 2, error: 3 }, this.logLevelPrefixs = { debug: "[DEBUG] ", info: "[INFO] ", warn: "[WARN] ", error: "[ERROR] " }, this.logLevel = "info", this.name = t, this.http = new s(this), this.data = null, this.dataFile = "box.dat", this.logs = [], this.isMute = !1, this.isNeedRewrite = !1, this.logSeparator = "\n", this.encoding = "utf-8", this.startTime = (new Date).getTime(), Object.assign(this, e), this.log("", `🔔${this.name}, 开始!`) } getEnv() { return "undefined" != typeof $environment && $environment["surge-version"] ? "Surge" : "undefined" != typeof $environment && $environment["stash-version"] ? "Stash" : "undefined" != typeof module && module.exports ? "Node.js" : "undefined" != typeof $task ? "Quantumult X" : "undefined" != typeof $loon ? "Loon" : "undefined" != typeof $rocket ? "Shadowrocket" : void 0 } isNode() { return "Node.js" === this.getEnv() } isQuanX() { return "Quantumult X" === this.getEnv() } isSurge() { return "Surge" === this.getEnv() } isLoon() { return "Loon" === this.getEnv() } isShadowrocket() { return "Shadowrocket" === this.getEnv() } isStash() { return "Stash" === this.getEnv() } toObj(t, e = null) { try { return JSON.parse(t) } catch { return e } } toStr(t, e = null, ...s) { try { return JSON.stringify(t, ...s) } catch { return e } } getjson(t, e) { let s = e; if (this.getdata(t)) try { s = JSON.parse(this.getdata(t)) } catch { } return s } setjson(t, e) { try { return this.setdata(JSON.stringify(t), e) } catch { return !1 } } getScript(t) { return new Promise((e => { this.get({ url: t }, ((t, s, i) => e(i))) })) } runScript(t, e) { return new Promise((s => { let i = this.getdata("@chavy_boxjs_userCfgs.httpapi"); i = i ? i.replace(/\n/g, "").trim() : i; let o = this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout"); o = o ? 1 * o : 20, o = e && e.timeout ? e.timeout : o; const [r, a] = i.split("@"), n = { url: `http://${a}/v1/scripting/evaluate`, body: { script_text: t, mock_type: "cron", timeout: o }, headers: { "X-Key": r, Accept: "*/*" }, timeout: o }; this.post(n, ((t, e, i) => s(i))) })).catch((t => this.logErr(t))) } loaddata() { if (!this.isNode()) return {}; { this.fs = this.fs ? this.fs : require("fs"), this.path = this.path ? this.path : require("path"); const t = this.path.resolve(this.dataFile), e = this.path.resolve(process.cwd(), this.dataFile), s = this.fs.existsSync(t), i = !s && this.fs.existsSync(e); if (!s && !i) return {}; { const i = s ? t : e; try { return JSON.parse(this.fs.readFileSync(i)) } catch (t) { return {} } } } } writedata() { if (this.isNode()) { this.fs = this.fs ? this.fs : require("fs"), this.path = this.path ? this.path : require("path"); const t = this.path.resolve(this.dataFile), e = this.path.resolve(process.cwd(), this.dataFile), s = this.fs.existsSync(t), i = !s && this.fs.existsSync(e), o = JSON.stringify(this.data); s ? this.fs.writeFileSync(t, o) : i ? this.fs.writeFileSync(e, o) : this.fs.writeFileSync(t, o) } } lodash_get(t, e, s) { const i = e.replace(/\[(\d+)\]/g, ".$1").split("."); let o = t; for (const t of i) if (o = Object(o)[t], void 0 === o) return s; return o } lodash_set(t, e, s) { return Object(t) !== t || (Array.isArray(e) || (e = e.toString().match(/[^.[\]]+/g) || []), e.slice(0, -1).reduce(((t, s, i) => Object(t[s]) === t[s] ? t[s] : t[s] = Math.abs(e[i + 1]) >> 0 == +e[i + 1] ? [] : {}), t)[e[e.length - 1]] = s), t } getdata(t) { let e = this.getval(t); if (/^@/.test(t)) { const [, s, i] = /^@(.*?)\.(.*?)$/.exec(t), o = s ? this.getval(s) : ""; if (o) try { const t = JSON.parse(o); e = t ? this.lodash_get(t, i, "") : e } catch (t) { e = "" } } return e } setdata(t, e) { let s = !1; if (/^@/.test(e)) { const [, i, o] = /^@(.*?)\.(.*?)$/.exec(e), r = this.getval(i), a = i ? "null" === r ? null : r || "{}" : "{}"; try { const e = JSON.parse(a); this.lodash_set(e, o, t), s = this.setval(JSON.stringify(e), i) } catch (e) { const r = {}; this.lodash_set(r, o, t), s = this.setval(JSON.stringify(r), i) } } else s = this.setval(t, e); return s } getval(t) { switch (this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": return $persistentStore.read(t); case "Quantumult X": return $prefs.valueForKey(t); case "Node.js": return this.data = this.loaddata(), this.data[t]; default: return this.data && this.data[t] || null } } setval(t, e) { switch (this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": return $persistentStore.write(t, e); case "Quantumult X": return $prefs.setValueForKey(t, e); case "Node.js": return this.data = this.loaddata(), this.data[e] = t, this.writedata(), !0; default: return this.data && this.data[e] || null } } initGotEnv(t) { this.got = this.got ? this.got : require("got"), this.cktough = this.cktough ? this.cktough : require("tough-cookie"), this.ckjar = this.ckjar ? this.ckjar : new this.cktough.CookieJar, t && (t.headers = t.headers ? t.headers : {}, t && (t.headers = t.headers ? t.headers : {}, void 0 === t.headers.cookie && void 0 === t.headers.Cookie && void 0 === t.cookieJar && (t.cookieJar = this.ckjar))) } get(t, e = (() => { })) { switch (t.headers && (delete t.headers["Content-Type"], delete t.headers["Content-Length"], delete t.headers["content-type"], delete t.headers["content-length"]), t.params && (t.url += "?" + this.queryStr(t.params)), void 0 === t.followRedirect || t.followRedirect || ((this.isSurge() || this.isLoon()) && (t["auto-redirect"] = !1), this.isQuanX() && (t.opts ? t.opts.redirection = !1 : t.opts = { redirection: !1 })), this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": default: this.isSurge() && this.isNeedRewrite && (t.headers = t.headers || {}, Object.assign(t.headers, { "X-Surge-Skip-Scripting": !1 })), $httpClient.get(t, ((t, s, i) => { !t && s && (s.body = i, s.statusCode = s.status ? s.status : s.statusCode, s.status = s.statusCode), e(t, s, i) })); break; case "Quantumult X": this.isNeedRewrite && (t.opts = t.opts || {}, Object.assign(t.opts, { hints: !1 })), $task.fetch(t).then((t => { const { statusCode: s, statusCode: i, headers: o, body: r, bodyBytes: a } = t; e(null, { status: s, statusCode: i, headers: o, body: r, bodyBytes: a }, r, a) }), (t => e(t && t.error || "UndefinedError"))); break; case "Node.js": let s = require("iconv-lite"); this.initGotEnv(t), this.got(t).on("redirect", ((t, e) => { try { if (t.headers["set-cookie"]) { const s = t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString(); s && this.ckjar.setCookieSync(s, null), e.cookieJar = this.ckjar } } catch (t) { this.logErr(t) } })).then((t => { const { statusCode: i, statusCode: o, headers: r, rawBody: a } = t, n = s.decode(a, this.encoding); e(null, { status: i, statusCode: o, headers: r, rawBody: a, body: n }, n) }), (t => { const { message: i, response: o } = t; e(i, o, o && s.decode(o.rawBody, this.encoding)) })); break } } post(t, e = (() => { })) { const s = t.method ? t.method.toLocaleLowerCase() : "post"; switch (t.body && t.headers && !t.headers["Content-Type"] && !t.headers["content-type"] && (t.headers["content-type"] = "application/x-www-form-urlencoded"), t.headers && (delete t.headers["Content-Length"], delete t.headers["content-length"]), void 0 === t.followRedirect || t.followRedirect || ((this.isSurge() || this.isLoon()) && (t["auto-redirect"] = !1), this.isQuanX() && (t.opts ? t.opts.redirection = !1 : t.opts = { redirection: !1 })), this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": default: this.isSurge() && this.isNeedRewrite && (t.headers = t.headers || {}, Object.assign(t.headers, { "X-Surge-Skip-Scripting": !1 })), $httpClient[s](t, ((t, s, i) => { !t && s && (s.body = i, s.statusCode = s.status ? s.status : s.statusCode, s.status = s.statusCode), e(t, s, i) })); break; case "Quantumult X": t.method = s, this.isNeedRewrite && (t.opts = t.opts || {}, Object.assign(t.opts, { hints: !1 })), $task.fetch(t).then((t => { const { statusCode: s, statusCode: i, headers: o, body: r, bodyBytes: a } = t; e(null, { status: s, statusCode: i, headers: o, body: r, bodyBytes: a }, r, a) }), (t => e(t && t.error || "UndefinedError"))); break; case "Node.js": let i = require("iconv-lite"); this.initGotEnv(t); const { url: o, ...r } = t; this.got[s](o, r).then((t => { const { statusCode: s, statusCode: o, headers: r, rawBody: a } = t, n = i.decode(a, this.encoding); e(null, { status: s, statusCode: o, headers: r, rawBody: a, body: n }, n) }), (t => { const { message: s, response: o } = t; e(s, o, o && i.decode(o.rawBody, this.encoding)) })); break } } time(t, e = null) { const s = e ? new Date(e) : new Date; let i = { "M+": s.getMonth() + 1, "d+": s.getDate(), "H+": s.getHours(), "m+": s.getMinutes(), "s+": s.getSeconds(), "q+": Math.floor((s.getMonth() + 3) / 3), S: s.getMilliseconds() }; /(y+)/.test(t) && (t = t.replace(RegExp.$1, (s.getFullYear() + "").substr(4 - RegExp.$1.length))); for (let e in i) new RegExp("(" + e + ")").test(t) && (t = t.replace(RegExp.$1, 1 == RegExp.$1.length ? i[e] : ("00" + i[e]).substr(("" + i[e]).length))); return t } queryStr(t) { let e = ""; for (const s in t) { let i = t[s]; null != i && "" !== i && ("object" == typeof i && (i = JSON.stringify(i)), e += `${s}=${i}&`) } return e = e.substring(0, e.length - 1), e } msg(e = t, s = "", i = "", o) { const r = t => { const { $open: e, $copy: s, $media: i, $mediaMime: o } = t; switch (typeof t) { case void 0: return t; case "string": switch (this.getEnv()) { case "Surge": case "Stash": default: return { url: t }; case "Loon": case "Shadowrocket": return t; case "Quantumult X": return { "open-url": t }; case "Node.js": return }case "object": switch (this.getEnv()) { case "Surge": case "Stash": case "Shadowrocket": default: { const r = {}; let a = t.openUrl || t.url || t["open-url"] || e; a && Object.assign(r, { action: "open-url", url: a }); let n = t["update-pasteboard"] || t.updatePasteboard || s; if (n && Object.assign(r, { action: "clipboard", text: n }), i) { let t, e, s; if (i.startsWith("http")) t = i; else if (i.startsWith("data:")) { const [t] = i.split(";"), [, o] = i.split(","); e = o, s = t.replace("data:", "") } else { e = i, s = (t => { const e = { JVBERi0: "application/pdf", R0lGODdh: "image/gif", R0lGODlh: "image/gif", iVBORw0KGgo: "image/png", "/9j/": "image/jpg" }; for (var s in e) if (0 === t.indexOf(s)) return e[s]; return null })(i) } Object.assign(r, { "media-url": t, "media-base64": e, "media-base64-mime": o ?? s }) } return Object.assign(r, { "auto-dismiss": t["auto-dismiss"], sound: t.sound }), r } case "Loon": { const s = {}; let i = t.openUrl || t.url || t["open-url"] || e; i && Object.assign(s, { openUrl: i }); let o = t.mediaUrl || t["media-url"]; return o && Object.assign(s, { mediaUrl: o }), s } case "Quantumult X": { const i = {}; let o = t["open-url"] || t.url || t.openUrl || e; o && Object.assign(i, { "open-url": o }); let r = t["media-url"] || t.mediaUrl; r && Object.assign(i, { "media-url": r }); let a = t["update-pasteboard"] || t.updatePasteboard || s; return a && Object.assign(i, { "update-pasteboard": a }), i } case "Node.js": return }default: return } }; if (!this.isMute) switch (this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": default: $notification.post(e, s, i, r(o)); break; case "Quantumult X": $notify(e, s, i, r(o)); break; case "Node.js": break }if (!this.isMuteLog) { let t = ["", "==============📣系统通知📣=============="]; t.push(e), s && t.push(s), i && t.push(i), console.log(t.join("\n")), this.logs = this.logs.concat(t) } } debug(...t) { this.logLevels[this.logLevel] <= this.logLevels.debug && (t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(`${this.logLevelPrefixs.debug}${t.map((t => t ?? String(t))).join(this.logSeparator)}`)) } info(...t) { this.logLevels[this.logLevel] <= this.logLevels.info && (t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(`${this.logLevelPrefixs.info}${t.map((t => t ?? String(t))).join(this.logSeparator)}`)) } warn(...t) { this.logLevels[this.logLevel] <= this.logLevels.warn && (t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(`${this.logLevelPrefixs.warn}${t.map((t => t ?? String(t))).join(this.logSeparator)}`)) } error(...t) { this.logLevels[this.logLevel] <= this.logLevels.error && (t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(`${this.logLevelPrefixs.error}${t.map((t => t ?? String(t))).join(this.logSeparator)}`)) } log(...t) { t.length > 0 && (this.logs = [...this.logs, ...t]), console.log(t.map((t => t ?? String(t))).join(this.logSeparator)) } logErr(t, e) { switch (this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": case "Quantumult X": default: this.log("", `❗️${this.name}, 错误!`, e, t); break; case "Node.js": this.log("", `❗️${this.name}, 错误!`, e, void 0 !== t.message ? t.message : t, t.stack); break } } wait(t) { return new Promise((e => setTimeout(e, t))) } done(t = {}) { const e = ((new Date).getTime() - this.startTime) / 1e3; switch (this.log("", `🔔${this.name}, 结束! 🕛 ${e} 秒`), this.log(), this.getEnv()) { case "Surge": case "Loon": case "Stash": case "Shadowrocket": case "Quantumult X": default: $done(t); break; case "Node.js": process.exit(1) } } }(t, e) } diff --git a/jd_price.js b/jd_price.js new file mode 100644 index 00000000..63393497 --- /dev/null +++ b/jd_price.js @@ -0,0 +1,788 @@ +/* + * 脚本名称:京东比价 + * 使用说明:进入APP商品详情页面触发。 + * 支持版本:App V15.0.80(自行测试) + * 脚本作者:小白脸 + * 特别鸣谢:数据逆向@苍井灰灰 + +[Script] +慢慢买 CK = type=http-request,pattern=^https?:\/\/apapia-sqk-weblogic\.manmanbuy\.com/baoliao\/center\/menu,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/MmmCK.js +京东比价 = type=http-response,pattern=^https:\/\/in\.m\.jd\.com\/product\/graphext\/\d+\.html,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/jd_price.js,timeout=30 +[MITM] +hostname = %APPEND% in.m.jd.com, apapia-sqk-weblogic.manmanbuy.com +*/ + +const { $log, $msg, $prs, $http, md5, jsonToCustomString, jsonToQueryString } = + init(); + +const priceHistoryTable = (data) => { + const themeDetection = ` + + `; + + const css = ``; + + let html = ` + ${css} + ${themeDetection} +
+ + + + + + + + + + `; + + data.atts.forEach((row) => { + const statusClass = row.status?.includes("↑") + ? "price-up" + : row.status.includes("↓") + ? "price-down" + : "price-same"; + + const td = Object.keys(row) + .map((item) => ``) + .join(""); + + html += ` + + ${td} + `; + }); + + html += `
+

${data.groupName}

+
类型日期价格状态
${row[item]}
`; + return html; +}; + +const Table = (result) => { + const toDate = (t = Date.now()) => { + const d = new Date(t - new Date().getTimezoneOffset() * 60000); + return d.toISOString().split("T")[0]; + }; + + const getJdData = (data) => { + return data.flatMap(({ ShowName, Difference, Price, Date }) => { + const re = /历史最高|常购价/; + if (re.test(ShowName)) return []; + + return [ + { + name: ShowName, + date: Date || toDate(), + price: Price, + status: Difference.replace("-", "●"), + }, + ]; + }); + }; + + const { ListPriceDetail } = result; + return priceHistoryTable({ + groupName: "历史比价", + atts: getJdData(ListPriceDetail), + }); +}; + +const JdLine = (data) => { + return ` + + + +
+
+

价格走势

+
+
+
+ + `; +}; + + +const getMMdata = async (id) => { + const getmmCK = () => { + const ck = $prs.get("慢慢买CK"); + if (ck) return ck; + throw new Error("未获取 ck,请先打开【慢慢买】APP→我的,获取 ck"); + }; + + const reqOpts = ({ url, buildBody, ...op }) => { + const opt = { + method: "post", + url, + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + "User-Agent": + "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 - mmbWebBrowse - ios", + }, + ...op, + }; + const cb = (args) => { + const reqBody = { + t: Date.now().toString(), + c_appver: "4.8.3.1", + c_mmbDevId: getmmCK(), + ...args, + }; + reqBody.token = md5( + encodeURIComponent( + "3E41D1331F5DDAFCD0A38FE2D52FF66F" + + jsonToCustomString(reqBody) + + "3E41D1331F5DDAFCD0A38FE2D52FF66F" + ) + ).toUpperCase(); + return jsonToQueryString(reqBody); + }; + return { ...opt, body: buildBody(cb) }; + }; + + const apiCall = (url, buildBody) => + $http(reqOpts({ url, buildBody })) + .then((resp) => { + const body = resp.json(); + const { code, msg } = body; + if (code && code !== 2000 && code !== 6001) throw new Error(`${url} ${msg}`); + return body; + }); + + const { result: { spbh, url } } = await apiCall( + "https://apapia-history-weblogic.manmanbuy.com/basic/getItemBasicInfo", + (set) => + set({ + methodName: "getHistoryInfoJava", + searchKey: `https://item.jd.com/${id}.html`, + }) + ) + + const { result: { trend: jiagequshiyh }, msg } = await apiCall( + "https://apapia-history-weblogic.manmanbuy.com/history/v2/getHistoryTrend", + (set) => + set({ + methodName: "getHistoryTrend2021", + spbh, + url, + }) + ) + + if (!jiagequshiyh) return { msg }; + + + const { remark: { ListPriceDetail } } = await apiCall( + "https://apapia-history-weblogic.manmanbuy.com/history/priceRemark", + (set) => + set({ + methodName: "priceRemarkJava", + jiagequshiyh, + }) + ) + + return { + ListPriceDetail, + jiagequshiyh, + } +}; + + +const Render = { + inject(html) { + const { body } = $response; + $response.body = body.replace("", `${html}`); + return this; + }, + done() { + const { body, headers } = $response; + $done({ body, headers }); + }, +}; + +const main = async () => { + try { + const ID = $request.url.match(/\d+/); + const { msg, ...result } = await getMMdata(ID); + + if (msg) { + Render.inject(`

${msg}

`).done(); + return; + } + + const hour = new Date().getHours(); + const isDark = hour >= 20 || hour < 6; + + Render + .inject(Table(result)) + .inject(``) + .done(); + } catch (e) { + $log(e); + $msg(e); + $done({}); + } +}; + +main(); + + + + +function init(){CryptoJS=function(t,r){var n;if("undefined"!=typeof window&&window.crypto&&(n=window.crypto),"undefined"!=typeof self&&self.crypto&&(n=self.crypto),"undefined"!=typeof globalThis&&globalThis.crypto&&(n=globalThis.crypto),!n&&"undefined"!=typeof window&&window.msCrypto&&(n=window.msCrypto),!n&&"undefined"!=typeof global&&global.crypto&&(n=global.crypto),!n&&"function"==typeof require)try{n=require("crypto")}catch(t){}var e=function(){if(n){if("function"==typeof n.getRandomValues)try{return n.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof n.randomBytes)try{return n.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")},i=Object.create||function(){function t(){}return function(r){var n;return t.prototype=r,n=new t,t.prototype=null,n}}(),o={},a=o.lib={},s=a.Base={extend:function(t){var r=i(this);return t&&r.mixIn(t),r.hasOwnProperty("init")&&this.init!==r.init||(r.init=function(){r.$super.init.apply(this,arguments)}),r.init.prototype=r,r.$super=this,r},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var r in t)t.hasOwnProperty(r)&&(this[r]=t[r]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},c=a.WordArray=s.extend({init:function(t,r){t=this.words=t||[],this.sigBytes=null!=r?r:4*t.length},toString:function(t){return(t||f).stringify(this)},concat:function(t){var r=this.words,n=t.words,e=this.sigBytes,i=t.sigBytes;if(this.clamp(),e%4)for(var o=0;o>>2]>>>24-o%4*8&255;r[e+o>>>2]|=a<<24-(e+o)%4*8}else for(var s=0;s>>2]=n[s>>>2];return this.sigBytes+=i,this},clamp:function(){var r=this.words,n=this.sigBytes;r[n>>>2]&=4294967295<<32-n%4*8,r.length=t.ceil(n/4)},clone:function(){var t=s.clone.call(this);return t.words=this.words.slice(0),t},random:function(r){var n,i=[],o=function(r){r=r;var n=987654321,e=4294967295;return function(){var i=((n=36969*(65535&n)+(n>>16)&e)<<16)+(r=18e3*(65535&r)+(r>>16)&e)&e;return i/=4294967296,(i+=.5)*(t.random()>.5?1:-1)}},a=!1;try{e(),a=!0}catch(t){}for(var s,u=0;u>>2]>>>24-i%4*8&255;e.push((o>>>4).toString(16)),e.push((15&o).toString(16))}return e.join("")},parse:function(t){for(var r=t.length,n=[],e=0;e>>3]|=parseInt(t.substr(e,2),16)<<24-e%8*4;return new c.init(n,r/2)}},h=u.Latin1={stringify:function(t){for(var r=t.words,n=t.sigBytes,e=[],i=0;i>>2]>>>24-i%4*8&255;e.push(String.fromCharCode(o))}return e.join("")},parse:function(t){for(var r=t.length,n=[],e=0;e>>2]|=(255&t.charCodeAt(e))<<24-e%4*8;return new c.init(n,r)}},p=u.Utf8={stringify:function(t){try{return decodeURIComponent(escape(h.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return h.parse(unescape(encodeURIComponent(t)))}},d=a.BufferedBlockAlgorithm=s.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=p.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(r){var n,e=this._data,i=e.words,o=e.sigBytes,a=this.blockSize,s=o/(4*a),u=(s=r?t.ceil(s):t.max((0|s)-this._minBufferSize,0))*a,f=t.min(4*u,o);if(u){for(var h=0;h>>24)|4278255360&(i<<24|i>>>8)}var o=this._hash.words,s=t[r+0],p=t[r+1],d=t[r+2],l=t[r+3],y=t[r+4],v=t[r+5],g=t[r+6],w=t[r+7],_=t[r+8],m=t[r+9],B=t[r+10],b=t[r+11],C=t[r+12],S=t[r+13],x=t[r+14],A=t[r+15],H=o[0],z=o[1],M=o[2],D=o[3];z=h(z=h(z=h(z=h(z=f(z=f(z=f(z=f(z=u(z=u(z=u(z=u(z=c(z=c(z=c(z=c(z,M=c(M,D=c(D,H=c(H,z,M,D,s,7,a[0]),z,M,p,12,a[1]),H,z,d,17,a[2]),D,H,l,22,a[3]),M=c(M,D=c(D,H=c(H,z,M,D,y,7,a[4]),z,M,v,12,a[5]),H,z,g,17,a[6]),D,H,w,22,a[7]),M=c(M,D=c(D,H=c(H,z,M,D,_,7,a[8]),z,M,m,12,a[9]),H,z,B,17,a[10]),D,H,b,22,a[11]),M=c(M,D=c(D,H=c(H,z,M,D,C,7,a[12]),z,M,S,12,a[13]),H,z,x,17,a[14]),D,H,A,22,a[15]),M=u(M,D=u(D,H=u(H,z,M,D,p,5,a[16]),z,M,g,9,a[17]),H,z,b,14,a[18]),D,H,s,20,a[19]),M=u(M,D=u(D,H=u(H,z,M,D,v,5,a[20]),z,M,B,9,a[21]),H,z,A,14,a[22]),D,H,y,20,a[23]),M=u(M,D=u(D,H=u(H,z,M,D,m,5,a[24]),z,M,x,9,a[25]),H,z,l,14,a[26]),D,H,_,20,a[27]),M=u(M,D=u(D,H=u(H,z,M,D,S,5,a[28]),z,M,d,9,a[29]),H,z,w,14,a[30]),D,H,C,20,a[31]),M=f(M,D=f(D,H=f(H,z,M,D,v,4,a[32]),z,M,_,11,a[33]),H,z,b,16,a[34]),D,H,x,23,a[35]),M=f(M,D=f(D,H=f(H,z,M,D,p,4,a[36]),z,M,y,11,a[37]),H,z,w,16,a[38]),D,H,B,23,a[39]),M=f(M,D=f(D,H=f(H,z,M,D,S,4,a[40]),z,M,s,11,a[41]),H,z,l,16,a[42]),D,H,g,23,a[43]),M=f(M,D=f(D,H=f(H,z,M,D,m,4,a[44]),z,M,C,11,a[45]),H,z,A,16,a[46]),D,H,d,23,a[47]),M=h(M,D=h(D,H=h(H,z,M,D,s,6,a[48]),z,M,w,10,a[49]),H,z,x,15,a[50]),D,H,v,21,a[51]),M=h(M,D=h(D,H=h(H,z,M,D,C,6,a[52]),z,M,l,10,a[53]),H,z,B,15,a[54]),D,H,p,21,a[55]),M=h(M,D=h(D,H=h(H,z,M,D,_,6,a[56]),z,M,A,10,a[57]),H,z,g,15,a[58]),D,H,S,21,a[59]),M=h(M,D=h(D,H=h(H,z,M,D,y,6,a[60]),z,M,b,10,a[61]),H,z,d,15,a[62]),D,H,m,21,a[63]),o[0]=o[0]+H|0,o[1]=o[1]+z|0,o[2]=o[2]+M|0,o[3]=o[3]+D|0},_doFinalize:function(){var r=this._data,n=r.words,e=8*this._nDataBytes,i=8*r.sigBytes;n[i>>>5]|=128<<24-i%32;var o=t.floor(e/4294967296),a=e;n[15+(i+64>>>9<<4)]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),n[14+(i+64>>>9<<4)]=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8),r.sigBytes=4*(n.length+1),this._process();for(var s=this._hash,c=s.words,u=0;u<4;u++){var f=c[u];c[u]=16711935&(f<<8|f>>>24)|4278255360&(f<<24|f>>>8)}return s},clone:function(){var t=i.clone.call(this);return t._hash=this._hash.clone(),t}});function c(t,r,n,e,i,o,a){var s=t+(r&n|~r&e)+i+a;return(s<>>32-o)+r}function u(t,r,n,e,i,o,a){var s=t+(r&e|n&~e)+i+a;return(s<>>32-o)+r}function f(t,r,n,e,i,o,a){var s=t+(r^n^e)+i+a;return(s<>>32-o)+r}function h(t,r,n,e,i,o,a){var s=t+(n^(r|~e))+i+a;return(s<>>32-o)+r}r.MD5=i._createHelper(s),r.HmacMD5=i._createHmacHelper(s)}(Math),function(){var t=CryptoJS,r=t.lib.WordArray;t.enc.Base64={stringify:function(t){var r=t.words,n=t.sigBytes,e=this._map;t.clamp();for(var i=[],o=0;o>>2]>>>24-o%4*8&255)<<16|(r[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|r[o+2>>>2]>>>24-(o+2)%4*8&255,s=0;s<4&&o+.75*s>>6*(3-s)&63));var c=e.charAt(64);if(c)for(;i.length%4;)i.push(c);return i.join("")},parse:function(t){var n=t.length,e=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var o=0;o>>6-a%4*2;i[o>>>2]|=(s|c)<<24-o%4*8,o++}return r.create(i,o)}(t,n,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); + +function md5(word){return CryptoJS.MD5(word).toString();} + +function jsonToQueryString(jsonObject) {return Object.keys(jsonObject).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(jsonObject[key])}`).join('&');} + + +function jsonToCustomString(jsonObject){return Object.keys(jsonObject).filter(key=>jsonObject[key]!==''&&key.toLowerCase()!=='token').sort().map(key=>`${key.toUpperCase()}${jsonObject[key].toUpperCase()}`).join('');} + + Promise.withResolvers ||= function () { + let resolve, reject; + const promise = new this((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + }; + + const $http = (op, t = 4) => { + const { promise, resolve, reject } = Promise.withResolvers(); + const HTTPError = (e, req, res) => + Object.assign(new Error(e), { + name: "HTTPError", + request: req, + response: res, + }); + + const handleRes = ({ bodyBytes, ...res }) => { + res.status ??= res.statusCode; + res.json = () => JSON.parse(res.body); + if (res.headers?.["binary-mode"] && bodyBytes) + res.body = new Uint8Array(bodyBytes); + + res.error || res.status < 200 || res.status > 307 + ? reject(HTTPError(res.error, op, res)) + : resolve(res); + }; + + const timer = setTimeout( + () => reject(HTTPError("timeout", op)), + op.$timeout ?? t * 1000 + ); + this.$httpClient?.[op.method || "get"](op, (error, resp, body) => { + handleRes({ error, ...resp, body }); + }); + this.$task?.fetch({ url: op, ...op }).then(handleRes, handleRes); + + return promise.finally(() => clearTimeout(timer)); + }; + + const $prs = { + get: this.$prefs?.valueForKey ?? $persistentStore.read, + getJson: (key) => JSON.parse($prs.get(key), null, 4), + set: (key, value) => + (this.$prefs?.setValueForKey ?? $persistentStore.write)(value, key), + setJson: (key, obj) => $prs.set(key, JSON.stringify(obj)), + }; + + const $msg = (...a) => { + const { $open, $copy, $media, ...r } = + typeof a.at(-1) === "object" && a.pop(); + const [t = "", s = "", b = ""] = a; + (this.$notify ??= $notification.post)(t, s, b, { + action: $copy ? "clipboard" : "open-url", + text: $copy, + "update-pasteboard": $copy, + clipboard: $copy, + "open-url": $open, + openUrl: $open, + url: $open, + mediaUrl: $media, + "media-url": $media, + ...r, + }); + }; + + const $log = new Proxy( + (...args) => + args.forEach((i) => + console.log( + i?.stack + ? `${i.toString()}\n${i.stack}` + : typeof i === "object" + ? JSON.stringify(i, null, 4) + : String(i) + ) + ), + { + get(target) { + if (!target.init) { + target.time = (id) => (target.time[id] = Date.now()); + target.timeEnd = (id) => target(Date.now() - target.time[id]); + target.show = + (...a) => + (b) => + b && target(...a); + target.init = true; + } + + return Reflect.get(...arguments); + }, + } + ); + + return { + $log, + $msg, + $prs, + $http, + md5, + jsonToCustomString, + jsonToQueryString, + }; +}; diff --git a/jd_price1.js b/jd_price1.js new file mode 100644 index 00000000..0e4e8ce1 --- /dev/null +++ b/jd_price1.js @@ -0,0 +1,425 @@ +/** + * 京东购物助手,京推推转链+比价图表 + * + * * 2025-07-28 15:38 + * 修复比价,参考灰灰代码 + * * 2025-05-29 16:55 + * 新增点击"价格趋势"标题,跳转至慢慢买app比价 + * * 2025-05-17 07:50 + * 优化代码,删除沉淀部分 + * * 2015-05-16 23:30 + * 更新接口 + * * 2025-05-01 14:53 + * 优化代码逻辑 + * * 2025-04-24 15:19 + * 比价图表适配暗黑模式,UI细节处理 + * * 2025-04-22 12:11 + * 增加京推推商品返利转链 + * 增加比价折线图表格显示 + * 比价代码@苍井灰灰 + * * Surge模块设置参数,详见模块注释内容 + * 模块链接:https://raw.githubusercontent.com/githubdulong/Script/master/Surge/JD_Helper.sgmodule + */ + +const path1 = "/product/graphext/"; +const path2 = "/baoliao/center/menu"; +const manmanbuy_key = "manmanbuy_val"; +const requestUrl = $request.url; +const $ = new Env("京东助手"); + +function checkRes(res, desc = '') { + if (!res || (typeof res.code !== 'undefined' && res.code !== 2000 && res.code !== 6001)) { + $.log('慢慢买请求异常:' + $.toStr(res)); + throw new Error(`慢慢买提示您:${res.msg || `${desc}失败`}`); + } + return res; +} + +async function mmbRequest(Params, url) { + if (!$.manmanbuy) { + $.manmanbuy = getck(); + } + const SECRET_KEY = '3E41D1331F5DDAFCD0A38FE2D52FF66F'; + const requestBody = { + ...$.manmanbuy, + ...Params, + t: Date.now().toString(), + c_appver: "4.8.3.1" + }; + delete requestBody.token; + + requestBody.token = md5(encodeURIComponent(SECRET_KEY + jsonToCustomString(requestBody) + SECRET_KEY)).toUpperCase(); + const payloadStr = jsonToQueryString(requestBody); + + const opt = { + url, + method: 'POST', + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 - mmbWebBrowse - ios" + }, + body: payloadStr, + _timeout: 5000 + }; + return await httpRequest(opt); +} + +async function get_spbh(link) { + const url = 'https://apapia-history-weblogic.manmanbuy.com/basic/getItemBasicInfo'; + const payload = { + methodName: "getHistoryInfoJava", + searchKey: link + }; + return await mmbRequest(payload, url); +} + +async function get_jiagequshi(link, spbh) { + const url = "https://apapia-history-weblogic.manmanbuy.com/history/v2/getHistoryTrend"; + const payload = { + methodName: "getHistoryTrend2021", + url: link, + spbh: spbh + }; + return await mmbRequest(payload, url); +} + +async function get_priceRemark(jiagequshiyh) { + const url = "https://apapia-history-weblogic.manmanbuy.com/history/priceRemark"; + const payload = { + methodName: "priceRemarkJava", + jiagequshiyh: jiagequshiyh + }; + return await mmbRequest(payload, url); +} + + +const getMMdata = async (id) => { + const JD_Url = `https://item.jd.com/${id}.html`; + + const basicInfo = checkRes(await get_spbh(JD_Url), '获取商品编号'); + const spbh = basicInfo?.result?.spbh; + const itemUrl = basicInfo?.result?.url; + if (!spbh || !itemUrl) throw new Error("未能获取到慢慢买商品编号(spbh)"); + + const historyTrend = checkRes(await get_jiagequshi(itemUrl, spbh), '获取价格趋势'); + const jiagequshiyh = historyTrend?.result?.trend; + if (!jiagequshiyh) { + $.log("警告:未能获取到价格趋势图(jiagequshiyh),但仍将尝试获取价格列表。"); + } + + const priceRemark = checkRes(await get_priceRemark(jiagequshiyh), '获取价格详情'); + const ListPriceDetail = priceRemark?.remark?.ListPriceDetail; + if (!ListPriceDetail) throw new Error("未能获取到详细价格列表"); + + return { + ListPriceDetail, + jiagequshiyh, + spbh, + }; +}; + +let args = + typeof $argument === "string" + ? $argument + : typeof $argument === "object" && $argument !== null + ? Object.entries($argument) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join("&") + : ""; +$.log(`读取参数: ${args}`); +const argObj = Object.fromEntries( + args.split("&").filter(item => item.includes("=")).map((item) => item.split("=").map(decodeURIComponent)) +); +const isEmpty = (val) => val === undefined || val === null || val === "" || val === "null"; + +$.jd_unionId = !isEmpty(argObj["jd_union_id"]) ? argObj["jd_union_id"] : $.getdata("jd_unionId") || ""; +$.jd_positionId = !isEmpty(argObj["jd_position_id"]) ? argObj["jd_position_id"] : $.getdata("jd_positionId") || ""; +$.jtt_appid = !isEmpty(argObj["jtt_appid"]) ? argObj["jtt_appid"] : $.getdata("jtt_appid") || ""; +$.jtt_appkey = !isEmpty(argObj["jtt_appkey"]) ? argObj["jtt_appkey"] : $.getdata("jtt_appkey") || ""; +$.disableNotice = argObj["disable_notice"] === "false"; +const defaultThemeTime = "7-19"; +$.themeTime = !isEmpty(argObj["theme_time"]) ? argObj["theme_time"] : $.getdata("theme_time") || defaultThemeTime; + +if (requestUrl.includes(path2)) { + const reqbody = $request.body; + $.setdata(reqbody, manmanbuy_key); + $.msg($.name, "获取CK成功 🎉", reqbody); +} + +if (requestUrl.includes(path1)) { + intCryptoJS(); + $.manmanbuy = getck(); + let currentReqUrl = $request.url; + $.appType = currentReqUrl.includes("lite-in.m.jd.com") ? "jdtj" : "jd"; + + (async () => { + const match = currentReqUrl.match(/product\/graphext\/(\d+)\.html/); + if (!match) { + $done({}); + return; + } + + const productId = match[1]; + try { + if (!$.disableNotice && $.jd_unionId && $.jtt_appid && $.jtt_appkey) { + $.sku = productId; + await jingfenJingTuiTui(); + await notice(); + } else if ($.disableNotice) { + $.log("京推推返利和通知已禁用,仅显示比价图表"); + } + + const { ListPriceDetail, msg: mmMsg, spbh } = await getMMdata(productId); + + if (!ListPriceDetail && !spbh) { + throw new Error(mmMsg || '从慢慢买获取价格详情或商品编号失败'); + } + if (!ListPriceDetail) { + $.log(`警告: 从慢慢买获取价格详情失败 (${mmMsg || ''}), 但获得了商品编号 spbh: ${spbh}`); + } + + const exclude = new Set(["常购价格", "历史最高价"]); + const list = (ListPriceDetail || []).filter((i) => !exclude.has(i.Name)); + + const html = buildPriceTableHTML(list, productId, spbh); + const newBody = $response.body.replace( + /]*>/, + (bodyMatch) => `${bodyMatch}\n${html}` + ); + $done({ body: newBody }); + } catch (err) { + $.logErr(err.message || $.toStr(err)); + const clickAction = `const url='https://item.jd.com/${productId}.html'; if(navigator.clipboard){navigator.clipboard.writeText(url).finally(()=>{window.location.href='manmanbuy://'})}else{window.location.href='manmanbuy://'}`; + const errorHtml = `
比价失败:${err.message || '未知错误'}
`; + const newBody = $response.body.replace(/]*>/, bodyMatch => bodyMatch + errorHtml); + $done({ body: newBody }); + } + })(); +} + +async function jingfenJingTuiTui() { + $.log("转链开始"); + return new Promise((resolve) => { + const options = { + url: `http://japi.jingtuitui.com/api/universal?appid=${$.jtt_appid}&appkey=${$.jtt_appkey}&v=v3&unionid=${$.jd_unionId}&positionid=${$.jd_positionId}&content=https://item.jd.com/${$.sku}.html`, + timeout: 20000, + headers: { "Content-Type": "application/json;charset=utf-8" }, + }; + + $.get(options, (err, resp, data) => { + if (err) { + $.log("京推推 universal 请求失败:" + $.toStr(err)); + } else { + try { + data = JSON.parse(data); + if (data["return"] == 0) { + const linkData = data?.result?.link_date?.[0] || {}; + const { chain_link, goods_info } = linkData; + if (goods_info) { + const { skuName, imageInfo, commissionInfo, priceInfo } = goods_info; + $.commissionShare = commissionInfo.commissionShare; + $.commission = commissionInfo.couponCommission; + $.price = priceInfo.lowestPrice; + $.skuName = skuName; + $.skuImg = imageInfo.imageList?.[0]?.url; + } + $.shortUrl = chain_link || ""; + $.log("转链完成,短链地址:" + $.shortUrl); + } else { + $.log("转链返回异常:" + JSON.stringify(data)); + } + } catch (e) { + $.logErr("JSON 解析失败: " + e.message); + } + } + resolve(); + }); + }); +} + +async function notice() { + $.log("发送通知"); + $.title = $.skuName || "商品信息"; + $.opts = { "auto-dismiss": 30 }; + $.desc = $.desc || ""; + if (/u\.jd\.com/.test($.shortUrl)) { + $.desc += `预计返利: ¥${(($.price * $.commissionShare) / 100).toFixed(2)} ${$.commissionShare}%`; + if ($.appType === "jdtj") { + $.jumpUrl = `openjdlite://virtual?params=${encodeURIComponent('{"category":"jump","des":"m","url":"' + $.shortUrl + '"}')}`; + } else { + $.jumpUrl = `openApp.jdMobile://virtual?params=${encodeURIComponent('{"category":"jump","des":"m","sourceValue":"babel-act","sourceType":"babel","url":"' + $.shortUrl + '"}')}`; + } + $.opts["$open"] = $.jumpUrl; + } else { + $.desc += "\n预计返利: 暂无"; + } + if ($.skuImg) $.opts["$media"] = $.skuImg; + $.msg($.title, $.subt, $.desc, $.opts); +} + +function buildPriceTableHTML(priceList, productId, spbh) { + const rows = (priceList || []).map(item => { + let { Name: name, Date: date, Price: price = "", Difference: diff = "" } = item; + date = name === "当前到手价" ? (typeof $.time === "function" ? $.time("yyyy-MM-dd") : new Date().toISOString().split("T")[0]) : date || "-"; + let diffClass = diff.startsWith("↑") ? "up" : diff.startsWith("↓") ? "down" : ""; + return `${name}${date}${price}${diff}`; + }).join(""); + + const chartData = (priceList || []).filter(i => i.Price && !isNaN(parseFloat(String(i.Price).replace(/[¥\s]/g, "")))).map(i => ({ + date: i.Name === "当前到手价" ? (typeof $.time === "function" ? $.time("yyyy-MM-dd") : new Date().toISOString().split("T")[0]) : i.Date || "-", + price: parseFloat(String(i.Price).replace(/[¥\s]/g, "")), + })).sort((a, b) => new Date(a.date) - new Date(b.date)); + + const labels = chartData.map(i => i.date); + const prices = chartData.map(i => i.price); + const sProductId = JSON.stringify(productId); + + return ` +
+ + + ${rows || ''} +
类型日期价格差价
暂无历史价格数据
+ +
+ + +`; +} + +function getck() { + const ck = $.getdata(manmanbuy_key); + if (!ck) throw new Error("请先打开【慢慢买】APP获取CK"); + const Params = parseQueryString(ck); + if (!Params?.c_mmbDevId) throw new Error("CK格式不正确,请重新获取"); + return Params; +} + +async function httpRequest(options) { + try { + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`⛔️ 请求超时: ${options.url}`)), options._timeout || 10000); + $[options.method.toLowerCase()](options, (error, response, data) => { + clearTimeout(timer); + if (error) reject(error); + else resolve($.toObj(data, data)); + }); + }); + } catch (err) { + return Promise.reject(err); + } +} + +function parseQueryString(queryString) { + const jsonObject = {}; + if (!queryString) return jsonObject; + const pairs = queryString.split("&"); + pairs.forEach((pair) => { + const parts = pair.split("=", 2); + if (parts.length >= 1 && parts[0] !== "") { + jsonObject[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1] || ""); + } + }); + return jsonObject; +} + +function jsonToQueryString(jsonObject) { + return Object.keys(jsonObject).map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(jsonObject[key])}`).join("&"); +} + +function jsonToCustomString(jsonObject) { + return Object.keys(jsonObject).filter((key) => jsonObject[key] != null && jsonObject[key] !== "" && key.toLowerCase() !== "token").sort().map((key) => `${key.toUpperCase()}${String(jsonObject[key]).toUpperCase()}`).join(""); +} + +function intCryptoJS(){CryptoJS=function(t,r){var n;if("undefined"!=typeof window&&window.crypto&&(n=window.crypto),"undefined"!=typeof self&&self.crypto&&(n=self.crypto),"undefined"!=typeof globalThis&&globalThis.crypto&&(n=globalThis.crypto),!n&&"undefined"!=typeof window&&window.msCrypto&&(n=window.msCrypto),!n&&"undefined"!=typeof global&&global.crypto&&(n=global.crypto),!n&&"function"==typeof require)try{n=require("crypto")}catch(t){}var e=function(){if(n){if("function"==typeof n.getRandomValues)try{return n.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof n.randomBytes)try{return n.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")},i=Object.create||function(){function t(){}return function(r){var n;return t.prototype=r,n=new t,t.prototype=null,n}}(),o={},a=o.lib={},s=a.Base={extend:function(t){var r=i(this);return t&&r.mixIn(t),r.hasOwnProperty("init")&&this.init!==r.init||(r.init=function(){r.$super.init.apply(this,arguments)}),r.init.prototype=r,r.$super=this,r},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var r in t)t.hasOwnProperty(r)&&(this[r]=t[r]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},c=a.WordArray=s.extend({init:function(t,r){t=this.words=t||[],this.sigBytes=null!=r?r:4*t.length},toString:function(t){return(t||f).stringify(this)},concat:function(t){var r=this.words,n=t.words,e=this.sigBytes,i=t.sigBytes;if(this.clamp(),e%4)for(var o=0;o>>2]>>>24-o%4*8&255;r[e+o>>>2]|=a<<24-(e+o)%4*8}else for(var s=0;s>>2]=n[s>>>2];return this.sigBytes+=i,this},clamp:function(){var r=this.words,n=this.sigBytes;r[n>>>2]&=4294967295<<32-n%4*8,r.length=t.ceil(n/4)},clone:function(){var t=s.clone.call(this);return t.words=this.words.slice(0),t},random:function(r){var n,i=[],o=function(r){r=r;var n=987654321,e=4294967295;return function(){var i=((n=36969*(65535&n)+(n>>16)&e)<<16)+(r=18e3*(65535&r)+(r>>16)&e)&e;return i/=4294967296,(i+=.5)*(t.random()>.5?1:-1)}},a=!1;try{e(),a=!0}catch(t){}for(var s,u=0;u>>2]>>>24-i%4*8&255;e.push((o>>>4).toString(16)),e.push((15&o).toString(16))}return e.join("")},parse:function(t){for(var r=t.length,n=[],e=0;e>>3]|=parseInt(t.substr(e,2),16)<<24-e%8*4;return new c.init(n,r/2)}},h=u.Latin1={stringify:function(t){for(var r=t.words,n=t.sigBytes,e=[],i=0;i>>2]>>>24-i%4*8&255;e.push(String.fromCharCode(o))}return e.join("")},parse:function(t){for(var r=t.length,n=[],e=0;e>>2]|=(255&t.charCodeAt(e))<<24-e%4*8;return new c.init(n,r)}},p=u.Utf8={stringify:function(t){try{return decodeURIComponent(escape(h.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return h.parse(unescape(encodeURIComponent(t)))}},d=a.BufferedBlockAlgorithm=s.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=p.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(r){var n,e=this._data,i=e.words,o=e.sigBytes,a=this.blockSize,s=o/(4*a),u=(s=r?t.ceil(s):t.max((0|s)-this._minBufferSize,0))*a,f=t.min(4*u,o);if(u){for(var h=0;h>>24)|4278255360&(i<<24|i>>>8)}var o=this._hash.words,s=t[r+0],p=t[r+1],d=t[r+2],l=t[r+3],y=t[r+4],v=t[r+5],g=t[r+6],w=t[r+7],_=t[r+8],m=t[r+9],B=t[r+10],b=t[r+11],C=t[r+12],S=t[r+13],x=t[r+14],A=t[r+15],H=o[0],z=o[1],M=o[2],D=o[3];z=h(z=h(z=h(z=h(z=f(z=f(z=f(z=f(z=u(z=u(z=u(z=u(z=c(z=c(z=c(z=c(z,M=c(M,D=c(D,H=c(H,z,M,D,s,7,a[0]),z,M,p,12,a[1]),H,z,d,17,a[2]),D,H,l,22,a[3]),M=c(M,D=c(D,H=c(H,z,M,D,y,7,a[4]),z,M,v,12,a[5]),H,z,g,17,a[6]),D,H,w,22,a[7]),M=c(M,D=c(D,H=c(H,z,M,D,_,7,a[8]),z,M,m,12,a[9]),H,z,B,17,a[10]),D,H,b,22,a[11]),M=c(M,D=c(D,H=c(H,z,M,D,C,7,a[12]),z,M,S,12,a[13]),H,z,x,17,a[14]),D,H,A,22,a[15]),M=u(M,D=u(D,H=u(H,z,M,D,p,5,a[16]),z,M,g,9,a[17]),H,z,b,14,a[18]),D,H,s,20,a[19]),M=u(M,D=u(D,H=u(H,z,M,D,v,5,a[20]),z,M,B,9,a[21]),H,z,A,14,a[22]),D,H,y,20,a[23]),M=u(M,D=u(D,H=u(H,z,M,D,m,5,a[24]),z,M,x,9,a[25]),H,z,l,14,a[26]),D,H,_,20,a[27]),M=u(M,D=u(D,H=u(H,z,M,D,S,5,a[28]),z,M,d,9,a[29]),H,z,w,14,a[30]),D,H,C,20,a[31]),M=f(M,D=f(D,H=f(H,z,M,D,v,4,a[32]),z,M,_,11,a[33]),H,z,b,16,a[34]),D,H,x,23,a[35]),M=f(M,D=f(D,H=f(H,z,M,D,p,4,a[36]),z,M,y,11,a[37]),H,z,w,16,a[38]),D,H,B,23,a[39]),M=f(M,D=f(D,H=f(H,z,M,D,S,4,a[40]),z,M,s,11,a[41]),H,z,l,16,a[42]),D,H,g,23,a[43]),M=f(M,D=f(D,H=f(H,z,M,D,m,4,a[44]),z,M,C,11,a[45]),H,z,A,16,a[46]),D,H,d,23,a[47]),M=h(M,D=h(D,H=h(H,z,M,D,s,6,a[48]),z,M,w,10,a[49]),H,z,x,15,a[50]),D,H,v,21,a[51]),M=h(M,D=h(D,H=h(H,z,M,D,C,6,a[52]),z,M,l,10,a[53]),H,z,B,15,a[54]),D,H,p,21,a[55]),M=h(M,D=h(D,H=h(H,z,M,D,_,6,a[56]),z,M,A,10,a[57]),H,z,g,15,a[58]),D,H,S,21,a[59]),M=h(M,D=h(D,H=h(H,z,M,D,y,6,a[60]),z,M,b,10,a[61]),H,z,d,15,a[62]),D,H,m,21,a[63]),o[0]=o[0]+H|0,o[1]=o[1]+z|0,o[2]=o[2]+M|0,o[3]=o[3]+D|0},_doFinalize:function(){var r=this._data,n=r.words,e=8*this._nDataBytes,i=8*r.sigBytes;n[i>>>5]|=128<<24-i%32;var o=t.floor(e/4294967296),a=e;n[15+(i+64>>>9<<4)]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),n[14+(i+64>>>9<<4)]=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8),r.sigBytes=4*(n.length+1),this._process();for(var s=this._hash,c=s.words,u=0;u<4;u++){var f=c[u];c[u]=16711935&(f<<8|f>>>24)|4278255360&(f<<24|f>>>8)}return s},clone:function(){var t=i.clone.call(this);return t._hash=this._hash.clone(),t}});function c(t,r,n,e,i,o,a){var s=t+(r&n|~r&e)+i+a;return(s<>>32-o)+r}function u(t,r,n,e,i,o,a){var s=t+(r&e|n&~e)+i+a;return(s<>>32-o)+r}function f(t,r,n,e,i,o,a){var s=t+(r^n^e)+i+a;return(s<>>32-o)+r}function h(t,r,n,e,i,o,a){var s=t+(n^(r|~e))+i+a;return(s<>>32-o)+r}r.MD5=i._createHelper(s),r.HmacMD5=i._createHmacHelper(s)}(Math),function(){var t=CryptoJS,r=t.lib.WordArray;t.enc.Base64={stringify:function(t){var r=t.words,n=t.sigBytes,e=this._map;t.clamp();for(var i=[],o=0;o>>2]>>>24-o%4*8&255)<<16|(r[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|r[o+2>>>2]>>>24-(o+2)%4*8&255,s=0;s<4&&o+.75*s>>6*(3-s)&63));var c=e.charAt(64);if(c)for(;i.length%4;)i.push(c);return i.join("")},parse:function(t){var n=t.length,e=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var o=0;o>>6-a%4*2;i[o>>>2]|=(s|c)<<24-o%4*8,o++}return r.create(i,o)}(t,n,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}();};function md5(word){return CryptoJS.MD5(word).toString();} + +function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;"POST"===e&&(s=this.post);const i=new Promise(((e,i)=>{s.call(this,t,((t,s,o)=>{t?i(t):e(s)}))}));return t.timeout?((t,e=1e3)=>Promise.race([t,new Promise(((t,s)=>{setTimeout((()=>{s(new Error("请求超时"))}),e)}))]))(i,t.timeout):i}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.logLevels={debug:0,info:1,warn:2,error:3},this.logLevelPrefixs={debug:"[DEBUG] ",info:"[INFO] ",warn:"[WARN] ",error:"[ERROR] "},this.logLevel="info",this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`🔔${this.name}, 开始!`)}getEnv(){return"undefined"!=typeof $environment&&$environment["surge-version"]?"Surge":"undefined"!=typeof $environment&&$environment["stash-version"]?"Stash":"undefined"!=typeof module&&module.exports?"Node.js":"undefined"!=typeof $task?"Quantumult X":"undefined"!=typeof $loon?"Loon":"undefined"!=typeof $rocket?"Shadowrocket":void 0}isNode(){return"Node.js"===this.getEnv()}isQuanX(){return"Quantumult X"===this.getEnv()}isSurge(){return"Surge"===this.getEnv()}isLoon(){return"Loon"===this.getEnv()}isShadowrocket(){return"Shadowrocket"===this.getEnv()}isStash(){return"Stash"===this.getEnv()}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null,...s){try{return JSON.stringify(t,...s)}catch{return e}}getjson(t,e){let s=e;if(this.getdata(t))try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise((e=>{this.get({url:t},((t,s,i)=>e(i)))}))}runScript(t,e){return new Promise((s=>{let i=this.getdata("@chavy_boxjs_userCfgs.httpapi");i=i?i.replace(/\n/g,"").trim():i;let o=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");o=o?1*o:20,o=e&&e.timeout?e.timeout:o;const[r,a]=i.split("@"),n={url:`http://${a}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:o},headers:{"X-Key":r,Accept:"*/*"},policy:"DIRECT",timeout:o};this.post(n,((t,e,i)=>s(i)))})).catch((t=>this.logErr(t)))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e);if(!s&&!i)return{};{const i=s?t:e;try{return JSON.parse(this.fs.readFileSync(i))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e),o=JSON.stringify(this.data);s?this.fs.writeFileSync(t,o):i?this.fs.writeFileSync(e,o):this.fs.writeFileSync(t,o)}}lodash_get(t,e,s){const i=e.replace(/\[(\d+)\]/g,".$1").split(".");let o=t;for(const t of i)if(o=Object(o)[t],void 0===o)return s;return o}lodash_set(t,e,s){return Object(t)!==t||(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce(((t,s,i)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[i+1])>>0==+e[i+1]?[]:{}),t)[e[e.length-1]]=s),t}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,i]=/^@(.*?)\.(.*?)$/.exec(t),o=s?this.getval(s):"";if(o)try{const t=JSON.parse(o);e=t?this.lodash_get(t,i,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,i,o]=/^@(.*?)\.(.*?)$/.exec(e),r=this.getval(i),a=i?"null"===r?null:r||"{}":"{}";try{const e=JSON.parse(a);this.lodash_set(e,o,t),s=this.setval(JSON.stringify(e),i)}catch(e){const r={};this.lodash_set(r,o,t),s=this.setval(JSON.stringify(r),i)}}else s=this.setval(t,e);return s}getval(t){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.read(t);case"Quantumult X":return $prefs.valueForKey(t);case"Node.js":return this.data=this.loaddata(),this.data[t];default:return this.data&&this.data[t]||null}}setval(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.write(t,e);case"Quantumult X":return $prefs.setValueForKey(t,e);case"Node.js":return this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0;default:return this.data&&this.data[e]||null}}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.cookie&&void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar)))}get(t,e=(()=>{})){switch(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"],delete t.headers["content-type"],delete t.headers["content-length"]),t.params&&(t.url+="?"+this.queryStr(t.params)),void 0===t.followRedirect||t.followRedirect||((this.isSurge()||this.isLoon())&&(t["auto-redirect"]=!1),this.isQuanX()&&(t.opts?t.opts.redirection=!1:t.opts={redirection:!1})),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,((t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,i)}));break;case"Quantumult X":this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then((t=>{const{statusCode:s,statusCode:i,headers:o,body:r,bodyBytes:a}=t;e(null,{status:s,statusCode:i,headers:o,body:r,bodyBytes:a},r,a)}),(t=>e(t&&t.error||"UndefinedError")));break;case"Node.js":let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",((t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}})).then((t=>{const{statusCode:i,statusCode:o,headers:r,rawBody:a}=t,n=s.decode(a,this.encoding);e(null,{status:i,statusCode:o,headers:r,rawBody:a,body:n},n)}),(t=>{const{message:i,response:o}=t;e(i,o,o&&s.decode(o.rawBody,this.encoding))}));break}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";switch(t.body&&t.headers&&!t.headers["Content-Type"]&&!t.headers["content-type"]&&(t.headers["content-type"]="application/x-www-form-urlencoded"),t.headers&&(delete t.headers["Content-Length"],delete t.headers["content-length"]),void 0===t.followRedirect||t.followRedirect||((this.isSurge()||this.isLoon())&&(t["auto-redirect"]=!1),this.isQuanX()&&(t.opts?t.opts.redirection=!1:t.opts={redirection:!1})),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,((t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,i)}));break;case"Quantumult X":t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then((t=>{const{statusCode:s,statusCode:i,headers:o,body:r,bodyBytes:a}=t;e(null,{status:s,statusCode:i,headers:o,body:r,bodyBytes:a},r,a)}),(t=>e(t&&t.error||"UndefinedError")));break;case"Node.js":let i=require("iconv-lite");this.initGotEnv(t);const{url:o,...r}=t;this.got[s](o,r).then((t=>{const{statusCode:s,statusCode:o,headers:r,rawBody:a}=t,n=i.decode(a,this.encoding);e(null,{status:s,statusCode:o,headers:r,rawBody:a,body:n},n)}),(t=>{const{message:s,response:o}=t;e(s,o,o&&i.decode(o.rawBody,this.encoding))}));break}}time(t,e=null){const s=e?new Date(e):new Date;let i={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in i)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?i[e]:("00"+i[e]).substr((""+i[e]).length)));return t}queryStr(t){let e="";for(const s in t){let i=t[s];null!=i&&""!==i&&("object"==typeof i&&(i=JSON.stringify(i)),e+=`${s}=${i}&`)}return e=e.substring(0,e.length-1),e}msg(e=t,s="",i="",o={}){const r=t=>{const{$open:e,$copy:s,$media:i,$mediaMime:o}=t;switch(typeof t){case void 0:return t;case"string":switch(this.getEnv()){case"Surge":case"Stash":default:return{url:t};case"Loon":case"Shadowrocket":return t;case"Quantumult X":return{"open-url":t};case"Node.js":return}case"object":switch(this.getEnv()){case"Surge":case"Stash":case"Shadowrocket":default:{const r={};let a=t.openUrl||t.url||t["open-url"]||e;a&&Object.assign(r,{action:"open-url",url:a});let n=t["update-pasteboard"]||t.updatePasteboard||s;if(n&&Object.assign(r,{action:"clipboard",text:n}),i){let t,e,s;if(i.startsWith("http"))t=i;else if(i.startsWith("data:")){const[t]=i.split(";"),[,o]=i.split(",");e=o,s=t.replace("data:","")}else{e=i,s=(t=>{const e={JVBERi0:"application/pdf",R0lGODdh:"image/gif",R0lGODlh:"image/gif",iVBORw0KGgo:"image/png","/9j/":"image/jpg"};for(var s in e)if(0===t.indexOf(s))return e[s];return null})(i)}Object.assign(r,{"media-url":t,"media-base64":e,"media-base64-mime":o??s})}return Object.assign(r,{"auto-dismiss":t["auto-dismiss"],sound:t.sound}),r}case"Loon":{const s={};let o=t.openUrl||t.url||t["open-url"]||e;o&&Object.assign(s,{openUrl:o});let r=t.mediaUrl||t["media-url"];return i?.startsWith("http")&&(r=i),r&&Object.assign(s,{mediaUrl:r}),console.log(JSON.stringify(s)),s}case"Quantumult X":{const o={};let r=t["open-url"]||t.url||t.openUrl||e;r&&Object.assign(o,{"open-url":r});let a=t["media-url"]||t.mediaUrl;i?.startsWith("http")&&(a=i),a&&Object.assign(o,{"media-url":a});let n=t["update-pasteboard"]||t.updatePasteboard||s;return n&&Object.assign(o,{"update-pasteboard":n}),console.log(JSON.stringify(o)),o}case"Node.js":return}default:return}};if(!this.isMute)switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:$notification.post(e,s,i,r(o));break;case"Quantumult X":$notify(e,s,i,r(o));break;case"Node.js":break}if(!this.isMuteLog){let t=["","==============📣系统通知📣=============="];t.push(e),s&&t.push(s),i&&t.push(i),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}debug(...t){this.logLevels[this.logLevel]<=this.logLevels.debug&&(t.length>0&&(this.logs=[...this.logs,...t]),console.log(`${this.logLevelPrefixs.debug}${t.map((t=>t??String(t))).join(this.logSeparator)}`))}info(...t){this.logLevels[this.logLevel]<=this.logLevels.info&&(t.length>0&&(this.logs=[...this.logs,...t]),console.log(`${this.logLevelPrefixs.info}${t.map((t=>t??String(t))).join(this.logSeparator)}`))}warn(...t){this.logLevels[this.logLevel]<=this.logLevels.warn&&(t.length>0&&(this.logs=[...this.logs,...t]),console.log(`${this.logLevelPrefixs.warn}${t.map((t=>t??String(t))).join(this.logSeparator)}`))}error(...t){this.logLevels[this.logLevel]<=this.logLevels.error&&(t.length>0&&(this.logs=[...this.logs,...t]),console.log(`${this.logLevelPrefixs.error}${t.map((t=>t??String(t))).join(this.logSeparator)}`))}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.map((t=>t??String(t))).join(this.logSeparator))}logErr(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:this.log("",`❗️${this.name}, 错误!`,e,t);break;case"Node.js":this.log("",`❗️${this.name}, 错误!`,e,void 0!==t.message?t.message:t,t.stack);break}}wait(t){return new Promise((e=>setTimeout(e,t)))}done(t={}){const e=((new Date).getTime()-this.startTime)/1e3;switch(this.log("",`🔔${this.name}, 结束! 🕛 ${e} 秒`),this.log(),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:$done(t);break;case"Node.js":process.exit(1)}}}(t,e)} \ No newline at end of file diff --git a/raycast_pro_patch.js b/raycast_pro_patch.js new file mode 100644 index 00000000..9474c65b --- /dev/null +++ b/raycast_pro_patch.js @@ -0,0 +1,73 @@ +/* +------------------------------------------ +# Raycast 解锁 +# 测试版本:1.0.2 +# 更新日期:2025.05.04 13:33 + +# 登录账户后先试用订阅(选择高级版本试用,记得试用后立马取消订阅) + +注意事项: +# 如果选择试用普通版 pro 订阅就只会解锁 Pro 订阅,需另行搭配通杀脚本解锁 Ai 模型订阅。如果选择试用高级版订阅(包含 Ai 模型)就会解锁高级版本订阅。 +# 解锁账户订阅,如果更换账户将失去订阅。 +# 该脚本仅供调试使用,请于24小时内删除,切勿传播。 +------------------------------------------ +[Script] +Raycast_pro = type=http-response,pattern=^https:\/\/backend\.raycast\.com\/api\/v1\/(me|ai\/models|me\/sync.*)$,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/githubdulong/Script/master/raycast_pro_patch.js + +[MITM] +hostname = %APPEND% backend.raycast.com +*/ + +console.log("[Raycast] Script started for URL: " + $request.url); + +try { + let body = $response.body; + if (!body) { + console.log("[Raycast] No response body for URL: " + $request.url); + $done({}); + } else { + console.log("[Raycast] Original body: " + body); + let obj = JSON.parse(body); + + let modified = false; + function replacePeriodEnd(data) { + if (typeof data !== "object" || data === null) return; + for (let key in data) { + if (key === "current_period_end") { + data[key] = 4102444800; + modified = true; + console.log("[Raycast] Set current_period_end to 4102444800 for key: " + key); + } else if (typeof data[key] === "object") { + replacePeriodEnd(data[key]); + } + } + } + + if ($request.url.includes("/api/v1/me") && !$request.url.includes("/sync")) { + console.log("[Raycast] Processing /api/v1/me"); + if (obj.mobile_subscription && "current_period_end" in obj.mobile_subscription) { + obj.mobile_subscription.current_period_end = 4102444800; + modified = true; + console.log("[Raycast] Set mobile_subscription.current_period_end to 4102444800 in /api/v1/me"); + } else { + console.log("[Raycast] mobile_subscription or current_period_end not found in /api/v1/me"); + } + } + + + if ($request.url.includes("/api/v1/ai/models") || $request.url.includes("/api/v1/me/sync")) { + console.log("[Raycast] Processing " + ($request.url.includes("/ai/models") ? "/api/v1/ai/models" : "/api/v1/me/sync")); + replacePeriodEnd(obj); + if (!modified) { + console.log("[Raycast] No current_period_end found in " + ($request.url.includes("/ai/models") ? "/api/v1/ai/models" : "/api/v1/me/sync")); + } + } + + body = JSON.stringify(obj); + console.log("[Raycast] Modified body: " + body); + $done({ body }); + } +} catch (e) { + console.log("[Raycast] Error: " + e.message); + $done({ body: $response.body }); +} \ No newline at end of file