Просмотр исходного кода

Merge pull request #428 from XingHeYuZhuan/pending

星河欲转 2 дней назад
Родитель
Сommit
7baf03a074
4 измененных файлов с 534 добавлено и 139 удалено
  1. 1 1
      README.md
  2. 1 1
      index/root_index.yaml
  3. 9 10
      resources/GLOBAL_TOOLS/adapters.yaml
  4. 523 127
      resources/GLOBAL_TOOLS/wake_up.js

+ 1 - 1
README.md

@@ -46,7 +46,7 @@ shiguang_warehouse/
 schools: #固定字段
   - id: "GLOBAL_TOOLS"
     name: "通用工具与服务"
-    initial: "G"
+    initial: "T"
     resource_folder: "GLOBAL_TOOLS"
 
   - id: "CUST"

+ 1 - 1
index/root_index.yaml

@@ -5,7 +5,7 @@
 schools:
   - id: "GLOBAL_TOOLS" # id 唯一值
     name: "通用工具与服务" # 名称
-    initial: "G" # 排序首字母
+    initial: "T" # 排序首字母
     resource_folder: "GLOBAL_TOOLS" # 资源文件夹名称
 
   - id: "zhengfang_jiaowu"

+ 9 - 10
resources/GLOBAL_TOOLS/adapters.yaml

@@ -14,16 +14,15 @@ adapters:
     asset_js_path: "test.js"
     import_url: ""
     maintainer: "星河欲转"
-    description: "空网站以及不存在适配代码,用于在不更新索引的情况下给开发者进行适配的软件测试"    
+    description: "空网站以及不存在适配代码,用于在不更新索引的情况下给开发者进行适配的软件测试"
 
-  # 该适配器已经失效因此注释掉,适配代码暂时不移除
-  # - adapter_id: "WakeUp"
-  #   adapter_name: "WakeUp课程表分享口令导入"
-  #   category: "GENERAL_TOOL"
-  #   asset_js_path: "wake_up.js"
-  #   import_url: "https://i.wakeup.fun/"
-  #   maintainer: "星河欲转"
-  #   description: "首先,先去wakeup获取分享口令,然后点击本适配,进入后不需要管网页,点击\"执行导入\"输入你刚刚获取的分享口令就好\n 特别鸣谢 钴-60 提供的思路"      
+  - adapter_id: "WakeUp"
+    adapter_name: "WakeUp课程表分享口令导入(支持新版)"
+    category: "GENERAL_TOOL"
+    asset_js_path: "wake_up.js"
+    import_url: "https://api.wakeup.fun/"
+    maintainer: "Airline233、星河欲转"
+    description: "从WakeUP获取分享口令后,进入适配直接点击\"执行导入\",输入分享口令即可"
 
   - adapter_id: "StarLink"
     adapter_name: "星链课表分享码导入"
@@ -31,4 +30,4 @@ adapters:
     asset_js_path: "starlink.js"
     import_url: "https://api.starlinkkb.cn/"
     maintainer: "星河欲转"
-    description: "首先,先去星链课表获取分享码,然后点击本适配,进入后不需要管网页,点击\"执行导入\"输入你刚刚获取的分享码就好"        
+    description: "首先,先去星链课表获取分享码,然后点击本适配,进入后不需要管网页,点击\"执行导入\"输入你刚刚获取的分享码就好"

+ 523 - 127
resources/GLOBAL_TOOLS/wake_up.js

@@ -1,181 +1,577 @@
 // 拾光课程表适配 Wakeup 课表分享口令
