Files
MAG160C/docs/android_app/verification_guide.md
T

25 KiB
Raw Blame History

核查指南 —— Phase A→F 执行轮(交给核查 AI 的入口)

核查对象commit 06c1f30..8e1312a7 个提交,基线 087e15c), 位于 C:\Project\MAG160C,源码在 android\Gradle,包 com.mag160c.thermal)。

第一原则:不要相信执行模型的总结与文档叙述。 所有结论必须来自 ① git 提交内容 ② 仓库内的官方逆向产物(可独立复核)③ 亲自重跑的脚本/构建 ④ 硬件实测。执行模型(下称"执行者")的文档只当"待验证的声明"处理。

本文件给出一条可直接粘贴的起始指令、阅读顺序、三道必过关口、 逐阶段核查方法、执行者主动交底的高风险项,以及本机无法验证的边界。


0. 可直接粘贴给核查 AI 的起始指令

你是本项目的独立核查者。项目根 C:\Project\MAG160Cgit 仓库),
核查范围是提交 06c1f30..8e1312a7 个提交,基线 087e15c),
任务是把 docs/android_app/execution_plan.md 中 Phase A→F 的每项声明
独立复核一遍并对每条给出 证实/证伪/无法判定 的结论。

铁律:
1. 不要采信执行者写的总结、session_state、checklist 的任何叙述,
   它们只是待验证声明;结论必须来自代码、官方逆向产物、可复现命令。
2. 先读 docs/android_app/verification_guide.md(核查指南),按它的
   三道关口顺序执行;第一道不通过就停止并报告。
3. 不要修改任何源码或提交;你是只读核查者。发现问题只报告,不修复。
4. 无法在本机验证的(需真机/双设备)必须显式标注"无法判定",不要默认通过。
5. 执行者的构建命令(Windows Git Bash):
   cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21" && \
   cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug test --no-daemon"
   (本机无 python3、无模拟器、无连接设备;JDK/SDK 见 android/local.properties
最后按"阶段 → 声明 → 方法 → 结论 → 反例(若证伪)"输出报告。

1. 阅读顺序(先建立判据,再看实现)

  1. docs/android_app/execution_plan.md —— 验收判据来源。特别是 §0 全局约束 (禁改清单、构建命令、提交格式)与各阶段的"精确规格"。核查就是"拿计划对实现"。
  2. docs/android_app/verification_guide.md(本文件)—— 核查路线。
  3. git log --oneline 087e15c..HEAD 与每个提交的 git show --stat —— 改动边界。
  4. docs/android_app/session_state.md 的"执行计划进度""本轮总结""待办"—— 执行者的自述(待验证)。
  5. docs/android_app/real_device_checklist.md —— 24 步真机清单(待验证, 见 §4.9:其中第 15 步的预期与实现可能不符)。
  6. 官方逆向产物(判据,非执行者产出):
    • analysis/magcx_official_flow.md(协议权威)
    • analysis/sdk_re/android_app/jadx_magcx/(官方普通版 Java 源码)
    • analysis/sdk_re/android_app/libcxsdk_decomp.txtGhidra 伪代码)
    • analysis/sdk_re/android_app/palette_extraction_findings.mdPhase C 结论)

判据优先级:官方产物 > 计划文档 > 执行者文档。三者冲突时以官方产物为准, 并指出冲突本身。


2. 第一关:硬门槛(不通过则停止核查)

2.1 git 完整性

cd /c/Project/MAG160C
git status --short          # 期望:无输出(工作树干净)
git log --oneline 087e15c..HEAD   # 期望:恰好 7 条,见下表
git log --oneline -1 origin/main  # 期望:087e15c**未 push**,符合用户指令)

期望的提交与消息(消息须与执行计划 §各阶段"提交信息"逐字一致):

hash 消息(计划要求) 阶段
1 06c1f30 android: lifetime query + cali consistency check + real-device checklist A
2 656d419 android: MDT temperature decode + analyzer probe UI B
3 b7a928e android: vendor palette extraction tooling (+exact tables if found) C
4 c34940e android: cloud module scaffold (retrofit, opt-in, disabled by default) D
5 f8b3200 android: visible-light PIP overlay (camera2, draggable, 3 sizes) E
6 512508e android: LAN remote preview (UDP discovery + raw-frame TCP stream, client-side rendering) F
7 8e1312a android: phase Z wrap-up (...)(Z 无强制消息,内容须仅为文档/清单/APK) Z

