frpsctl 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {frpsctl-0.2.2 → frpsctl-0.2.3}/.github/workflows/ci.yml +7 -2
  2. {frpsctl-0.2.2 → frpsctl-0.2.3}/.github/workflows/release.yml +8 -1
  3. {frpsctl-0.2.2 → frpsctl-0.2.3}/CHANGELOG.md +61 -0
  4. {frpsctl-0.2.2 → frpsctl-0.2.3}/PKG-INFO +62 -6
  5. {frpsctl-0.2.2 → frpsctl-0.2.3}/README.md +61 -5
  6. {frpsctl-0.2.2 → frpsctl-0.2.3}/frpsctl-/350/256/276/350/256/241/346/226/271/346/241/210.md +138 -26
  7. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/__init__.py +1 -1
  8. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/cli/__init__.py +450 -12
  9. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/cli/ui.py +14 -21
  10. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/admin.py +15 -2
  11. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/config.py +42 -10
  12. frpsctl-0.2.3/src/frpsctl/core/diagnostics.py +38 -0
  13. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/doctor.py +29 -0
  14. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/health.py +8 -2
  15. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/lifecycle.py +54 -24
  16. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/lock.py +1 -7
  17. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/release.py +1 -2
  18. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/systemd.py +11 -3
  19. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/transaction.py +122 -1
  20. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/version.py +1 -4
  21. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/plugin/server.py +7 -25
  22. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/web/api.py +71 -14
  23. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/web/auth.py +18 -0
  24. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/web/server.py +25 -2
  25. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/web/static/index.html +27 -8
  26. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/test_cli.py +519 -0
  27. frpsctl-0.2.3/tests/test_docs.py +116 -0
  28. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/test_facts.py +12 -5
  29. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/test_integration.py +93 -0
  30. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/test_units.py +202 -2
  31. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/test_web.py +196 -2
  32. {frpsctl-0.2.2 → frpsctl-0.2.3}/.gitignore +0 -0
  33. {frpsctl-0.2.2 → frpsctl-0.2.3}/LICENSE +0 -0
  34. {frpsctl-0.2.2 → frpsctl-0.2.3}/NOTICE +0 -0
  35. {frpsctl-0.2.2 → frpsctl-0.2.3}/install.sh +0 -0
  36. {frpsctl-0.2.2 → frpsctl-0.2.3}/pyproject.toml +0 -0
  37. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/__main__.py +0 -0
  38. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/cli/context.py +0 -0
  39. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/__init__.py +0 -0
  40. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/healthcheck.py +0 -0
  41. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/instance.py +0 -0
  42. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/logs.py +0 -0
  43. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/platform.py +0 -0
  44. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/core/schema.py +0 -0
  45. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/errors.py +0 -0
  46. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/plugin/__init__.py +0 -0
  47. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/plugin/audit.py +0 -0
  48. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/plugin/engine.py +0 -0
  49. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/plugin/policy.py +0 -0
  50. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/plugin/quota.py +0 -0
  51. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/plugin/types.py +0 -0
  52. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/py.typed +0 -0
  53. {frpsctl-0.2.2 → frpsctl-0.2.3}/src/frpsctl/web/__init__.py +0 -0
  54. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/__init__.py +0 -0
  55. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/conftest.py +0 -0
  56. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/fake_frps.py +0 -0
  57. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/faults.py +0 -0
  58. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/test_faults.py +0 -0
  59. {frpsctl-0.2.2 → frpsctl-0.2.3}/tests/test_plugin.py +0 -0
@@ -18,7 +18,7 @@ jobs:
18
18
  strategy:
19
19
  fail-fast: false
20
20
  matrix:
21
- python-version: ["3.11", "3.12", "3.13"]
21
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
22
22
 
23
23
  steps:
24
24
  - uses: actions/checkout@v4
@@ -117,9 +117,14 @@ jobs:
117
117
  assert call("/api/session")["csrf"] == csrf
118
118
  config = call("/api/config")
119
119
  assert any(item["key"] == "bindPort" for item in config["entries"]), config
120
+ history = call("/api/config/history")
121
+ assert isinstance(history["entries"], list), history
122
+ # 端到端冒烟里的 config set 会留下快照:history 必须真的读得到
123
+ # (只断言"是列表"的话,空列表也能骗过这层守卫)。
124
+ assert any(str(e.get("action", "")).startswith("set ") for e in history["entries"]), history
120
125
  html = urllib.request.urlopen(base + "/", timeout=10).read().decode("utf-8")
121
126
  assert "frpsctl 管理台" in html
122
- print("web 冒烟通过:login / session / status / config / 静态页")
127
+ print("web 冒烟通过:login / session / status / config / history / 静态页")
123
128
  EOF
124
129
  kill "$WEB_PID"
125
130
  wait "$WEB_PID" 2>/dev/null || true
@@ -51,7 +51,14 @@ jobs:
51
51
 
52
52
  wheel = next(pathlib.Path("dist").glob("*.whl"))
53
53
  names = set(zipfile.ZipFile(wheel).namelist())
54
- required = {"frpsctl/py.typed", "frpsctl/cli/__init__.py", "frpsctl/core/lifecycle.py"}
54
+ required = {
55
+ "frpsctl/py.typed",
56
+ "frpsctl/cli/__init__.py",
57
+ "frpsctl/core/lifecycle.py",
58
+ "frpsctl/core/diagnostics.py",
59
+ # 前端是 package data:丢它 = 管理台页面 500,而 CLI 测试全绿
60
+ "frpsctl/web/static/index.html",
61
+ }
55
62
  missing = required - names
56
63
  if missing:
57
64
  raise SystemExit(f"wheel 缺少文件:{sorted(missing)}")
@@ -3,6 +3,67 @@
3
3
  格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
4
  版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。
5
5
 
