mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
被控端 - 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%) - 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败) - 每分钟回头找局域网,自动切回快 400 倍的路径 - 画面心跳帧:静止时省流量,客户端仍知道流是活的 - 锁屏时给出可操作的提示 手表端 - 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException - 连接错误翻译成人话(证书/时间/解析/超时分别提示) - 经中转连上后用服务器报的地址刷新已配对记录 - 新增局域网连接页面、服务器延迟检测 服务器端 - 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502) - 网络调优脚本 net-tune.sh(开机自动应用) - 管理页:批量删除、网络体检、下载区 - 隧道改 HTTP/2 + 双连接 文档 - 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
474 lines
19 KiB
Python
474 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""重做中转管理页的界面。
|
||
|
||
功能一个不少:
|
||
公网地址展示 + 一键复制、统计卡片、设备表格、
|
||
单台删除、批量删除、删除全部、登录保护
|
||
|
||
界面上的改进:
|
||
* 头部:品牌 + 状态灯 + 版本/运行时长
|
||
* 公网地址做成醒目的大卡片,带「复制」按钮
|
||
* 统计卡片换成更清楚的样式
|
||
* 表格:在线/离线用徽章,配对码用等宽 chip,行悬停高亮
|
||
* 空状态给一句人话,而不是一行灰字
|
||
* 加「自动刷新」开关(默认 10 秒一次),省得手动 F5
|
||
* 窄屏(手机)表格自动变成卡片式,不用左右拖
|
||
* 操作结果用页面内提示条,不再打断式 alert(删除确认仍用 confirm)
|
||
|
||
⚠️ 两个必须注意的坑:
|
||
1. 这段是 f-string,CSS/JS 里的花括号都要写两遍 {{{{ }}}}
|
||
2. JS 字符串里的换行要写 \\\\n(源码里两反斜杠),
|
||
否则 f-string 会把它变成真换行,整段脚本报错(幽灵按键)
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py")
|
||
|
||
NEW_PAGE = ''' def _admin_page(self) -> None:
|
||
snap = RELAY.snapshot()
|
||
rows = []
|
||
for device in snap["devices"]:
|
||
online = device["online"]
|
||
badge = ("<span class='badge on'>在线</span>" if online
|
||
else "<span class='badge off'>离线</span>")
|
||
rows.append(
|
||
"<tr>"
|
||
"<td class=pickcell>"
|
||
f'<input type=checkbox class=pick value="{_esc(device["deviceId"])}">'
|
||
"</td>"
|
||
f'<td class=name>'
|
||
f'<span class="dot {"on" if online else "off"}"></span>'
|
||
f'{_esc(device["name"])}<div class=sub2>'
|
||
f'{_esc(device["deviceId"])}</div></td>'
|
||
f'<td><code>{_esc(device["code"])}</code></td>'
|
||
f'<td>{badge}</td>'
|
||
f'<td>{device["idleSeconds"]}s</td>'
|
||
f'<td>{device["queuedToClient"]} / {device["queuedToAgent"]}</td>'
|
||
f'<td class=num>{_kb(device["bytesIn"])} / {_kb(device["bytesOut"])}</td>'
|
||
'<td class=ops><button class="btn del" '
|
||
f'data-device="{_esc(device["deviceId"])}" '
|
||
f'data-name="{_esc(device["name"])}" '
|
||
f'data-code="{_esc(device["code"])}">删除</button></td>'
|
||
"</tr>")
|
||
|
||
online_count = sum(1 for d in snap["devices"] if d["online"])
|
||
url_text = public_url() or ""
|
||
url_display = _esc(url_text) if url_text else "(还没有地址,隧道可能没起来)"
|
||
|
||
empty = ('<tr><td colspan=8 class=empty>'
|
||
'<div class=big>还没有电脑连上来</div>'
|
||
'<div>在被控端双击 ykxmz.exe 设好配对码,'
|
||
'手表上输那 6 位数字就能连上。</div></td></tr>')
|
||
|
||
html = f"""<!doctype html><html lang=zh>
|
||
<meta charset=utf-8>
|
||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||
<title>ykxmz 中转服务器</title>
|
||
<style>
|
||
*{{box-sizing:border-box}}
|
||
:root{{
|
||
--bg:#0d1117; --panel:#161b22; --panel2:#1c2128; --border:#30363d;
|
||
--fg:#e6edf3; --dim:#8b949e; --muted:#6e7681;
|
||
--accent:#2f81f7; --ok:#3fb950; --danger:#f85149; --warn:#d29922;
|
||
}}
|
||
body{{margin:0;padding:0;background:var(--bg);color:var(--fg);
|
||
font:14px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',
|
||
'Microsoft YaHei',sans-serif;-webkit-font-smoothing:antialiased}}
|
||
.wrap{{max-width:1120px;margin:0 auto;padding:22px 20px 40px}}
|
||
|
||
/* ---------- 头部 ---------- */
|
||
header{{display:flex;align-items:center;gap:12px;
|
||
padding-bottom:16px;border-bottom:1px solid var(--border);
|
||
margin-bottom:20px;flex-wrap:wrap}}
|
||
.logo{{width:38px;height:38px;border-radius:10px;flex:none;
|
||
background:linear-gradient(140deg,#2f81f7,#1f6feb);
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-weight:700;font-size:15px;color:#fff;letter-spacing:-.5px}}
|
||
.brand h1{{margin:0;font-size:17px;font-weight:600;letter-spacing:.2px}}
|
||
.brand .sub{{color:var(--dim);font-size:12px}}
|
||
.meta{{margin-left:auto;text-align:right;color:var(--muted);font-size:12px;
|
||
line-height:1.5}}
|
||
.meta b{{color:var(--dim);font-weight:500}}
|
||
|
||
/* ---------- 通用卡片 ---------- */
|
||
.panel{{background:var(--panel);border:1px solid var(--border);
|
||
border-radius:12px;padding:16px 18px;margin-bottom:16px}}
|
||
.panel .label{{color:var(--dim);font-size:12px;margin-bottom:8px;
|
||
display:flex;align-items:center;gap:6px}}
|
||
|
||
/* ---------- 公网地址 ---------- */
|
||
.urlrow{{display:flex;align-items:center;gap:10px;flex-wrap:wrap}}
|
||
.url{{flex:1;min-width:220px;font:600 15px/1.5 ui-monospace,Consolas,
|
||
'Courier New',monospace;color:#79c0ff;word-break:break-all;
|
||
background:#0b1220;border:1px solid #1f6feb44;border-radius:8px;
|
||
padding:10px 12px}}
|
||
.url.empty{{color:var(--muted);font-weight:400;font-style:normal}}
|
||
|
||
/* ---------- 统计 ---------- */
|
||
.stats{{display:grid;gap:12px;margin-bottom:16px;
|
||
grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}}
|
||
.stat{{background:var(--panel);border:1px solid var(--border);
|
||
border-radius:12px;padding:14px 16px;position:relative;overflow:hidden}}
|
||
.stat .n{{font-size:26px;font-weight:650;line-height:1.15;
|
||
letter-spacing:-.5px}}
|
||
.stat .t{{color:var(--dim);font-size:12px;margin-top:2px}}
|
||
.stat::after{{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
|
||
background:var(--accent);opacity:.65}}
|
||
.stat.ok::after{{background:var(--ok)}}
|
||
.stat.warn::after{{background:var(--warn)}}
|
||
.stat.mute::after{{background:#484f58}}
|
||
|
||
/* ---------- 工具条 ---------- */
|
||
.toolbar{{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
|
||
margin-bottom:12px}}
|
||
.toolbar .spacer{{flex:1}}
|
||
.tip{{color:var(--muted);font-size:12px}}
|
||
|
||
/* ---------- 按钮 ---------- */
|
||
button{{font-family:inherit;font-size:13px;cursor:pointer;
|
||
border-radius:8px;padding:7px 14px;border:1px solid var(--border);
|
||
background:var(--panel2);color:var(--fg);transition:.12s}}
|
||
button:hover{{background:#262c36;border-color:#484f58}}
|
||
button:active{{transform:translateY(1px)}}
|
||
button:disabled{{opacity:.4;cursor:not-allowed;transform:none}}
|
||
button.primary{{background:var(--accent);border-color:var(--accent);
|
||
color:#fff;font-weight:600}}
|
||
button.primary:hover{{background:#4a95f8;border-color:#4a95f8}}
|
||
button.del{{background:#2d1214;border-color:#6e2b2b;color:#ff7b72}}
|
||
button.del:hover{{background:#4a1a1e;border-color:#a33;color:#ffa198}}
|
||
button.del:disabled{{opacity:.4}}
|
||
button.small{{padding:5px 11px;font-size:12px}}
|
||
|
||
/* ---------- 表格 ---------- */
|
||
.tablewrap{{background:var(--panel);border:1px solid var(--border);
|
||
border-radius:12px;overflow:hidden}}
|
||
table{{width:100%;border-collapse:collapse}}
|
||
th,td{{padding:11px 14px;text-align:left;font-size:13px;
|
||
border-bottom:1px solid #21262d;vertical-align:middle}}
|
||
th{{background:var(--panel2);color:var(--dim);font-weight:500;
|
||
font-size:12px;white-space:nowrap}}
|
||
tbody tr{{transition:background .1s}}
|
||
tbody tr:hover td{{background:#1b222c}}
|
||
tbody tr:last-child td{{border-bottom:none}}
|
||
td.name{{font-weight:500}}
|
||
td.num{{font-family:ui-monospace,Consolas,monospace;color:var(--dim);
|
||
font-size:12px}}
|
||
td.ops{{text-align:right;white-space:nowrap}}
|
||
.sub2{{color:var(--muted);font-size:11px;font-weight:400;
|
||
font-family:ui-monospace,Consolas,monospace;margin-top:1px}}
|
||
td.empty{{text-align:center;padding:44px 20px;color:var(--dim)}}
|
||
td.empty .big{{font-size:15px;color:var(--fg);margin-bottom:6px;font-weight:500}}
|
||
code{{background:#21262d;padding:2px 8px;border-radius:6px;
|
||
letter-spacing:1.5px;font-family:ui-monospace,Consolas,monospace;
|
||
font-size:13px;color:#79c0ff}}
|
||
.dot{{display:inline-block;width:7px;height:7px;border-radius:50%;
|
||
margin-right:7px;vertical-align:middle;background:#484f58}}
|
||
.dot.on{{background:var(--ok);box-shadow:0 0 0 3px #3fb95022}}
|
||
.badge{{font-size:11px;padding:2px 8px;border-radius:20px;
|
||
border:1px solid transparent;white-space:nowrap}}
|
||
.badge.on{{color:var(--ok);background:#3fb9501a;border-color:#3fb95040}}
|
||
.badge.off{{color:var(--muted);background:#6e76811a;border-color:#6e768140}}
|
||
.pickcell{{width:38px;text-align:center;padding-right:0}}
|
||
input[type=checkbox]{{width:15px;height:15px;cursor:pointer;
|
||
accent-color:var(--accent);vertical-align:middle}}
|
||
tr.picked td{{background:#1c2733}}
|
||
tr.picked:hover td{{background:#213042}}
|
||
|
||
/* ---------- 提示条 ---------- */
|
||
#toast{{position:fixed;left:50%;bottom:26px;transform:translate(-50%,20px);
|
||
background:#1f6feb;color:#fff;padding:11px 20px;border-radius:10px;
|
||
font-size:13px;box-shadow:0 8px 24px #0008;opacity:0;
|
||
transition:.22s;pointer-events:none;max-width:88vw;text-align:center}}
|
||
#toast.show{{opacity:1;transform:translate(-50%,0)}}
|
||
#toast.err{{background:#b62324}}
|
||
#toast.ok{{background:#1a7f37}}
|
||
|
||
footer{{color:var(--muted);font-size:12px;margin-top:22px;
|
||
padding-top:14px;border-top:1px solid var(--border);line-height:1.7}}
|
||
|
||
/* ---------- 窄屏:表格变卡片 ---------- */
|
||
@media (max-width:720px){{
|
||
.wrap{{padding:16px 12px 32px}}
|
||
.meta{{margin-left:0;text-align:left;width:100%}}
|
||
thead{{display:none}}
|
||
tbody tr{{display:block;border-bottom:1px solid var(--border);
|
||
padding:12px 14px;position:relative}}
|
||
tbody tr:last-child{{border-bottom:none}}
|
||
tbody td{{display:flex;justify-content:space-between;gap:12px;
|
||
border:none;padding:4px 0;text-align:right}}
|
||
tbody td::before{{content:attr(data-l);color:var(--dim);font-size:12px;
|
||
text-align:left;flex:none}}
|
||
td.pickcell{{position:absolute;right:12px;top:12px;width:auto;
|
||
padding:0}}
|
||
td.pickcell::before{{content:''}}
|
||
td.name{{display:block;text-align:left;padding-right:30px}}
|
||
td.name::before{{content:''}}
|
||
td.ops{{justify-content:flex-end;padding-top:8px}}
|
||
td.ops::before{{content:''}}
|
||
td.empty{{display:block}}
|
||
td.empty::before{{content:''}}
|
||
}}
|
||
</style>
|
||
|
||
<div class=wrap>
|
||
<header>
|
||
<div class=logo>yx</div>
|
||
<div class=brand>
|
||
<h1>ykxmz 中转服务器</h1>
|
||
<div class=sub>远程控制 · 跨网络中转</div>
|
||
</div>
|
||
<div class=meta>
|
||
<div>版本 <b>{_esc(snap['version'])}</b> · 已运行 <b>{_uptime(snap['uptimeSeconds'])}</b></div>
|
||
<div>本进程内存 <b>{snap['memoryKB'] // 1024} MB</b> · 累计请求 <b>{snap['totalRequests']}</b></div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class=panel>
|
||
<div class=label>当前公网地址 —— 手表「中转设置」里填这个</div>
|
||
<div class=urlrow>
|
||
<div class="url{' empty' if not url_text else ''}" id=urlBox>{url_display}</div>
|
||
<button class="btn primary" id=copyUrl {'disabled' if not url_text else ''}>复制</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class=stats>
|
||
<div class=stat><div class=n>{len(snap['devices'])}</div><div class=t>登记设备</div></div>
|
||
<div class="stat {'ok' if online_count else 'mute'}"><div class=n>{online_count}</div><div class=t>在线</div></div>
|
||
<div class="stat {'warn' if snap['sessions'] else 'mute'}"><div class=n>{snap['sessions']}</div><div class=t>手表会话</div></div>
|
||
<div class=stat><div class=n>{len(RELAY.codes)}</div><div class=t>配对码占用</div></div>
|
||
</div>
|
||
|
||
<div class=toolbar>
|
||
<button class="btn del" id=delPicked>删除选中</button>
|
||
<button class="btn del" id=delAll>删除全部设备</button>
|
||
<div class=spacer></div>
|
||
<label class=tip style="cursor:pointer;display:flex;align-items:center;gap:6px">
|
||
<input type=checkbox id=autoRef checked> 自动刷新(10 秒)
|
||
</label>
|
||
</div>
|
||
<div class=tip style="margin:-4px 0 12px">
|
||
删除会连配对码一起清掉,那台设备下次可以重新设码。
|
||
</div>
|
||
|
||
<div class=tablewrap>
|
||
<table>
|
||
<thead><tr>
|
||
<th class=pickcell><input type=checkbox id=pickAll title="全选"></th>
|
||
<th>设备</th><th>配对码</th><th>状态</th>
|
||
<th>空闲</th><th>待发(表/机)</th><th>流量(入/出)</th><th></th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{''.join(rows) or empty}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<footer>
|
||
数据来自 /v1/health · 只显示配对码与流量,看不到任何传输内容<br>
|
||
中转只是转发字节,不解析、不存储会话内容。
|
||
</footer>
|
||
</div>
|
||
|
||
<div id=toast></div>
|
||
|
||
<script>
|
||
const toast = (msg, kind) => {{
|
||
const el = document.getElementById('toast');
|
||
el.textContent = msg;
|
||
el.className = kind ? ('show ' + kind) : 'show';
|
||
clearTimeout(el._t);
|
||
el._t = setTimeout(() => {{ el.className = ''; }}, 2600);
|
||
}};
|
||
|
||
const post = async (path, payload) => {{
|
||
const r = await fetch(path, {{
|
||
method: 'POST',
|
||
headers: {{'Content-Type': 'application/json'}},
|
||
body: JSON.stringify(payload || {{}})
|
||
}});
|
||
return r.json();
|
||
}};
|
||
|
||
// ---------------- 单台删除 ----------------
|
||
document.querySelectorAll('button.del[data-device]').forEach(b => {{
|
||
b.onclick = async () => {{
|
||
const name = b.dataset.name, code = b.dataset.code;
|
||
if (!confirm('删除设备「' + name + '」(配对码 ' + code + ')?\\\\n\\\\n'
|
||
+ '这会:\\\\n 1. 断开它现在的手表连接\\\\n'
|
||
+ ' 2. 从服务器删掉这台设备\\\\n'
|
||
+ ' 3. 清掉它的配对码绑定')) return;
|
||
try {{
|
||
const j = await post('/v1/admin/delete', {{deviceId: b.dataset.device}});
|
||
if (j.ok) {{
|
||
toast('已删除「' + (j.name || name) + '」,清掉配对码 '
|
||
+ (j.codesCleared || []).length + ' 条', 'ok');
|
||
setTimeout(() => location.reload(), 700);
|
||
}} else {{
|
||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
||
}}
|
||
}} catch (e) {{
|
||
toast('请求出错:' + e, 'err');
|
||
}}
|
||
}};
|
||
}});
|
||
|
||
// ---------------- 批量选择 ----------------
|
||
const picks = () => Array.from(document.querySelectorAll('input.pick'));
|
||
const picked = () => picks().filter(c => c.checked).map(c => c.value);
|
||
|
||
function refreshPickState() {{
|
||
const chosen = picked().length;
|
||
const btn = document.getElementById('delPicked');
|
||
if (btn) {{
|
||
btn.disabled = chosen === 0;
|
||
btn.textContent = chosen ? ('删除选中 (' + chosen + ')') : '删除选中';
|
||
}}
|
||
picks().forEach(c => {{
|
||
const tr = c.closest('tr');
|
||
if (tr) tr.classList.toggle('picked', c.checked);
|
||
}});
|
||
const all = document.getElementById('pickAll');
|
||
if (all) {{
|
||
const total = picks().length;
|
||
all.checked = total > 0 && chosen === total;
|
||
all.indeterminate = chosen > 0 && chosen < total;
|
||
}}
|
||
}}
|
||
|
||
picks().forEach(c => c.addEventListener('change', refreshPickState));
|
||
const pickAll = document.getElementById('pickAll');
|
||
if (pickAll) pickAll.addEventListener('change', () => {{
|
||
picks().forEach(c => {{ c.checked = pickAll.checked; }});
|
||
refreshPickState();
|
||
}});
|
||
refreshPickState();
|
||
|
||
// ---------------- 批量删除 ----------------
|
||
const delPicked = document.getElementById('delPicked');
|
||
if (delPicked) delPicked.onclick = async () => {{
|
||
const ids = picked();
|
||
if (!ids.length) return;
|
||
if (!confirm('删除选中的 ' + ids.length + ' 台设备?\\\\n\\\\n'
|
||
+ '会断开它们现在的手表连接,并清掉它们占的配对码。'))
|
||
return;
|
||
delPicked.disabled = true;
|
||
try {{
|
||
const j = await post('/v1/admin/delete_many', {{deviceIds: ids}});
|
||
if (j.ok) {{
|
||
toast('已删除 ' + j.deleted + ' 台设备'
|
||
+ (j.failed ? (',' + j.failed + ' 台失败') : ''), 'ok');
|
||
setTimeout(() => location.reload(), 700);
|
||
}} else {{
|
||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
||
delPicked.disabled = false;
|
||
}}
|
||
}} catch (e) {{
|
||
toast('请求出错:' + e, 'err');
|
||
delPicked.disabled = false;
|
||
}}
|
||
}};
|
||
|
||
// ---------------- 删除全部 ----------------
|
||
const delAll = document.getElementById('delAll');
|
||
if (delAll) delAll.onclick = async () => {{
|
||
if (!confirm('删除**全部**设备(连同所有配对码)?\\\\n\\\\n'
|
||
+ '清场用,删完所有电脑都要重新设码。')) return;
|
||
try {{
|
||
const j = await post('/v1/admin/delete_all');
|
||
toast('已删除 ' + (j.deleted || 0) + ' 台设备', 'ok');
|
||
setTimeout(() => location.reload(), 700);
|
||
}} catch (e) {{
|
||
toast('请求出错:' + e, 'err');
|
||
}}
|
||
}};
|
||
|
||
// ---------------- 复制公网地址 ----------------
|
||
const copyUrl = document.getElementById('copyUrl');
|
||
if (copyUrl) copyUrl.onclick = async () => {{
|
||
const text = document.getElementById('urlBox').textContent.trim();
|
||
try {{
|
||
await navigator.clipboard.writeText(text);
|
||
toast('地址已复制', 'ok');
|
||
}} catch (e) {{
|
||
// 非安全上下文(http 且不是 localhost)没有 clipboard API,
|
||
// 退回到老办法:选中 + execCommand
|
||
const range = document.createRange();
|
||
range.selectNodeContents(document.getElementById('urlBox'));
|
||
const sel = window.getSelection();
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
try {{ document.execCommand('copy'); toast('地址已复制', 'ok'); }}
|
||
catch (e2) {{ toast('复制失败,请手动选中', 'err'); }}
|
||
}}
|
||
}};
|
||
|
||
// ---------------- 自动刷新 ----------------
|
||
const autoRef = document.getElementById('autoRef');
|
||
let autoTimer = null;
|
||
function setupAuto() {{
|
||
clearInterval(autoTimer);
|
||
if (autoRef && autoRef.checked) {{
|
||
autoTimer = setInterval(() => {{
|
||
// 有勾选/删除操作进行中就先别刷新,免得打断
|
||
if (picked().length === 0 && !document.hidden) location.reload();
|
||
}}, 10000);
|
||
}}
|
||
}}
|
||
if (autoRef) {{
|
||
autoRef.addEventListener('change', () => {{
|
||
try {{ localStorage.setItem('lr_autoref', autoRef.checked ? '1' : '0'); }}
|
||
catch (e) {{}}
|
||
setupAuto();
|
||
}});
|
||
try {{
|
||
const saved = localStorage.getItem('lr_autoref');
|
||
if (saved === '0') autoRef.checked = false;
|
||
}} catch (e) {{}}
|
||
setupAuto();
|
||
}}
|
||
</script>
|
||
</html>"""
|
||
self._send(200, html.encode("utf-8"), "text/html; charset=utf-8")
|
||
'''
|
||
|
||
|
||
def main() -> int:
|
||
text = RELAY.read_text(encoding="utf-8")
|
||
lines = text.splitlines(keepends=True)
|
||
|
||
start = None
|
||
end = None
|
||
for index, line in enumerate(lines):
|
||
if line.startswith(" def _admin_page(self) -> None:"):
|
||
start = index
|
||
elif start is not None and index > start and (
|
||
line.startswith(" def ")
|
||
or line.startswith("def ")
|
||
or line.startswith("class ")):
|
||
end = index
|
||
break
|
||
if start is None or end is None:
|
||
print(" [失败] 找不到 _admin_page 的边界")
|
||
return 1
|
||
|
||
new_text = "".join(lines[:start]) + NEW_PAGE + "\n" + "".join(lines[end:])
|
||
try:
|
||
ast.parse(new_text)
|
||
except SyntaxError as exc:
|
||
print(f" [失败] 生成的代码语法错误: 行 {exc.lineno}: {exc.msg}")
|
||
return 1
|
||
|
||
RELAY.write_text(new_text, encoding="utf-8", newline="\n")
|
||
print(f" 已重写 _admin_page({end - start} 行 -> "
|
||
f"{NEW_PAGE.count(chr(10))} 行)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|