mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
用户这次提了一整套升级。这个提交先落地三块(其余几块在做)。
== 一、静默自动更新 ==
用户原话:"手表端点击一个更新按钮,电脑端就可以自动进行静默更新,
其他什么设置都不变 …… 不弹任何窗口出来"。
链路:
手表点按钮 -> 被控端问中转"最新是哪版"
-> 下载到 %TEMP%\ykxmz-update\
-> 校验 sha256
-> 起一个**分离的 .cmd 帮手**
-> 自己退出
-> 帮手等它退出 -> 静默跑安装器 -> 重新拉起
关键设计说明:
* **为什么要有那个 .cmd 帮手**:Windows 上正在运行的 exe 是锁着的,
安装器覆盖不了,所以必须"先退出、再装、再起来",而这三步
不能由被控端自己做(它一退出就没人执行后面的了)。
用 DETACHED_PROCESS 起的助手不受父进程退出影响。
* **配置为什么不会丢**:配置在 %ProgramData%\ykxmz\data\,
**不在程序目录里**。安装器升级只替换 {app} 下的文件,
不碰 ProgramData —— 中转地址、配对码、各种开关原样保留。
这是这个功能能做的前提。
* **更新源是用户自己的中转站**,不是作者的服务器,也不是任何
写死的公网地址。被控端问的是它自己配置的那个地址。
中转没有这个接口(老版本服务端)时会明确提示"先手动升级一次服务端"。
* **sha256 必须校验**:自更新最大的风险是下到一半断了或被换了,
装上去是个坏程序、而且它还会自己再更新 —— 螺旋下去很难救。
对不上就**放弃这一轮**,保持现状(现状至少能用)。
中转新增:GET /v1/update/latest?platform=windows
返回 version / name / url / size / sha256。版本比较按**数字段**
(不能按字符串,否则 "1.9.0" > "2.1.4")。sha256 按
(文件名,大小,mtime) 缓存,避免每次重算 9MB。
手表端:"电脑端版本:点这里查看" 按钮,查完显示
"电脑端 2.1.4 → 2.1.5(点这里更新)",确认后才真的更新。
== 二、远程鼠标控制 ==
新增 windows/lanremote/input.py,用 SendInput 模拟鼠标键盘。
* **为什么用 SendInput 而不是 mouse_event**:后者是 Win95 时代的
API,会被 UIPI 拦,而且没有 dwExtraInfo(多个注入源会互相干扰)。
SendInput 还能一次打包多个事件,按下+抬起之间不会插进别的输入。
* **坐标是 0..65535 归一化**,而且**必须除以 (屏幕宽-1)**。
少了那个 -1,最右边和最下边几像素点不到 —— 表现为
"鼠标到不了屏幕边缘",任务栏在底下的机器上尤其明显。
* **INPUT 结构在 32/64 位下大小不同**(28 / 40),
ctypes 里按进程位数算,不能写死。
* 权限说明:目标窗口如果是管理员权限的(UAC 提升过的),
普通权限的注入会被 UIPI 拦掉。这不是 bug,是 Windows 的设计。
被控端以管理员运行就都能点。
== 三、一键显示桌面 ==
用户要求快捷栏里那个"不能改"的按钮就是这个功能,等于 Win+D。
和鼠标控制共用 input.py。
== 顺手修的一个自己造的坑 ==
往 server.py 插方法时,**两次**把 dispatch 的兜底
`return {"ok": False, "error": f"未知命令: {cmd}"}` 一起卷进了新方法里:
* pyflakes 报 "undefined name 'cmd'"
* 而且 dispatch 丢了兜底分支(未知命令会返回 None)
第一次靠 pyflakes 抓到,第二次也是。为了以后不再犯,
`_tests/test_update_flow.py` 里加了**结构断言**:
兜底必须在带 CMD_UPDATE 的那个方法体里,而各新方法里不能出现它。
(那条断言第一版写得太死 —— 要求"紧跟 CMD_UPDATE 分支",
后来往 dispatch 里插了新分支就误报了。改成验结构真相。)
== 验证 ==
全量回归(含新增的 3a10 / 3a11 两关)全部 PASS
静默自动更新:协议 + updater 15 项 + 服务端结构 8 项
远程鼠标:坐标归一化 / 滚轮夹取 / Win+D 事件顺序 / 非法参数 22 项
610 个 Python 文件语法通过
实际注入鼠标成功(move / click / scroll / show_desktop)
== 还没做的(下一批) ==
手表顶部快捷栏(可自定义最多 4 个,1 个固定"显示桌面")
屏幕镜像去掉提示条、去掉所有弹窗
托盘图标改成设置项
「允许新设备连接」开关(中转后台 + 安卓端)
弹文字窗口位置/大小可自定义
中转后台设备列表显示版本 + 远程点更新
== 顺手清理 ==
_tests/out_app/ 下面那堆 .class 是 Java 编译检查的产物,
以前误提交进仓库了(152 个文件)。这次从索引里移除并加进 .gitignore。
134 lines
5.2 KiB
PowerShell
134 lines
5.2 KiB
PowerShell
# 用 aapt2 + javac + d8 + zipalign + apksigner 手工构建 APK(不需要 Gradle)
|
||
# 用法: powershell -File android\build_apk.ps1
|
||
#
|
||
# 注意:刻意不用 $ErrorActionPreference="Stop"。
|
||
# 原生工具的告警(javac 的 deprecation note 等)会写 stderr,
|
||
# 在 Stop 模式下会被 PowerShell 当成致命错误中断构建。
|
||
# 改为每一步显式检查 $LASTEXITCODE。
|
||
$ErrorActionPreference = "Continue"
|
||
|
||
$android = $PSScriptRoot # LanRemote/android(本脚本所在目录)
|
||
$root = Split-Path -Parent $android # LanRemote
|
||
$tc = Join-Path $root "..\starklink\toolchain"
|
||
$jdk = Join-Path $tc "jdk17"
|
||
$sdk = Join-Path $tc "android-sdk"
|
||
$bt = Join-Path $sdk "build-tools\34.0.0"
|
||
$androidJar = Join-Path $sdk "platforms\android-34\android.jar"
|
||
$keystore = Join-Path $tc "debug.keystore"
|
||
|
||
$javac = Join-Path $jdk "bin\javac.exe"
|
||
$keytool = Join-Path $jdk "bin\keytool.exe"
|
||
$jar = Join-Path $jdk "bin\jar.exe"
|
||
|
||
$aapt2 = Join-Path $bt "aapt2.exe"
|
||
$zipalign = Join-Path $bt "zipalign.exe"
|
||
$apksigner= Join-Path $bt "apksigner.bat"
|
||
$d8 = Join-Path $bt "d8.bat"
|
||
|
||
$work = Join-Path $android "build"
|
||
$outDir = Join-Path $root "dist"
|
||
$apkName = "ykxmz-watch-2.1.6.apk"
|
||
|
||
function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan }
|
||
|
||
# 前置检查
|
||
foreach ($tool in @($javac, $aapt2, $d8, $zipalign, $apksigner)) {
|
||
if (-not (Test-Path $tool)) { throw "缺少工具: $tool" }
|
||
}
|
||
if (-not (Test-Path $androidJar)) { throw "缺少 android.jar: $androidJar" }
|
||
if (-not (Test-Path $keystore)) { throw "缺少签名用 keystore: $keystore" }
|
||
|
||
$env:JAVA_HOME = $jdk
|
||
$env:PATH = "$(Join-Path $jdk 'bin');$env:PATH"
|
||
|
||
if (Test-Path $work) { Remove-Item -Recurse -Force $work }
|
||
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
||
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||
|
||
# ---------------- 1. 编译资源 ----------------
|
||
Step "1/6 aapt2 compile 资源"
|
||
$compiled = Join-Path $work "res.zip"
|
||
& $aapt2 compile --dir (Join-Path $android "res") -o $compiled
|
||
if ($LASTEXITCODE -ne 0) { throw "aapt2 compile 失败" }
|
||
|
||
# ---------------- 2. 链接资源 + 生成 R.java ----------------
|
||
Step "2/6 aapt2 link 生成资源表与 R.java"
|
||
$genDir = Join-Path $work "gen"
|
||
New-Item -ItemType Directory -Force -Path $genDir | Out-Null
|
||
$unsigned = Join-Path $work "app-unsigned.apk"
|
||
& $aapt2 link `
|
||
-o $unsigned `
|
||
-I $androidJar `
|
||
--manifest (Join-Path $android "AndroidManifest.xml") `
|
||
-R $compiled `
|
||
--java $genDir `
|
||
--min-sdk-version 24 `
|
||
--target-sdk-version 34 `
|
||
--version-code 5 `
|
||
--version-name "2.1.6" `
|
||
--auto-add-overlay
|
||
if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" }
|
||
|
||
# ---------------- 3. 编译 Java ----------------
|
||
Step "3/6 javac 编译 Java 源码"
|
||
$classesDir = Join-Path $work "classes"
|
||
New-Item -ItemType Directory -Force -Path $classesDir | Out-Null
|
||
|
||
$javaSources = @()
|
||
$javaSources += Get-ChildItem -Recurse -Filter *.java -Path (Join-Path $android "src") | ForEach-Object { $_.FullName }
|
||
$javaSources += Get-ChildItem -Recurse -Filter R.java -Path $genDir -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
|
||
if ($javaSources.Count -eq 0) { throw "没有找到 Java 源文件" }
|
||
Write-Host "源文件 $($javaSources.Count) 个"
|
||
|
||
& $javac -source 8 -target 8 -nowarn -Xlint:none -encoding UTF-8 `
|
||
-bootclasspath $androidJar `
|
||
-d $classesDir `
|
||
$javaSources 2>&1 | Out-String | Write-Host
|
||
if ($LASTEXITCODE -ne 0) { throw "javac 编译失败" }
|
||
|
||
# ---------------- 4. d8 转 dex ----------------
|
||
Step "4/6 d8 转换为 classes.dex"
|
||
$classesJar = Join-Path $work "classes.jar"
|
||
& $jar cf $classesJar -C $classesDir .
|
||
if ($LASTEXITCODE -ne 0) { throw "打包 classes.jar 失败" }
|
||
|
||
$dexDir = Join-Path $work "dex"
|
||
New-Item -ItemType Directory -Force -Path $dexDir | Out-Null
|
||
& $d8 --lib $androidJar --min-api 24 --output $dexDir $classesJar 2>&1 | Out-String | Write-Host
|
||
if ($LASTEXITCODE -ne 0) { throw "d8 失败" }
|
||
$dex = Join-Path $dexDir "classes.dex"
|
||
if (-not (Test-Path $dex)) { throw "未生成 classes.dex" }
|
||
Write-Host ("classes.dex 大小 {0:N1} KB" -f ((Get-Item $dex).Length / 1KB))
|
||
|
||
# ---------------- 5. 组装 APK ----------------
|
||
Step "5/6 组装 APK"
|
||
& python (Join-Path $root "_build\pack_apk.py") $unsigned $dex
|
||
if ($LASTEXITCODE -ne 0) { throw "写入 classes.dex 失败" }
|
||
|
||
$aligned = Join-Path $work "app-aligned.apk"
|
||
& $zipalign -f 4 $unsigned $aligned
|
||
if ($LASTEXITCODE -ne 0) { throw "zipalign 失败" }
|
||
|
||
# ---------------- 6. 签名 ----------------
|
||
Step "6/6 apksigner 签名"
|
||
$final = Join-Path $outDir $apkName
|
||
if (Test-Path $final) { Remove-Item $final -Force }
|
||
& $apksigner sign `
|
||
--ks $keystore `
|
||
--ks-pass pass:android `
|
||
--key-pass pass:android `
|
||
--ks-key-alias androiddebugkey `
|
||
--v1-signing-enabled true `
|
||
--v2-signing-enabled true `
|
||
--out $final `
|
||
$aligned
|
||
if ($LASTEXITCODE -ne 0) { throw "签名失败" }
|
||
|
||
Step "验证签名"
|
||
& $apksigner verify --print-certs $final
|
||
if ($LASTEXITCODE -ne 0) { throw "签名校验失败" }
|
||
|
||
Step "构建完成"
|
||
Write-Host ("APK: {0}" -f $final) -ForegroundColor Green
|
||
Write-Host ("大小: {0:N2} MB" -f ((Get-Item $final).Length / 1MB)) -ForegroundColor Green
|