6
+ ## [0.2.3] - 2026-09-17
7
+
8
+ 全量通读后的根治性迭代:Web 参数边界、systemd 交叉状态、限速来源模型、分层
9
+ 倒置,以及六项新能力(config unset / traffic / plugin user / web password /
10
+ 配置历史回滚 / dry-run 与 stdin 输入通道)。
11
+
12
+ ### 修复
13
+
14
+ - **Web 动作接口的负参数会立即 SIGKILL**:`/api/actions/stop` 的 `timeout` 走
15
+ 宽松解析,`-1` 让等待循环一次都不执行、直接升级 SIGKILL——与 CLI 侧
16
+ `stop --timeout -1`(v0.2.0 修复)是同一缺陷的镜像。现在全部动作参数做范围
17
+ 校验(越界 / 非数值 → 400),并有一条测试断言"进程必须仍存活"。
18
+ - **`status` 在 systemd 托管 + 损坏 state.json 时误报 STOPPED**:损坏分支完全
19
+ 不探测 unit,而 systemd 下 state.json 本就不参与判定(direct → systemd 迁移
20
+ 的残留即可触发)。现在 systemd 实例照常报告;非 systemd 仍报"不可判定"。
21
+ 同时给 status 的"永不异常"承诺补上 TOCTOU 收口(损坏检查与读取之间的竞态)。
22
+ - **认证失败限速的来源模型**:来源表加上限(1024,防慢速内存放大);反代部署
23
+ 下所有请求同源、攻击者 5 次失败即可连带锁住管理员 → 新增
24
+ `web serve --trusted-proxy`(默认关;开启后按 `X-Forwarded-For` 最后一跳
25
+ 限速,`web service install` 亦可写入 unit)。
26
+ - **`config rollback -1` / `config diff --steps 0` 静默归一**:步数必须 ≥ 1
27
+ (用法错误 2),参数笔误不再变成另一个动作。
28
+ - 健康 detail 在 L2 与 L3 同时失败时只显示 L3 的信息 → 现在汇总两层,且
29
+ L2 失败时也会渲染失败原因(控制面是恢复顺序上的第一层)。
30
+ - 发布前回归 review 追加修复:Web 动作参数的**布尔值**(`{"timeout": true}`
31
+ 曾被当作 1.0 秒静默接受);`plugin user set/remove` 的读-改-写**没有锁**
32
+ (两个并发调用互相覆盖——与第四轮 `config set` 并发丢失同形态,现与 config
33
+ 写共用实例锁);**首尾空白**(`parse_scalar` 此前保留未 strip 的原文,现按
34
+ TOML 裸值语义去空白;纯空白的值一律拒绝);`--prompt` 在无输入时抛裸
35
+ `EOFError`(现为用法错误并提示改用 `--stdin`)。
36
+
37
+ ### 新增
38
+
39
+ - `frpsctl config unset <key>`:删键回落 frp 默认值,与 `config set` 同一事务
40
+ 闭环(校验 → 快照 → 重启 → 失败自动回滚);键不存在报配置错误(3)。
41
+ - `frpsctl config set --dry-run`:跑完全部真实校验(含 `frps verify` 与危险组合
42
+ 拦截)但零落盘、零快照——CLI 版的"预览";`config unset` 同样支持。
43
+ - `config set --stdin / --prompt`:敏感值不再必须走 argv(shell 历史与
44
+ `/proc/<pid>/cmdline`);三种值来源互斥。
45
+ - `frpsctl traffic [name]`:近 7 天流量历史(无参 = 全部代理逐日汇总;离线
46
+ 代理 404 = 无数据,单代理失败不拖垮整体)。
47
+ - `frpsctl plugin user set|remove|list`:策略的结构化编辑(写入前同
48
+ `plugin check` 判据复验、0600 原子写、未知键原样保留)。
49
+ - `frpsctl web password show`:读回 `web service install` 生成的口令(权限
50
+ 过宽时向 stderr 告警)。
51
+ - Web 管理台的"历史与回滚"卡片:列出快照(时间 / 动作 / 步数)并可回滚到
52
+ 任意一份;新接口 `GET /api/config/history` 只读 meta.json,不下发快照原文。
53
+
54
+ ### 工程
55
+
56
+ - **分层倒置根治**:`core/` 不再反向 import `cli/`(此前 8 处
57
+ `cli.ui.trace` / `mask_secret`)——诊断开关下沉为 `core/diagnostics.py`,
58
+ 打码统一走 `config.mask_value`。
59
+ - 死代码清理(零调用即删):`lock.held_locks`、`plugin.server.serve` /
60
+ `wait_ready`、`WebSettings.password`(构造后从未被读,容易误以为生效)。
61
+ - 新增 `tests/test_docs.py`:README 命令 / 环境变量 / 退出码与设计文档命令面的
62
+ **双向一致性守卫**——文档 drift 从此在 CI 里直接暴露;同步刷新设计文档
63
+ §1.3 / §5 / §7.2。
64
+ - CI 矩阵加入 Python 3.14;Web 与插件的监听 backlog 设为 64(过载行为可预期)。
65
+ - 新增 75 条测试(435 → 510;非契约 480),覆盖率 82% → **85%**。
66
+
6
67
  ## [0.2.2] - 2026-09-17
7
68
 
8
69
  Web 管理台(内置界面)与 `kick` 语义修正。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: frpsctl
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: 把 frp 服务端(frps)包装成命令行工具:配置翻译器 + 进程保镖 + 状态聚合器
5
5
  Project-URL: Homepage, https://github.com/ThzxxArt/frpsctl
6
6
  Project-URL: Repository, https://github.com/ThzxxArt/frpsctl
@@ -411,6 +411,21 @@ alice.alice-ssh alice tcp 6000 online 0 0 B / 0 B
411
411
  $ frpsctl proxies --type http # 只看某类型
