ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性

被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b62ac865ea
3195 changed files with 203292 additions and 0 deletions
+559
View File
@@ -0,0 +1,559 @@
# -*- coding: utf-8 -*-
"""手表端 UI 彻底重做:分页 + 37 个功能。
原来是一长条 28 个按钮的网格,要滚 274dp。
现在分成 6 页,每页最多 7 个按钮 —— **完全不用滚动**。
媒体 (7) 屏幕 (6) 文件 (6) 语音 (5) 电脑 (6) 系统 (7)
"""
from __future__ import annotations
import sys
from pathlib import Path
MAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\android\src"
r"\com\lanremote\watch\MainActivity.java")
START = " // 按钮网格\n"
END = " flushGrid(grid); // 按当前列数把上面入队的按钮切成行\n"
NEW_PANEL = ''' // ---- 分页标签 ----
HorizontalScrollView tabScroll = new HorizontalScrollView(this);
tabScroll.setHorizontalScrollBarEnabled(false);
tabBar = new LinearLayout(this);
tabBar.setOrientation(LinearLayout.HORIZONTAL);
tabBar.setPadding(0, 0, 0, 0);
tabScroll.addView(tabBar);
controlPanel.addView(tabScroll, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(42)));
// ---- 分页内容 ----
tabContent = new LinearLayout(this);
tabContent.setOrientation(LinearLayout.VERTICAL);
tabContent.setPadding(0, dp(6), 0, dp(6));
ScrollView contentScroll = new ScrollView(this);
contentScroll.addView(tabContent, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
controlPanel.addView(contentScroll, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
'''
# ---------------------------------------------------------------- tabs 机制
TAB_METHODS_ANCHOR = ''' private Button bigButton(String text, int color, float sizeSp) {'''
TAB_METHODS = ''' // ==================================================================
// 分页
// ==================================================================
private static final class Act {
final String label;
final Runnable task;
Act(String label, Runnable task) {
this.label = label;
this.task = task;
}
}
private static final class TabDef {
final String name;
final Act[] acts;
TabDef(String name, Act[] acts) {
this.name = name;
this.acts = acts;
}
}
private TabDef[] tabDefs;
private LinearLayout tabBar;
private LinearLayout tabContent;
private int currentTab = 0;
private Act act(String label, Runnable task) {
return new Act(label, task);
}
/** 定义所有分页。每页最多 7 个按钮,一屏放得下,不用滚。 */
private void initTabs() {
tabDefs = new TabDef[]{
new TabDef("媒体", new Act[]{
act("播放/暂停", new Runnable() {
public void run() { media("play_pause"); }
}),
act("停止", new Runnable() {
public void run() { media("stop"); }
}),
act("上一曲", new Runnable() {
public void run() { media("prev"); }
}),
act("下一曲", new Runnable() {
public void run() { media("next"); }
}),
act("音量 +", new Runnable() {
public void run() { media("vol_up"); }
}),
act("音量 -", new Runnable() {
public void run() { media("vol_down"); }
}),
act("静音", new Runnable() {
public void run() { media("mute"); }
}),
}),
new TabDef("屏幕", new Act[]{
act("屏幕镜像", new Runnable() {
public void run() { openMirrorPanel(); }
}),
act("截屏", new Runnable() {
public void run() { doScreenshot(); }
}),
act("投图到电脑", new Runnable() {
public void run() { pickFile(REQ_IMAGE, "image/*"); }
}),
act("放音到电脑", new Runnable() {
public void run() { pickFile(REQ_AUDIO, "audio/*"); }
}),
act("关显示器", new Runnable() {
public void run() { confirmMonitor(); }
}),
act("设为壁纸", new Runnable() {
public void run() { confirmWallpaper(); }
}),
}),
new TabDef("文件", new Act[]{
act("发送文件", new Runnable() {
public void run() { pickFile(REQ_FILE, "*/*"); }
}),
act("接收文件", new Runnable() {
public void run() { showReceivedFiles(); }
}),
act("剪贴板→电脑", new Runnable() {
public void run() { promptClipSet(); }
}),
act("取电脑剪贴板", new Runnable() {
public void run() { fetchClip(); }
}),
act("剪贴板历史", new Runnable() {
public void run() { pickClipHistory(); }
}),
act("打开文件夹", new Runnable() {
public void run() { pickFolder(); }
}),
}),
new TabDef("语音", new Act[]{
act("语音播报", new Runnable() {
public void run() { promptSay(); }
}),
act("音色", new Runnable() {
public void run() { pickVoice(); }
}),
act("朗读剪贴板", new Runnable() {
public void run() {
sendCommand(Json.obj("cmd", "say_clip"), "朗读剪贴板");
}
}),
act("电脑音乐", new Runnable() {
public void run() { pickLocalAudio(); }
}),
act("弹通知", new Runnable() {
public void run() { promptNotify(); }
}),
}),
new TabDef("电脑", new Act[]{
act("快捷命令", new Runnable() {
public void run() { pickQuickApp(); }
}),
act("打开网址", new Runnable() {
public void run() { promptOpenUrl(); }
}),
act("进程管理", new Runnable() {
public void run() { pickProcess(); }
}),
act("性能", new Runnable() {
public void run() { showPerf(); }
}),
act("磁盘", new Runnable() {
public void run() { showDisk(); }
}),
act("电量", new Runnable() {
public void run() { showBattery(); }
}),
}),
new TabDef("系统", new Act[]{
act("锁屏", new Runnable() {
public void run() { confirmPower("lock", "锁屏"); }
}),
act("睡眠", new Runnable() {
public void run() { confirmPower("sleep", "睡眠"); }
}),
act("关机", new Runnable() {
public void run() { confirmPower("shutdown", "关机"); }
}),
act("重启", new Runnable() {
public void run() { confirmPower("reboot", "重启"); }
}),
act("取消定时关机", new Runnable() {
public void run() { cancelShutdown(); }
}),
act("定时关机", new Runnable() {
public void run() { pickShutdownTimer(); }
}),
act("设备诊断", new Runnable() {
public void run() { openDiagPanel(); }
}),
}),
};
tabBar.removeAllViews();
for (int i = 0; i < tabDefs.length; i++) {
final int index = i;
TextView tab = new TextView(this);
tab.setText(tabDefs[i].name);
tab.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
tab.setGravity(Gravity.CENTER);
tab.setPadding(dp(14), dp(8), dp(14), dp(8));
tab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
selectTab(index);
}
});
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT);
params.setMargins(0, 0, dp(6), 0);
tab.setLayoutParams(params);
tabBar.addView(tab);
}
selectTab(0);
}
private void selectTab(int index) {
if (tabDefs == null || index < 0 || index >= tabDefs.length) {
return;
}
currentTab = index;
for (int i = 0; i < tabBar.getChildCount(); i++) {
View child = tabBar.getChildAt(i);
boolean on = (i == index);
child.setBackground(rounded(on ? C_ACCENT : C_CARD, 8));
if (child instanceof TextView) {
((TextView) child).setTextColor(on ? 0xFFFFFFFF : C_FG);
}
}
tabContent.removeAllViews();
cellQueue.clear();
for (Act item : tabDefs[index].acts) {
addRow(tabContent, action(item.label, item.task), null);
}
flushGrid(tabContent);
}
// ==================================================================
// 新功能
// ==================================================================
private void media(String what) {
sendCommand(Json.obj("cmd", "media", "action", what), "媒体");
}
private void confirmMonitor() {
new AlertDialog.Builder(this)
.setTitle("关闭显示器")
.setMessage("要关掉电脑显示器吗?(动一下鼠标或按键就能唤醒)")
.setPositiveButton("关掉", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface d, int w) {
sendCommand(Json.obj("cmd", "monitor", "on", false), "关显示器");
}
})
.setNegativeButton("取消", null)
.show();
}
private void confirmWallpaper() {
new AlertDialog.Builder(this)
.setTitle("设为壁纸")
.setMessage("把电脑上最近收到的那张图片设为桌面壁纸?")
.setPositiveButton("设置", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface d, int w) {
sendCommand(Json.obj("cmd", "set_wallpaper"), "设置壁纸");
}
})
.setNegativeButton("取消", null)
.show();
}
private void pickShutdownTimer() {
final int[] minutes = {15, 30, 60, 120};
final String[] labels = {"15 分钟后", "30 分钟后", "1 小时后", "2 小时后"};
showChooser("定时关机", new ArrayList<String>(java.util.Arrays.asList(labels)),
new Chooser() {
@Override
public void onPick(int index) {
sendCommand(Json.obj("cmd", "shutdown_timer",
"minutes", minutes[index]), "定时关机");
}
});
}
private void cancelShutdown() {
sendCommand(Json.obj("cmd", "shutdown_timer", "cancel", true), "取消定时关机");
}
private void promptOpenUrl() {
promptInput("在电脑上打开网址", "如 baidu.com", "https://", new InputHandler() {
@Override
public void onInput(String text) {
if (!text.isEmpty()) {
sendCommand(Json.obj("cmd", "open_url", "url", text), "打开网址");
}
}
});
}
private void pickQuickApp() {
final String[][] apps = {
{"任务管理器", "taskmgr"},
{"资源管理器", "explorer"},
{"命令提示符", "cmd"},
{"计算器", "calc"},
{"记事本", "notepad"},
{"画图", "mspaint"},
{"控制面板", "control"},
{"设备管理器", "devmgmt"},
};
List<String> labels = new ArrayList<String>();
for (String[] item : apps) {
labels.add(item[0]);
}
showChooser("在电脑上打开", labels, new Chooser() {
@Override
public void onPick(int index) {
sendCommand(Json.obj("cmd", "quick", "app", apps[index][1]),
"打开" + apps[index][0]);
}
});
}
private void pickFolder() {
final String[][] folders = {
{"下载", "downloads"},
{"文档", "documents"},
{"桌面", "desktop"},
{"图片", "pictures"},
{"音乐", "music"},
{"接收目录", "received"},
{"程序目录", "app"},
};
List<String> labels = new ArrayList<String>();
for (String[] item : folders) {
labels.add(item[0]);
}
showChooser("打开电脑文件夹", labels, new Chooser() {
@Override
public void onPick(int index) {
sendCommand(Json.obj("cmd", "open_folder", "folder", folders[index][1]),
"打开" + folders[index][0]);
}
});
}
private void pickProcess() {
appendLog("正在获取电脑进程…");
io.execute(new Runnable() {
@Override
public void run() {
try {
final Map<String, Object> reply =
client.call(Json.obj("cmd", "proc_list"), 20000);
final List<String> names = stringList(reply, "names");
final List<String> pids = stringList(reply, "pids");
ui.post(new Runnable() {
@Override
public void run() {
if (names.isEmpty()) {
toast("没读到进程,或没连上");
return;
}
showChooser("占内存最多的进程", names, new Chooser() {
@Override
public void onPick(int index) {
confirmKill(names.get(index), pids.get(index));
}
});
}
});
} catch (final IOException e) {
postError("获取进程", e);
}
}
});
}
private void confirmKill(String label, String pid) {
final int target;
try {
target = Integer.parseInt(pid);
} catch (NumberFormatException e) {
toast("进程号不对");
return;
}
new AlertDialog.Builder(this)
.setTitle("结束进程")
.setMessage("确定要结束这个进程吗?\n\n" + label)
.setPositiveButton("结束", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface d, int w) {
sendCommand(Json.obj("cmd", "proc_kill", "pid", target), "结束进程");
}
})
.setNegativeButton("取消", null)
.show();
}
private void showPerf() {
fetchAndShow("性能", Json.obj("cmd", "perf"), "note");
}
private void showDisk() {
fetchAndShow("磁盘占用", Json.obj("cmd", "disk"), "note");
}
private void showBattery() {
fetchAndShow("电脑电量", Json.obj("cmd", "battery"), "note");
}
/** 拉一条命令,把 note 用对话框显示出来。 */
private void fetchAndShow(final String title, final Map<String, Object> request,
final String field) {
appendLog("正在获取" + title + "…");
io.execute(new Runnable() {
@Override
public void run() {
try {
final Map<String, Object> reply = client.call(request, 15000);
final boolean ok = Json.bool(reply, "ok", false);
final String note = ok
? Json.str(reply, field, "(无内容)")
: Json.str(reply, "error", "读取失败");
ui.post(new Runnable() {
@Override
public void run() {
appendLog(title + ":" + note);
new AlertDialog.Builder(MainActivity.this)
.setTitle(title)
.setMessage(note)
.setPositiveButton("知道了", null)
.show();
}
});
} catch (final IOException e) {
postError(title, e);
}
}
});
}
private void pickClipHistory() {
appendLog("正在获取剪贴板历史…");
io.execute(new Runnable() {
@Override
public void run() {
try {
final Map<String, Object> reply =
client.call(Json.obj("cmd", "clip_history"), 12000);
final List<String> texts = stringList(reply, "texts");
final List<String> times = stringList(reply, "times");
ui.post(new Runnable() {
@Override
public void run() {
if (texts.isEmpty()) {
toast("电脑剪贴板还没有历史");
return;
}
List<String> labels = new ArrayList<String>();
for (int i = 0; i < texts.size(); i++) {
String preview = texts.get(i).replace("\\n", " ");
if (preview.length() > 22) {
preview = preview.substring(0, 22) + "…";
}
String when = i < times.size() ? times.get(i) + " " : "";
labels.add(when + preview);
}
showChooser("剪贴板历史(点一条取到手表)", labels, new Chooser() {
@Override
public void onPick(int index) {
copyToWatchClipboard(texts.get(index));
toast("已复制到手表剪贴板");
appendLog("取回一条剪贴板历史");
}
});
}
});
} catch (final IOException e) {
postError("剪贴板历史", e);
}
}
});
}
'''
def main() -> int:
text = MAIN.read_text(encoding="utf-8")
start = text.find(START)
end = text.find(END)
if start < 0 or end < 0:
print(f" [失败] 找不到网格区块 (start={start}, end={end})")
return 1
end += len(END)
text = text[:start] + NEW_PANEL + text[end:]
print(" 分页面板: OK")
if TAB_METHODS_ANCHOR not in text:
print(" [失败] bigButton 锚点未匹配")
return 1
text = text.replace(TAB_METHODS_ANCHOR, TAB_METHODS + TAB_METHODS_ANCHOR, 1)
print(" 分页机制 + 新功能: OK")
# addRow 要能接受 null(一页里按钮数可能是奇数)
old_add = ''' private void addRow(LinearLayout parent, View left, View right) {
// 只入队,真正的行切分在 flushGrid() 里按 gridColumns 统一做。
// 这样上层那串 addRow(a, b) 不用改,列数就能自适应。
cellQueue.add(left);
cellQueue.add(right);
}'''
new_add = ''' private void addRow(LinearLayout parent, View left, View right) {
// 只入队,真正的行切分在 flushGrid() 里按 gridColumns 统一做。
// 允许传 null:分页里某一页的按钮数可能是奇数。
if (left != null) {
cellQueue.add(left);
}
if (right != null) {
cellQueue.add(right);
}
}'''
if old_add in text:
text = text.replace(old_add, new_add, 1)
print(" addRow 支持 null: OK")
else:
print(" [警告] addRow 未替换")
# 初始化分页
old_init = " showConnectPanel();"
new_init = " initTabs();\n showConnectPanel();"
if old_init in text:
text = text.replace(old_init, new_init, 1)
print(" initTabs 调用: OK")
MAIN.write_text(text, encoding="utf-8")
return 0
if __name__ == "__main__":
sys.exit(main())