frpsctl 0.2.2__tar.gz → 0.2.4__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 (61) hide show
  1. {frpsctl-0.2.2 → frpsctl-0.2.4}/.github/workflows/ci.yml +25 -2
  2. {frpsctl-0.2.2 → frpsctl-0.2.4}/.github/workflows/release.yml +8 -1
  3. {frpsctl-0.2.2 → frpsctl-0.2.4}/CHANGELOG.md +122 -0
  4. {frpsctl-0.2.2 → frpsctl-0.2.4}/PKG-INFO +73 -10
  5. {frpsctl-0.2.2 → frpsctl-0.2.4}/README.md +72 -9
  6. {frpsctl-0.2.2 → frpsctl-0.2.4}/frpsctl-/350/256/276/350/256/241/346/226/271/346/241/210.md +240 -32
  7. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/__init__.py +1 -1
  8. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/cli/__init__.py +462 -31
  9. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/cli/ui.py +14 -21
  10. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/admin.py +15 -2
  11. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/config.py +119 -16
  12. frpsctl-0.2.4/src/frpsctl/core/diagnostics.py +38 -0
  13. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/doctor.py +29 -0
  14. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/health.py +8 -2
  15. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/lifecycle.py +54 -24
  16. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/lock.py +1 -7
  17. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/release.py +1 -2
  18. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/systemd.py +11 -3
  19. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/transaction.py +184 -10
  20. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/version.py +1 -4
  21. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/plugin/server.py +7 -25
  22. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/web/api.py +155 -22
  23. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/web/auth.py +37 -0
  24. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/web/server.py +40 -2
  25. frpsctl-0.2.4/src/frpsctl/web/static/index.html +984 -0
  26. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/test_cli.py +519 -0
  27. frpsctl-0.2.4/tests/test_docs.py +174 -0
  28. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/test_facts.py +12 -5
  29. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/test_integration.py +179 -1
  30. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/test_units.py +322 -2
  31. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/test_web.py +354 -2
  32. frpsctl-0.2.4/tests/test_web_frontend.py +137 -0
  33. frpsctl-0.2.2/src/frpsctl/web/static/index.html +0 -631
  34. {frpsctl-0.2.2 → frpsctl-0.2.4}/.gitignore +0 -0
  35. {frpsctl-0.2.2 → frpsctl-0.2.4}/LICENSE +0 -0
  36. {frpsctl-0.2.2 → frpsctl-0.2.4}/NOTICE +0 -0
  37. {frpsctl-0.2.2 → frpsctl-0.2.4}/install.sh +0 -0
  38. {frpsctl-0.2.2 → frpsctl-0.2.4}/pyproject.toml +0 -0
  39. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/__main__.py +0 -0
  40. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/cli/context.py +0 -0
  41. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/__init__.py +0 -0
  42. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/healthcheck.py +0 -0
  43. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/instance.py +0 -0
  44. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/logs.py +0 -0
  45. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/platform.py +0 -0
  46. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/core/schema.py +0 -0
  47. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/errors.py +0 -0
  48. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/plugin/__init__.py +0 -0
  49. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/plugin/audit.py +0 -0
  50. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/plugin/engine.py +0 -0
  51. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/plugin/policy.py +0 -0
  52. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/plugin/quota.py +0 -0
  53. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/plugin/types.py +0 -0
  54. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/py.typed +0 -0
  55. {frpsctl-0.2.2 → frpsctl-0.2.4}/src/frpsctl/web/__init__.py +0 -0
  56. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/__init__.py +0 -0
  57. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/conftest.py +0 -0
  58. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/fake_frps.py +0 -0
  59. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/faults.py +0 -0
  60. {frpsctl-0.2.2 → frpsctl-0.2.4}/tests/test_faults.py +0 -0
  61. {frpsctl-0.2.2 → frpsctl-0.2.4}/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
@@ -112,14 +112,37 @@ jobs:
112
112
  with urllib.request.urlopen(req, timeout=10) as resp:
113
113
  return json.load(resp)
114
114
 
115
+ def post(path, body):
116
+ req = urllib.request.Request(
117
+ base + path, method="POST",
118
+ data=json.dumps(body).encode(),
119
+ headers={"Content-Type": "application/json", "Cookie": cookie, "X-CSRF-Token": csrf},
120
+ )
121
+ with urllib.request.urlopen(req, timeout=10) as resp:
122
+ return json.load(resp)
123
+
115
124
  status = call("/api/status")
