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
+529
View File
@@ -0,0 +1,529 @@
# -*- coding: utf-8 -*-
"""安卓端新增:局域网连接页面 + 服务器延迟检测。
用户要求:
"在首页没连之前可以加一个进入局域网连接页面,
就可以用局域网进行连接,不经过中转服务器"
"加个东西就是可以检查延迟检查连接服务器"
设计:
首页(未连接时)三个按钮:
[ 局域网连接(同一个 WiFi) ] <- 新增,走局域网直连
[ 输入配对码连接 ] <- 原来的,经中转
[ 中转设置(跨网络用) ]
外加一个延迟状态行:
[ 检查服务器 ] 显示"延迟 83 毫秒 / 连不上"
局域网页面:
* 自动搜索(UDP 广播,被控端监听 8766)
* 搜到的电脑列出来,点一下就连
* 也能手动填 IP:端口
* 配对码
* 全部走 RemoteClient.connect()(直连),不碰中转
两条路并存:局域网友好、中转兜底。
"""
from __future__ import annotations
import ast
import sys
from pathlib import Path
MAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\android\src"
r"\com\lanremote\watch\MainActivity.java")
# ===================== 1. 首页加按钮 =====================
HOME_ANCHOR = ''' Button codeButton = bigButton("输入配对码连接", C_OK, 15);'''
HOME_NEW = ''' // 局域网连接:手表和电脑在同一个 WiFi 时走这条,不经过中转。
// 用户要求:"在首页没连之前可以加一个进入局域网连接页面,
// 就可以用局域网进行连接,不经过中转服务器"
Button lanButton = bigButton("局域网连接(同一个 WiFi)", C_ACCENT, 15);
LinearLayout.LayoutParams lanParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52));
lanParams.setMargins(0, 0, 0, dp(8));
lanButton.setLayoutParams(lanParams);
lanButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showLanPanel();
}
});
box.addView(lanButton);
Button codeButton = bigButton("输入配对码连接", C_OK, 15);'''
HOME_LATENCY_ANCHOR = ''' statusView = new TextView(this);
statusView.setTextColor(C_DIM);
statusView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
statusView.setPadding(0, 0, 0, dp(6));
box.addView(statusView);'''
HOME_LATENCY_NEW = ''' // 服务器延迟:连不上中转的时候,这一条最有用
LinearLayout serverRow = new LinearLayout(this);
serverRow.setOrientation(LinearLayout.HORIZONTAL);
serverRow.setGravity(Gravity.CENTER_VERTICAL);
serverStatus = new TextView(this);
serverStatus.setTextColor(C_DIM);
serverStatus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
serverStatus.setText("服务器:还没测");
LinearLayout.LayoutParams ssParams = new LinearLayout.LayoutParams(
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
serverStatus.setLayoutParams(ssParams);
serverRow.addView(serverStatus);
Button pingButton = smallButton("检查服务器", C_CARD, new Runnable() {
@Override
public void run() {
checkServer();
}
});
serverRow.addView(pingButton);
box.addView(serverRow);
statusView = new TextView(this);
statusView.setTextColor(C_DIM);
statusView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
statusView.setPadding(0, dp(6), 0, dp(6));
box.addView(statusView);'''
# ===================== 2. 字段 =====================
FIELD_ANCHOR = ''' private TextView statusView;'''
FIELD_NEW = ''' // ---- 局域网连接页面 ----
private FrameLayout lanPanel;
private LinearLayout lanResultBox;
private EditText lanIpInput;
private EditText lanPortInput;
private EditText lanCodeInput;
private TextView lanStatus;
private volatile boolean lanSearching;
// ---- 服务器延迟 ----
private TextView serverStatus;
private TextView statusView;'''
# ===================== 3. onCreate 里建这个面板 =====================
CREATE_ANCHOR = ''' buildConnectPanel();
buildControlPanel();
buildMirrorPanel();'''
CREATE_NEW = ''' buildConnectPanel();
buildControlPanel();
buildMirrorPanel();
buildLanPanel();'''
CREATE_ADD_ANCHOR = ''' root.addView(connectPanel, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));'''
CREATE_ADD_NEW = ''' root.addView(connectPanel, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
root.addView(lanPanel, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));'''
CREATE_HIDE_ANCHOR = ''' controlPanel.setVisibility(View.GONE);
mirrorPanel.setVisibility(View.GONE);
connectPanel.setVisibility(View.VISIBLE);'''
CREATE_HIDE_NEW = ''' controlPanel.setVisibility(View.GONE);
mirrorPanel.setVisibility(View.GONE);
lanPanel.setVisibility(View.GONE);
connectPanel.setVisibility(View.VISIBLE);'''
# ===================== 4. 新方法(插在 showConnectPanel 前) =====================
METHOD_ANCHOR = ''' private void showConnectPanel() {'''
METHOD_NEW = ''' // ==================================================================
// 局域网连接(不经过中转)
// ==================================================================
private void buildLanPanel() {
lanPanel = new FrameLayout(this);
lanPanel.setBackgroundColor(C_BG);
ScrollView scroll = new ScrollView(this);
scroll.setBackgroundColor(C_BG);
LinearLayout box = new LinearLayout(this);
box.setOrientation(LinearLayout.VERTICAL);
box.setPadding(dp(10), dp(12), dp(10), dp(12));
scroll.addView(box, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
lanPanel.addView(scroll, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
// ---- 标题 + 返回 ----
LinearLayout top = new LinearLayout(this);
top.setOrientation(LinearLayout.HORIZONTAL);
top.setGravity(Gravity.CENTER_VERTICAL);
TextView title = new TextView(this);
title.setText("局域网连接");
title.setTextColor(C_FG);
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 17);
LinearLayout.LayoutParams tParams = new LinearLayout.LayoutParams(
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
title.setLayoutParams(tParams);
top.addView(title);
top.addView(smallButton("返回", C_CARD, new Runnable() {
@Override
public void run() {
hideLanPanel();
}
}));
box.addView(top);
TextView hint = new TextView(this);
hint.setText("手表和电脑连同一个 WiFi 时用这个,速度更快、不占流量。\\n"
+ "电脑上要开着 ykxmz。");
hint.setTextColor(C_DIM);
hint.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
hint.setPadding(0, dp(6), 0, dp(10));
box.addView(hint);
// ---- 自动搜索 ----
Button search = bigButton("自动搜索电脑", C_ACCENT, 14);
LinearLayout.LayoutParams sParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(50));
sParams.setMargins(0, 0, 0, dp(8));
search.setLayoutParams(sParams);
search.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startLanSearch();
}
});
box.addView(search);
lanStatus = new TextView(this);
lanStatus.setTextColor(C_DIM);
lanStatus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
lanStatus.setPadding(0, 0, 0, dp(6));
box.addView(lanStatus);
lanResultBox = new LinearLayout(this);
lanResultBox.setOrientation(LinearLayout.VERTICAL);
box.addView(lanResultBox);
// ---- 手动填地址 ----
TextView manualTip = new TextView(this);
manualTip.setText("搜不到就自己填(电脑上 ykxmz 会显示 IP)");
manualTip.setTextColor(C_DIM);
manualTip.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
manualTip.setPadding(0, dp(10), 0, dp(4));
box.addView(manualTip);
lanIpInput = makeInput("电脑 IP,如 192.168.10.105",
InputType.TYPE_CLASS_TEXT);
box.addView(labeled("电脑 IP", lanIpInput));
lanPortInput = makeInput("端口(默认 8765)",
InputType.TYPE_CLASS_NUMBER);
box.addView(labeled("端口", lanPortInput));
lanCodeInput = makeInput("6 位配对码", InputType.TYPE_CLASS_NUMBER);
box.addView(labeled("配对码", lanCodeInput));
Button connect = bigButton("连上这台电脑", C_OK, 15);
LinearLayout.LayoutParams cParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52));
cParams.setMargins(0, dp(10), 0, 0);
connect.setLayoutParams(cParams);
connect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
connectLanTyped();
}
});
box.addView(connect);
}
private void showLanPanel() {
connectPanel.setVisibility(View.GONE);
controlPanel.setVisibility(View.GONE);
if (mirrorPanel != null) {
mirrorPanel.setVisibility(View.GONE);
}
lanPanel.setVisibility(View.VISIBLE);
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
// 进来先自动搜一次,省得用户还要自己点
String savedCode = savedPairingCode();
if (lanCodeInput != null && savedCode != null && !savedCode.isEmpty()
&& lanCodeInput.getText().length() == 0) {
lanCodeInput.setText(savedCode);
}
startLanSearch();
}
private void hideLanPanel() {
lanPanel.setVisibility(View.GONE);
showConnectPanel();
}
/** 广播搜一圈,找到的电脑列出来。 */
private void startLanSearch() {
if (lanSearching) {
return;
}
lanSearching = true;
if (lanResultBox != null) {
lanResultBox.removeAllViews();
}
setLanStatus("正在搜索…(最多 6 秒)", C_DIM);
appendLog("局域网搜索开始");
io.execute(new Runnable() {
@Override
public void run() {
java.util.List<DiscoveryCore.Found> found;
try {
found = Discovery.search(MainActivity.this, 4000);
} catch (Throwable error) {
found = new java.util.ArrayList<DiscoveryCore.Found>();
appendLog("搜索失败:" + error);
}
final java.util.List<DiscoveryCore.Found> result = found;
runOnUiThread(new Runnable() {
@Override
public void run() {
lanSearching = false;
if (lanResultBox == null) {
return;
}
lanResultBox.removeAllViews();
if (result.isEmpty()) {
setLanStatus("没搜到电脑。确认:① 电脑开着 ykxmz ② "
+ "同一个 WiFi ③ 路由器没开 AP 隔离。"
+ "\\n也可以直接填下面的 IP。", C_WARN);
appendLog("局域网没搜到电脑");
return;
}
setLanStatus("搜到 " + result.size() + " 台,点一下就连",
C_OK);
appendLog("搜到 " + result.size() + " 台电脑");
for (final DiscoveryCore.Found item : result) {
Button b = bigButton(
item.name + " · " + item.host,
C_CARD, 14);
LinearLayout.LayoutParams lp =
new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
dp(50));
lp.setMargins(0, 0, 0, dp(6));
b.setLayoutParams(lp);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (lanIpInput != null) {
lanIpInput.setText(item.host);
}
if (lanPortInput != null) {
lanPortInput.setText(
String.valueOf(item.port));
}
connectLanTyped();
}
});
lanResultBox.addView(b);
}
}
});
}
});
}
private void setLanStatus(String text, int color) {
if (lanStatus != null) {
lanStatus.setText(text);
lanStatus.setTextColor(color);
}
}
/** 用手填/点选的地址直连,不走中转。 */
private void connectLanTyped() {
String host = lanIpInput == null ? ""
: lanIpInput.getText().toString().trim();
String portText = lanPortInput == null ? ""
: lanPortInput.getText().toString().trim();
String code = lanCodeInput == null ? ""
: lanCodeInput.getText().toString().trim();
if (host.isEmpty()) {
setLanStatus("先填电脑 IP,或者上面搜一下", C_WARN);
toast("还没填电脑 IP");
return;
}
int port = RemoteClient.DEFAULT_PORT;
if (!portText.isEmpty()) {
try {
port = Integer.parseInt(portText);
} catch (NumberFormatException error) {
setLanStatus("端口要是数字", C_WARN);
return;
}
}
if (code.isEmpty()) {
setLanStatus("要填 6 位配对码", C_WARN);
toast("还没填配对码");
return;
}
rememberPairingCode(code);
lastHost = host;
lastPort = port;
lastCode = code;
lanDirect = true; // 断了自动切回中转
appendLog("局域网直连 " + host + ":" + port);
connect(host, port, code, null,
DeviceStore.deviceKey(host, port, store.deviceId()));
}
// ==================================================================
// 服务器延迟检测
// ==================================================================
private void checkServer() {
final String relay = relayUrl();
if (relay.isEmpty()) {
if (serverStatus != null) {
serverStatus.setText("服务器:还没设中转地址");
serverStatus.setTextColor(C_WARN);
}
return;
}
if (serverStatus != null) {
serverStatus.setText("服务器:正在测…");
serverStatus.setTextColor(C_DIM);
}
appendLog("开始检查服务器 " + relay);
io.execute(new Runnable() {
@Override
public void run() {
final long started = System.currentTimeMillis();
String text;
int color = C_DIM;
try {
java.net.URL url = new java.net.URL(relay + "/v1/health");
java.net.HttpURLConnection conn =
(java.net.HttpURLConnection) url.openConnection();
conn.setRequestProperty("User-Agent", "LanRemote/1.4.0");
conn.setConnectTimeout(8000);
conn.setReadTimeout(8000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
long ms = System.currentTimeMillis() - started;
if (code == 200) {
text = "服务器:延迟 " + ms + " 毫秒";
color = ms < 300 ? C_OK : (ms < 900 ? C_WARN : C_DANGER);
} else {
text = "服务器:HTTP " + code;
color = C_DANGER;
}
try {
conn.disconnect();
} catch (Throwable ignored) {
// 关不掉也无所谓
}
} catch (Throwable error) {
text = "服务器:连不上(" + shortReason(error) + ")";
color = C_DANGER;
}
final String shown = text;
final int tint = color;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (serverStatus != null) {
serverStatus.setText(shown);
serverStatus.setTextColor(tint);
}
appendLog(shown);
}
});
}
});
}
/** 异常名压短,免得界面上一长串。 */
private static String shortReason(Throwable error) {
String name = error.getClass().getSimpleName();
if (name.isEmpty()) {
name = "错误";
}
String message = String.valueOf(error.getMessage());
if (message.length() > 40) {
message = message.substring(0, 40);
}
return message.isEmpty() || "null".equals(message)
? name : (name + " " + message);
}
private void showConnectPanel() {'''
# ===================== 5. 返回键处理 =====================
BACK_ANCHOR = ''' protected void onDestroy() {'''
BACK_NEW = ''' private boolean closeLanIfOpen() {
if (lanPanel != null && lanPanel.getVisibility() == View.VISIBLE) {
hideLanPanel();
return true;
}
return false;
}
@Override
public void onBackPressed() {
// 局域网页面开着的话,先退回首页
if (closeLanIfOpen()) {
return;
}
super.onBackPressed();
}
@Override
protected void onDestroy() {'''
def main() -> int:
text = MAIN.read_text(encoding="utf-8")
if "buildLanPanel" in text:
print(" 已经加过")
return 0
ok = True
for old, new, label in (
(HOME_ANCHOR, HOME_NEW, "首页加局域网按钮"),
(HOME_LATENCY_ANCHOR, HOME_LATENCY_NEW, "首页加延迟状态"),
(FIELD_ANCHOR, FIELD_NEW, "加字段"),
(CREATE_ANCHOR, CREATE_NEW, "onCreate 建面板"),
(CREATE_ADD_ANCHOR, CREATE_ADD_NEW, "onCreate 挂面板"),
(CREATE_HIDE_ANCHOR, CREATE_HIDE_NEW, "onCreate 默认隐藏"),
(METHOD_ANCHOR, METHOD_NEW, "加局域网页面和延迟检测"),
(BACK_ANCHOR, BACK_NEW, "加返回键处理")):
if old not in text:
print(f" [失败] {label} —— 锚点没找到")
ok = False
continue
if text.count(old) != 1:
print(f" [警告] {label} 锚点出现 {text.count(old)} 次")
text = text.replace(old, new, 1)
print(f" {label}")
MAIN.write_text(text, encoding="utf-8", newline="\n")
try:
ast.parse(text)
except SyntaxError:
pass # Java 不是 Python,这里只占位
print(" 已写入")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())