412
412
  ```
413
413
 
414
+ 近 7 天的流量历史(日粒度;数据源与 Web 趋势图相同):
415
+
416
+ ```console
417
+ $ frpsctl traffic # 全部代理逐日汇总
418
+ date in out
419
+ 2026-09-16 1.2 MiB 3.4 MiB
420
+ 2026-09-17 0.4 MiB 1.1 MiB
421
+ 合计 1.6 MiB 4.5 MiB
422
+
423
+ $ frpsctl traffic alice.alice-ssh # 单个代理的明细
424
+ ```
425
+
426
+ 离线或已删除的代理在数据源上返回 404 = 无数据(不是错误);单个代理查询失败
427
+ 也不拖垮整体。
428
+
414
429
  `prune` 清理离线代理记录;在线代理无法从服务端强制下线(frp 没有该 API,
415
430
  见"清理离线记录"一节)。
416
431
 
@@ -450,6 +465,10 @@ $ frpsctl config set maxPortsPerClient 30
450
465
 
451
466
  ```bash
452
467
  frpsctl config set bindPort 8000 --no-restart # 只写不重启(输出会提示"尚未生效")
468
+ frpsctl config set bindPort 8000 --dry-run # 只校验并展示 diff,不写入、不重启
469
+ frpsctl config set webServer.password --prompt # 敏感值隐藏输入(不进 argv / shell 历史)
470
+ frpsctl config set webServer.password --stdin # 或从管道读:echo -n "$PW" | frpsctl ...
471
+ frpsctl config unset maxPortsPerClient # 删键回落 frp 默认值(同一事务闭环)
453
472
  frpsctl config list # 列出全部键(值自动打码)
454
473
  frpsctl config list --prefix webServer # 只看某张表
455
474
  frpsctl config list --tree # 按表分组缩进展示
@@ -462,6 +481,10 @@ frpsctl config rollback # 回滚到上一份
462
481
  frpsctl config rollback 3 # 回滚到 3 份之前
463
482
  ```
464
483
 
484
+ > `--dry-run` 与 `--no-restart` 的区别:前者**什么都不写**(只校验+预览),
485
+ > 后者已经落盘、只是没有重启;`config unset` 删除不存在的键会报配置错误(3)
486
+ > ——拼错键名的"成功删除"会让人以为清掉了某个设置。
487
+
465
488
  **机密保护**:`config get` 默认打码(`SU***56` 形式,保留首尾便于核对是不是同一个
466
489
  值),`--json` 与所有 diff 输出同样打码。要看明文必须 `--reveal`。
467
490
 
@@ -606,6 +629,20 @@ frpsctl plugin check # 离线校验 + 试算典型裁决
606
629
  frpsctl plugin serve # 启动(只允许绑回环)
607
630
  ```
608
631
 
632
+ 策略里的用户可以用命令维护——不必手写 JSON(写入前用与 `plugin check` 相同的
633
+ 判据复验,0600 原子写,`_comment` 等自定义字段原样保留):
634
+
635
+ ```bash
636
+ frpsctl plugin user list # 用户与权限摘要(不显示策略级凭据)
637
+ frpsctl plugin user set alice --ports 6000-6010 --max-proxies 5
638
+ frpsctl plugin user set alice --no-random-port # 只改这一个字段,其余保持
639
+ frpsctl plugin user set bob --names "" # 空串 = 删除该字段(不限名称)
640
+ frpsctl plugin user remove alice
641
+ ```
642
+
643
+ > 插件服务在启动时载入策略:改完记得重启它才生效
644
+ > (`systemctl restart frpsctl-plugin@<实例>`)。
645
+
609
646
  `plugin check` 让你在部署前就看到"策略会怎么判":
610
647
 
611
648
  ```console
@@ -782,6 +819,22 @@ sudo frpsctl web service uninstall
782
819
  > `web service install` 同样需要 `frpsctl` 位于系统路径(不能被 `ProtectHome`
783
820
  > 挡住)——与插件服务的部署要求一致。
784
821
 
822
+ 配置页里还有**历史与回滚**:列出最近 10 份快照(时间 / 动作 / 步数),可回滚到
823
+ 任意一份(接口 `GET /api/config/history` 只读快照的元数据,**不下发配置原文**
824
+ ——快照是含 token 与口令的完整副本)。
825
+
826
+ 生成的口令随时可以取回(权限过宽时会告警):
827
+
828
+ ```bash
829
+ frpsctl web password show
830
+ ```
831
+
832
+ **在反向代理后运行**(`--allow-non-loopback` + Nginx 等)时加 `--trusted-proxy`:
833
+ 登录失败限速按 `X-Forwarded-For` 的**最后一跳**区分来源。默认关闭时该头完全
834
+ 不被读取;开启的前提是"前面确实有一层会重写该头的可信代理"——否则攻击者的
835
+ 失败会与管理员同源,5 次失败就能把管理员锁在冷却之外。
836
+ `web service install --trusted-proxy` 会把该标志写进 unit。
837
+
785
838
  ## 用 systemd 托管
786
839
 
787
840
  ```bash
@@ -1169,14 +1222,15 @@ FRPSCTL_TRACEBACK=1 frpsctl status
1169
1222
 
1170
1223
  ```bash
1171
1224
  uv venv && uv pip install -e ".[dev]"
1172
- .venv/bin/pytest # 全部 435 条(契约层缺二进制时自动 skip)
1173
- .venv/bin/pytest -m "not contract" # 快速回归(405 条)
1174
- .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 83%)
1225
+ .venv/bin/pytest # 全部 510 条(契约层缺二进制时自动 skip)
1226
+ .venv/bin/pytest -m "not contract" # 快速回归(480 条)
1227
+ .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 85%)
1175
1228
  .venv/bin/ruff check src/ tests/ # 静态分析
1176
1229
  ```
1177
1230
 
1178
- CI(Linux,Python 3.11/3.12/3.13)还包含:ruff、覆盖率门禁、真 frp 0.71.0 契约层、
1179
- **真 frp 0.70.0 下界契约矩阵**、无二进制降级路径、端到端冒烟。
1231
+ CI(Linux,Python 3.11/3.12/3.13/3.14)还包含:ruff、覆盖率门禁、真 frp 0.71.0
1232
+ 契约层、**真 frp 0.70.0 下界契约矩阵**、无二进制降级路径、端到端冒烟与
1233
+ Web 管理台冒烟、**文档一致性守卫**(README/设计文档 vs 代码的三向核对)。
1180
1234
 
1181
1235
  ### 测试分六层