+// 目前采用v6.1.70 官渠apk中 提取到的apk签名md5与signA算法
+// signA二次发送至antispam 取signB
+
+const WAKEUP_V6170 = Object.freeze({
+    host: "https://api.wakeup.fun",
+    antispamPath: "/pluto/app/antispam",
+    sharePath: "/share_schedule/getv2",
+    packageName: "com.suda.yzune.wakeupschedule",
+    versionName: "6.1.70",
+    versionCode: 450,
+    channel: "100271a",
+    publicToken: "1_XPXQH3c5HRPtFHkSwi3sCCURmT25QfxM",
+    signatureMd5: "318c6d4f74655d4f032fb0466bcfdfbc",
+    magic: "8&%d*",
+    signAKey: "@fG2SuLA",
+    keySalt: "@#AIjd83#@6B"
+});
+
+const WAKEUP_DEVICE_DEFAULTS = Object.freeze({
+    androidId: "0000000000000000",
+    sdk: "35",
+    device: "Pixel 7",
+    brand: "google",
+    screensize: "1080x2400",
+    abis: "arm64-v8a",
+    appBit: "64",
+    appId: "wakeup",
+    downloadType: "1",
+    nt: "wifi",
+    province: "",
+    city: "",
+    area: "",
+    deviceId: "",
+    operatorid: "",
+    adid: "",
+    did: ""
+});
 
 /**
- * 验证用户输入。
- * 优化逻辑:检查输入中是否包含 32 位的 Hex 分享口令(允许是整段分享文本)。
+ * 验证用户输入。WakeUP v6.1.70 的分享口令可包含字母、数字、下划线和短横线。
  * @param {string} input 用户输入的原始文本
  * @returns {false|string} 验证成功返回 false,否则返回错误信息。
  */
 function validateKey(input) {
-    if (input === null || input.trim().length === 0) {
-        return "输入不能为空!";
+    const key = extractKeyFromText(input);
+    if (!key) return "输入不能为空!";
+    if (!/^[A-Za-z0-9_-]{8,200}$/.test(key)) {
+        return "未检测到有效的 WakeUP 分享口令,请粘贴完整分享文本或口令。";
     }
-    // 匹配 32 位字母或数字组成的 Key 特征
-    const keyPattern = /[a-fA-F0-9]{32}/;
-    if (!keyPattern.test(input)) {
-        return "未检测到有效的分享口令,请确保文本中包含 32 位 Key。";
+    return false;
+}
+
+/**
+ * 从 WakeUP 的标准分享文案或裸口令中提取分享口令。
+ * 规则与 WakeUP v6.1.70 官方渠道一致,兼容旧版 32 位十六进制口令。
+ */
+function extractKeyFromText(value) {
+    const text = typeof value === "string" ? value.trim() : "";
+    if (!text) return "";
+
+    const labelled = text.match(/分享口令(?:为)?\s*[「“"]?\s*([A-Za-z0-9_-]{1,200})\s*[」”"]?/u);
+    if (labelled) return labelled[1];
+
+    const quoted = text.match(/[「“"]\s*([A-Za-z0-9_-]{8,200})\s*[」”"]/u);
+    if (quoted) return quoted[1];
+
+    const candidates = text.match(/[A-Za-z0-9_-]{8,200}/g);
+    return candidates ? candidates[candidates.length - 1] : text;
+}
+
+/** WakeUP 协议的浏览器字节、Base64 与 Android quote_plus 兼容工具。 */
+function wakeupToBytes(input) {
+    return typeof input === "string" ? new TextEncoder().encode(input) : new Uint8Array(input);
+}
+
+function wakeupBytesToBinary(bytes) {
+    const source = wakeupToBytes(bytes);
+    let output = "";
+    const chunkSize = 0x8000;
+    for (let index = 0; index < source.length; index += chunkSize) {
+        output += String.fromCharCode.apply(null, source.subarray(index, index + chunkSize));
     }
-    return false; // 验证通过
+    return output;
+}
+
+function wakeupBytesToBase64(bytes) {
+    return btoa(wakeupBytesToBinary(bytes));
+}
+
+function wakeupBase64ToBytes(text) {
+    const binary = atob(text);
+    const output = new Uint8Array(binary.length);
+    for (let index = 0; index < binary.length; index += 1) output[index] = binary.charCodeAt(index);
+    return output;
+}
+
+function wakeupBytesToLatin1(bytes) {
+    return wakeupBytesToBinary(bytes);
+}
+
+function wakeupBytesToUtf8(bytes) {
+    return new TextDecoder("utf-8").decode(wakeupToBytes(bytes));
+}
+
+function wakeupAndroidQuote(value) {
+    return encodeURIComponent(String(value))
+        .replace(/[!'()*]/g, char => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
+        .replace(/%20/g, "+");
+}
+
+function wakeupFormEncode(items) {
+    return items.map(([key, value]) => `${key}=${wakeupAndroidQuote(value == null ? "" : value)}`).join("&");
+}
+
+function wakeupRotateLeft(value, bits) {
+    return ((value << bits) | (value >>> (32 - bits))) >>> 0;
 }
 
 /**
- * 辅助函数:从长文本中提取 32 位 Key。
- * 优先匹配「」内部,若无则匹配文本中第一个符合特征的字符串。
+ * 纯 JavaScript MD5。Web Crypto 不提供 MD5,故保持适配脚本不依赖 Node 或第三方库
+ * 输入按 UTF-8 字节计算,与 WakeUP 官方协议中的 native/Java 实现对齐
  */
-function extractKeyFromText(text) {
-    // 1. 尝试匹配「」内的 32 位 Key
-    const bracketMatch = text.match(/「([a-fA-F0-9]{32})」/);
-    if (bracketMatch) return bracketMatch[1];
+function wakeupMd5Bytes(input) {
+    const source = wakeupToBytes(input);
+    const paddedLength = (((source.length + 8) >>> 6) + 1) * 64;
+    const data = new Uint8Array(paddedLength);
+    data.set(source);
+    data[source.length] = 0x80;
+
+    const bitLength = source.length * 8;
+    for (let index = 0; index < 8; index += 1) {
+        data[paddedLength - 8 + index] = Math.floor(bitLength / Math.pow(256, index)) & 0xff;
+    }
+
+    const shifts = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
+        5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
+        4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
+        6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21];
+    const constants = Array.from({ length: 64 }, (_, index) => Math.floor(Math.abs(Math.sin(index + 1)) * 0x100000000) >>> 0);
+
+    let a0 = 0x67452301;
+    let b0 = 0xefcdab89;
+    let c0 = 0x98badcfe;
+    let d0 = 0x10325476;
+
+    for (let offset = 0; offset < data.length; offset += 64) {
+        const words = new Uint32Array(16);
+        for (let index = 0; index < 16; index += 1) {
+            const wordOffset = offset + index * 4;
+            words[index] = (data[wordOffset] | (data[wordOffset + 1] << 8) |
+                (data[wordOffset + 2] << 16) | (data[wordOffset + 3] << 24)) >>> 0;
+        }
+
+        let a = a0;
+        let b = b0;
+        let c = c0;
+        let d = d0;
+        for (let index = 0; index < 64; index += 1) {
+            let f;
+            let g;
+            if (index < 16) {
+                f = (b & c) | ((~b) & d);
+                g = index;
+            } else if (index < 32) {
+                f = (d & b) | ((~d) & c);
+                g = (5 * index + 1) % 16;
+            } else if (index < 48) {
+                f = b ^ c ^ d;
+                g = (3 * index + 5) % 16;
+            } else {
+                f = c ^ (b | (~d));
+                g = (7 * index) % 16;
+            }
+            const next = d;
+            d = c;
+            c = b;
+            b = (b + wakeupRotateLeft((a + f + constants[index] + words[g]) >>> 0, shifts[index])) >>> 0;
+            a = next;
+        }
+        a0 = (a0 + a) >>> 0;
+        b0 = (b0 + b) >>> 0;
+        c0 = (c0 + c) >>> 0;
+        d0 = (d0 + d) >>> 0;
+    }
+
+    const digest = new Uint8Array(16);
+    const view = new DataView(digest.buffer);
+    view.setUint32(0, a0, true);
+    view.setUint32(4, b0, true);
+    view.setUint32(8, c0, true);
+    view.setUint32(12, d0, true);
+    return digest;
+}
+
+function wakeupMd5Hex(input) {
+    return Array.from(wakeupMd5Bytes(input), byte => byte.toString(16).padStart(2, "0")).join("");
+}
+
+function wakeupMd5Upper(input) {
+    return wakeupMd5Hex(input).toUpperCase();
+}
+
+// 以下表与位序来自 WakeUP v6.1.70 官方协议。该私有 DES 并非标准 DES,禁止替换。
+const WAKEUP_DES_IP = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6];
+const WAKEUP_DES_FP = [39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24];
+const WAKEUP_DES_E = [31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8, 7, 8, 9, 10, 11, 12, 11, 12, 13, 14, 15, 16, 15, 16, 17, 18, 19, 20, 19, 20, 21, 22, 23, 24, 23, 24, 25, 26, 27, 28, 27, 28, 29, 30, 31, 0];
+const WAKEUP_DES_P = [15, 6, 19, 20, 28, 11, 27, 16, 0, 14, 22, 25, 4, 17, 30, 9, 1, 7, 23, 13, 31, 26, 2, 8, 18, 12, 29, 5, 21, 10, 3, 24];
+const WAKEUP_DES_PC1 = [56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3];
+const WAKEUP_DES_PC2 = [13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 46, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31];
+const WAKEUP_DES_SHIFTS = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
+const WAKEUP_DES_SBOX = [
+    [[14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7],[0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8],[4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0],[15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13]],
+    [[15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10],[3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5],[0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15],[13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9]],
+    [[10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8],[13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1],[13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7],[1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12]],
+    [[7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15],[13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9],[10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4],[3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14]],
+    [[2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9],[14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6],[4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14],[11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3]],
+    [[12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11],[10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8],[9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6],[4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13]],
+    [[4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1],[13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6],[1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2],[6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12]],
+    [[13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7],[1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2],[7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8],[2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11]]
+];
+
+function wakeupDesBitsFromBytes(bytes) {
+    const output = new Array(bytes.length * 8);
+    for (let index = 0; index < bytes.length; index += 1) {
+        for (let bit = 0; bit < 8; bit += 1) output[index * 8 + bit] = (bytes[index] >> bit) & 1;
+    }
+    return output;
+}
+
+function wakeupDesBytesFromBits(bits) {
+    const output = new Uint8Array(bits.length / 8);
+    for (let index = 0; index < output.length; index += 1) {
+        for (let bit = 0; bit < 8; bit += 1) output[index] |= (bits[index * 8 + bit] & 1) << bit;
+    }
+    return output;
+}
+
+function wakeupDesPermute(bits, table) {
+    return table.map(index => bits[index] | 0);
+}
+
+function wakeupDesSubkeys(key) {
+    const keyBytes = wakeupToBytes(key);
+    if (keyBytes.length !== 8) throw new Error("WakeUP 私有 DES 密钥长度必须为 8 字节。");
+    const bits = wakeupDesPermute(wakeupDesBitsFromBytes(keyBytes), WAKEUP_DES_PC1);
+    let left = bits.slice(0, 28);
+    let right = bits.slice(28);
+    return WAKEUP_DES_SHIFTS.map(shift => {
+        left = left.slice(shift).concat(left.slice(0, shift));
+        right = right.slice(shift).concat(right.slice(0, shift));
+        return wakeupDesPermute(left.concat(right), WAKEUP_DES_PC2);
+    });
+}
+
+function wakeupDesFunction(right, subkey) {
+    const expanded = wakeupDesPermute(right, WAKEUP_DES_E);
+    const mixed = expanded.map((bit, index) => bit ^ subkey[index]);
+    const output = [];
+    for (let box = 0; box < 8; box += 1) {
+        const block = mixed.slice(box * 6, box * 6 + 6);
+        const row = block[0] * 2 + block[5];
+        const column = block[1] * 8 + block[2] * 4 + block[3] * 2 + block[4];
+        const value = WAKEUP_DES_SBOX[box][row][column];
+        output.push((value >> 3) & 1, (value >> 2) & 1, (value >> 1) & 1, value & 1);
+    }
+    return wakeupDesPermute(output, WAKEUP_DES_P);
+}
+
+function wakeupDesBlock(block, subkeys) {
+    const bits = wakeupDesPermute(wakeupDesBitsFromBytes(block), WAKEUP_DES_IP);
+    let left = bits.slice(0, 32);
+    let right = bits.slice(32);
+    for (const subkey of subkeys) {
+        const mixed = wakeupDesFunction(right, subkey);
+        const nextRight = left.map((bit, index) => bit ^ mixed[index]);
+        left = right;
+        right = nextRight;
+    }
+    return wakeupDesBytesFromBits(wakeupDesPermute(right.concat(left), WAKEUP_DES_FP));
+}
+
+function wakeupConcatBytes(chunks) {
+    const output = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.length, 0));
+    let offset = 0;
+    chunks.forEach(chunk => {
+        output.set(chunk, offset);
+        offset += chunk.length;
+    });
+    return output;
+}
+
+function wakeupDesEncrypt(plain, key) {
+    const source = wakeupToBytes(plain);
+    const paddedLength = (source.length & ~7) + 8;
+    const padded = new Uint8Array(paddedLength);
+    padded.set(source);
+    padded[paddedLength - 1] = paddedLength - source.length;
+    const subkeys = wakeupDesSubkeys(key);
+    const blocks = [];
+    for (let offset = 0; offset < paddedLength; offset += 8) blocks.push(wakeupDesBlock(padded.subarray(offset, offset + 8), subkeys));
+    return wakeupConcatBytes(blocks);
+}
+
+function wakeupDesDecrypt(cipher, key) {
+    const source = wakeupToBytes(cipher);
+    if (source.length % 8 !== 0) throw new Error("WakeUP 私有 DES 密文长度无效。");
+    const subkeys = wakeupDesSubkeys(key).reverse();
+    const blocks = [];
+    for (let offset = 0; offset < source.length; offset += 8) blocks.push(wakeupDesBlock(source.subarray(offset, offset + 8), subkeys));
+    const plain = wakeupConcatBytes(blocks);
+    const padLength = plain[plain.length - 1];
+    if (padLength > plain.length) throw new Error("WakeUP 私有 DES 填充无效。");
+    return plain.subarray(0, plain.length - padLength);
+}
+
+function wakeupReverseNibble(value) {
+    return ((value & 1) << 3) | ((value & 2) << 1) | ((value & 4) >> 1) | ((value & 8) >> 3);
+}
+
+function wakeupNativeHexEncode(bytes) {
+    return Array.from(wakeupToBytes(bytes), byte => {
+        const low = wakeupReverseNibble(byte & 0x0f).toString(16).padStart(2, "0");
+        const high = wakeupReverseNibble((byte >> 4) & 0x0f).toString(16).padStart(2, "0");
+        return low + high;
+    }).join("");
+}
+
+function wakeupNativeHexDecode(text) {
+    const source = String(text).slice(0, Math.floor(String(text).length / 4) * 4);
+    const output = new Uint8Array(source.length / 4);
+    for (let index = 0; index < source.length; index += 4) {
+        const low = wakeupReverseNibble(parseInt(source[index + 1], 16));
+        const high = wakeupReverseNibble(parseInt(source[index + 3], 16));
+        output[index / 4] = low | (high << 4);
+    }
+    return output;
+}
+
+function wakeupRc4(data, key) {
+    const keyBytes = wakeupToBytes(key);
+    const state = new Uint8Array(256);
+    for (let index = 0; index < 256; index += 1) state[index] = index;
+    let swapIndex = 0;
+    for (let index = 0; index < 256; index += 1) {
+        swapIndex = (swapIndex + state[index] + keyBytes[index % keyBytes.length]) & 0xff;
+        [state[index], state[swapIndex]] = [state[swapIndex], state[index]];
+    }
+    const input = wakeupToBytes(data);
+    const output = new Uint8Array(input.length);
+    let left = 0;
+    let right = 0;
+    for (let index = 0; index < input.length; index += 1) {
+        left = (left + 1) & 0xff;
+        right = (right + state[left]) & 0xff;
+        [state[left], state[right]] = [state[right], state[left]];
+        output[index] = input[index] ^ state[(state[left] + state[right]) & 0xff];
+    }
+    return output;
+}
 
-    // 2. 兜底:直接查找文本中第一个符合 32 位特征的字符串
-    const directMatch = text.match(/[a-fA-F0-9]{32}/);
-    return directMatch ? directMatch[0] : text.trim();
+/** WakeUP v6.1.70 的设备标识、签名、反爬握手和加密请求实现。 */
+function wakeupCuidFromAndroidId(androidId) {
+    return `${wakeupMd5Upper(`com.baidu${androidId || ""}`)}|0`;
+}
+
+function wakeupAdidFromAndroidId(androidId) {
+    const prefix = wakeupMd5Hex(`alpha.beta${androidId || ""}`);
+    const folded = BigInt(`0x${prefix.slice(0, 16)}`) ^ BigInt(`0x${prefix.slice(16)}`);
+    const checksum = (((folded >> 32n) ^ folded) & 0xffffffffn).toString(16).padStart(8, "0");
+    return prefix + checksum;
+}
+
+function wakeupGenerateRand10() {
+    const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+    const bytes = new Uint8Array(10);
+    if (!globalThis.crypto || !globalThis.crypto.getRandomValues) throw new Error("当前 WebView 不支持安全随机数生成。");
+    globalThis.crypto.getRandomValues(bytes);
+    return Array.from(bytes, byte => alphabet[byte % alphabet.length]).join("");
+}
+
+function wakeupCreateDevice() {
+    const device = { ...WAKEUP_DEVICE_DEFAULTS };
+    device.cuid = wakeupCuidFromAndroidId(device.androidId);
+    device.adid = device.adid || wakeupAdidFromAndroidId(device.androidId);
+    return device;
+}
+
+function wakeupMakeCommonParams(device) {
+    return [["area", device.area], ["screensize", device.screensize], ["cuid", device.cuid], ["os", "android"],
+        ["city", device.city], ["abis", device.abis], ["channel", WAKEUP_V6170.channel], ["appBit", device.appBit],
+        ["vc", String(WAKEUP_V6170.versionCode)], ["deviceId", device.deviceId], ["token", WAKEUP_V6170.publicToken],
+        ["adid", device.adid], ["province", device.province], ["pkgName", WAKEUP_V6170.packageName], ["appId", device.appId],
+        ["download_type", device.downloadType], ["vcname", WAKEUP_V6170.versionName], ["sdk", String(device.sdk)],
+        ["device", device.device], ["brand", device.brand], ["operatorid", device.operatorid]]
+        .map(([key, value]) => [key, value == null ? "" : String(value)]);
+}
+
+function wakeupMakeSignA(cuid) {
+    const rand10 = wakeupGenerateRand10();
+    const plain = `${WAKEUP_V6170.magic}##${rand10}##${WAKEUP_V6170.signatureMd5}##${cuid}`;
+    return { signA: wakeupNativeHexEncode(wakeupDesEncrypt(plain, WAKEUP_V6170.signAKey)), rand10 };
+}
+
+function wakeupTokenFromSignB(signB, rand10) {
+    const plain = wakeupDesDecrypt(wakeupNativeHexDecode(signB), `${rand10.slice(0, 5)}#G4`);
+    const text = wakeupBytesToLatin1(plain);
+    if (text.length < 22 || text.slice(0, 10) !== rand10) throw new Error("WakeUP antispam token 校验失败。");
+    return text.slice(12, 22);
+}
+
+function wakeupGetRc4Key(token) {
+    const first = wakeupMd5Hex(WAKEUP_V6170.keySalt);
+    const second = wakeupMd5Hex(String(WAKEUP_V6170.versionCode));
+    const raw = wakeupMd5Hex(`[${token}]@`);
+    const reversed = raw.slice(17).split("").reverse().join("") + raw.slice(15, 17) + raw.slice(0, 15).split("").reverse().join("");
+    const chars = (first + second + reversed).split("");
+    for (let index = 0; index < 3; index += 1) {
+        const right = chars.length - 1 - index;
+        [chars[index], chars[right]] = [chars[right], chars[index]];
+    }
+    const output = (chars.join("") + wakeupMd5Hex(chars.join(""))).split("");
+    for (let index = 0; index < 60; index += 1) {
+        [output[index], output[output.length - 1 - index]] = [output[output.length - 1 - index], output[index]];
+    }
+    return output.join("");
+}
+
+function wakeupGetSign(base64Params, token) {
+    return wakeupMd5Hex(`${WAKEUP_V6170.magic}[${wakeupMd5Hex(token)}]@${base64Params}`);
+}
+
+function wakeupBuildShareRequest(code, token, device, commonParams) {
+    const rc4Key = wakeupGetRc4Key(token);
+    const dataValue = wakeupBytesToBase64(wakeupRc4(`key=${wakeupAndroidQuote(code)}`, rc4Key));
+    const serverTime = Math.floor(Date.now() / 1000);
+    const kakorr = Date.now();
+    const signItems = [`data=${dataValue}`, ...commonParams.map(([key, value]) => `${key}=${value}`)];
+    if (device.did) signItems.push(`did=${device.did}`);
+    signItems.push(`nt=${device.nt}`, `_t_=${serverTime}`, `kakorrhaphiophobia=${kakorr}`);
+    const sign = wakeupGetSign(wakeupBytesToBase64(new TextEncoder().encode(signItems.sort().join(""))), token);
+    const extras = device.did ? [["did", device.did]] : [];
+    const body = `&${wakeupFormEncode([["data", dataValue], ...commonParams, ...extras, ["nt", device.nt]])}&sign=${sign}&_t_=${serverTime}&kakorrhaphiophobia=${kakorr}`;
+    return { rc4Key, body };
+}
+
+function wakeupExtractSignB(payload) {
+    if (!payload || typeof payload !== "object") return "";
+    let candidate = payload.data;
+    if (candidate && typeof candidate === "object") candidate = candidate.data;
+    if (typeof candidate !== "string" || !candidate) candidate = payload.result && typeof payload.result === "object" ? payload.result.data : null;
+    return typeof candidate === "string" ? candidate : "";
+}
+
+async function wakeupPostForm(url, body, device) {
+    const headers = { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "na__zyb_source__": "wakeup", "zyb-cuid": device.cuid, "zyb-adid": device.adid };
+    if (device.did) headers["zyb-did"] = device.did;
+    const controller = typeof AbortController === "undefined" ? null : new AbortController();
+    const timeout = controller ? setTimeout(() => controller.abort(), 15000) : null;
+    try {
+        const response = await fetch(url, { method: "POST", headers, body, signal: controller ? controller.signal : undefined });
+        const text = await response.text();
+        if (!response.ok) throw new Error(`WakeUP 服务请求失败(HTTP ${response.status})。`);
+        return text;
+    } finally {
+        if (timeout) clearTimeout(timeout);
+    }
 }
 
 /**
- * 将原始 JSON 字符串数组解析成各个部分。
- * 原始数据是多个 JSON 块用换行符分隔。
- * @param {string} rawData 原始的 JSON 字符串,包含多个部分。
- * @returns {object} 包含解析后数据的对象。
+ * 执行 WakeUP 官方 v6.1.70 两段式请求,并返回解密后的课表分享数据。
+ * 敏感口令、签名和 token 不写入日志,避免泄漏。
  */
+async function wakeupDecodeShareCode(code) {
+    const device = wakeupCreateDevice();
+    const commonParams = wakeupMakeCommonParams(device);
+    const { signA, rand10 } = wakeupMakeSignA(device.cuid);
+    const antispamBody = `${wakeupFormEncode([["data", signA], ...commonParams])}&`;
+    const antispamText = await wakeupPostForm(`${WAKEUP_V6170.host}${WAKEUP_V6170.antispamPath}`, antispamBody, device);
+    let antispamJson;
+    try {
+        antispamJson = JSON.parse(antispamText);
+    } catch {
+        throw new Error("WakeUP antispam 响应格式无效。");
+    }
+    const signB = wakeupExtractSignB(antispamJson);
+    if (!signB) throw new Error("WakeUP antispam 响应未包含签名数据。");
+
+    const token = wakeupTokenFromSignB(signB, rand10);
+    const shareRequest = wakeupBuildShareRequest(code, token, device, commonParams);
+    const shareText = await wakeupPostForm(`${WAKEUP_V6170.host}${WAKEUP_V6170.sharePath}`, shareRequest.body, device);
+    let shareJson;
+    try {
+        shareJson = JSON.parse(shareText);
+    } catch {
+        throw new Error("WakeUP 课表响应格式无效。");
+    }
+
+    const encrypted = shareJson && typeof shareJson.data === "object" ? shareJson.data.data : shareJson && shareJson.data;
+    if (typeof encrypted !== "string" || !encrypted) throw new Error("WakeUP 课表响应未包含加密数据。");
+    let decrypted;
+    try {
+        decrypted = JSON.parse(wakeupBytesToUtf8(wakeupRc4(wakeupBase64ToBytes(encrypted), shareRequest.rc4Key)));
+    } catch {
+        throw new Error("WakeUP 课表响应解密失败。");
+    }
+    if (!decrypted || typeof decrypted.shareData !== "string") throw new Error("WakeUP 解密数据中未找到 shareData。");
+    return decrypted.shareData;
+}
+
+/** 将 WakeUP 分享数据(多个换行分隔的 JSON 块)解析成各部分。 */
 function parseRawScheduleData(rawData) {
     AndroidBridge.showToast("正在解析原始数据...");
-    
-    const parts = rawData.trim().split('\n');
-    if (parts.length < 5) { // 至少需要 5 个部分
-        throw new Error("数据格式不完整,预期至少包含 5 个部分。");
-    }
-    
-    // 尝试解析关键部分
-    const baseConfig = JSON.parse(parts[0]);
-    const timeSlotsRaw = JSON.parse(parts[1]);
-    const uiConfig = JSON.parse(parts[2]);
-    const coursesRaw = JSON.parse(parts[3]);
-    const courseDetailRaw = JSON.parse(parts[4]);
-    
+    const parts = rawData.trim().split("\n");
+    if (parts.length < 5) throw new Error("数据格式不完整,预期至少包含 5 个部分。");
     return {
-        baseConfig,
-        timeSlotsRaw,
-        uiConfig,
-        coursesRaw,
-        courseDetailRaw
+        baseConfig: JSON.parse(parts[0]),
+        timeSlotsRaw: JSON.parse(parts[1]),
+        uiConfig: JSON.parse(parts[2]),
+        coursesRaw: JSON.parse(parts[3]),
+        courseDetailRaw: JSON.parse(parts[4])
     };
 }
 
-/**
- * 格式化日期对象为 YYYY-MM-DD 字符串。
- * @param {Date} dateObj 日期对象
- * @returns {string} YYYY-MM-DD 格式的字符串
- */
 function formatDateToYYYYMMDD(dateObj) {
     const year = dateObj.getFullYear();
-    // 确保月份和日期带有前导零(例如 9 -> 09)
-    const month = String(dateObj.getMonth() + 1).padStart(2, '0'); 
-    const day = String(dateObj.getDate()).padStart(2, '0');       
+    const month = String(dateObj.getMonth() + 1).padStart(2, "0");
+    const day = String(dateObj.getDate()).padStart(2, "0");
     return `${year}-${month}-${day}`;
 }
 
-/**
- * 尝试将原始日期值转换为 YYYY-MM-DD 格式的字符串。
- * @param {*} rawDate 原始日期值 
- * @returns {string|null} YYYY-MM-DD 格式的日期字符串,或 null。
- */
 function convertToSemesterStartDate(rawDate) {
-    if (!rawDate) {
-        return null;
-    }
-    
-    let dateString = String(rawDate).trim();
-    if (dateString.length === 0) {
-        return null;
-    }
-
-    // 尝试替换斜杠为短横线
-    dateString = dateString.replace(/\//g, '-');
-    
-    // 尝试解析为 Date 对象
-    const dateObj = new Date(dateString);
-
+    if (!rawDate || String(rawDate).trim().length === 0) return null;
+    const dateObj = new Date(String(rawDate).trim().replace(/\//g, "-"));
     if (isNaN(dateObj.getTime())) {
         console.warn(`WARN: 无法将原始日期值 "${rawDate}" 转换为有效日期。`);
-        return null; 
+        return null;
     }
-
     return formatDateToYYYYMMDD(dateObj);
 }
 
-
-/**
- * 网络请求、数据解析和转换。
- * @param {string} shareKey 用户输入的 Key。
- * @returns {object|null} 包含转换后的 timeSlots 和 config 数据的对象,失败返回 null。
- */
+/** 通过 WakeUP v6.1.70 官方渠道获取、解密、解析并转换课程表。 */
 async function fetchAndParseData(shareKey) {
     try {
-        const apiUrl = `https://i.wakeup.fun/share_schedule/get?key=${shareKey.trim()}`;
-        console.log("正在请求课表数据:", apiUrl);
-        AndroidBridge.showToast("正在请求课表数据...");
-
-        const response = await fetch(apiUrl);
-        if (!response.ok) {
-            throw new Error(`网络请求失败,状态码: ${response.status}`);
-        }
-        
-        const apiJson = await response.json();
-
-        if (apiJson.status !== 1) {
-            throw new Error(`API 返回失败信息: ${apiJson.message}`);
-        }
-        
-        const rawData = apiJson.data;
-        const parsedData = parseRawScheduleData(rawData);
-        
-        // --- 转换数据结构 ---
+        AndroidBridge.showToast("正在通过 WakeUP 官方渠道请求课表数据...");
+        const parsedData = parseRawScheduleData(await wakeupDecodeShareCode(shareKey.trim()));
         let rawNodes = parsedData.uiConfig.nodes;
-
         if (!Array.isArray(rawNodes)) {
-             if (typeof rawNodes === 'number' && rawNodes > 0) {
-                 console.warn(`WARN: uiConfig.nodes 预期为数组,但获取到总节数: ${rawNodes}。正在生成 1 到 ${rawNodes} 的节次列表。`);
-                 
-                 rawNodes = Array.from({ length: rawNodes }, (_, i) => i + 1);
-             } else {
-                 // 既不是数组也不是有效数字,退回空数组
-                 console.warn(`WARN: uiConfig.nodes 数据无效 (${rawNodes}),已重置为空数组。`);
-                 rawNodes = [];
-             }
+            if (typeof rawNodes === "number" && rawNodes > 0) {
+                rawNodes = Array.from({ length: rawNodes }, (_, index) => index + 1);
+            } else {
+                console.warn(`WARN: uiConfig.nodes 数据无效 (${rawNodes}),已重置为空数组。`);
+                rawNodes = [];
+            }
         }
 
         const validNodes = new Set(rawNodes);
-
-        // 1. 转换预设时间段 (TimeSlotJsonModel)
         const timeSlots = parsedData.timeSlotsRaw
             .filter(slot => slot.startTime !== "00:00" && slot.endTime !== "00:00")
-            .filter(slot => validNodes.has(slot.node)) 
-            .map(slot => ({
-                "number": slot.node,
-                "startTime": slot.startTime,
-                "endTime": slot.endTime
-            }));
-            
-        const semesterStartDate = convertToSemesterStartDate(parsedData.uiConfig.startDate);
-        
+            .filter(slot => validNodes.has(slot.node))
+            .map(slot => ({ number: slot.node, startTime: slot.startTime, endTime: slot.endTime }));
         const courseConfig = {
-            "semesterStartDate": semesterStartDate,
-            "semesterTotalWeeks": parsedData.uiConfig.maxWeek, 
-            "defaultClassDuration": parsedData.baseConfig.courseLen, 
-            "defaultBreakDuration": parsedData.baseConfig.theBreakLen, 
+            semesterStartDate: convertToSemesterStartDate(parsedData.uiConfig.startDate),
+            semesterTotalWeeks: parsedData.uiConfig.maxWeek,
+            defaultClassDuration: parsedData.baseConfig.courseLen,
+            defaultBreakDuration: parsedData.baseConfig.theBreakLen
         };
-        
         const courses = convertToCourseJsonModel(parsedData);
-        
         AndroidBridge.showToast(`数据解析成功,共 ${courses.length} 门课程`);
-        
         return { timeSlots, courseConfig, courses };
-
     } catch (error) {
-        console.error("数据获取或解析失败:", error);
-        AndroidBridge.showToast("数据获取或解析失败: " + error.message);
-        return null; // 失败时返回 null
+        console.error("WakeUP 数据获取或解析失败:", error);
+        AndroidBridge.showToast(`数据获取或解析失败: ${error.message}`);
+        return null;
     }
 }