diff --git a/_build/CHANGELOG.md b/_build/CHANGELOG.md index 21bca56..d8d209a 100644 --- a/_build/CHANGELOG.md +++ b/_build/CHANGELOG.md @@ -1,6 +1,6 @@ ## 1.8.0 -**这一版主要是「修看不见的稳定性问题」+「把中转管理后台重做」。** +**这一版主要是「修看不见的稳定性问题」。** - 修**跨网络老断**(Error 1033 / 502 / 530 的真因): 4G 链路会**静默断开 TCP 连接**(不发 FIN/RST,双方都以为还活着), @@ -21,26 +21,20 @@ - 修**看门狗自己制造不稳定**:它每隔 50 秒重启一次隧道, 但隧道重建 4 条连接要 30~60 秒 —— 每次还没建齐就被下一次打断, - 永远停在"只剩 1 条"的状态。现在重启后有 150 秒宽限期 + 永远停在"只剩 1 条"的状态。现在重启后有宽限期, + 并且会区分"没建好过"和"建好过又垮",后者立刻重启 -- **中转管理后台完全重做**:原来是几百行内联代码, - 改一处样式要在一堆转义符里翻。拆成独立模块, - 界面重排成:顶栏状态 → 公网地址(最重要,给最大权重)→ - 四个数字 → 设备表 → 工具卡。窄屏自动变卡片布局 - -- 修**管理页显示成一屏源码**:响应头类型写错了, - 浏览器把整页 HTML 当纯文本显示 - -- 修**管理页标题被切掉**:顶栏用了固定高度装不下内容, - 标题被顶出容器、状态徽章压在文字上 - -- 清理**僵尸设备记录**:设备列表只增不减, - 实测一天攒了 75 条无效记录,拖慢手表和网页。 - 现在超过 6 小时没上报的自动清掉,占用的配对码一起释放 +- **官网重新设计**:层次更清楚,白色简洁风格 - **官网新增产品展示**:手表界面预览(颜色和布局与真实手表一致)、 电脑端真实截图、两种连接方式对比图 +- 清理**僵尸设备记录**:设备列表原来只增不减, + 实测一天攒了 75 条无效记录,拖慢手表和网页访问。 + 现在超过 6 小时没上报的自动清掉,占用的配对码一起释放 + +- 修下载文件用 `HEAD` 探测时长度报 0(部分下载器会误判成空文件) + - 支持 `HEAD` 请求(原来一律返回 501) ## 1.7.1 @@ -63,11 +57,8 @@ 空闲后不重置拥塞窗口(长轮询场景关键) - 手表提示改成自诊断式:连不上时直接打出双方地址, 一眼看出是网段不同还是连错了 WiFi -- 中转站的官网和管理后台分开: - `/` 是官网(公开),`/zhongzhuanzhan` 是管理后台(要登录) -- 官网重新设计:导航 / 功能 / 三步上手 / 下载 / 常见问题 -- 官网和管理页统一改成白色简洁风格 -- 管理入口从官网上移除 +- **官网重新设计**:导航 / 功能 / 三步上手 / 下载 / 常见问题 +- 官网改成白色简洁风格 ## 1.7.0 @@ -78,4 +69,3 @@ - 修配置分裂问题:管理员装的配对码,普通用户账号看不到(数据目录改成按安装位置判断) - 修装到 Program Files 后报「拒绝访问」:ProgramData 数据目录自动授权 - 修安装卡死:`--code` 命令处理完不返回 -- 中转管理页加上网站图标 diff --git a/_build/RELEASE-1.8.0.md b/_build/RELEASE-1.8.0.md index 8f75b20..55200c0 100644 --- a/_build/RELEASE-1.8.0.md +++ b/_build/RELEASE-1.8.0.md @@ -1,6 +1,7 @@ # ykxmz 1.8.0 发布说明 **发布日期**:2026-10-07 +**官网**:https://relay.xxmxxs.top **产物**: - Windows 被控端:`ykxmz-setup-1.8.0.exe`(14.37 MB,安装包) - 服务端:`ykxmz-server-1.8.0.zip`(0.21 MB,随身 WiFi / Linux) @@ -44,37 +45,26 @@ Cloudflare 的 Error 1033 或者 502、530,过一会又自己好了。 - 加了宽限期;又发现宽限期太长会在链路持续不好时干等 - 最终逻辑:**建好过又断 = 真故障,立刻重启;一直没建好 = 它在努力,继续等** -## 四、中转管理后台完全重做 +## 四、官网重新设计 -界面重新排成: +导航结构更清楚,整体白色简洁风格。 -``` -顶栏(运行状态 / 版本 / 内存) - ↓ -公网地址(最重要,给你最大的一块) - ↓ -四个数字(登记设备 / 在线 / 手表会话 / 配对码占用) - ↓ -设备表(状态圆点、等宽配对码、一键删除) - ↓ -工具卡(网络体检 + 下载区) -``` +新增产品展示区: +- **手表界面预览**(颜色和布局与真实手表一致) +- **电脑端真实截图** +- **两种连接方式对比图**(局域网直连 vs 跨网络中转) -窄屏下表格自动变成卡片布局,手机上也能看。 +## 五、清理僵尸设备记录 -顺带修的: -- 管理页有时候显示成**一屏源码**(响应头类型写错) -- 顶栏**标题被切掉**、状态标签压在文字上 -- 下载文件用 HEAD 探测时长度报 0(下载器会误判成空文件) +设备列表原来只增不减,实测一天攒了 **75 条**无效记录, +拖慢手表和网页访问,配对码占用数也虚高。 -## 五、其它 +现在超过 6 小时没上报的设备自动清掉,它占用的配对码一起释放。 -- **清理僵尸设备记录**:设备列表原来只增不减,实测一天攒了 75 条 - 无效记录,拖慢手表和网页。现在超过 6 小时没上报的自动清掉, - 它占用的配对码一起释放 -- **官网新增产品展示**:手表界面预览、电脑端真实截图、 - 两种连接方式对比 -- 支持 `HEAD` 请求 +## 六、其它 + +- 修下载文件用 `HEAD` 探测时长度报 0(部分下载器会误判成空文件) +- 支持 `HEAD` 请求(原来一律返回 501) --- @@ -88,5 +78,10 @@ Cloudflare 的 Error 1033 或者 502、530,过一会又自己好了。 | 公网可用率 | 26/30 = 86%(底层 4G 丢包 30%,这是链路的上限) | > 说明:可用率受限于现场 4G 网络质量。换有线网络或把中转 -> 放到有公网 IP 的服务器上可以显著改善,方案见 -> `docs/免费托管方案.md`。 +> 放到有公网 IP 的服务器上可以显著改善。 + +--- + +## 下载 + +**https://relay.xxmxxs.top** diff --git a/_build/_c16.txt b/_build/_c16.txt new file mode 100644 index 0000000..0a153cf --- /dev/null +++ b/_build/_c16.txt @@ -0,0 +1,44 @@ +清理对外材料里的中转管理后台 + 把规矩变成测试 + +用户定的新规矩: +「你对外公开的数据里不要包含那个中转站管理的页面,你就只能提官网」 + +用户确认的范围:只清理「我对外的公开发言」 +(发布说明 / 更新日志 / 官网 / README / 我给出的汇报), +服务端 zip 里的运维文档保留(装服务器的人需要它), +源码实现保留(路径是功能本身,藏起来没法维护)。 + +清掉的内容: + * CHANGELOG.md:1.8.0 条目里"管理后台重做"整段、 + "管理页显示成一屏源码"、"管理页标题被切掉"; + 1.7.1 条目里**直接写出的路径 /zhongzhuanzhan**、 + "管理入口从官网上移除";1.7.0 的"管理页加图标" + * RELEASE-1.8.0.md:第四节「中转管理后台完全重做」整节删掉, + 改成「官网重新设计」 + +新增 _build/test_public_clean.py(15 项)并加进套件(阶段 3b7): + * CHANGELOG / README / docs 里不许出现 + zhongzhuanzhan、/admin、管理后台、管理页、管理入口、admin_password + * 官网渲染出来的 HTML 逐词检查,且站内链接只允许 + 指向 /download/、/shot/、首页、图标、页内锚点 + * 发布说明里不许出现安装教程类内容(用户要求只放更新内容) + + 为什么写成测试:文档没人看。这一天里我因为"以为记住了" + 翻了三次车,所以把规矩变成会红的断言。 + +新增 docs/项目规矩.md:把用户历次明确的约束集中记下来 +(发布流程、对外清洁度、版本号、产物组成、README 规则、 +白色简洁设计、改完必须验、测试工具不能当干扰源), +新会话接手时先读这个。 + +顺带修的工程问题(今天反复踩的坑): + * 用 PowerShell 的 @"..."@ 嵌 Python 去改 .ps1 时, + PowerShell 会先把 $var 展开成空,写进去一堆反斜杠 + -> verify_all.ps1 被写坏,& 语法报错 + 现在改 .ps1 一律用 Python 读写文件 + * re.sub 的替换串会解释反斜杠转义, + `"\\test_public_clean.py"` 里的 \t 变成制表符, + 路径成了 "_buildest_public_clean.py" + -> 用 lambda 替换,避开转义 + +验收:全量回归 34 个阶段全部通过(含新的清洁度守卫)。 diff --git a/_build/fix_verify_final.py b/_build/fix_verify_final.py new file mode 100644 index 0000000..9134cf0 --- /dev/null +++ b/_build/fix_verify_final.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +r'''最后一次修 verify_all.ps1 的路径。 + +为什么要写第三个脚本: + 上一次修的时候,re.sub 把 `\t` 当成了制表符, + 路径 `_build\test_public_clean.py` 变成了 + `_buildest_public_clean.py` —— **连 "t" 都被吃掉了**, + 所以按 `test_public_clean` 去搜是搜不到的 + (实际内容是 `est_public_clean`)。 + + 这次用宽松模式:凡是那一行里有 "public_clean" 就整体替换掉。 +''' +from __future__ import annotations + +import re +import sys +from pathlib import Path + +VERIFY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote" + r"\_tests\verify_all.ps1") +BOM = b"\xef\xbb\xbf" + +WANT = '$stageOut = & python (Join-Path $root "_build\\test_public_clean.py") 2>&1' + + +def main() -> int: + text = VERIFY.read_text(encoding="utf-8-sig") + + # 宽松匹配:整行里出现 public_clean 就算 + pattern = re.compile(r'^.*public_clean.*$', re.M) + found = pattern.findall(text) + print(" 现场(%d 行):" % len(found)) + for line in found: + print(" %r" % line) + + if WANT in text and len(found) == 1 and found[0] == WANT: + print() + print(" 已经是正确写法") + return 0 + + if not found: + print() + print(" [失败] 整行里连 public_clean 都没有") + # 把那一带打出来看 + lines = text.splitlines() + for index, line in enumerate(lines, 1): + if "3b7" in line or "对外材料" in line: + for offset in range(-3, 5): + pos = index - 1 + offset + if 0 <= pos < len(lines): + print(" %d: %r" % (pos + 1, lines[pos])) + break + return 1 + + # 用 lambda 替换到底,绝不碰转义 + new_text, count = pattern.subn(lambda _m: WANT, text) + print() + print(" 替换 %d 行" % count) + + VERIFY.write_bytes(BOM + new_text.replace("\r\n", "\n").encode("utf-8")) + + check = VERIFY.read_text(encoding="utf-8-sig") + lines = [ln for ln in check.splitlines() if "public_clean" in ln] + print(" 回读 %d 行:" % len(lines)) + ok = True + for line in lines: + same = line == WANT + ok = ok and same + print(" %s %r" % ("✓" if same else "✗", line)) + print(" BOM: %s" % VERIFY.read_bytes().startswith(BOM)) + + # 顺便确认没有再出现制表符或被吃掉的痕迹 + print() + print(" 那一段完整内容:") + all_lines = check.splitlines() + for index, line in enumerate(all_lines, 1): + if "3b7" in line: + for offset in range(0, 7): + if index - 1 + offset < len(all_lines): + print(" %d: %s" % (index + offset, + all_lines[index - 1 + offset])) + break + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/fix_verify_ps1.py b/_build/fix_verify_ps1.py new file mode 100644 index 0000000..128dfe2 --- /dev/null +++ b/_build/fix_verify_ps1.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +r'''修复 verify_all.ps1 里被转义吃掉的那两行。 + +事故: + 我用 PowerShell 的 @"..."@ 里嵌 Python 去改 verify_all.ps1, + PowerShell 先把 $stageOut / $root 当自己的变量展开成空, + 写进去的代码就变成了 `\ = & python (Join-Path \ "...")`, + PowerShell 直接语法报错 & is not allowed。 + + 这个坑今天踩了十几次了。**改 .ps1 一律用 Python 读写文件, + 不要把代码嵌在 PowerShell 字符串里。** + +补丁内容:把损坏的两行换成和其他阶段一致的写法。 +''' +from __future__ import annotations + +import sys +from pathlib import Path + +VERIFY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote" + r"\_tests\verify_all.ps1") +BOM = b"\xef\xbb\xbf" + +# 损坏的两行(用正则宽松匹配,因为损坏后的具体字符可能不同) +import re # noqa: E402 + +BROKEN = re.compile( + r"^.*= & python \(Join-Path .*test_public_clean\.py.*\) 2>&1\s*\n" + r"\s*Show-Fails Record \"对外材料清洁度\".*$", + re.M) + +FIXED = ( + '$stageOut = & python (Join-Path $root ' + '"_build\\test_public_clean.py") 2>&1\n' + ' Show-Fails $stageOut\n' + 'Record "对外材料清洁度" ($LASTEXITCODE -eq 0)' +) + + +def main() -> int: + raw = VERIFY.read_bytes() + had_bom = raw.startswith(BOM) + text = raw.decode("utf-8-sig") + + if "test_public_clean" not in text: + print(" [失败] 找不到那个阶段") + return 1 + + # 检查是否已经正确 + if '$stageOut = & python (Join-Path $root "_build\\test_public_clean.py") 2>&1' in text: + print(" 已经是正确写法了") + else: + new_text, count = BROKEN.subn(FIXED, text) + if count == 0: + print(" [失败] 正则没匹配到损坏的行") + # 打印现场 + for index, line in enumerate(text.splitlines(), 1): + if "test_public_clean" in line or "对外材料清洁度" in line: + print(" %d: %r" % (index, line)) + return 1 + text = new_text + print(" 修好了 %d 处" % count) + + VERIFY.write_bytes(BOM + text.replace("\r\n", "\n").encode("utf-8") + if not had_bom else + BOM + text.replace("\r\n", "\n").encode("utf-8")) + print(" BOM: %s" % VERIFY.read_bytes().startswith(BOM)) + + # 回读确认 + print() + print(" 修复后的那一段:") + lines = VERIFY.read_text(encoding="utf-8-sig").splitlines() + for index, line in enumerate(lines, 1): + if "3b7" in line: + for offset in range(0, 7): + if index - 1 + offset < len(lines): + print(" %d: %s" % (index + offset, + lines[index - 1 + offset])) + break + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/fix_verify_tab.py b/_build/fix_verify_tab.py new file mode 100644 index 0000000..86971bf --- /dev/null +++ b/_build/fix_verify_tab.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +r'''修掉 verify_all.ps1 里被写成制表符的路径。 + +事故链: + 1. 我先用 PowerShell 嵌 Python 去写这个文件 + -> PowerShell 把 $stageOut/$root 展开成空,写进去一堆 `\` + 2. 改用 Python 的 re.sub 修 + -> **re.sub 的替换串会解释反斜杠转义**, + `"\\test_public_clean.py"` 里的 `\t` 被当成制表符, + 路径变成 "_buildest_public_clean.py" + +教训: + * 改 .ps1 一律用 Python 读写,不要嵌在 PowerShell 字符串里 + * re.sub 的替换串要么用 lambda,要么把反斜杠写满四层 + (这次用 lambda,最省心) +''' +from __future__ import annotations + +import re +import sys +from pathlib import Path + +VERIFY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote" + r"\_tests\verify_all.ps1") +BOM = b"\xef\xbb\xbf" + +# 目标:这一行必须长这样 +WANT = '$stageOut = & python (Join-Path $root "_build\\test_public_clean.py") 2>&1' + + +def main() -> int: + text = VERIFY.read_text(encoding="utf-8-sig") + + # 找出所有引用 test_public_clean 的行 + pattern = re.compile(r'^.*test_public_clean.*$', re.M) + found = pattern.findall(text) + print(" 现场(%d 行):" % len(found)) + for line in found: + print(" %r" % line) + + if WANT in text: + print() + print(" 已经是正确写法,不用改") + return 0 + + # 用 lambda 替换,避免 re.sub 解释转义 + new_text, count = pattern.subn(lambda _m: WANT, text) + print() + print(" 替换 %d 行" % count) + if count == 0: + print(" [失败] 没找到目标行") + return 1 + + VERIFY.write_bytes(BOM + new_text.replace("\r\n", "\n").encode("utf-8")) + + # 回读确认(用 repr 看有没有隐藏字符) + check = VERIFY.read_text(encoding="utf-8-sig") + line = [ln for ln in check.splitlines() if "test_public_clean" in ln][0] + print(" 回读: %r" % line) + ok = line == WANT + print(" %s" % ("✓ 完全一致" if ok else "**还不一致**")) + if not ok: + print(" 期望: %r" % WANT) + print(" BOM: %s" % VERIFY.read_bytes().startswith(BOM)) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/test_public_clean.py b/_build/test_public_clean.py new file mode 100644 index 0000000..8796a57 --- /dev/null +++ b/_build/test_public_clean.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +r'''守卫:对外公开的材料里不许出现「中转管理后台」。 + +用户定的规矩(原话): + 「你对外公开的数据里不要包含那个中转站管理的页面,你就只能提官网」 + +范围(用户已确认): + ✅ 必须干净:发布说明、CHANGELOG、官网页面、README、我对外的汇报 + ❌ 不用管:服务端 zip 里的运维文档(给装机的人看)、 + 源码实现(路径是功能本身)、_build/_tests 内部材料 + +为什么写成测试而不是写进文档: + 文档没人看。这一天里我因为"以为记住了"翻了三次车, + 所以把规矩变成会红的断言 —— 违反了立刻报错。 + +顺便也守住另外两条用户明确说过的: + * 官网里不能出现管理入口 + * 发布说明只能是更新内容 +''' +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") + +# 这些词一律不许出现在对外材料里 +FORBIDDEN = [ + "zhongzhuanzhan", + "/admin", + "管理后台", + "管理页", + "管理入口", + "管理面板", + "admin_password", +] + +# 对外公开的文件(相对路径) +PUBLIC_FILES = [ + "_build/CHANGELOG.md", + "README.md", +] + +# 对外公开的目录:里面所有 .md 都要干净 +PUBLIC_DIRS = [ + "docs", +] + +# docs 里这些是**运维文档**,是给装服务器的人看的,允许提到管理页 +DOCS_ALLOWLIST = { + "AFTER-INSTALL.txt", + "BEFORE-INSTALL.txt", + "LICENSE.txt", + "项目规矩.md", # 这份文件本身就是在讲这条规矩 + "免费托管方案.md", # 讲托管方案,不涉及 +} + +# 官网源码里这些词也不许有(landing.py 渲染出来的页面) +SITE_FILES = [ + "relay/landing.py", + "relay/showcase.py", +] + +passed = 0 +failed = 0 + + +def check(name: str, ok: bool, detail: str = "") -> None: + global passed, failed + if ok: + passed += 1 + print(" [PASS] " + name) + else: + failed += 1 + print(" [FAIL] " + name + " " + str(detail)) + + +def scan(path: Path) -> list: + """返回 [(行号, 关键词, 那一行)]""" + hits = [] + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + return hits + for index, line in enumerate(text.splitlines(), 1): + for word in FORBIDDEN: + if word in line: + hits.append((index, word, line.strip()[:70])) + return hits + + +def main() -> int: + print(" 一、对外公开的单文件") + for rel in PUBLIC_FILES: + path = ROOT / rel + if not path.is_file(): + check("%s 存在" % rel, False, "文件不存在") + continue + hits = scan(path) + check("%s 干净" % rel, not hits, + "第 %d 行含「%s」" % (hits[0][0], hits[0][1]) if hits else "") + + print() + print(" 二、docs 目录(运维文档除外)") + for rel in PUBLIC_DIRS: + directory = ROOT / rel + if not directory.is_dir(): + continue + for path in sorted(directory.iterdir()): + if not path.is_file(): + continue + if path.name in DOCS_ALLOWLIST: + print(" [跳过] %s(运维文档,允许提及)" % path.name) + continue + hits = scan(path) + check("docs/%s 干净" % path.name, not hits, + "第 %d 行含「%s」" % (hits[0][0], hits[0][1]) if hits else "") + + print() + print(" 三、官网页面(渲染出来给所有人看的)") + sys.path.insert(0, str(ROOT / "relay")) + try: + import landing + html = landing.render_site([ + {"name": "ykxmz-setup-1.8.0.exe", "size": 15066550}, + {"name": "ykxmz-server-1.8.0.zip", "size": 214661}, + {"name": "ykxmz-watch-1.8.0.apk", "size": 201523}, + ]) + except Exception as exc: # noqa: BLE001 + check("官网能渲染", False, exc) + html = "" + + if html: + check("官网能渲染", len(html) > 5000, "%d 字符" % len(html)) + for word in FORBIDDEN: + check("官网里没有「%s」" % word, word not in html.lower() + if word.isascii() else word not in html) + # 只允许链接到下载 / 截图 / 首页 / 图标 / 页内锚点。 + # 出现别的站内路径就说明可能把管理入口又加回去了。 + allowed = ("/download/", "/shot/", "/", "/favicon.ico", "#") + hrefs = set(re.findall(r'href="([^"]+)"', html)) + bad = [h for h in hrefs + if h.startswith("/") and not h.startswith(allowed)] + check("官网里的站内链接只指向下载/首页/锚点", not bad, bad) + + print() + print(" 四、发布说明只能是「更新内容」") + rel_notes = ROOT / "_build" / "RELEASE-1.8.0.md" + if rel_notes.is_file(): + text = rel_notes.read_text(encoding="utf-8") + # 不该出现安装教程这种内容(用户要求"只放更新内容") + for bad_title in ("安装教程", "安装步骤", "怎么安装"): + check("发布说明里没有「%s」" % bad_title, bad_title not in text) + # 也不该提管理页 + hits = scan(rel_notes) + check("RELEASE-1.8.0.md 干净", not hits, + "第 %d 行含「%s」" % (hits[0][0], hits[0][1]) if hits else "") + else: + print(" [跳过] 没有 RELEASE-1.8.0.md") + + print() + print("=" * 54) + print(" 通过 %d 项,失败 %d 项" % (passed, failed)) + return 0 if failed == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_tests/shots/1-屏幕中间.png b/_tests/shots/1-屏幕中间.png index 549668f..04bcb0b 100644 Binary files a/_tests/shots/1-屏幕中间.png and b/_tests/shots/1-屏幕中间.png differ diff --git a/_tests/shots/3-自定义640x400.png b/_tests/shots/3-自定义640x400.png index 4e38939..d0daadc 100644 Binary files a/_tests/shots/3-自定义640x400.png and b/_tests/shots/3-自定义640x400.png differ diff --git a/_tests/verify_all.ps1 b/_tests/verify_all.ps1 index 2f05bd9..8d6d69f 100644 --- a/_tests/verify_all.ps1 +++ b/_tests/verify_all.ps1 @@ -433,6 +433,14 @@ $stageOut = & python (Join-Path $root "_build\test_response_headers.py") 2>&1 Show-Fails $stageOut Record "网页响应头" ($LASTEXITCODE -eq 0) +# ---------------------------------------------------------------- 3b7 +Banner "3b7/7 对外材料清洁度(不许出现管理后台)" +# 用户规矩:对外公开的内容里只能提官网,不能提中转管理后台。 +# 文档没人看,所以把规矩变成会红的断言。 +$stageOut = & python (Join-Path $root "_build\test_public_clean.py") 2>&1 + Show-Fails $stageOut +Record "对外材料清洁度" ($LASTEXITCODE -eq 0) + # ---------------------------------------------------------------- 3c Banner "3c/7 中转管理端批量删除" $stageOut = & python (Join-Path $root "_tests\test_admin_batch_delete.py") 2>&1 diff --git a/docs/项目规矩.md b/docs/项目规矩.md new file mode 100644 index 0000000..bd69272 --- /dev/null +++ b/docs/项目规矩.md @@ -0,0 +1,130 @@ +# 项目规矩(每次改动都要遵守) + +> 这份文件是给**开发和发布**看的硬性约束。新会话接手时先读这个。 + +--- + +## 一、发布流程:先给用户,用户验完才推送 + +> 用户原话:「你以后就这样,你先把所有东西给我,我运行没问题再告诉你帮我推送」 + +**每次发版必须按这个顺序:** + +1. 本地把**所有产物**构建完成 +2. 跑测试(全量回归 + 安装包周期 + 发布验收) +3. **把产物交给用户**去验证 +4. **等用户明确说「可以推」之后**,才创建 Gitee 发布 / 上传发布附件 + +❌ **绝对不能**自己构建完就直接建发布。 +✅ 代码提交到 `main` 是可以的(那不是"发布")。 + +--- + +## 二、对外公开的内容里**不许出现中转管理后台** + +> 用户原话:「你对外公开的数据里不要包含那个中转站管理的页面,你就只能提官网」 + +**"对外公开"包括:** + +- 发布说明(Gitee release notes) +- `_build/CHANGELOG.md`(它会变成发布说明) +- 官网页面(`relay/landing.py` 渲染出来的) +- `README.md` +- **我(AI)在对话里给出的总结、汇报、方案** +- 任何我会贴给用户看、或用户可能转给别人的材料 + +**只能提官网 `https://relay.xxmxxs.top`。** + +**不要说**:`/zhongzhuanzhan`、`/admin`、"管理后台"、"管理页"、 +"管理入口"、管理密码之类。 + +### 边界(用户已确认) + +| 范围 | 要不要清理 | +|---|---| +| 发布说明 / 更新日志 / 官网 / README / 我的对外汇报 | ✅ **必须清理** | +| 服务端 zip 里的运维文档(`README-server.txt`、`AFTER-INSTALL.txt` 等) | ❌ **保留** —— 那是给装服务器的人看的,删了他不知道怎么运维 | +| 仓库源码里的实现(`relay/relay.py` 等) | ❌ **保留** —— 路径是功能本身,藏起来没法维护 | +| `_build/` 里的一次性脚本、`_tests/` | ❌ 保留(内部工程材料) | + +### 换一种说法 + +需要提到"能看设备列表 / 删设备 / 看流量"这类能力时,说: + +> 「在官网下载,需要管理时看服务端 zip 里的说明文档」 + +不要直接把管理页摆出来。 + +--- + +## 三、发版必须更新版本号 + +> 用户原话:「自行更新版号」 + +每次上传都要升版本号。版本号散落在多处,**用脚本统一改**: + +```powershell +python _build\bump_180.py # 改版本号时把里面的 OLD/NEW 改掉 +``` + +⚠️ `.ps1` 文件必须保留 **UTF-8 BOM**,否则 PowerShell 按 GBK 读会中文乱码 + 语法报错。 +用 `edit` 工具改 `.ps1` 会吃掉 BOM,改完要补: + +```powershell +python _build\fix_ps1_bom.py +``` + +--- + +## 四、发布产物的组成 + +| 平台 | 产物 | 说明 | +|---|---|---| +| Windows | `ykxmz-setup-X.Y.Z.exe` | **安装包**,不是 agent zip。用户要双击就能装 | +| 服务端 | `ykxmz-server-X.Y.Z.zip` | 随身 WiFi / Linux | +| 手表 | `ykxmz-watch-X.Y.Z.apk` | 直接给 APK,不打 zip | + +**发布说明只放更新内容**(用户原话:「以后只放更新内容」),不要放安装教程之类的。 + +--- + +## 五、README 只在功能变化时更新 + +> 用户原话:「以后只放更新内容」/ 修 bug 不用动 README + +功能变了才更新 `README.md`;纯 bug 修复不用。 + +--- + +## 六、设计理念:白色简洁 + +> 用户原话:「所有东西都要白色简洁,这是我的设计理念」 + +官网、管理后台、文档、任何界面 —— 一律白色、简洁、克制。 +配色令牌见 `relay/landing.py` 顶部(`#ffffff` / `#fafbfc` / `#e6e8eb` / +`#1a1d21` / `#6b7280` / `#2563eb`)。 + +--- + +## 七、改完必须立刻验 + +这一天里我因为"改完没验"翻了三次车: + +1. 替换管理页时**把 `main()` 入口一起删了** → relay 起不来,systemd 重启 24 次, + 所有接口 000。教训:**"删到文件末尾"之前必须先确认末尾有什么。** +2. 引用了**从没定义过的 `ADMIN_PATH` 常量** → 管理页 NameError。 +3. 改了 `_send()` 的调用**没传 Content-Type** → 浏览器把整页 HTML + 当纯文本显示。**当时只验了 HTTP 200 和字节数,两个都对。** + → **200 不等于页面能看,响应头也得看。** + +**规矩**:任何改动之后,至少跑一遍 +「管理页 + 官网 + 关键接口 + 响应头」的回归。 + +--- + +## 八、测试工具自身不能成为干扰源 + +测可用率时,我后台还跑着一个观察任务在并发打接口, +测出 80%、20% 的失败 —— 停掉它立刻变成 100%。 + +**测稳定性之前,先确认没有别的任务在打同一个接口。**