1182
1236
 
@@ -1226,6 +1280,8 @@ fail)——它们是"插件真的接得住 frp 调用"的唯一证明,因此
1226
1280
  | **`max_proxies` 计数需配 `admin_url`** | 不配时只在插件进程内计数(重启归零、多实例各算各的),`plugin check` 会告警 |
1227
1281
  | **frps 没有热重载** | 改配置必然重启,因此 `config set` 的设计目标就是"失败了要能退回去" |
1228
1282
  | **systemd 模式下 pid 文件不参与判定** | 所有权委托 systemctl;`install` 换版本后需 `systemctl restart` |
1283
+ | **Web 管理台不限制并发连接数** | 单机管理工具的取舍(请求线程随连接创建,监听 backlog 64);公网暴露请在前置反代上做限流 |
1284
+ | **`--trusted-proxy` 只信 X-Forwarded-For 的最后一跳** | 前提是前面确实有一层会重写该头的可信代理;直连部署不要开启 |
1229
1285
 
1230
1286
  ---
1231
1287
 
@@ -388,6 +388,21 @@ alice.alice-ssh alice tcp 6000 online 0 0 B / 0 B
388
388
  $ frpsctl proxies --type http # 只看某类型
389
389
  ```
390
390
 
391
+ 近 7 天的流量历史(日粒度;数据源与 Web 趋势图相同):
392
+
393
+ ```console
394
+ $ frpsctl traffic # 全部代理逐日汇总
395
+ date in out
396
+ 2026-09-16 1.2 MiB 3.4 MiB
397
+ 2026-09-17 0.4 MiB 1.1 MiB
398
+ 合计 1.6 MiB 4.5 MiB
399
+
400
+ $ frpsctl traffic alice.alice-ssh # 单个代理的明细
401
+ ```
402
+
403
+ 离线或已删除的代理在数据源上返回 404 = 无数据(不是错误);单个代理查询失败
404
+ 也不拖垮整体。
405
+
391
406
  `prune` 清理离线代理记录;在线代理无法从服务端强制下线(frp 没有该 API,
392
407
  见"清理离线记录"一节)。
393
408
 
@@ -427,6 +442,10 @@ $ frpsctl config set maxPortsPerClient 30
427
442
 
428
443
  ```bash
429
444
  frpsctl config set bindPort 8000 --no-restart # 只写不重启(输出会提示"尚未生效")
445
+ frpsctl config set bindPort 8000 --dry-run # 只校验并展示 diff,不写入、不重启
446
+ frpsctl config set webServer.password --prompt # 敏感值隐藏输入(不进 argv / shell 历史)
447
+ frpsctl config set webServer.password --stdin # 或从管道读:echo -n "$PW" | frpsctl ...
448
+ frpsctl config unset maxPortsPerClient # 删键回落 frp 默认值(同一事务闭环)
430
449
  frpsctl config list # 列出全部键(值自动打码)
431
450
  frpsctl config list --prefix webServer # 只看某张表
432
451
  frpsctl config list --tree # 按表分组缩进展示
@@ -439,6 +458,10 @@ frpsctl config rollback # 回滚到上一份
439
458
  frpsctl config rollback 3 # 回滚到 3 份之前
440
459
  ```
441
460
 
461
+ > `--dry-run` 与 `--no-restart` 的区别:前者**什么都不写**(只校验+预览),
462
+ > 后者已经落盘、只是没有重启;`config unset` 删除不存在的键会报配置错误(3)
463
+ > ——拼错键名的"成功删除"会让人以为清掉了某个设置。
464
+
442
465
  **机密保护**:`config get` 默认打码(`SU***56` 形式,保留首尾便于核对是不是同一个
443
466
  值),`--json` 与所有 diff 输出同样打码。要看明文必须 `--reveal`。
444
467
 
@@ -583,6 +606,20 @@ frpsctl plugin check # 离线校验 + 试算典型裁决
583
606
  frpsctl plugin serve # 启动(只允许绑回环)
584
607
  ```
585
608
 
609
+ 策略里的用户可以用命令维护——不必手写 JSON(写入前用与 `plugin check` 相同的
610
+ 判据复验,0600 原子写,`_comment` 等自定义字段原样保留):
611
+
612
+ ```bash
613
+ frpsctl plugin user list # 用户与权限摘要(不显示策略级凭据)
614
+ frpsctl plugin user set alice --ports 6000-6010 --max-proxies 5
615
+ frpsctl plugin user set alice --no-random-port # 只改这一个字段,其余保持
616
+ frpsctl plugin user set bob --names "" # 空串 = 删除该字段(不限名称)
617
+ frpsctl plugin user remove alice
618
+ ```
619
+
620
+ > 插件服务在启动时载入策略:改完记得重启它才生效
621
+ > (`systemctl restart frpsctl-plugin@<实例>`)。
622
+
586
623
  `plugin check` 让你在部署前就看到"策略会怎么判":
587
624
 
588
625
  ```console
@@ -759,6 +796,22 @@ sudo frpsctl web service uninstall
759
796
  > `web service install` 同样需要 `frpsctl` 位于系统路径(不能被 `ProtectHome`
760
797
  > 挡住)——与插件服务的部署要求一致。
761
798
 
799
+ 配置页里还有**历史与回滚**:列出最近 10 份快照(时间 / 动作 / 步数),可回滚到
800
+ 任意一份(接口 `GET /api/config/history` 只读快照的元数据,**不下发配置原文**
801
+ ——快照是含 token 与口令的完整副本)。
802
+
803
+ 生成的口令随时可以取回(权限过宽时会告警):
804
+
805
+ ```bash
806
+ frpsctl web password show
807
+ ```
808
+
809
+ **在反向代理后运行**(`--allow-non-loopback` + Nginx 等)时加 `--trusted-proxy`:
810
+ 登录失败限速按 `X-Forwarded-For` 的**最后一跳**区分来源。默认关闭时该头完全
811
+ 不被读取;开启的前提是"前面确实有一层会重写该头的可信代理"——否则攻击者的
812
+ 失败会与管理员同源,5 次失败就能把管理员锁在冷却之外。
813
+ `web service install --trusted-proxy` 会把该标志写进 unit。
814
+
762
815
  ## 用 systemd 托管
763
816
 
764
817
  ```bash
