2.2.0:手表端连上后先到「远控 / 静默更新被控端」选择页

== 用户要求 ==

"安卓那个逻辑也要改一下,就是你输入配对码后,进入一个页面,
 页面有两个按钮,第一个按钮是远控,第二个按钮是静默更新被控端"

== 做成什么样 ==

新增 choicePanel,连接成功后先进这一页:

    已连接「电脑名」
    电脑端 2.1.5 → 可更新到 2.2.0

    [        远控        ]   <- 第一个按钮
    [   静默更新被控端    ]   <- 第二个按钮

按钮顺序按用户说的来(远控在前)。

页面顶上显示电脑名和**电脑端当前版本** —— 用户之前要求
"手表端连接那个电脑端也可以查看电脑端目前所处的版本",
这里是最自然的展示位置(不弹框,只显示一行)。

为什么不让它直接进控制页:老版本的被控端可能缺功能或有 bug,
"先更新再远控"是更合理的顺序 —— 但不强制,给用户自己选。

== 一个刻意不改的地方 ==

自动切到中转服务器**恢复**时的 `showControlPanel()` **没动** ——
用户本来就在远控页,中途弹一层选择页会打断他。
测试里专门断言了这一点,免得以后有人"顺手统一一下"改坏。

== 顺手修的几个问题 ==

1. **忘了定义 lastDeviceName** —— 写选择页时凭直觉用了这个变量,
   编译报"找不到符号"。补上字段并在连接回调里赋值。
   (教训:写代码时要用的变量,先确认它存在。)

2. **泄漏闸门拦下我自己** —— 这两天写的一批服务器诊断/配置脚本里
   硬编码了服务器 IP(26 处,5 个文件)。规则是 IP 不能进任何
   被跟踪的文件。已全部改成占位符,真实值仍从
   _build/.lightsail_host(已 gitignore)读。

3. **verify_all.ps1 删掉「锁屏时记 SKIP」的逻辑** ——
   用户明确要求删。那个好意带来一个更糟的后果:锁屏时几关根本不跑,
   回归"全绿"了但关键测试其实一跳而过。
   **"因为环境不满足所以不测"比"测了发现失败"更危险。**
   删掉之后果然暴露出 3 个 FAIL —— 屏幕解锁后重跑全部通过,
   确认是环境问题而不是代码问题。

== 服务器这边(不在这份提交里,但同一天做的)==

  * nginx 按用户要求重排:
      IP(80) 任何路径 -> 404(没东西)
      域名 / -> 官网;/zhongzhuanzhan -> 中转站设置
      9100 -> 同样的内容(等 AWS 开防火墙)
  * 删掉早年 Cloudflare Tunnel 的 systemd 残留
  * **补上 ACME 挑战例外** —— 这是最要紧的一条:
    80 端口原来把所有请求都 301 到 https,包括
    /.well-known/acme-challenge/,**续期时会失败**。
    而且它是延迟爆发的(最长 90 天后证书到期才暴露)。
  * 清理 1Panel 安装包残留(223MB)

== 验证 ==

    全量回归 21 关全部 PASS(零 FAIL,桌面可用状态)
    Java 编译通过(android-34, --release 8)
    623+ Python 文件语法通过
    泄漏闸门通过
    全部产物重建,Win7 兼容校验通过