反例:条数≠7;消息被改写;Z 里混入功能代码改动。

2.2 禁改清单(执行计划 §0 第 1 条)

逐条独立验证,不要相信"执行者说没改":

# ① 竖屏锁定:源码 + 已构建 APK 双重确认
git diff 087e15c..HEAD -- android/app/src/main/AndroidManifest.xml | grep -i orientation
#   期望:无 '-' 开头的行(只有新增注释/权限行)
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump xmltree \
  build-artifacts/mag160c-app-debug.apk --file AndroidManifest.xml | grep -i screenOrientation
#   期望:screenOrientation(0x0101001e)=1   1 = portrait

# ② 命令字节序锁:MagProtocolTest 与 cmd4/cmd8 实现都不得变
git diff 087e15c..HEAD --stat -- android/app/src/test/kotlin/com/mag160c/thermal/usb/MagProtocolTest.kt
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/usb/MagProtocol.kt
#   期望:测试文件无 diffMagProtocol 仅新增 RSP_SEND_LIFETIME 常量

# ③ LiveRenderer 构图逻辑
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveRenderer.kt
#   期望:空

# ④ analysis/ 只读参考:已存在文件不得被改(新增文件允许)
git diff 087e15c..HEAD --stat --diff-filter=M -- analysis/
#   期望:空

# ⑤ IrSession 握手序列(A 阶段按要求加了代码,需人工确认序列未被改)
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/usb/IrSession.kt
#   核查点:66b(GetParameter1)→66c→66f→[670]→673 顺序、800ms 超时、
#   前半段失败即 abort/回退 的行为均未被改动;删除行应仅有两处
#   cache.readBytes() 改为先比 MD5stats 行末尾追加 lifetime=

反例APK 里 screenOrientation≠1MagProtocolTest 有改动; LiveRenderer 有 diff--diff-filter=M 的 analysis/ 输出非空。

2.3 构建与测试(全绿是计划的硬性验收)

cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21"
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug :app:assembleRelease test --no-daemon"
#   期望:BUILD SUCCESSFULdebug + release(R8) + 单测)

单测计数须可复现(执行者报告为 44/0/0):

cd /c/Project/MAG160C
grep -h -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' \
  android/app/build/test-results/testDebugUnitTest/*.xml \
  | awk -F'"' '{t+=$2;f+=$6;e+=$8} END {print "tests="t" failures="f" errors="e}'

期望的逐类分布(缺一类即说明有用例被静默跳过):

测试类 用例数 覆盖
usb.MagProtocolTest 3 命令字节序(既有,未改)
core.RenderPipelineTest 3 与 C 参考逐字节一致(既有)
core.TempMathTest 5 Phase B 温度图
core.PalettesTest 8 Phase C 调色板
media.MdtTest 6 Phase B MDT 容器
cloud.CloudClientTest 4 Phase D 默认关闭契约
net.RemoteContractTest 12 Phase F 协议/粘包/截断
net.RemoteLoopbackTest 3 Phase F 真实 TCP 回环端到端

重要RenderPipelineTest.matchesCReferencePixelExact 必须仍然通过—— 它是整个渲染管线正确性的地基;若它挂了,后面所有阶段都无意义。


3. 第二关:逐阶段声明 → 核查方法

Phase A06c1f30

声明 核查方法 通过判据 / 反例
RSP_SEND_LIFETIME = 0x5BB5B561 等于官方 D2P_SendLifeTime jadx_magcx/\cn\com\magnity\magnitycx\sdk\D2PCmd.java 的十进制值,手算十六进制 =1538635105;反例:数值不符
CMD_GET_LIFETIME 未被改动 同上(P2DCmd.java)对照 =1807136373=0x6BB6B675
lifetime 失败不阻断连接 IrSession.startInternal 第 3b 步 失败仅 DebugLog.log,无 return/abort
cali 缓存 MD5 对照不改变返回值 obtainCali cache-hit 分支 仍 return cached;日志文案 cali cache vs bundled DDT: identical/differ
心跳 stats 追加 lifetime streamLoop 行尾 lifetime=${deviceLifetimeMs}
checklist 的日志串与代码一致 analysis/tools/CheckStrings.java(见下),再人工追溯它标出的 7 条 无凭空编造的日志行