@@ -1146,14 +1199,15 @@ FRPSCTL_TRACEBACK=1 frpsctl status
1146
1199
 
1147
1200
  ```bash
1148
1201
  uv venv && uv pip install -e ".[dev]"
1149
- .venv/bin/pytest # 全部 435 条(契约层缺二进制时自动 skip)
1150
- .venv/bin/pytest -m "not contract" # 快速回归(405 条)
1151
- .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 83%)
1202
+ .venv/bin/pytest # 全部 510 条(契约层缺二进制时自动 skip)
1203
+ .venv/bin/pytest -m "not contract" # 快速回归(480 条)
1204
+ .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 85%)
1152
1205
  .venv/bin/ruff check src/ tests/ # 静态分析
1153
1206
  ```
1154
1207
 
1155
- CI(Linux,Python 3.11/3.12/3.13)还包含:ruff、覆盖率门禁、真 frp 0.71.0 契约层、
1156
- **真 frp 0.70.0 下界契约矩阵**、无二进制降级路径、端到端冒烟。
1208
+ CI(Linux,Python 3.11/3.12/3.13/3.14)还包含:ruff、覆盖率门禁、真 frp 0.71.0
1209
+ 契约层、**真 frp 0.70.0 下界契约矩阵**、无二进制降级路径、端到端冒烟与
1210
+ Web 管理台冒烟、**文档一致性守卫**(README/设计文档 vs 代码的三向核对)。
1157
1211
 
1158
1212
  ### 测试分六层
1159
1213
 
@@ -1203,6 +1257,8 @@ fail)——它们是"插件真的接得住 frp 调用"的唯一证明,因此
1203
1257
  | **`max_proxies` 计数需配 `admin_url`** | 不配时只在插件进程内计数(重启归零、多实例各算各的),`plugin check` 会告警 |
1204
1258
  | **frps 没有热重载** | 改配置必然重启,因此 `config set` 的设计目标就是"失败了要能退回去" |
1205
1259
  | **systemd 模式下 pid 文件不参与判定** | 所有权委托 systemctl;`install` 换版本后需 `systemctl restart` |
1260
+ | **Web 管理台不限制并发连接数** | 单机管理工具的取舍(请求线程随连接创建,监听 backlog 64);公网暴露请在前置反代上做限流 |
1261
+ | **`--trusted-proxy` 只信 X-Forwarded-For 的最后一跳** | 前提是前面确实有一层会重写该头的可信代理;直连部署不要开启 |
1206
1262
 
1207
1263
  ---
1208
1264
 
@@ -70,10 +70,19 @@
70
70
 
71
71
  ```
72
72
  frpsctl 命令行程序(pip/pipx 安装)
73
- ├── 主命令集 init / start / stop / restart / status / log
74
- ├── 配置子命令 config get|set|edit|diff|rollback
75
- ├── 运维子命令 install / service / doctor / kick
76
- └── 可选:插件服务 独立进程,多用户鉴权与端口白名单(§11)
73
+ ├── 二进制与配置 install / init / verify
74
+ ├── 生命周期 start / stop / restart / status / log
75
+ ├── 配置子命令 config get|set|unset|edit|list|diff|rollback
76
+ │ (set 支持 --dry-run / --stdin / --prompt)
77
+ ├── 观测与运维 clients / proxies / traffic / instances / doctor / prune
78
+ ├── systemd 集成 service install|uninstall|status|logs
79
+ ├── 服务端插件 plugin init|check|serve
80
+ │ plugin user set|remove|list(策略结构化编辑)
81
+ │ plugin service install|uninstall|status
82
+ └── Web 管理台 web serve / web service install|uninstall|status
83
+ web password show
84
+
85
+ 可选:插件服务与 Web 管理台都是独立进程,由各自的 systemd unit 守护。
77
86
  ```
78
87
 
79
88
  外部依赖只有四个纯 Python 包(`typer` / `pydantic` / `tomlkit` / `httpx`),其余全部走标准库。
@@ -388,21 +397,32 @@ resolve_owner(instance):
388
397
  │ frpsctl (Python 3.11+) │
389
398
  │ │
390
399
  │ cli/ Typer 命令层(薄:只做参数解析与输出渲染) │
391
- │ ├─ lifecycle_cmd.py init / start / stop / restart / status / log
392
- │ ├─ config_cmd.py config get|set|edit|diff|rollback │
393
- │ ├─ service_cmd.py install / service / kick │
394
- │ └─ doctor_cmd.py doctor │
400
+ │ ├─ __init__.py 全部命令(按 install/init、config、plugin、 │
401
+ │ │ web、观测类命令分组;实现即文档 §7.2 的命令表) │
402
+ │ ├─ context.py 全局选项 → AppContext、异常 → 退出码映射 │
403
+ │ └─ ui.py 人读/JSON 渲染(--verbose 委托 core.diagnostics)│
404
+ │ │
405
+ │ web/ 浏览器界面(与 cli/ 平级的又一个前端,§18) │
406
+ │ ├─ server.py ThreadingHTTPServer:路由/安全头/CSRF │
407
+ │ ├─ api.py JSON API(只调 core)+ 错误 → HTTP 映射 │
408
+ │ ├─ auth.py 会话 / CSRF / 失败限速 │
409
+ │ └─ static/index.html 单文件前端(零外部资源) │
410
+ │ │
411
+ │ plugin/ 服务端插件(§11):协议解析 / 裁决 / 配额 / 审计 │
395
412
  │ │
396
413
  │ core/ │
397
414
  │ ├─ instance.py 实例布局与全局选项(--instance / --root) │
398
415
  │ ├─ platform.py pid_alive / proc_start_time / proc_cmdline │
399
- │ ├─ lock.py flock 实例级互斥 │
416
+ │ ├─ lock.py flock 实例级互斥(同线程可重入) │
400
417
  │ ├─ lifecycle.py 所有权探测 + 状态机 + 启动早退检测 │