116
125
  assert status["state"] == "STOPPED", status
117
126
  assert call("/api/session")["csrf"] == csrf
118
127
  config = call("/api/config")
119
128
  assert any(item["key"] == "bindPort" for item in config["entries"]), config
129
+ history = call("/api/config/history")
130
+ assert isinstance(history["entries"], list), history
131
+ # 端到端冒烟里的 config set 会留下快照:history 必须真的读得到
132
+ # (只断言"是列表"的话,空列表也能骗过这层守卫)。
133
+ assert any(str(e.get("action", "")).startswith("set ") for e in history["entries"]), history
134
+ # 回滚前的"看差异":先经 API 改一个键(实例未运行 → 只写不重启),
135
+ # 再断言差里真的出现它。
136
+ preview = post("/api/config/preview", {"changes": [["maxPortsPerClient", "35"]]})
137
+ applied = post("/api/config/apply", {"preview_id": preview["preview_id"]})
138
+ assert applied["applied"] is True, applied
139
+ diff = call("/api/config/history/1/diff")
140
+ assert "maxPortsPerClient" in diff["diff"], diff
141
+ favicon = urllib.request.urlopen(base + "/favicon.ico", timeout=10)
142
+ assert favicon.status == 204
120
143
  html = urllib.request.urlopen(base + "/", timeout=10).read().decode("utf-8")
121
144
  assert "frpsctl 管理台" in html
122
- print("web 冒烟通过:login / session / status / config / 静态页")
145
+ print("web 冒烟通过:login / session / status / config / history / diff / favicon / 静态页")
123
146
  EOF
124
147
  kill "$WEB_PID"
