import { getMiNA } from "@mi-gpt/miot"; import { createHash } from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; import * as stream from "node:stream"; import { promisify } from "node:util"; const kSupportedDevices = [ "LX06", // 小爱音箱 Pro "OH2P", // Xiaomi 智能音箱 Pro ]; // 获取 OTA 信息 async function getOTA(channel: "release" | "current" | "stable" = "release") { const MiNA = await getMiNA({ userId: process.env.MI_USER!, password: process.env.MI_PASS!, }); if (!MiNA) { console.log(`❌ 登录失败,请检查小米账号密码是否正确:`); console.log(`当前账号:${process.env.MI_USER}`); console.log(`当前密码:${process.env.MI_PASS}`); return; } const devices = await MiNA.getDevices(); const speaker = devices?.find( (e: any) => e.name === process.env.MI_DID || e.miotDID === process.env.MI_DID ); if (!speaker) { console.log(`❌ 找不到设备: ${process.env.MI_DID}`); console.log(`可用设备列表:`); console.log( JSON.stringify( devices.map((e: any) => ({ name: e.name, did: e.miotDID, })), null, 4 ) ); return; } if (!kSupportedDevices.includes(speaker.hardware)) { console.log( `❌ 暂不支持当前设备型号: ${speaker.hardware}(${speaker.name})` ); console.log(`可用设备列表:`); console.log( JSON.stringify( devices .filter((e: any) => kSupportedDevices.includes(e.hardware)) .map((e: any) => ({ name: e.name, did: e.miotDID, })), null, 4 ) ); return; } const sn = speaker.serialNumber; const model = speaker.hardware; const version = speaker.romVersion; const time = new Date().getTime(); const otaInfo = `channel=${channel}&filterID=${sn}&locale=zh_CN&model=${model}&time=${time}&version=${version}&8007236f-a2d6-4847-ac83-c49395ad6d65`; const base64Str = Buffer.from(otaInfo).toString("base64"); const code = createHash("md5").update(base64Str).digest("hex"); return { sn, model, version, url: `http://api.miwifi.com/rs/grayupgrade/v2/${model}?model=${model}&version=${version}&channel=${channel}&filterID=${sn}&locale=zh_CN&time=${time}&s=${code}`, }; } async function main() { console.log(`🔥 正在获取设备信息...`); const ota = await getOTA(); if (!ota) { console.log(`❌ 获取设备信息失败`); process.exit(1); } console.log(`🔥 正在获取 OTA 信息...`); const res = await fetch(ota.url); const data = await res.json(); if (data.code === "0" && data.data) { if (data.data.currentInfo) { console.log("\n=== 当前版本固件 ===\n"); const filePath = await downloadFirmware(data.data.currentInfo); if (filePath) { fs.writeFileSync( path.join(process.cwd(), "assets", ".model"), ota.model ); fs.writeFileSync( path.join(process.cwd(), "assets", ".version"), ota.version ); } } } else { console.log(`❌ 获取固件信息失败: ${data.code || "未知错误"}`); process.exit(1); } } main(); // 下载固件 async function downloadFirmware(firmware: { link: string; hash: string; toVersion?: string; size?: number; }): Promise { if (!firmware || !firmware.link) { console.log("❌ 无效的固件信息"); return; } const assetsDir = path.join(process.cwd(), "assets"); // 从链接中提取文件名 const url = new URL(firmware.link); const filename = path.basename(url.pathname); // 打印固件信息 console.log(`- 版本: ${firmware.toVersion || "未知"}`); console.log( `- 大小: ${ firmware.size ? (firmware.size / 1024 / 1024).toFixed(2) + "MB" : "未知" }` ); console.log(`- 文件: ${filename}`); console.log(`- MD5: ${firmware.hash || "未知"}\n`); try { const filePath = await downloadFile(firmware.link, assetsDir, filename); return filePath; } catch (error) { console.error(`❌ 下载固件失败: ${error}`); return; } } async function ensureDir(dirPath: string): Promise { try { await fs.promises.access(dirPath); } catch (error) { await fs.promises.mkdir(dirPath, { recursive: true }); } } async function downloadFile( url: string, destDir: string, filename: string ): Promise { await ensureDir(destDir); const destPath = path.join(destDir, filename); // 检查文件是否已存在 try { await fs.promises.access(destPath); console.log(`ℹ️ 文件已存在: ${destPath}`); return destPath; } catch (error) { // 文件不存在,下载它 console.log(`⬇️ 开始下载: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } // 获取文件大小 const contentLength = response.headers.get("content-length"); const totalSize = contentLength ? parseInt(contentLength, 10) : 0; // 创建文件写入流 const fileStream = fs.createWriteStream(destPath); if (!response.body) { throw new Error("下载失败: 无法获取响应体"); } // 创建进度显示变量 let downloadedBytes = 0; let lastLoggedPercent = -1; let startTime = Date.now(); // 使用 TransformStream 来跟踪下载进度 const progressStream = new stream.Transform({ transform(chunk, encoding, callback) { downloadedBytes += chunk.length; if (totalSize) { const percent = Math.floor((downloadedBytes / totalSize) * 100); // 确保每增加1%才打印一次,避免日志过多 if (percent > lastLoggedPercent) { lastLoggedPercent = percent; // 格式化输出 const downloaded = (downloadedBytes / 1024 / 1024).toFixed(2); const total = (totalSize / 1024 / 1024).toFixed(2); // 使用\r使光标回到行首,实现同行更新 process.stdout.write( `\r下载进度: ${percent}% | ${downloaded}MB/${total}MB` ); // 如果下载完成,打印换行符 if (downloadedBytes === totalSize) { process.stdout.write("\n"); } } } else { // 无法获取总大小时,只显示已下载大小 if (downloadedBytes % (1024 * 1024) === 0) { // 每1MB打印一次 const downloaded = (downloadedBytes / 1024 / 1024).toFixed(2); process.stdout.write(`\r已下载: ${downloaded}MB`); } } this.push(chunk); callback(); }, }); // 完成后清理并打印最终结果 progressStream.on("end", () => { const totalTime = ((Date.now() - startTime) / 1000).toFixed(2); console.log(`\n✅ 下载完成: ${destPath}`); console.log( ` 总大小: ${(downloadedBytes / 1024 / 1024).toFixed( 2 )}MB, 用时: ${totalTime}秒` ); }); // 使用pipeline连接流 await promisify(stream.pipeline)( // @ts-ignore - response.body 在 Node.js 中是 ReadableStream response.body, progressStream, fileStream ); return destPath; } }