401
418
  │ ├─ config.py tomlkit 无损补丁 + 原子写 + 备份历史 │
419
+ │ ├─ transaction.py 变更事务(锁内读-改-写、CAS、自动回滚) │
402
420
  │ ├─ admin.py Admin API 客户端(只走 v2,§ADR-3) │
403
421
  │ ├─ release.py 二进制下载 + sha256 强校验 │
404
- │ ├─ systemd.py unit 渲染与 systemctl 委托 │
405
- │ └─ doctor.py 体检与安全 lint │
422
+ │ ├─ systemd.py unit 渲染与 systemctl 委托(frps/插件/web) │
423
+ │ ├─ doctor.py 体检与安全 lint │
424
+ │ ├─ logs.py 日志路径解析与 tail(CLI 与 web 共用) │
425
+ │ └─ diagnostics.py 进程级诊断开关(--verbose,core 不依赖 cli) │
406
426
  │ │
407
427
  │ errors.py 统一异常 → 退出码映射(§7.3) │
408
428
  └──────────┬──────────────────────────────────┬─────────────────────┘
@@ -513,22 +533,37 @@ class Instance:
513
533
 
514
534
  | 命令 | 语义 | 关键行为 |
515
535
  |------|------|---------|
516
- | `frpsctl install [--version 0.71.0] [--force] [--only-download]` | 获取 frps 二进制 | 下载 → **强校验 sha256** → 落盘为 `frps-<version>` → `-v` 复验 → 换软链(§8.6.1)。`< 0.70.0` 直接拒绝 |
517
- | `frpsctl init` | 交互式生成配置 | 强制随机口令、`tls.force=true`、引导设置 `allowPorts`;已存在则要求 `--force` 并先备份 |
536
+ | `frpsctl install [--version V] [--force] [--only-download] [--mirror U] [--with-frpc]` | 获取 frps 二进制 | 下载 → **强校验 sha256** → 落盘为 `frps-<version>` → `-v` 复验 → 换软链(§8.6.1)。`< 0.70.0` 直接拒绝 |
537
+ | `frpsctl init [--no-input] [--force] [--bind-port P] [--dashboard-port P] [--allow-ports S]` | 交互式生成配置 | 强制随机口令、`tls.force=true`、引导设置 `allowPorts`;已存在则要求 `--force` 并先备份 |
518
538
  | `frpsctl verify [--file P]` | 双保险校验 | pydantic 语义校验 + `frps verify`;用临时副本,不动线上文件 |
519
- | `frpsctl start [--foreground]` | 启动 | verify → 加锁 → 派生进程 → **早退检测** → 写 state → 健康检查 |
520
- | `frpsctl stop [--force] [--timeout 10]` | 停止 | SIGTERM → 轮询确认退出 → 超时 SIGKILL;身份不符则拒绝 |
521
- | `frpsctl restart [--health-timeout 10] [--timeout 10]` | 重启 | stop → start → 健康检查。**没有 `--no-rollback`**:重启不读配置,也就没有"新旧版本"可比,自动回滚只属于配置变更路径(`config set` / `edit` / `rollback`) |
522
- | `frpsctl status [--watch]` | 状态聚合 | `owner / 状态 / pid / 版本 / 运行时长 / 客户端 / 各类型代理 / 今日流量 / 健康` |
523
- | `frpsctl config get <key>` | 读单键 | 点分路径,如 `transport.tls.force` |
524
- | `frpsctl config set <key> <value> [--no-restart]` | 写单键 | 走 §9 事务闭环 |
525
- | `frpsctl config edit` | `$EDITOR` 编辑 | 保存后走同一闭环 |
526
- | `frpsctl config diff` | 当前 vs 上一份快照 | unified diff |
527
- | `frpsctl config rollback [N]` | 回滚到 N 份之前 | 同样走闭环 |
528
- | `frpsctl log [-f] [-n 100]` | 看日志 | tail `log.to`;缺失时回退到 startup 日志 |
529
- | `frpsctl service install\|uninstall\|status` | systemd 集成 | 渲染 unit + `daemon-reload` + `enable`(需 root,明确提示) |
530
- | `frpsctl doctor [--json]` | 体检 | §8.7 检查项,按 severity 输出 |
531
- | `frpsctl prune` | 清理离线代理记录 | `DELETE /api/proxies?status=offline`;frp 不支持强制下线在线代理 |
539
+ | `frpsctl start [--foreground] [--health-timeout S]` | 启动 | verify → 加锁 → 派生进程 → **早退检测** → 写 state → 健康检查 |
540
+ | `frpsctl stop [--force] [--timeout S]` | 停止 | SIGTERM → 轮询确认退出 → 超时 SIGKILL;身份不符则拒绝 |
541
+ | `frpsctl restart [--health-timeout S] [--timeout S]` | 重启 | stop → start → 健康检查。**没有 `--no-rollback`**:重启不读配置,也就没有"新旧版本"可比,自动回滚只属于配置变更路径 |
542
+ | `frpsctl status [--watch] [--interval S]` | 状态聚合 | `owner / 状态 / pid / 版本 / 运行时长 / listen / dashboard / 健康 / 客户端 / 各类型代理 / 今日流量`;`--watch --json` 为 NDJSON |
543
+ | `frpsctl log [-f] [-n N]` | 看日志 | 纯 Python tail `log.to`;缺失时回退到 startup 日志(轮转后自动重开) |
544
+ | `frpsctl config get <key> [--reveal]` | 读单键 | 点分路径;敏感值默认打码(`--reveal` 显式取明文) |
545
+ | `frpsctl config set <key> <value> [--no-restart] [--dry-run] [--stdin] [--prompt]` | 写单键 | 走 §9 事务闭环;`--dry-run` 只校验并展示 diff;`--stdin`/`--prompt` 让敏感值不进 argv;`--health-timeout S` |
546
+ | `frpsctl config unset <key> [--no-restart] [--dry-run]` | 删键回落默认 | 与 set 同一闭环;键不存在报配置错误(3) |
547
+ | `frpsctl config list [--prefix P] [--tree]` | 列出全部键 | 值自动打码;`--tree` 按表分组缩进 |
548
+ | `frpsctl config edit [--yes]` | `$EDITOR` 编辑 | 保存后走同一闭环(锁内 CAS:编辑期间被并发修改则拒绝草稿) |
549
+ | `frpsctl config diff [--steps N]` | 当前 vs 第 N 新快照 | unified diff(打码) |
550
+ | `frpsctl config rollback [N]` | 回滚到 N 份之前 | 同样走闭环;`N ≥ 1` |
551
+ | `frpsctl service install\|uninstall\|status` | frps 的 systemd 集成 | 渲染 `frps@.service` + `daemon-reload` + `enable`(需 root;安装前四项部署体检) |
552
+ | `frpsctl service logs [-f] [-n N]` | journald 集成 | unit 级日志(启动失败 / OOM / 权限拒绝),与 `log` 互补 |
553
+ | `frpsctl doctor [--json]` | 体检 | §8.7 检查项(含 Web 口令文件权限),按 severity 输出;有 ERROR 时退出码 1 |
554
+ | `frpsctl clients [--json]` | 在线客户端列表 | v2 `/api/v2/clients`,**自动翻页取全量** |
555
+ | `frpsctl proxies [--type T] [--json]` | 代理列表 | v2 `/api/v2/proxies`(嵌套 `spec/status` 形状),自动翻页 |
556
+ | `frpsctl traffic [name] [--json]` | 近 7 天流量历史 | 无参 = 全部代理逐日汇总;单代理失败记空不拖垮整体(离线 = 404 无数据) |
557
+ | `frpsctl instances [--health] [--json]` | 多实例一行式概览 | 默认只读本地状态;`--health` 额外做三层探测 |
558
+ | `frpsctl prune` | 清理离线代理记录 | `DELETE /api/proxies?status=offline`;**不存在**强制下线在线代理的 API(§18.6) |
559
+ | `frpsctl plugin init [--force]` | 生成策略模板 | fail-closed 默认;0600 原子写 |
560
+ | `frpsctl plugin check [--bind B]` | 离线校验策略 | 载入 + 回环校验 + 典型裁决试算 |
561
+ | `frpsctl plugin serve [--bind B] [--path P]` | 插件服务(前台) | 只允许绑回环;SIGTERM 优雅退出并刷审计;生产用 `plugin service install` 守护 |
562
+ | `frpsctl plugin user set\|remove\|list` | 策略用户的结构化编辑 | 只改显式给出的字段;写入前同 `plugin check` 判据复验;未知键保留 |
563
+ | `frpsctl plugin service install\|uninstall\|status` | 插件的 systemd 集成 | `Restart=always`(登录单点)+ 四项体检 |
564
+ | `frpsctl web serve [--bind B] [--password P] [--password-file F] [--allow-non-loopback] [--trusted-proxy]` | Web 管理台(前台,§18) | 默认只绑回环、不允许空口令;`--trusted-proxy` 支持反代后的按来源限速 |
565
+ | `frpsctl web service install\|uninstall\|status` | 管理台的 systemd 集成 | 0600 口令文件(明文不进 unit)+ 四项体检;`--trusted-proxy` 可写入 unit |
566
+ | `frpsctl web password show [--json]` | 读回管理台口令 | 显式索取明文;权限过宽时向 stderr 告警 |
532
567
 