125
148
  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,128 @@
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.4] - 2026-09-17
7
+
8
+ Web 管理台的体验迭代:把后端已有的能力全部交付到界面,并给单文件前端补上
9
+ 自动化守卫。修复 5 条真实缺陷(含一条"白屏而 CI 全绿"的测试盲区)。
10
+
11
+ ### 修复
12
+
13
+ - **前端在 CI 里几乎零防护**:所有测试与冒烟都只走 HTTP、不执行 JS——一处
14
+ 语法错误会让整站白屏而 CI 全绿。新增 `tests/test_web_frontend.py`:逐
15
+ `<script>` 块 `node --check`(无 node 时 skip)+ 静态纪律守卫(禁 innerHTML
16
+ 家族、禁外部资源引用、CSS 变量双向对齐、亮色主题必须覆盖全部颜色变量)。
17
+ - **配置页切换视图会丢失未保存的修改**:每次切回配置页都重新拉取并清空草稿。
18
+ 现在已加载过就不重载,"重新加载(丢弃修改)"是显式动作(有未保存修改时
19
+ 二次确认)。
20
+ - **日志面板永远自动滚底**:`logScroll` 恒为 true,用户向上翻日志会被 5 秒
21
+ 刷新拽回底部。现在按真实滚动位置维护"跟随/暂停"(显示在标题栏,回到底部
22
+ 自动恢复)。
23
+ - **清理离线记录后不刷新列表**:用户看到条目还在,以为操作没生效。
24
+ - **认证会话表无上限**:持有口令的调用方反复登录可持续推高内存——与失败
25
+ 来源表同类的"输入驱动的表必须有界",现在上限 32(驱逐最早到期者)。
26
+ - 发布前回归 review 追加修复:**`plan_change_many` 把字符串当列表逐字符迭代**
27
+ (`unsets="ab"` 会静默删掉 `a` 与 `b` 两个键;对抗性实测复现,现为用法错误,
28
+ 输入归一化单点化在 core 并覆盖 `changes`/`unsets` 两种形状);`snapshot_diff`
29
+ 与 `rollback_to` 的 `steps < 1` 曾被 `max(0, steps-1)` 静默归一成"一步"
30
+ (core 入口补防御,与 CLI 的 min=1 同一条纪律);代理柱状图的柱宽 clamp
31
+ (防御"负宽度静默不渲染")。
32
+
33
+ ### 新增
34
+
35
+ - **回滚前"查看差异"**:新接口 `GET /api/config/history/{steps}/diff`(打码,
36
+ 与 CLI `config diff --steps` 共用 core 的 `snapshot_diff`)——回滚从盲操作
37
+ 变为可预览;历史表每行都有"查看差异"按钮。
38
+ - **Web 配置删除键**:配置表单每行"删除"按钮 → 与修改合并成一次事务
39
+ (`plan_change_many` + `apply_sets(unsets=…)`:一份快照、一次重启);
40
+ "删除不存在的键"照旧是配置错误。
41
+ - **Web 配置新增键**:此前只能改已有键,新增必须回到 CLI;现在表单底部可
42
+ 添加任意键(预览/校验/危险组合拦截与 CLI 同一套)。
43
+ - **单代理流量曲线**:点击代理行展开该代理的 7 天曲线(数据源与 CLI
44
+ `traffic <name>` 相同)。
45
+ - **操作进行中状态**:启动/重启/停止/回滚/应用期间按钮禁用、状态徽章显示
46
+ "操作中…"(启动最长等 10 秒,此前完全无反馈);轮询与手动刷新不会叠加。
47
+ - **状态面板补齐**:state.json 损坏红色横幅(含处置指引)、0.70.x 版本告警、
48
+ L3 插件告警(`plugin_warning` 此前没有下发)、systemd unit 行、流量超 50
49
+ 个代理时"已截断"提示(CLI 有、Web 曾静默)。
50
+ - **双主题**:`prefers-color-scheme` 自动 + 手动切换(本地持久化);图表
51
+ 颜色改由 CSS 变量控制,切换即时生效。
52
+ - 界面打磨:diff 语法高亮(+绿/−红)、图表 hover 数值与合计、实时速率文本、
53
+ 表格数字右对齐与状态 tag、错误 toast 常驻(手动关闭)、日志"跟随/暂停"、
54
+ 内嵌 favicon(`/favicon.ico` 收尾返回 204)、窄屏"菜单"折叠。
55
+
56
+ ### 工程
57
+
58
+ - `snapshot_diff` 下沉 core(CLI `config diff` 与 Web 差异接口的唯一实现);
59
+ `plan_change_many` 统一混合变更;快照动作文案 `set many:` → `edit many:`。
60
+ - 设计文档 §18.2 的 API 表补上 `/api/session`、`/api/config/history`、
61
+ `/api/config/history/{steps}/diff`(长期 drift),并把"API 表 ↔ 路由"
62
+ 双向核对加入 `tests/test_docs.py`。
63
+ - 新增 35 条测试(510 → 545;非契约 515),覆盖率 85%;前端单文件 650 → 984 行。
64
+ 其中前端守卫含 **JS 对 id 的引用与 HTML 定义的双向核对**——`node --check`
65
+ 抓不到的"id 拼错 = 白屏"由此变成 CI 断言。
66
+
67
+ ## [0.2.3] - 2026-09-17
68
+
69
+ 全量通读后的根治性迭代:Web 参数边界、systemd 交叉状态、限速来源模型、分层
70
+ 倒置,以及六项新能力(config unset / traffic / plugin user / web password /
71
+ 配置历史回滚 / dry-run 与 stdin 输入通道)。
72
+
73
+ ### 修复
74
+
75
+ - **Web 动作接口的负参数会立即 SIGKILL**:`/api/actions/stop` 的 `timeout` 走
76
+ 宽松解析,`-1` 让等待循环一次都不执行、直接升级 SIGKILL——与 CLI 侧
77
+ `stop --timeout -1`(v0.2.0 修复)是同一缺陷的镜像。现在全部动作参数做范围
78
+ 校验(越界 / 非数值 → 400),并有一条测试断言"进程必须仍存活"。
79
+ - **`status` 在 systemd 托管 + 损坏 state.json 时误报 STOPPED**:损坏分支完全
80
+ 不探测 unit,而 systemd 下 state.json 本就不参与判定(direct → systemd 迁移
81
+ 的残留即可触发)。现在 systemd 实例照常报告;非 systemd 仍报"不可判定"。
82
+ 同时给 status 的"永不异常"承诺补上 TOCTOU 收口(损坏检查与读取之间的竞态)。
83
+ - **认证失败限速的来源模型**:来源表加上限(1024,防慢速内存放大);反代部署
84
+ 下所有请求同源、攻击者 5 次失败即可连带锁住管理员 → 新增
85
+ `web serve --trusted-proxy`(默认关;开启后按 `X-Forwarded-For` 最后一跳
86
+ 限速,`web service install` 亦可写入 unit)。
87
+ - **`config rollback -1` / `config diff --steps 0` 静默归一**:步数必须 ≥ 1
88
+ (用法错误 2),参数笔误不再变成另一个动作。
89
+ - 健康 detail 在 L2 与 L3 同时失败时只显示 L3 的信息 → 现在汇总两层,且
90
+ L2 失败时也会渲染失败原因(控制面是恢复顺序上的第一层)。
91
+ - 发布前回归 review 追加修复:Web 动作参数的**布尔值**(`{"timeout": true}`
92
+ 曾被当作 1.0 秒静默接受);`plugin user set/remove` 的读-改-写**没有锁**
93
+ (两个并发调用互相覆盖——与第四轮 `config set` 并发丢失同形态,现与 config
94
+ 写共用实例锁);**首尾空白**(`parse_scalar` 此前保留未 strip 的原文,现按
95
+ TOML 裸值语义去空白;纯空白的值一律拒绝);`--prompt` 在无输入时抛裸
96
+ `EOFError`(现为用法错误并提示改用 `--stdin`)。
97
+
98
+ ### 新增
99
+
100
+ - `frpsctl config unset <key>`:删键回落 frp 默认值,与 `config set` 同一事务
101
+ 闭环(校验 → 快照 → 重启 → 失败自动回滚);键不存在报配置错误(3)。
102
+ - `frpsctl config set --dry-run`:跑完全部真实校验(含 `frps verify` 与危险组合
103
+ 拦截)但零落盘、零快照——CLI 版的"预览";`config unset` 同样支持。
104
+ - `config set --stdin / --prompt`:敏感值不再必须走 argv(shell 历史与
105
+ `/proc/<pid>/cmdline`);三种值来源互斥。
106
+ - `frpsctl traffic [name]`:近 7 天流量历史(无参 = 全部代理逐日汇总;离线
107
+ 代理 404 = 无数据,单代理失败不拖垮整体)。
108
+ - `frpsctl plugin user set|remove|list`:策略的结构化编辑(写入前同
109
+ `plugin check` 判据复验、0600 原子写、未知键原样保留)。
110
+ - `frpsctl web password show`:读回 `web service install` 生成的口令(权限
111
+ 过宽时向 stderr 告警)。
112
+ - Web 管理台的"历史与回滚"卡片:列出快照(时间 / 动作 / 步数)并可回滚到
113
+ 任意一份;新接口 `GET /api/config/history` 只读 meta.json,不下发快照原文。
114
+
115
+ ### 工程
116
+
117
+ - **分层倒置根治**:`core/` 不再反向 import `cli/`(此前 8 处
118
+ `cli.ui.trace` / `mask_secret`)——诊断开关下沉为 `core/diagnostics.py`,
119
+ 打码统一走 `config.mask_value`。
120
+ - 死代码清理(零调用即删):`lock.held_locks`、`plugin.server.serve` /
121
+ `wait_ready`、`WebSettings.password`(构造后从未被读,容易误以为生效)。
122
+ - 新增 `tests/test_docs.py`:README 命令 / 环境变量 / 退出码与设计文档命令面的
123
+ **双向一致性守卫**——文档 drift 从此在 CI 里直接暴露;同步刷新设计文档
124
+ §1.3 / §5 / §7.2。
125
+ - CI 矩阵加入 Python 3.14;Web 与插件的监听 backlog 设为 64(过载行为可预期)。
126
+ - 新增 75 条测试(435 → 510;非契约 480),覆盖率 82% → **85%**。
127
+
6
128
  ## [0.2.2] - 2026-09-17