checklist 日志串机械核对(执行者已自测,核查者须独立重跑):

cd /c/Project/MAG160C && \
"C:\Tools\jdk-21\bin\java" analysis/tools/CheckStrings.java \
  docs/android_app/real_device_checklist.md android/app/src/main
#   执行者 2026-09-11 实测:exact=16, skeleton=30, needs-review=7

那 7 条不是缺陷,而是"整行都是实例数据"的情形(模板在一个字符串字面量里, 命令名由调用点传入),工具无法自动匹配,必须人工追溯。执行者已逐条追溯完毕:

标出的行 源码出处(已核对)
[cmd] GetParameter1 write=4/4 "$name write=$n/${packet.size}" (IrSession.kt:418) + 调用点 "GetParameter1" (:185)
[cmd] GetParameter1 resp=0x5BB5B55B len=60 head=… "$name resp=0x%08X len=%d head=%s" (:433) + 同调用点
[cmd] GetParameter2 resp=0x5BB5B55C len=… 同上模板 + 66c 调用点 (:192)
[cmd] BasePara1: serial=… devType=3 160x120 @15fps "BasePara1: serial=$serial devType=$devType ${width}x$height @${fps}fps" (:292)
[usb] ep 0x81 fail#… halted=… "ep 0x%02X fail#$failureCount get_status rc=%d halted=%d clear_halt rc=%d" (:408)
[cmd](空)与 [crash] 正则产物,非清单条目

核查者要做的是独立确认这张对应表(尤其模板与调用点是否真能拼出清单示例), 而不是重跑得到同样的 7 条后就放行。

A 的 obtainCali 无单测(需 UsbDeviceConnection)。核查者应显式标注 "仅静态审查,无运行时证据"。

Phase B656d419