This commit is contained in:
xmz7891
2026-10-10 17:13:24 +08:00
parent 48bc392466
commit 1d368979aa
29 changed files with 2000 additions and 122 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ def _latest_apk() -> Path:
(ROOT / "dist").glob("*watch*.apk"),
key=lambda p: p.stat().st_mtime, reverse=True)
return candidates[0] if candidates else \
ROOT / "dist" / "ykxmz-watch-2.1.8.apk"
ROOT / "dist" / "ykxmz-watch-2.2.0.apk"
APK = _latest_apk()
+4 -4
View File
@@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent
# 被控端 -> 一个 zip
# 服务器端 -> 一个 zip
# 控制端 -> 直接给 APK(不打 zip)
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.8.zip"
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.8.zip"
WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.8.apk"
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.2.0.zip"
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.2.0.zip"
WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.2.0.apk"
TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe"
JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe"
@@ -89,7 +89,7 @@ def main() -> int:
for label, path in (("被控端", agent_dir / "README-agent.txt"),
("服务器端", relay_dir / "README-server.txt"),
("控制端(旁边那份)",
ROOT / "dist" / "ykxmz-watch-2.1.8-说明.txt")):
ROOT / "dist" / "ykxmz-watch-2.2.0-说明.txt")):
raw = path.read_bytes() if path.is_file() else b""
check(f"{label}说明有 UTF-8 BOM",
raw[:3] == b"\xef\xbb\xbf", raw[:3])
+41 -3
View File
@@ -141,9 +141,16 @@ def main() -> int:
oncreate.find("root.addView(quickBar")
> oncreate.find("root.addView(mousePanel"),
"加在面板之前会被面板整个盖住")
check("★ showOnly 的数组里有 mousePanel",
"settingsPanel, mousePanel};" in main_java,
"漏了的话鼠标面板永远不隐藏")
# ⚠ 断言写成"数组里有这几个面板"而不是"精确等于某个字符串" ——
# 第一版写的是 `"settingsPanel, mousePanel};" in main_java`,
# 后来往数组里加了 choicePanel,那串就不匹配了(假失败)。
# 验"包含"而不是"全等",才不会因为多一个面板就红。
m_arr = re.search(r"View\[\] all = \{([^}]*)\}", main_java)
arr = m_arr.group(1) if m_arr else ""
for panel in ("connectPanel", "controlPanel", "mirrorPanel", "lanPanel",
"settingsPanel", "mousePanel", "choicePanel"):
check("★ showOnly 数组里有 %s" % panel, panel in arr,
"漏了的话那个面板永远不隐藏")
check("有 applyPanelTopMargin(面板给栏让位)",
"private void applyPanelTopMargin" in main_java)
@@ -178,6 +185,37 @@ def main() -> int:
check("isConnected() 存在(第一版漏了,编译才发现)",
"private boolean isConnected()" in main_java)
# ---------------- 五之二、连接后的选择页 ----------------
print()
print("五之二、连接后的选择页(远控 / 静默更新被控端)")
#
# 用户要求:"输入配对码后,进入一个页面,页面有两个按钮,
# 第一个按钮是远控,第二个按钮是静默更新被控端"
check("有 choicePanel 字段", "private FrameLayout choicePanel;" in main_java)
check("有 buildChoicePanel", "private void buildChoicePanel()" in main_java)
check("有 showChoicePanel", "private void showChoicePanel()" in main_java)
check("★ 第一个按钮是「远控」", 'bigButton("远控"' in main_java,
"按钮顺序要跟用户说的一致")
check("★ 第二个按钮是「静默更新被控端」",
'bigButton("静默更新被控端"' in main_java)
check(" 更新按钮复用 checkPcUpdate()", "checkPcUpdate();" in main_java)
check("★ 连接成功后进的是选择页(不是直接进控制页)",
"showChoicePanel();" in main_java,
"改漏了的话用户根本看不到选择页")
check(" 选择页也做了顶部让位",
"applyPanelTopMargin(choicePanel);" in main_java)
check(" 选择页显示电脑端版本", "choiceVersionText" in main_java)
check(" 记下了电脑名(不然只能显示 IP)",
"private volatile String lastDeviceName" in main_java,
"第一版忘了定义这个字段,编译报「找不到符号」")
# ⚠ 自动切中转恢复那个调用点**不该**改成选择页 ——
# 用户本来就在远控页,中途弹选择页会打断他。
i = main_java.find("已切到中转服务器,连接恢复")
check("★ 自动切中转恢复后仍是控制页(没被误改)",
i > 0 and "showControlPanel();" in main_java[i:i + 300],
"改错地方的话,用着用着会突然被弹回选择页")
# ---------------- 六、电脑端 allow_new_pairing ----------------
print()
print("六、电脑端 allow_new_pairing feature")
+14 -60
View File
@@ -24,69 +24,23 @@ $androidJar = Join-Path $tc "android-sdk\platforms\android-34\android.jar"
$script:results = @()
# 当前会话是不是锁屏 / 没有交互桌面?
# 锁屏时截屏类测试**必然**失败(拿不到屏幕内容),但那不是代码问题。
# 不识别的话汇总表一片红,真问题反而被淹没。
# ⚠ 这里**原来有一段锁屏检测**,锁屏时把 needsDesktop 的阶段记成
# `SKIP(锁屏)` 而不是 FAIL。**已经按用户要求删掉了。**
#
# ⚠ 判断不能只看 OpenInputDesktop 成不成功 —— **锁屏时它同样成功**
# (那时输入桌面是 Winlogon)。实测漏判过一次:屏幕明明锁着,
# desktopAvailable 却是 true,于是 6/7、7/7 两个截屏阶段记成 FAIL,
# 把真正的结论淹掉了。
# 正确做法是拿输入桌面的**名字**:Default = 正常,Winlogon = 锁屏。
$script:desktopAvailable = $true
$script:desktopName = "(未检测)"
try {
Add-Type -Namespace Ykxmz -Name Desk -MemberDefinition @'
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError=true)]
public static extern System.IntPtr OpenInputDesktop(int flags, bool inherit, int access);
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool CloseDesktop(System.IntPtr h);
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError=true, CharSet=System.Runtime.InteropServices.CharSet.Unicode)]
public static extern bool GetUserObjectInformationW(System.IntPtr h, int idx, System.Text.StringBuilder pv, int len, ref int need);
public static string InputDesktopName() {
System.IntPtr h = OpenInputDesktop(0, false, 0x0001);
if (h == System.IntPtr.Zero) return "";
try {
System.Text.StringBuilder sb = new System.Text.StringBuilder(256);
int need = 0;
if (GetUserObjectInformationW(h, 2, sb, 512, ref need)) return sb.ToString();
return "";
} finally { CloseDesktop(h); }
}
public static bool HasDesktop() {
string n = InputDesktopName();
if (n.Length == 0) return false;
if (n.Equals("Winlogon", System.StringComparison.OrdinalIgnoreCase)) return false;
return true;
}
'@ -ErrorAction Stop
$script:desktopName = [Ykxmz.Desk]::InputDesktopName()
if ($script:desktopName -eq "") { $script:desktopName = "(拿不到)" }
$script:desktopAvailable = [Ykxmz.Desk]::HasDesktop()
} catch {
$script:desktopName = "(检测失败: $($_.Exception.GetType().Name))"
$script:desktopAvailable = $true # 查不出来就当可用,别把失败藏起来
}
# 无论可不可用都打一行 —— 上次漏判就是因为什么都没打,无从判断。
Write-Host (" 输入桌面: {0} -> {1}" -f $script:desktopName,
$(if ($script:desktopAvailable) { "可用" } else { "不可用(锁屏)" })) `
-ForegroundColor $(if ($script:desktopAvailable) { "DarkGray" } else { "Yellow" })
if (-not $script:desktopAvailable) {
Write-Host ""
Write-Host " ⚠ 检测到电脑处于锁屏状态。" -ForegroundColor Yellow
Write-Host " 截屏相关的测试会失败,这是环境限制,不是代码问题。" -ForegroundColor Yellow
Write-Host " 这些阶段会记为 SKIP 而不是 FAIL。解锁后重跑即可验证。" -ForegroundColor Yellow
}
# 删的理由:那个好意带来一个更糟的后果 —— 锁屏时那几关根本不跑,
# 于是回归"全绿"了,但关键的 Java 端到端、协议回归其实一跳而过。
# 用户看到"全部通过"会以为没问题。
#
# **"因为环境不满足所以不测"比"测了发现失败"更危险**:
# 失败至少是信息,跳过是假的安全感。
#
# 现在一律真跑、真记。环境不合适导致失败的话,就去修环境或改测试,
# 而不是让它变成 SKIP。
function Record($name, $ok, $needsDesktop = $false) {
# needsDesktop 的阶段在锁屏时记 SKIP —— 避免误报把真问题淹没
if ($needsDesktop -and -not $script:desktopAvailable) {
$script:results += [pscustomobject]@{
项目 = $name; 结果 = "SKIP(锁屏)" }
return
}
# ⚠ $needsDesktop 参数**保留但不再使用** ——
# 6 个调用点还在传它,留着就不用改那些地方。
# 以后真要重新引入"特定环境下跳过",签名已经在了。
$script:results += [pscustomobject]@{
项目 = $name; 结果 = $(if ($ok) { "PASS" } else { "FAIL" }) }
}