7
129
 
8
130
  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.4
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
@@ -755,12 +792,15 @@ Web 管理台:http://127.0.0.1:8787/
755
792
  Ctrl-C 停止。
756
793
  ```
757
794
 
758
- 打开浏览器即可——单文件前端(暗色主题),零外部资源(不加载任何 CDN):
795
+ 打开浏览器即可——单文件前端(明暗双主题,跟随系统并可手动切换),零外部资源(不加载任何 CDN):
759
796
 
760
797
  | 页面 | 内容 |
761
798
  |------|------|
762
- | 仪表盘 | 实例状态 / 三层健康 / 客户端与代理列表 / 近 7 天流量柱状图 / 会话内实时流量曲线 / 日志(5 秒自动刷新) |
763
- | 配置 | 逐字段表单(敏感值不回显,留空表示不改)→ 预览打码 diff → 确认应用(一次事务、一次重启)→ 一键回滚 |
799
+ | 仪表盘 | 实例状态 / 三层健康 / 客户端与代理列表 / 近 7 天流量柱状图(悬停看数值,点代理行展开单代理曲线)/ 会话内实时流量曲线 / 日志(5 秒自动刷新,向上翻自动暂停跟随)。操作进行中有"操作中…"状态,清理离线记录后列表即时刷新 |
800
+ | 配置 | 逐字段表单(敏感值以打码形式提示"已设置",留空表示不改;可**删除**任意键回落默认值、也可**新增**键)→ 预览打码 diff(+绿/−红)→ 确认应用(一次事务、一次重启)→ 历史回滚(**先看差异再回滚**) |
801
+
802
+ 状态异常不会藏在角落:state.json 损坏、0.70.x 缺安全修复、插件不可达(客户端
803
+ 将无法登录)、流量超 50 个代理被截断——都会在页面顶部横幅或图表下方明确写出。
764
804
 
765
805
  **安全设计**(比 frp 自带 dashboard 更严——它正是本项目安全决策的来源):
766
806
 
@@ -782,6 +822,23 @@ sudo frpsctl web service uninstall
782
822
  > `web service install` 同样需要 `frpsctl` 位于系统路径(不能被 `ProtectHome`
783
823
  > 挡住)——与插件服务的部署要求一致。
784
824
 
825
+ 配置页里还有**历史与回滚**:列出最近 10 份快照(时间 / 动作 / 步数),每行可
826
+ 先"查看差异"再决定是否回滚到那一份(差异接口 `GET /api/config/history/{steps}/diff`
827
+ 与 `config diff --steps` 共用同一实现,同样打码;列表接口只读快照元数据,
828
+ **不下发配置原文**——快照是含 token 与口令的完整副本)。
829
+
830
+ 生成的口令随时可以取回(权限过宽时会告警):
831
+
832
+ ```bash
833
+ frpsctl web password show
834
+ ```
835
+
836
+ **在反向代理后运行**(`--allow-non-loopback` + Nginx 等)时加 `--trusted-proxy`:
837
+ 登录失败限速按 `X-Forwarded-For` 的**最后一跳**区分来源。默认关闭时该头完全
838
+ 不被读取;开启的前提是"前面确实有一层会重写该头的可信代理"——否则攻击者的
839
+ 失败会与管理员同源,5 次失败就能把管理员锁在冷却之外。
840
+ `web service install --trusted-proxy` 会把该标志写进 unit。
841
+
785
842
  ## 用 systemd 托管
786
843
 
787
844
  ```bash