声明 核查方法 通过判据 / 反例
Mdt.parsecompose 互逆 读代码 + 跑 MdtTest另写独立用例:构造含 EXIF 缩略图的 JPEG(内部含 FF D9)再 compose/parse jpg 字节往返一致(extractJpg 反向搜索最后一个 FFD9,应取主图 EOI,不被缩略图干扰)
text 去 NUL 填充 读 parse trimEnd('\u0000')
温度图 = u16LE → countsToTempMc TempMath.tempMapFromPixels独立复算:用 csdk/src/mag160c_temp.c 或已提交的 C 参考对同一 counts 算温度 TempMathTest.mapMatchesScalarConversionPerPixel 同值
分析页探针映射为"直接映射"而非 90° 逆映射 AnalyzeViewer:画布 aspectRatio(4/3)drawImage 无 canvas.rotate 判断成立则直接映射正确;反例:若实际绘制有旋转,则探针温度取错像素
温度条固定在未缩放 fitRect 顶部 drawTemperatureOsd(传 imageRect(size,1f,Offset.Zero) 规格歧义(计划原文"随 fitRect 走,不随手势矩阵"),核查者需判定取舍是否可接受

Phase C(b7a928e)——本轮最需要深度复核的一相

声明 核查方法 通过判据 / 反例
libcxsdk.so 无静态调色板表 analysis/tools/PalScan.java决定性:正对照);python 版仅作弱旁证 三种编码(B,G,R,0 / B,G,R,0xFF / 忽略第 4 字节的 B,G,R)全部 not found
索引↔case 映射(0..10 ①读 libcxsdk_decomp.txtCFunctions::SetColorPalette,确认是 switch(param_2) 且 case 标签即调色板序号;②锚点:case 2 → OfficialTables.PALETTE256_ARGB 256/256;③预览图交叉验证 ①②为结构性证据;③仅作旁证
索引 11 未解决 检查 VendorPalettes.SOURCE_CASE[11] == -1Palettes.buildAll()[11] 为近似曲线 与 findings 文档一致
铁虹仍是官方表 Palettes.officialIronbow()PalettesTest.ironbowMatchesOfficialTables 返回 OfficialTables.PALETTE256_ARGB 本身

否证复核 + 生成物可复现(执行者已自测,核查者须独立重跑;完整命令见 analysis/sdk_re/android_app/palette_extraction_findings.md 的"可复现产物"节):

cd /c/Project/MAG160C
# 决定性:官方铁虹表是否真的不在库里(3 种编码都要 not found)
"C:\Tools\jdk-21\bin\java" analysis/tools/PalScan.java \
  analysis/sdk_re/android_app/bin/libcxsdk.so \
  csdk/src/mag160c_official_palette256.h

# 生成物可复现(两文件一起编译;重新生成后 diff 应为空)
#   实测:VendorPalettes.kt 与 palette_candidates.json diff 均为空,锚点 256/256

⚠️ 核查者必须避开的坑:铁虹表在内存里的布局是 (B, G, R, 0)——第 4 字节 是 0,而 OfficialTables.kt 的 ARGB int 被生成脚本强制 alpha=0xFF。 所以"直接把 Kotlin 表当针去二进制里搜"必然搜不到,会得到假的"未找到"。 针必须从 csdk/src/mag160c_official_palette256.h(真实内存布局)构造, 或用已修正的 PalScan.java。执行者在第一版工具里就犯过这个错,已修正。

另:通用"256 项渐变"启发式扫描在本库上误报约 11% 的位置(全是指令/整数 数组),PalScan.java 的 [C] 段已明确标注不可作为证据,不要引用该计数。

重新生成并比对(Phase C 最强核查手段;执行者已自测通过,核查者须独立重跑):

注意:PalExport2.java 不是单文件可直跑——它依赖同一目录 PalIdentify.java 中的 PalBody(13 个生成体)。必须两文件一起编译。 另外 Windows 版 java 不认 Git Bash 的 /tmp 路径,必须用 C:\... 形式。

# ① 解包官方普通版的调色板预览图(交叉验证用)
mkdir -p /c/Users/zxc/AppData/Local/Temp/cxres && \
cd /c/Users/zxc/AppData/Local/Temp/cxres && \
"C:\Tools\jdk-21\bin\jar" xf "C:\Project\MAG160C\app\【普通版】MAG-Cx.apk" res/mipmap-hdpi-v4/

# ② 两文件编译
mkdir -p /c/Users/zxc/AppData/Local/Temp/regen_build && \
cd /c/Users/zxc/AppData/Local/Temp/regen_build && \
cp "C:\Project\MAG160C\analysis\tools\PalIdentify.java" . && \
cp "C:\Project\MAG160C\analysis\tools\PalExport2.java" . && \
"C:\Tools\jdk-21\bin\javac" -d out PalIdentify.java PalExport2.java

# ③ 重新生成(输出到临时目录,勿覆盖仓库)
"C:\Tools\jdk-21\bin\java" -cp out PalExport2 \
  "C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\OfficialTables.kt" \
  "C:\Users\zxc\AppData\Local\Temp\cxres\res\mipmap-hdpi-v4" \
  "C:\Users\zxc\AppData\Local\Temp\regen_core" \
  "C:\Users\zxc\AppData\Local\Temp\regen_out"
#   期望最后一行:iron_bow vs OfficialTables anchor: 256/256

# ④ 与仓库文件逐字节比对(两条都必须为空)
diff "C:\Users\zxc\AppData\Local\Temp\regen_core\VendorPalettes.kt" \
     "C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\VendorPalettes.kt"
diff "C:\Users\zxc\AppData\Local\Temp\regen_out\palette_candidates.json" \
     "C:\Project\MAG160C\analysis\sdk_re\android_app\palette_candidates.json"

执行者自测结果(2026-09-11,供核查者对照):上述四条命令全部成功, 铁虹锚点 256/256,两份生成物 diff 均为空,即从 .so 伪代码 → 生成体移植 → Kotlin 表这条链完全可复现。这证明的是"移植实现与提交物一致",证明 "移植的语义与厂商二进制一致"(后者只有 case 2 锚点能证明)。

执行者主动交底(务必复核)index 4/8/9(琥珀/喷射/红饱和)的映射在 两种独立度量下结论不一致——像素加权的最近色距离倾向把"红饱和"的预览图 判给灰阶表(case 0),而颜色集合覆盖率倾向 case 9。原因是该预览图近乎无彩 (执行者测得 meanChroma≈9.7)。执行者最终采用了"结构性证据(switch(param_2) 的 case 标签即调色板序号)+ case 2 锚点"的论证,预览图仅作旁证。核查者应 独立判定索引 9(以及 4、8)的映射是否成立;更强证据需反解 ARM32 跳表 (文件偏移 0x26cd2 附近,执行者尝试未成功)或真机抓帧比对。

可复现的"红热预览图是占位副本"检查(支撑索引 11 未解决的结论):

# palette_red_hot.png 与 palette_white_hot.png11914 个不透明像素中 11777 个完全相同
# 可用任意图像工具逐像素比对(执行者用自写 Java 工具测得 meanAbsDiff=0.5/255

Phase Dc34940e

声明 核查方法 通过判据 / 反例
Retrofit 2.11.0 已接入 gradle/libs.versions.tomlapp/build.gradle.kts 两处均在
默认关闭 AppSettings.cloudEnabled(默认 false);跑 CloudClientTest 4 项全绿
任何生产代码路径都不会静默联网 grep -rn "CloudClient" android/app/src/main 只应有 AppSettings 的 setEnabled 与声明;反例:任何 api() 调用点
release 混淆不破 assembleRelease BUILD SUCCESSFUL
PROGUARD 规则存在 app/proguard-rules.pro -keep class com.mag160c.thermal.cloud.**

Phase Ef8b3200

声明 核查方法 通过判据 / 反例
CAMERA 权限已加且相机可选 aapt2 dump badging 看 uses-feature camera/camera.any/autofocus 均为 not-requiredusb.host 仍为 required
顶栏第 5 项 + 图标 LiveScreenres/drawable/ic_pip.xml 双弧圆 + 右下实心矩形(符合计划图标约定)
三档尺寸 96/128/160dp、高=宽×3/4 LiveViewModel.PIP_WIDTHS_DPPipOverlay 一致
右侧留色标条位 PIP_RIGHT_MARGIN_DP = 32 一致
异常不崩溃 PipCameraView:所有回调 try/catch + release 无未捕获路径
相机释放(关闭/离页/ON_STOP PipOverlay.DisposableEffectLiveScreen 生命周期观察者 三处均有 release

执行者主动交底(务必真机验证)ON_STOP 释放相机后,是否有代码自动重新 打开未经证实。实现中 PipCameraEngine.released 标志被置位后不再复位,重新 打开依赖 TextureView surface 重建路径(onSurfaceTextureAvailable)。 real_device_checklist.md 第 15 步写了"返回后若 PIP 仍为开启态,画面重新出现", 这是推断而非实测;若真机不符,应判定为该步预期有误(清单过强), 而非功能必须修复——请按实测修正文档结论。

另一处待验证PIP 浮层用了两个并列 pointerInput(一个拖拽、一个点按/双击)。 Compose 中多 detector 并存的行为需真机确认(单击换档、双击关闭、拖拽是否互不干扰)。

Phase F512508e

声明 核查方法 通过判据 / 反例
端口/魔数/帧长与计划一致 RemoteContract47510/47511/0x1BB1B11B/38400/38412/3s/10s 逐个对表
封包格式 encodeFramePacket + 跑 RemoteContractTest [magic][counter][len][38400B]
粘包/截断/坏长度/重同步 net.RemoteContractTest12 项) 全绿
端到端可用 net.RemoteLoopbackTest(真实 TCP 全绿;核查者亦可自写独立回环测试(契约见 RemoteContract 注释)
渲染在客户端本地 RemoteViewerViewModel:本地 RenderPipeline+内置 DDTsetPalette/setZoom 只动本地 调色板/变倍无网络调用
主机侧不拖慢相机 RemoteHost:帧队列 DROP_OLDEST、单协程写 socket 一致
默认不启动 读设置页与 setRemoteHostEnabled 默认关闭;开启需活跃 USB 会话,否则返回 false 弹"先连接热像仪"
INTERNET 权限 aapt2 dump badging 已声明

执行者主动交底(潜在缺陷,建议构造用例) RemoteSession.send() 每条命令 launch 一个新协程写同一 socket两条相邻命令 (如 hello 后紧跟 startStream)理论上可能交错写入(小包在 TCP 上通常原子, 但无保证)。建议核查者写一个高频命令用例(如连续多次 requestFfc())观察主机侧 是否出现解析不了的半行 JSON。若复现,属真实缺陷(应串行化写协程)。

未验证边界UDP 255.255.255.255 广播在真机/真实 Wi-Fi 上是否可达 (部分网络/设备会过滤广播,且息屏策略可能影响收包)——仅回环测试通过。


4. 第三关:执行者主动交底的可疑点汇总(按风险排序)

# 事项 风险 建议动作
1 Phase C 索引 4/8/9 的映射在两种度量下不一致 高(可能装错表) 独立复验;必要时反解跳表/真机比对
2 Phase C 静态扫描判据仅覆盖 alpha=0xFF 补扫 alpha=0x00 / RGB888 / BGR 等编码
3 PIP 从后台返回能否自动恢复相机 真机验证;不符则修正清单预期
4 PIP 双 pointerInput 手势并存 真机验证三种手势
5 Phase F 命令写入未串行化 高频命令用例压测
6 Phase B 探针映射方向(直接 vs 旋转) 真机点已知位置比对温度
7 Phase B 温度条"随 fitRect"的规格歧义 判定取舍
8 Phase A lifetime 负载接受两种布局的"对冲" 判定是否应只留官方一种(官方 Java 为 [magic][ms],共 8B
9 checklist 第 15 步预期与实现可能不符 见 #3
10 build-artifacts 内 APK 非可复现字节(时间戳) 以"重建并验证字符串/安装行为"替代字节比对
11 超出计划的一处主动改动:camera feature 显式设为 required=false 判定是否可接受(不影响 USB 主机功能)

5. 核查边界:本机无法验证的事项(须标注"无法判定")

本机(WindowsC:\Tools\android-sdk没有模拟器组件、没有连接的真机 adb devices 为空。因此以下只能在用户真机上判定:

  1. USB 出流与温度绝对值标定(real_device_checklist.md 第 1-12 步)。
  2. 可见光 PIP 的全部运行时行为(第 13-15 步)。
  3. 局域网远程预览双机行为(第 16-24 步)。
  4. 分析页温度条/探针的真机显示与取温正确性(第 11 步)。
  5. 任何"画面/像素"层面的确认(执行者无视觉能力,只能保证编译与单测)。

核查者不应把"未实测"一律记为失败;应记为无法判定,并保留执行者的 真机清单作为待用户执行的验证脚本(但清单本身的日志串与预期须先按 §3A/§4 机械核对一遍)。


6. 复现命令速查

# 构建 + 全量单测(Windows Git Bash
cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21" && \
  cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug :app:assembleRelease test --no-daemon"

# 单类测试
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:testDebugUnitTest --tests 'com.mag160c.thermal.net.*' --no-daemon"

# 单测计数
grep -h -o 'tests="[0-9]*"' /c/Project/MAG160C/android/app/build/test-results/testDebugUnitTest/*.xml | ...

# APK 清单核验(竖屏 + 权限 + feature
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump badging build-artifacts/mag160c-app-debug.apk
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump xmltree build-artifacts/mag160c-app-debug.apk --file AndroidManifest.xml

# dex 中文串(防乱码回归;无需脚本)
unzip -p build-artifacts/mag160c-app-debug.apk 'classes*.dex' | grep -ac '画中画'
unzip -p build-artifacts/mag160c-app-debug.apk 'classes*.dex' | grep -ac '正在扫描局域网主机'

# checklist 日志串核对(执行者已跑通:exact=16 skeleton=30 review=7
"C:\Tools\jdk-21\bin\java" analysis/tools/CheckStrings.java \
  docs/android_app/real_device_checklist.md android/app/src/main

# Phase C 重新生成并比对(见 §3 Phase C 完整命令)

可用工具C:\Tools\jdk-21java/javac/jar)、 C:\Tools\android-sdk\build-tools\36.0.0aapt2)、C:\Tools\jadx-1.5.1

不可用 / 易踩的坑(执行者已实测,核查者省得重踩):

事项 实际
python3 不可用Microsoft Store 桩,退出码 49)。extract_palettes.py 需改用 JDK 复写或安装 python
node 未安装
模拟器 / 连接设备 无(adb devices 为空,SDK 无 emulator 组件)
analysis/tools/PalExport2.java 不能单文件直跑:依赖 PalIdentify.javaPalBody,须两文件一起 javac -d out
Git Bash 的 /tmp 路径 Windows 版 java 不认,必须写 C:\Users\zxc\AppData\Local\Temp\...
analysis/tools/CheckStrings.java 可单文件直跑(无外部依赖)