diff --git a/packages/client-patch/.dockerignore b/packages/client-patch/.dockerignore new file mode 100644 index 0000000..77d5f92 --- /dev/null +++ b/packages/client-patch/.dockerignore @@ -0,0 +1,8 @@ +# 排除全部文件 +**/* + +# 只保留构建相关的文件 +!src +!package.json +!package-lock.json +!tsconfig.json \ No newline at end of file diff --git a/packages/client-patch/.env.example b/packages/client-patch/.env.example new file mode 100644 index 0000000..5a7d735 --- /dev/null +++ b/packages/client-patch/.env.example @@ -0,0 +1,9 @@ +# 你的小米账号/密码 +MI_USER=23333333 +MI_PASS=xxxxxxxxx + +# 你的小爱音箱名称/DID +MI_DID=小爱音箱Pro + +# 你的 SSH 登录密码(默认为 open-xiaoai) +SSH_PASSWORD=open-xiaoai diff --git a/packages/client-patch/.gitignore b/packages/client-patch/.gitignore new file mode 100644 index 0000000..57161bb --- /dev/null +++ b/packages/client-patch/.gitignore @@ -0,0 +1,3 @@ +temp +assets/* +!assets/.gitkeep diff --git a/packages/client-patch/Dockerfile b/packages/client-patch/Dockerfile new file mode 100644 index 0000000..98ffc8a --- /dev/null +++ b/packages/client-patch/Dockerfile @@ -0,0 +1,20 @@ +FROM node:22-alpine + +ENV TZ=Asia/Shanghai + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone \ + && apk add --no-cache \ + file \ + patch \ + python3 \ + openssl \ + squashfs-tools + +WORKDIR /app + +COPY . . + +RUN npm install + +CMD ["npm", "run", "build"] \ No newline at end of file diff --git a/packages/client-patch/README.md b/packages/client-patch/README.md new file mode 100644 index 0000000..f5fd4b1 --- /dev/null +++ b/packages/client-patch/README.md @@ -0,0 +1,87 @@ +# OpenXiaoAI Patch + +> [!CAUTION] +> 刷机有风险,操作需谨慎。 + +小爱音箱 Pro 补丁固件制作流程: +- 固件提取(登录小米账号获取 OTA 链接) +- 开启固化 SSH(支持自定义登录密码) +- 禁用系统自动更新(系统更新后需要重新刷机打补丁) +- 添加开机启动脚本 `/data/init.sh`(方便执行一些初始化脚本) + +## 准备条件 + +> [!IMPORTANT] +> 本教程仅适用于 **小爱音箱 Pro(LX06)** 和 **Xiaomi 智能音箱 Pro(OH2P)** 这两款机型,**其他型号**的小爱音箱可能存在兼容性问题,请勿直接使用!🚨 + + +- Windows 电脑(需为 x86_64 架构,用于刷机) +- 数据线(不能只是充电线,需要连接到电脑传输数据) + - Type-C(适用于新款小爱音箱 Pro,无需拆机) + - Micro USB(旧款小爱音箱 Pro 用这种,需要拆机) + + +## 下载固件 + +> [!NOTE] +> 构建版本待发布,敬请期待。 + + +你可以直接在 Github Release 页面下载打包好的补丁固件。 + +> 默认 SSH 登录密码为 `open-xiaoai` + +## 制作固件 + +或者你也可以按照下面的 2 种方法,制作自定义固件。 + +### 基础配置 + +修改 `.env.example` 文件里的配置,然后重命名为 `.env`。 + +```shell +# 你的小米账号/密码 +MI_USER=23333333 +MI_PASS=xxxxxxxxx + +# 你的小爱音箱名称/DID +MI_DID=小爱音箱Pro + +# 你的 SSH 登录密码(默认为 open-xiaoai) +SSH_PASSWORD=open-xiaoai +``` + +### 1. 使用 Docker 打包固件(推荐) + +> [!NOTE] +> Docker 镜像待发布,敬请期待。 + +```shell +# 使用 Docker 进行构建 +docker run -it --rm --env-file $(pwd)/.env -v $(pwd)/assets:/app/assets -v $(pwd)/patches:/app/patches open-xiaoai + +# ✅ 打包完成,固件文件已复制到 assets 目录... +# /app/assets/mico_all_92db90ed6_1.88.197/root.squashfs +``` + +### 2. 本地构建(macOS、Linux) + +为了能够正常编译运行该项目,你需要安装以下依赖环境: + +- Docker:https://www.docker.com/get-started/ +- Python:https://www.python.org/downloads/ +- Node.js: https://nodejs.org/zh-cn/download + +```bash +# 安装依赖 +npm install + +# 打包固件 +npm run build + +# ✅ 打包成功后,原始固件和补丁固件会保存在 assets 目录下 +``` + +> [!TIP] +> 如果你想要更进一步的定制自己的固件,可以参考 `src/build.sh` 脚本里的构建流程:在提取固件后自行修改固件内的脚本、配置和应用程序,然后重新打包即可。 + diff --git a/packages/client-patch/assets/.gitkeep b/packages/client-patch/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/client-patch/package-lock.json b/packages/client-patch/package-lock.json new file mode 100644 index 0000000..44e3567 --- /dev/null +++ b/packages/client-patch/package-lock.json @@ -0,0 +1,893 @@ +{ + "name": "open-xiaoai-patch", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "open-xiaoai-patch", + "version": "1.0.0", + "dependencies": { + "@mi-gpt/miot": "^1.0.0" + }, + "devDependencies": { + "@mi-gpt/config": "1.0.0", + "@types/node": "^22.13.11", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@mi-gpt/config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mi-gpt/config/-/config-1.0.0.tgz", + "integrity": "sha512-oo/roYiRcGiHsZESjukwPTsRFqddShTJ8gkccR5ZMYn8SySj07A0nCsEsLEfB9mLkvNY8D6PCO1NxbRDDRBBlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@mi-gpt/miot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mi-gpt/miot/-/miot-1.0.0.tgz", + "integrity": "sha512-smWhddHzK1du3/LW2FifBrIIyeZNx/RvTSe0Zi4v3lYZwK8NeEwomg1jBjktQCCV4Fh8OuqNbCdNbmiVXmr5kw==", + "license": "MIT", + "dependencies": { + "@mi-gpt/utils": "1.0.0", + "axios": "^1.8.4", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@mi-gpt/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mi-gpt/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-hAU/uYPNiNJgXWkQrVqRLe/9Uz8AgtoiNwcbbwQs8iXJcyq3NjcSHkkwUseJAnaQFE9iv/kvHZZsx3r2HhlKjw==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/packages/client-patch/package.json b/packages/client-patch/package.json new file mode 100644 index 0000000..261c4ee --- /dev/null +++ b/packages/client-patch/package.json @@ -0,0 +1,22 @@ +{ + "name": "open-xiaoai-patch", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "ota": "tsx --env-file=.env src/ota.ts", + "extract": "sh src/extract.sh", + "patch": "sh src/patch.sh", + "squashfs": "sh src/squashfs.sh", + "build": "sh src/build.sh" + }, + "dependencies": { + "@mi-gpt/miot": "^1.0.0" + }, + "devDependencies": { + "@mi-gpt/config": "1.0.0", + "@types/node": "^22.13.11", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } +} diff --git a/packages/client-patch/patches/01-ssh.patch b/packages/client-patch/patches/01-ssh.patch new file mode 100644 index 0000000..802a2a6 --- /dev/null +++ b/packages/client-patch/patches/01-ssh.patch @@ -0,0 +1,48 @@ +--- squashfs-root/etc/init.d/dropbear 2024-12-06 15:11:58 ++++ squashfs-root-patched/etc/init.d/dropbear 2025-02-13 21:19:31 +@@ -53,8 +53,7 @@ + MaxAuthTries mdns ipaddrs + + validate_section_dropbear "${1}" || { +- echo "validation failed" +- return 1 ++ echo "validation failed and skip" + } + + [ -n "${Interface}" ] && { +@@ -64,7 +63,6 @@ + } + } + +- [ "${enable}" = "0" ] && return 1 + PIDCOUNT="$(( ${PIDCOUNT} + 1))" + local pid_file="/var/run/${NAME}.${PIDCOUNT}.pid" + +@@ -111,25 +109,12 @@ + load_interfaces() + { + config_get interface "$1" Interface +- config_get enable "$1" enable 1 +- +- [ "${enable}" = "1" ] && interfaces=" ${interface} ${interfaces}" ++ interfaces=" ${interface} ${interfaces}" + } + + start_service() + { +- [ x"bluetooth" == x`micocfg_work_mode` ] && { +- logger -t dropbear.init.d -p 3 "in bluetooth mode, will not start" +- return +- } +- +- ssh_en=`cat /data/ssh_en 2>/dev/null` +- ssh_en_bind=`cat /data/.ssh_en 2>/dev/null` +- ssh_en_tmp=`cat /tmp/ssh_en 2>/dev/null` +- channel=`micocfg_channel 2>/dev/null` +- if [ "$ssh_en" != "1" -a "$ssh_en_bind" != "1" -a "$ssh_en_tmp" != "1" -a "$channel" = "release" ]; then +- return 0 +- fi ++ mkdir -p /data/etc/dropbear + [ -s /data/etc/dropbear/dropbear_rsa_host_key ] || keygen + + . /lib/functions.sh diff --git a/packages/client-patch/patches/02-login.patch b/packages/client-patch/patches/02-login.patch new file mode 100644 index 0000000..2b988e2 --- /dev/null +++ b/packages/client-patch/patches/02-login.patch @@ -0,0 +1,35 @@ +--- squashfs-root/etc/shadow 2024-12-06 15:11:58 ++++ squashfs-root-patched/etc/shadow 2025-02-13 21:07:41 +@@ -1,4 +1,4 @@ +-root:$1$N0Iz0LLs$kZ5zG/Y2AUWHNE5I3ElWm1:18128:0:99999:7::: ++root:{SSH_PASSWORD}:18128:0:99999:7::: + daemon:*:0:0:99999:7::: + ftp:*:0:0:99999:7::: + network:*:0:0:99999:7::: +diff --color -Naur squashfs-root/etc/inittab squashfs-root-patched/etc/inittab +--- squashfs-root/etc/inittab 2024-12-06 15:11:58 ++++ squashfs-root-patched/etc/inittab 2025-02-13 20:56:13 +@@ -1,5 +1,5 @@ + ::sysinit:/etc/init.d/rcS S boot + ::shutdown:/etc/init.d/rcS K shutdown + tts/0::askfirst:/bin/ash --login +-ttyS0::askfirst:/bin/login ++ttyS0::askfirst:/bin/ash --login + tty1::askfirst:/bin/ash --login +diff --color -Naur squashfs-root/etc/pam.d/common-auth squashfs-root-patched/etc/pam.d/common-auth +--- squashfs-root/etc/pam.d/common-auth 2024-12-06 15:11:58 ++++ squashfs-root-patched/etc/pam.d/common-auth 2025-02-13 21:09:02 +@@ -8,10 +8,10 @@ + # traditional Unix authentication mechanisms. + # + +-auth sufficient libmico-pam.so +-account sufficient libmico-pam.so ++# auth sufficient libmico-pam.so ++# account sufficient libmico-pam.so + # here are the per-package modules (the "Primary" block) +-# auth [success=1 default=ignore] pam_unix.so nullok_secure ++auth [success=1 default=ignore] pam_unix.so nullok_secure + # here's the fallback if no module succeeds + auth requisite pam_deny.so + # prime the stack with a positive return value if there isn't one already; diff --git a/packages/client-patch/patches/03-ota.patch b/packages/client-patch/patches/03-ota.patch new file mode 100644 index 0000000..4a254d9 --- /dev/null +++ b/packages/client-patch/patches/03-ota.patch @@ -0,0 +1,46 @@ +--- squashfs-root/bin/ota 2024-12-06 15:11:58 ++++ squashfs-root-patched/bin/ota 2025-02-13 21:50:40 +@@ -133,6 +133,9 @@ + + + download_upgrade() { ++ logger -t ota -p 3 "download upgrade failed" ++ return 1 ++ + if [ -f $OTA_FILE ]; then + logger -t ota -p 3 "ota is in progress now, exit this one." + exit 1 +@@ -278,6 +281,9 @@ + } + + upgrade() { ++ logger -t ota -p 3 "no upgrade" ++ return 1 ++ + if [ -z "$1" -o -z "$2" ]; then + upgrade_failed + return 1 +--- squashfs-root/bin/flash.sh 2024-12-06 15:11:58 ++++ squashfs-root-patched/bin/flash.sh 2025-02-18 23:21:57 +@@ -5,6 +5,9 @@ + . /bin/boardupgrade.sh + + upgrade_param_check() { ++ klogger "no upgrade" ++ return 1 ++ + if [ -z "$1" -o ! -f "$1" ]; then + klogger "USAGE: $0 input.bin [1: slient] [1:don't reboot]" + return 1 +--- squashfs-root/etc/init.d/wireless 2024-12-06 15:11:58 ++++ squashfs-root-patched/etc/init.d/wireless 2025-02-18 23:15:31 +@@ -1361,6 +1361,9 @@ + password=$3 + identity=$4 + ++ echo "download_fail" ++ exit 3; ++ + trylock "ota" + [ $? != 0 ] && { + # shut_led 10 diff --git a/packages/client-patch/patches/04-start.patch b/packages/client-patch/patches/04-start.patch new file mode 100644 index 0000000..10935a3 --- /dev/null +++ b/packages/client-patch/patches/04-start.patch @@ -0,0 +1,9 @@ +--- squashfs-root/etc/rc.local 2024-12-06 15:11:58 ++++ squashfs-root-patched/etc/rc.local 2025-02-13 22:06:13 +@@ -1,4 +1,6 @@ + # Put your custom commands here that should be executed once + # the system init finished. By default this file does nothing. + ++[ -f "/data/init.sh" ] && /data/init.sh & ++ + exit 0 diff --git a/packages/client-patch/patches/LX06/01-asound.patch b/packages/client-patch/patches/LX06/01-asound.patch new file mode 100644 index 0000000..49d242a --- /dev/null +++ b/packages/client-patch/patches/LX06/01-asound.patch @@ -0,0 +1,33 @@ +--- squashfs-root/etc/asound.conf 2024-12-06 15:11:58 ++++ squashfs-root-patched/etc/asound.conf 2025-02-18 22:14:10 +@@ -125,16 +125,20 @@ + } + + pcm.noop { +- type dsnoop +- ipc_key 1024 +- slave { +- pcm "hw:0,3" +- format S32_LE +- rate 48000 +- channels 8 +- period_size 480 +- buffer_size 3840 +- } ++ type plug ++ slave.pcm { ++ type dsnoop ++ ipc_key 1024 ++ ipc_perm 0666 ++ slave { ++ pcm "hw:0,3" ++ format S32_LE ++ rate 48000 ++ channels 8 ++ period_size 480 ++ buffer_size 3840 ++ } ++ } + } + + defaults.pcm.rate_converter "speexrate_medium" diff --git a/packages/client-patch/patches/LX06/02-libxaudio_engine.sh b/packages/client-patch/patches/LX06/02-libxaudio_engine.sh new file mode 100755 index 0000000..81c01e0 --- /dev/null +++ b/packages/client-patch/patches/LX06/02-libxaudio_engine.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +# 动态获取偏移量 +OFFSET=$(strings -t d usr/lib/libxaudio_engine.so | grep hw:0,3 | awk '{print $1}') + +# 检查偏移量是否存在 +if [ -z "$OFFSET" ]; then + echo "未找到 \"hw:0,3\" 字符串偏移量,取消 patch usr/lib/libxaudio_engine.so" + exit -1 +fi + +echo -n -e "noop\x00\x00" | dd of=usr/lib/libxaudio_engine.so bs=1 count=6 seek=$OFFSET conv=notrunc + +echo "patched file usr/lib/libxaudio_engine.so" \ No newline at end of file diff --git a/packages/client-patch/src/build.sh b/packages/client-patch/src/build.sh new file mode 100644 index 0000000..3a81f7f --- /dev/null +++ b/packages/client-patch/src/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +# 下载固件 +if [ -z "$CI" ]; then + npm run ota +else + npx tsx src/ota.ts +fi + +# 提取固件 +npm run extract + +# 打补丁 +npm run patch + +# 打包固件 +npm run squashfs \ No newline at end of file diff --git a/packages/client-patch/src/extract.py b/packages/client-patch/src/extract.py new file mode 100644 index 0000000..73479b0 --- /dev/null +++ b/packages/client-patch/src/extract.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 + +# -*- coding: utf-8 -*- + +# Source: https://github.com/csftech/Xiaomi-OpenWrt-firmware-toolkit +# Author: @csftech (Sheng-Fu Chang) +# MIT Licensed + +import argparse +import binascii +import ctypes +import hashlib +import logging +import os +import time + + +class ImageHeader(ctypes.Structure): + _fields_ = [ + ("magic", ctypes.c_uint), + ("signature_offset", ctypes.c_uint), + ("crc32_checksum", ctypes.c_uint), + ("file_type", ctypes.c_ushort), + ("model", ctypes.c_ushort), + ("segment_offsets", ctypes.c_uint * 8), + ] + + +class SegmentHeader(ctypes.Structure): + _fields_ = [ + ("magic", ctypes.c_uint), + ("flash_address", ctypes.c_uint), + ("length", ctypes.c_uint), + ("partition", ctypes.c_uint), + ("segment_name", ctypes.c_char * 32), + ] + + +class Firmware: + def __init__(self, path): + self.path = path + self.image_header = ImageHeader() + self.fd = open(self.path, "rb") + + def verify(self, ignore_hash=False): + logging.info("[Jobs] Verifying firmware image...") + assert self.fd.readinto(self.image_header) == ctypes.sizeof(self.image_header) + + # magic + logging.info(f"firmware magic: {hex(self.image_header.magic)}") + assert self.image_header.magic in [0x31524448, 0x32524448] + + if self.image_header.magic == 0x32524448: + logging.info("NOTE: firmware seems to be encrypted?") + + # signature + self.fd.seek(self.image_header.signature_offset) + signature_length = int.from_bytes(self.fd.read(16), "little") + signature = self.fd.read() + logging.info( + f"firmware signature_offset: {hex(self.image_header.signature_offset)}" + ) + logging.info(f"firmware signature_length: {signature_length}") + logging.info(f"firmware signature: {signature.hex()}") + assert len(signature) == signature_length + + # crc32 + self.fd.seek(12) + logging.info( + f"firmware crc32_checksum: {hex(self.image_header.crc32_checksum)}" + ) + computed_checksum = ~binascii.crc32(self.fd.read()) & 0xFFFFFFFF + logging.info(f"computed crc32_checksum: {hex(computed_checksum)}") + assert self.image_header.crc32_checksum == computed_checksum + + # md5 + self.fd.seek(0) + m = hashlib.md5() + m.update(self.fd.read()) + hash = m.hexdigest() + logging.info(f"computed md5 hash: {hash}") + filename_hash = os.path.basename(self.path).split("_")[-2] + try: + assert hash[(len(filename_hash) * -1) :] == filename_hash + except AssertionError: + if ignore_hash: + logging.warning( + f'Warning: hash "{filename_hash}" does not match expected!' + ) + pass + else: + raise + + self.fd.seek(0) + return True + + def extract(self, dest: str = None): + logging.info("[Jobs] Extracting firmware...") + current_time = time.strftime("%Y%m%d_%H%M%S", time.localtime()) + self.dest_dir = os.path.join( + dest, f'{os.path.basename(self.path).replace(".bin", "")}_{current_time}' + ) + if dest is not None: + self.dest_dir = dest + os.mkdir(self.dest_dir) + logging.info(f"create destination directory: {self.dest_dir}") + + for address in self.image_header.segment_offsets: + if address: + self.fd.seek(address) + segment_header = SegmentHeader() + assert self.fd.readinto(segment_header) == ctypes.sizeof(segment_header) + + # extract segment + with open( + os.path.join(self.dest_dir, segment_header.segment_name.decode()), + "wb", + ) as s: + s.write(self.fd.read(segment_header.length)) + logging.info( + f'extracting segment: {segment_header.segment_name.decode("ascii")}' + ) + + +def run(path, extract=False, dest=None, ignore_hash=False): + logging.info(f"[MSG] Input file: {path}") + firmware = Firmware(path) + if firmware.verify(ignore_hash): + logging.info( + "[Jobs] Verification success: it's a genuine firmware from Xiaomi." + ) + + if extract: + firmware.extract(dest) + logging.info("[Jobs] Extraction complete.") + + +if __name__ == "__main__": + logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO) + + parser = argparse.ArgumentParser() + parser.add_argument( + "-d", + "--dest", + help="Destination directory to store extracted files, default=current working directory", + action="store", + ) + parser.add_argument( + "-i", + "--ignore-hash", + help="Do not fail if md5 hash does not match", + action="store_true", + ) + parser.add_argument("-e", "--extract", help="Input your firmware", action="store") + parser.add_argument("-s", "--show", help="Show firmware info", action="store") + args = parser.parse_args() + + if args.extract: + firmware_path = os.path.abspath(args.extract) + dest_path = os.getcwd() + if args.dest: + dest_path = args.dest + run(firmware_path, extract=True, dest=dest_path, ignore_hash=args.ignore_hash) + elif args.show: + firmware_path = os.path.abspath(args.show) + run(firmware_path, ignore_hash=args.ignore_hash) + else: + parser.print_help() diff --git a/packages/client-patch/src/extract.sh b/packages/client-patch/src/extract.sh new file mode 100644 index 0000000..c5ffc97 --- /dev/null +++ b/packages/client-patch/src/extract.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +BASE_DIR=$(pwd) +WORK_DIR=$BASE_DIR/temp + +FIRMWARE=$(basename $(ls $BASE_DIR/assets/*.bin 2>/dev/null | head -n 1) .bin) + +if [ ! -f "$BASE_DIR/assets/$FIRMWARE.bin" ]; then + echo "❌ 固件文件不存在,请先下载固件到:$BASE_DIR/assets/" + exit 1 +fi + +rm -rf "$WORK_DIR" && mkdir -pv "$WORK_DIR" && cd $WORK_DIR + +python3 $BASE_DIR/src/extract.py -e "$BASE_DIR/assets/$FIRMWARE.bin" -d "$WORK_DIR/$FIRMWARE" + +ln -sf $WORK_DIR/$FIRMWARE/root.squashfs $WORK_DIR/root.squashfs + +unsquashfs $WORK_DIR/root.squashfs \ No newline at end of file diff --git a/packages/client-patch/src/ota.ts b/packages/client-patch/src/ota.ts new file mode 100644 index 0000000..bd2e50a --- /dev/null +++ b/packages/client-patch/src/ota.ts @@ -0,0 +1,261 @@ +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; + } +} diff --git a/packages/client-patch/src/patch.sh b/packages/client-patch/src/patch.sh new file mode 100755 index 0000000..ae25e4e --- /dev/null +++ b/packages/client-patch/src/patch.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -e + +BASE_DIR=$(pwd) +WORK_DIR=$BASE_DIR/temp + +PASSWORD=${SSH_PASSWORD:-"open-xiaoai"} +PASSWORD=$(openssl passwd -1 -salt "open-xiaoai" "$PASSWORD") + +# 应用指定目录下的补丁文件 +apply_patches() { + local patch_dir="$1" + local message="$2" + + echo "🔥 $message" + + if [ -d "$patch_dir" ]; then + for file in "$patch_dir"/*; do + if [ -f "$file" ]; then + if [[ "$file" == *.patch ]]; then + # 创建临时文件用于占位符替换 + local temp_patch=$(mktemp) + # 将补丁文件中的 {SSH_PASSWORD} 替换为 PASSWORD + sed "s/{SSH_PASSWORD}/$PASSWORD/g" "$file" > "$temp_patch" + # 应用替换后的补丁 + patch -p1 < "$temp_patch" + # 清理临时文件 + rm "$temp_patch" + elif [[ "$file" == *.sh ]]; then + sh "$file" + fi + fi + done + fi +} + +if [ ! -f "$BASE_DIR/assets/.model" ]; then + echo "❌ 固件信息不存在,请先下载固件到:$BASE_DIR/assets/" + exit 1 +fi + +PATCH_DIR=$BASE_DIR/patches +MODEL=$(cat $BASE_DIR/assets/.model) + +cd $WORK_DIR/squashfs-root + +# 应用通用补丁 +apply_patches "$PATCH_DIR" "正在应用通用补丁..." + +# 应用特定型号补丁 +apply_patches "$PATCH_DIR/$MODEL" "正在应用 $MODEL 补丁..." + +echo "✅ 补丁应用完成" + diff --git a/packages/client-patch/src/squashfs.sh b/packages/client-patch/src/squashfs.sh new file mode 100755 index 0000000..9076e13 --- /dev/null +++ b/packages/client-patch/src/squashfs.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -e + +BASE_DIR=$(pwd) +WORK_DIR=$BASE_DIR/temp + +FIRMWARE=$(basename $(ls $BASE_DIR/assets/*.bin 2>/dev/null | head -n 1) .bin) + +cd $WORK_DIR + +if [ ! -f "$BASE_DIR/assets/$FIRMWARE.bin" ]; then + echo "❌ 固件文件不存在,请先下载固件到:$BASE_DIR/assets/" + exit 1 +fi + +if [ ! -d "$FIRMWARE" ]; then + echo "❌ 解压后的固件文件夹不存在,请先提取固件" + exit 1 +fi + +SQUASHFS_INFO=$(file $FIRMWARE/root.squashfs) +echo "🚗 原始固件信息: $SQUASHFS_INFO" + +COMPRESSION=$(echo "$SQUASHFS_INFO" | grep -o "xz\|gzip\|lzo\|lz4\|zstd compressed" | cut -d' ' -f1) +BLOCKSIZE=$(echo "$SQUASHFS_INFO" | grep -o "blocksize: [0-9]* bytes" | cut -d' ' -f2) + +echo "🔥 使用原始参数重新打包固件..." +mksquashfs squashfs-root $FIRMWARE/root.squashfs \ + -comp $COMPRESSION -b $BLOCKSIZE \ + -noappend -all-root -always-use-fragments -no-xattrs -no-exports + +cp -rf $FIRMWARE $BASE_DIR/assets/$FIRMWARE + +echo "✅ 打包完成,固件文件已复制到 assets 目录..." +echo $BASE_DIR/assets/$FIRMWARE/root.squashfs diff --git a/packages/client-patch/tsconfig.json b/packages/client-patch/tsconfig.json new file mode 100644 index 0000000..fd8b32b --- /dev/null +++ b/packages/client-patch/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@mi-gpt/config/typescript", + "include": ["src"], + "exclude": ["node_modules"] +}