@@ -1169,16 +1226,19 @@ FRPSCTL_TRACEBACK=1 frpsctl status
1169
1226
 
1170
1227
  ```bash
1171
1228
  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%)
1229
+ .venv/bin/pytest # 全部 545 条(契约层缺二进制时自动 skip)
1230
+ .venv/bin/pytest -m "not contract" # 快速回归(515 条)
1231
+ .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 85%)
1175
1232
  .venv/bin/ruff check src/ tests/ # 静态分析
1176
1233
  ```
1177
1234
 
1178
- CI(Linux,Python 3.11/3.12/3.13)还包含:ruff、覆盖率门禁、真 frp 0.71.0 契约层、
1179
- **真 frp 0.70.0 下界契约矩阵**、无二进制降级路径、端到端冒烟。
1235
+ CI(Linux,Python 3.11/3.12/3.13/3.14)还包含:ruff、覆盖率门禁、真 frp 0.71.0
1236
+ 契约层、**真 frp 0.70.0 下界契约矩阵**、无二进制降级路径、端到端冒烟与
1237
+ Web 管理台冒烟(含配置差异接口)、**前端静态守卫**(逐 `<script>` 块
1238
+ `node --check` + 禁 innerHTML/外部资源)、**文档一致性守卫**(README/设计文档/
1239
+ API 表 vs 代码的交叉核对)。
1180
1240
 
1181
- ### 测试分六层
1241
+ ### 测试分七层
1182
1242
 
1183
1243
  | 层 | 文件 | 目标 |
1184
1244
  |----|------|------|
@@ -1188,6 +1248,7 @@ CI(Linux,Python 3.11/3.12/3.13)还包含:ruff、覆盖率门禁、真 fr
1188
1248
  | 契约 | `tests/test_facts.py` | **设计文档事实基线的自动化守卫**(需真 frps) |