533
568
  ### 7.3 退出码
534
569
 
@@ -2557,6 +2592,83 @@ JS 经 `node --check` 语法验证通过。
2557
2592
 
2558
2593
  ---
2559
2594
 
2595
+ ## 19. 第六轮全量迭代(v0.2.3:边界根治与六项新能力)
2596
+
2597
+ 本轮以"全量通读 + 疑点实测"的方式复核全部源码、测试与两篇文档,聚焦便捷性、
2598
+ 易用性、稳定性、可靠性、功能性五轴。统计:修复 **5 条**真实缺陷(含 1 条可
2599
+ 造成不可逆动作),新增 **6 项能力**、**75 条测试**(435 → 510),覆盖率
2600
+ 82% → **85%**。
2601
+
2602
+ ### 19.1 修复(根因清单)
2603
+
2604
+ | # | 问题 | 根因 | 根治方式 |
2605
+ |---|------|------|---------|
2606
+ | 1 | Web `stop` 负 timeout **立即 SIGKILL** | `_float` 宽松解析——CLI 侧 v0.2.0 已修,Web 是同一缺陷的镜像 | `_bounded_float` / `_bounded_int`:越界与非数值一律 400;回归断言"进程必须仍存活" |
2607
+ | 2 | systemd + 损坏 state.json 误报 STOPPED | 损坏分支在所有权探测**之前**直接返回 | 状态判定以 owner 为先(systemd 下 state.json 本不参与判定);并收口"检查之后才损坏"的 TOCTOU |
2608
+ | 3 | 失败来源表无上限 + 反代误伤 | 来源固定为对端地址(反代下全部同源);表只按窗口清理 | 1024 上限(驱逐最早失败者)+ `--trusted-proxy`(只信 XFF 最后一跳,默认关) |
2609
+ | 4 | `rollback -1` 静默归一成"回滚 1 步" | `max(0, steps-1)` 吞掉非法输入 | Click `min=1` → 用法错误(2) |
2610
+ | 5 | L2 失败详情被 L3 覆盖 | `detail` 单变量被后写的层覆盖 | 各层详情合并;L2 失败也渲染原因(控制面是恢复顺序上的第一层) |
2611
+
2612
+ ### 19.2 新增能力
2613
+
2614
+ | 能力 | 动机 | 落点 |
2615
+ |------|------|------|
2616
+ | `config unset <key>` | 配置闭环缺"删除"(恢复默认只能手编) | `plan_unset` + `apply_unset`(同一锁与闭环) |
2617
+ | `config set --dry-run` | CLI 无预览;Web 已有两段式 | `_dry_run_check`:跑全量校验(含 verify 与危险组合),零落盘零快照 |
2618
+ | `--stdin` / `--prompt` | 敏感值进 argv = shell 历史 + `/proc/<pid>/cmdline` 泄露 | `_resolve_value_input`(三源互斥,空值拒绝) |
2619
+ | `traffic [name]` | `proxy_traffic` 早已实现却没有 CLI 出口 | 逐日汇总 / 单代理明细;404=无数据(C10 语义精确化到客户端层) |
2620
+ | `plugin user set/remove/list` | 策略是安全单点,手写 JSON 易错 | raw dict 编辑 + 同 `plugin check` 判据复验 + 未知键保留 |
2621
+ | `web password show` + 历史回滚 UI | 口令遗忘无出路;Web 只能回滚一步 | `GET /api/config/history`(只读 meta.json)+ 前端卡片按 steps 回滚 |
2622
+
2623
+ ### 19.3 工程
2624
+
2625
+ - `tests/test_docs.py`:README 命令 / 环境变量 / 退出码 ↔ 代码 ↔ 设计文档
2626
+ §7.2 的**双向一致性守卫**——把发布前的一次性核对变成 CI 常态;
2627
+ - `core/` 反向依赖 `cli/` 的 8 处全部清除(`core/diagnostics.py` 承接诊断开关,
2628
+ 打码统一 `config.mask_value`);
2629
+ - 死代码清理(零调用即删)、CI 加入 Python 3.14、Web/插件监听 backlog 64。
2630
+
2631
+ ### 19.4 发布前回归 review
2632
+
2633
+ 方法:对本轮全部 diff 做逐行审查 + 对抗性实测(unset 的数组表 / 子表 / 内联表 /
2634
+ 唯一键边界、traffic 以 mock dashboard 端到端对照 README 示例、布尔与空白参数、
2635
+ 并发锁不变量),并复查测试质量、workflow YAML 与工作区残留。发现并修复
2636
+ **5 条**(2 条为真实缺陷),新增 **5 条**回归用例(505 → 510):
2637
+
2638
+ | # | 问题 | 性质 | 修复 |
2639
+ |---|------|------|------|
2640
+ | 1 | `{"timeout": true}` 被当作 1.0 秒静默接受 | 新代码内部不一致(`_bounded_int` 已排除 bool) | `_bounded_float` 显式排除 bool |
2641
+ | 2 | `plugin user set/remove` 的读-改-写无锁 | **真实缺陷**(两个并发调用互相覆盖,与第四轮 `config set` 并发丢失同形态) | 读写包进实例锁 + "锁内保存"不变量测试 |
2642
+ | 3 | `parse_scalar` 对裸文本返回未 strip 的原文 | 既有 quirk(TOML 裸值不允许首尾空白) | 返回 strip 后文本;`'" x "'` 引号形式仍可保留空格 |
2643
+ | 4 | 纯空白的位置参数绕过空值检查 | **真实缺陷**(配合 #3 会把空串写进配置) | 空检查改 `text.strip()` |
2644
+ | 5 | `--prompt` 无输入时抛裸 `EOFError` | 异常契约(映射成"未分类错误(1)") | 收口为用法错误(2) 并提示改用 `--stdin` |
2645
+
2646
+ 其余为整理:死变量 `ui._MASK`、函数内 `import math`、`config.__all__` 补齐
2647
+ (`plan_unset` / `parse_scalar`)、`status()` 的三元表达式改显式 if。对抗性
2648
+ 实测全部通过:unset 五类边界、traffic 端到端(人读输出与 README 示例逐字符
2649
+ 一致)、sdist 干净安装、两份 workflow 的 YAML 解析、diff 调试残留扫描。
2650
+
2651
+ ### 19.5 发布前最终验收(v0.2.3)
2652
+
2653
+ 方法同 §17.12 / §18.9:**验证发布产物本身 + 复刻 CI 全套**。结论:**可发布**。
2654
+
2655
+ | 项 | 结果 |
2656
+ |----|------|
2657
+ | wheel / sdist(`frpsctl-0.2.3`) | 干净安装通过;**47 条命令路径 help 失败 0**(全命令树遍历) |
2658
+ | wheel 自检(release.yml 的 required 集合,本轮强化) | `py.typed` / `diagnostics.py` / web 前端资源齐全(42 条目) |
2659
+ | tag 一致性 / 前端资源 | `v0.2.3` == `__version__`;静态页可读(26,987 字符) |
2660
+ | CI 复刻:ruff / 覆盖率门禁(80%) | 全绿 / 84.88% |
2661
+ | 全量(含契约 0.71 + 真 frpc 插件契约) | **510 passed / 0 skipped** |
2662
+ | CLI 冒烟(12 步,含 dry-run / unset / traffic) | 通过 |
2663
+ | Web 冒烟(含 history 的**真实快照**:set / unset 两条记录与 steps 语义) | 通过 |
2664
+ | 无二进制降级 / 0.70.0 下界矩阵 | 3 passed / 23 skipped / 26 passed |
2665
+
2666
+ 验收中顺手强化两处守卫:CI 的 Web 冒烟断言升级为"history 必须能读到真实快照"
2667
+ (空列表不再能骗过它);release.yml 的产物自检集合加入 `core/diagnostics.py`
2668
+ 与 `web/static/index.html`(丢前端 = 页面 500 而 CLI 测试全绿)。
2669
+
2670
+ ---
2671
+
2560
2672
  ## 附录 A:frps 配置键速查表
2561
2673
 
2562
2674
  > 全部取自 `v0.71.0` 源码;「默认值」栏是 `Complete()` 之后的**生效值**。合法取值来自校验器。
@@ -11,4 +11,4 @@ __all__ = ["__version__"]
11
11
 
12
12
  #: 版本号唯一来源:pyproject.toml 通过 hatchling 从这里读取(tool.hatch.version)。
13
13
  #: 发布新版本时只改这里,然后打 `v<version>` tag——release workflow 会校验两者一致。
14
- __version__ = "0.2.2"
14
+ __version__ = "0.2.3"