1189
1249
  | 故障注入 | `tests/test_faults.py` | 注入系统调用失败,验证异常路径的五项不变量 |
1190
1250
  | 插件 | `tests/test_plugin.py` | 协议报文、裁决、审计、配额;含真 frpc 端到端契约 |
1251
+ | 前端与文档 | `tests/test_web_frontend.py`、`tests/test_docs.py` | 单文件前端的静态守卫(`node --check`、禁 innerHTML/外部资源、CSS 变量对齐)与 README / 设计文档 / API 表的双向一致性 |
1191
1252
 
1192
1253
  让契约层跑起来(需要真实二进制):
1193
1254
 
@@ -1226,6 +1287,8 @@ fail)——它们是"插件真的接得住 frp 调用"的唯一证明,因此
1226
1287
  | **`max_proxies` 计数需配 `admin_url`** | 不配时只在插件进程内计数(重启归零、多实例各算各的),`plugin check` 会告警 |
1227
1288
  | **frps 没有热重载** | 改配置必然重启,因此 `config set` 的设计目标就是"失败了要能退回去" |
1228
1289
  | **systemd 模式下 pid 文件不参与判定** | 所有权委托 systemctl;`install` 换版本后需 `systemctl restart` |
1290
+ | **Web 管理台不限制并发连接数** | 单机管理工具的取舍(请求线程随连接创建,监听 backlog 64);公网暴露请在前置反代上做限流 |
1291
+ | **`--trusted-proxy` 只信 X-Forwarded-For 的最后一跳** | 前提是前面确实有一层会重写该头的可信代理;直连部署不要开启 |
1229
1292
 
1230
1293
  ---
1231
1294
 
@@ -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
@@ -732,12 +769,15 @@ Web 管理台:http://127.0.0.1:8787/
732
769
  Ctrl-C 停止。
733
770
  ```
734
771
 
735
- 打开浏览器即可——单文件前端(暗色主题),零外部资源(不加载任何 CDN):
772
+ 打开浏览器即可——单文件前端(明暗双主题,跟随系统并可手动切换),零外部资源(不加载任何 CDN):
736
773
 
737
774
  | 页面 | 内容 |
738
775
  |------|------|
739
- | 仪表盘 | 实例状态 / 三层健康 / 客户端与代理列表 / 近 7 天流量柱状图 / 会话内实时流量曲线 / 日志(5 秒自动刷新) |
740
- | 配置 | 逐字段表单(敏感值不回显,留空表示不改)→ 预览打码 diff → 确认应用(一次事务、一次重启)→ 一键回滚 |
776
+ | 仪表盘 | 实例状态 / 三层健康 / 客户端与代理列表 / 近 7 天流量柱状图(悬停看数值,点代理行展开单代理曲线)/ 会话内实时流量曲线 / 日志(5 秒自动刷新,向上翻自动暂停跟随)。操作进行中有"操作中…"状态,清理离线记录后列表即时刷新 |
777
+ | 配置 | 逐字段表单(敏感值以打码形式提示"已设置",留空表示不改;可**删除**任意键回落默认值、也可**新增**键)→ 预览打码 diff(+绿/−红)→ 确认应用(一次事务、一次重启)→ 历史回滚(**先看差异再回滚**) |
778
+
779
+ 状态异常不会藏在角落:state.json 损坏、0.70.x 缺安全修复、插件不可达(客户端
780
+ 将无法登录)、流量超 50 个代理被截断——都会在页面顶部横幅或图表下方明确写出。
741
781
 
742
782
  **安全设计**(比 frp 自带 dashboard 更严——它正是本项目安全决策的来源):
743
783
 
@@ -759,6 +799,23 @@ sudo frpsctl web service uninstall
759
799
  > `web service install` 同样需要 `frpsctl` 位于系统路径(不能被 `ProtectHome`
760
800
  > 挡住)——与插件服务的部署要求一致。
761
801
 
802
+ 配置页里还有**历史与回滚**:列出最近 10 份快照(时间 / 动作 / 步数),每行可
803
+ 先"查看差异"再决定是否回滚到那一份(差异接口 `GET /api/config/history/{steps}/diff`
804
+ 与 `config diff --steps` 共用同一实现,同样打码;列表接口只读快照元数据,
805
+ **不下发配置原文**——快照是含 token 与口令的完整副本)。
806
+
807
+ 生成的口令随时可以取回(权限过宽时会告警):
808
+
809
+ ```bash
810
+ frpsctl web password show
811
+ ```
812
+
813
+ **在反向代理后运行**(`--allow-non-loopback` + Nginx 等)时加 `--trusted-proxy`:
814
+ 登录失败限速按 `X-Forwarded-For` 的**最后一跳**区分来源。默认关闭时该头完全
815
+ 不被读取;开启的前提是"前面确实有一层会重写该头的可信代理"——否则攻击者的
816
+ 失败会与管理员同源,5 次失败就能把管理员锁在冷却之外。
817
+ `web service install --trusted-proxy` 会把该标志写进 unit。
818
+
762
819
  ## 用 systemd 托管
763
820
 
764
821
  ```bash
@@ -1146,16 +1203,19 @@ FRPSCTL_TRACEBACK=1 frpsctl status
1146
1203
 
1147
1204
  ```bash
1148
1205
  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%)
1206
+ .venv/bin/pytest # 全部 545 条(契约层缺二进制时自动 skip)
1207
+ .venv/bin/pytest -m "not contract" # 快速回归(515 条)
1208
+ .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 85%)
1152
1209
  .venv/bin/ruff check src/ tests/ # 静态分析
1153
1210
  ```
1154
1211
 
1155
- CI(Linux,Python 3.11/3.12/3.13)还包含:ruff、覆盖率门禁、真 frp 0.71.0 契约层、
1156
- **真 frp 0.70.0 下界契约矩阵**、无二进制降级路径、端到端冒烟。
1212
+ CI(Linux,Python 3.11/3.12/3.13/3.14)还包含:ruff、覆盖率门禁、真 frp 0.71.0
1213
+ 契约层、**真 frp 0.70.0 下界契约矩阵**、无二进制降级路径、端到端冒烟与
1214
+ Web 管理台冒烟(含配置差异接口)、**前端静态守卫**(逐 `<script>` 块
1215
+ `node --check` + 禁 innerHTML/外部资源)、**文档一致性守卫**(README/设计文档/
1216
+ API 表 vs 代码的交叉核对)。
1157
1217
 
1158
- ### 测试分六层
1218
+ ### 测试分七层
1159
1219
 
1160
1220
  | 层 | 文件 | 目标 |
1161
1221
  |----|------|------|
@@ -1165,6 +1225,7 @@ CI(Linux,Python 3.11/3.12/3.13)还包含:ruff、覆盖率门禁、真 fr
1165
1225
  | 契约 | `tests/test_facts.py` | **设计文档事实基线的自动化守卫**(需真 frps) |
1166
1226
  | 故障注入 | `tests/test_faults.py` | 注入系统调用失败,验证异常路径的五项不变量 |
1167
1227
  | 插件 | `tests/test_plugin.py` | 协议报文、裁决、审计、配额;含真 frpc 端到端契约 |
1228
+ | 前端与文档 | `tests/test_web_frontend.py`、`tests/test_docs.py` | 单文件前端的静态守卫(`node --check`、禁 innerHTML/外部资源、CSS 变量对齐)与 README / 设计文档 / API 表的双向一致性 |
1168
1229
 
1169
1230
  让契约层跑起来(需要真实二进制):
1170
1231
 
@@ -1203,6 +1264,8 @@ fail)——它们是"插件真的接得住 frp 调用"的唯一证明,因此
1203
1264
  | **`max_proxies` 计数需配 `admin_url`** | 不配时只在插件进程内计数(重启归零、多实例各算各的),`plugin check` 会告警 |
1204
1265
  | **frps 没有热重载** | 改配置必然重启,因此 `config set` 的设计目标就是"失败了要能退回去" |
1205
1266
  | **systemd 模式下 pid 文件不参与判定** | 所有权委托 systemctl;`install` 换版本后需 `systemctl restart` |
1267
+ | **Web 管理台不限制并发连接数** | 单机管理工具的取舍(请求线程随连接创建,监听 backlog 64);公网暴露请在前置反代上做限流 |
1268
+ | **`--trusted-proxy` 只信 X-Forwarded-For 的最后一跳** | 前提是前面确实有一层会重写该头的可信代理;直连部署不要开启 |
1206
1269
 
1207
1270
  ---
1208
1271