frpsctl 0.2.0__tar.gz → 0.2.2__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.0 → frpsctl-0.2.2}/.github/workflows/ci.yml +39 -0
  2. frpsctl-0.2.2/CHANGELOG.md +160 -0
  3. {frpsctl-0.2.0 → frpsctl-0.2.2}/PKG-INFO +122 -15
  4. {frpsctl-0.2.0 → frpsctl-0.2.2}/README.md +121 -14
  5. {frpsctl-0.2.0 → frpsctl-0.2.2}/frpsctl-/350/256/276/350/256/241/346/226/271/346/241/210.md +416 -4
  6. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/__init__.py +1 -1
  7. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/cli/__init__.py +702 -127
  8. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/cli/ui.py +68 -10
  9. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/admin.py +129 -5
  10. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/config.py +152 -14
  11. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/doctor.py +54 -34
  12. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/healthcheck.py +36 -2
  13. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/instance.py +43 -9
  14. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/lifecycle.py +59 -11
  15. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/lock.py +4 -0
  16. frpsctl-0.2.2/src/frpsctl/core/logs.py +52 -0
  17. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/release.py +42 -17
  18. frpsctl-0.2.2/src/frpsctl/core/systemd.py +933 -0
  19. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/transaction.py +258 -79
  20. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/plugin/audit.py +5 -0
  21. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/plugin/engine.py +46 -4
  22. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/plugin/policy.py +78 -16
  23. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/plugin/server.py +24 -26
  24. frpsctl-0.2.2/src/frpsctl/web/__init__.py +35 -0
  25. frpsctl-0.2.2/src/frpsctl/web/api.py +452 -0
  26. frpsctl-0.2.2/src/frpsctl/web/auth.py +142 -0
  27. frpsctl-0.2.2/src/frpsctl/web/server.py +339 -0
  28. frpsctl-0.2.2/src/frpsctl/web/static/index.html +631 -0
  29. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/test_cli.py +382 -5
  30. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/test_facts.py +77 -0
  31. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/test_faults.py +7 -27
  32. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/test_integration.py +287 -37
  33. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/test_plugin.py +140 -0
  34. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/test_units.py +663 -11
  35. frpsctl-0.2.2/tests/test_web.py +547 -0
  36. frpsctl-0.2.0/CHANGELOG.md +0 -67
  37. frpsctl-0.2.0/src/frpsctl/core/systemd.py +0 -420
  38. {frpsctl-0.2.0 → frpsctl-0.2.2}/.github/workflows/release.yml +0 -0
  39. {frpsctl-0.2.0 → frpsctl-0.2.2}/.gitignore +0 -0
  40. {frpsctl-0.2.0 → frpsctl-0.2.2}/LICENSE +0 -0
  41. {frpsctl-0.2.0 → frpsctl-0.2.2}/NOTICE +0 -0
  42. {frpsctl-0.2.0 → frpsctl-0.2.2}/install.sh +0 -0
  43. {frpsctl-0.2.0 → frpsctl-0.2.2}/pyproject.toml +0 -0
  44. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/__main__.py +0 -0
  45. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/cli/context.py +0 -0
  46. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/__init__.py +0 -0
  47. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/health.py +0 -0
  48. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/platform.py +0 -0
  49. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/schema.py +0 -0
  50. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/core/version.py +0 -0
  51. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/errors.py +0 -0
  52. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/plugin/__init__.py +0 -0
  53. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/plugin/quota.py +0 -0
  54. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/plugin/types.py +0 -0
  55. {frpsctl-0.2.0 → frpsctl-0.2.2}/src/frpsctl/py.typed +0 -0
  56. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/__init__.py +0 -0
  57. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/conftest.py +0 -0
  58. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/fake_frps.py +0 -0
  59. {frpsctl-0.2.0 → frpsctl-0.2.2}/tests/faults.py +0 -0
@@ -85,6 +85,45 @@ jobs:
85
85
  .venv/bin/frpsctl stop
86
86
  .venv/bin/frpsctl status
87
87
 
88
+ - name: Web 管理台冒烟(后台起服务 + HTTP 全流程)
89
+ run: |
90
+ set -euo pipefail
91
+ export FRPSCTL_ROOT="$RUNNER_TEMP/smoke/instances"
92
+ export FRPSCTL_DATA_HOME="$RUNNER_TEMP/smoke/data"
93
+ WP=$(python3 -c "import socket;s=socket.socket();s.bind(('127.0.0.1',0));print(s.getsockname()[1])")
94
+ .venv/bin/frpsctl web serve --bind "127.0.0.1:$WP" --password smoke-pw &
95
+ WEB_PID=$!
96
+ sleep 2
97
+ python3 - "$WP" <<'EOF'
98
+ import json, sys, urllib.request
99
+
100
+ base = f"http://127.0.0.1:{int(sys.argv[1])}"
101
+ login = urllib.request.Request(
102
+ base + "/api/login", method="POST",
103
+ data=json.dumps({"password": "smoke-pw"}).encode(),
104
+ headers={"Content-Type": "application/json"},
105
+ )
106
+ with urllib.request.urlopen(login, timeout=10) as resp:
107
+ cookie = resp.headers["Set-Cookie"].split(";")[0]
108
+ csrf = json.load(resp)["csrf"]
109
+
110
+ def call(path):
111
+ req = urllib.request.Request(base + path, headers={"Cookie": cookie})
112
+ with urllib.request.urlopen(req, timeout=10) as resp:
113
+ return json.load(resp)
114
+
115
+ status = call("/api/status")
116
+ assert status["state"] == "STOPPED", status
117
+ assert call("/api/session")["csrf"] == csrf
118
+ config = call("/api/config")
119
+ assert any(item["key"] == "bindPort" for item in config["entries"]), config
120
+ html = urllib.request.urlopen(base + "/", timeout=10).read().decode("utf-8")
121
+ assert "frpsctl 管理台" in html
122
+ print("web 冒烟通过:login / session / status / config / 静态页")
123
+ EOF
124
+ kill "$WEB_PID"
125
+ wait "$WEB_PID" 2>/dev/null || true
126
+
88
127
  no-binary:
89
128
  name: 无二进制降级路径(Python 3.13)
90
129
  runs-on: ubuntu-latest
@@ -0,0 +1,160 @@
1
+ # 更新日志
2
+
3
+ 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
+ 版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。
5
+
6
+ ## [0.2.2] - 2026-09-17
7
+
8
+ Web 管理台(内置界面)与 `kick` 语义修正。
9
+
10
+ ### 新增
11
+
12
+ - `frpsctl web serve`:内置 Web 管理台——仪表盘(状态 / 三层健康 / 7 天流量
13
+ 柱状图 / 会话内实时曲线)、客户端与代理列表、日志面板、进程启停、配置编辑
14
+ (预览打码 diff → 应用 → 失败自动回滚 → 一键回滚)。单文件前端、零外部
15
+ 资源(CSP `default-src 'none'`),默认只绑回环。
16
+ - `frpsctl web service install|uninstall|status`:Web 管理台的 systemd 集成
17
+ (生成 0600 口令文件并移交服务用户;口令明文不进 unit)。
18
+ - `frpsctl prune`:清理 dashboard 统计里的离线代理记录。
19
+ - 配置编辑的多键事务 `apply_sets` / `plan_set_many`:一次快照、一次重启,
20
+ 带 CAS(预览之后文件被改 → 拒绝而不是覆盖)。
21
+ - 契约层新增 **C9**(代理写 API 的真实语义)与 **C10**(traffic 端点的"无数据
22
+ = 404"语义,Web 容错的前提),0.70.0 上同样成立。
23
+
24
+ ### 修复
25
+
26
+ - **修正 `kick` 的语义错误**:frp 的 `DELETE /api/proxies` 实际是
27
+ `ClearOfflineProxies()`(只接受 `?status=offline`),**不存在**强制下线在线
28
+ 代理的 API。原 `kick` 按"按 name 下线"实现该端点,真机永远返回 400——一个
29
+ 从未工作过的功能(Web 端到端测试暴露)。已由 `prune` 取代。
30
+
31
+ ### 工程
32
+
33
+ - 新增 78 条测试(357 → 435),覆盖率 82%。含 Web 层的全路由认证扫描(16 条)、
34
+ 浏览器刷新恢复、TOML datetime 序列化、单代理故障注入等回归用例。
35
+ - core 下沉三处共用逻辑(消除重复实现):`parse_bind`(插件服务同时受益)、
36
+ `mask_value`(打码单点)、`core/logs.py`(`frpsctl log` 与 web 共用)。
37
+ - CI 新增 **Web 管理台冒烟**步骤(此前 CI 从不触碰 web):登录 → 会话恢复 →
38
+ 状态 → 配置 → 静态页,全链路验证。
39
+
40
+ ## [0.2.1] - 2026-09-17
41
+
42
+ 第四轮全量迭代:并发根治、策略 fail-open 修复与便捷性命令。
43
+
44
+ ### 新增
45
+
46
+ - `frpsctl instances [--health]`:多实例一行式概览(owner / 状态 / pid / 版本 / 健康)。
47
+ - `frpsctl clients` / `frpsctl proxies [--type]`:v2 Admin API 的客户端与代理列表
48
+ (自动翻页取全量,`--json` 可管道)。
49
+ - `frpsctl config list [--prefix] [--tree]`:列出全部配置键(值自动打码;
50
+ `--tree` 按表分组缩进展示)。
51
+ - `frpsctl plugin service install|uninstall|status`:插件服务的 systemd unit
52
+ (`Restart=always`,安装前四项体检:账户 / frpsctl 可达且不在家目录 / 策略文件 / 回环)。
53
+ - `frpsctl service logs [-f] [-n]`:journald 集成(unit 级日志)。
54
+ - `start`/`restart` 等待健康检查时输出**逐轮进度**(终端原地刷新,非终端按行
55
+ 限流;`--json` 不输出进度);`install` 在终端上恢复 curl 下载进度条
56
+ (管道中自动静默)。
57
+
58
+ ### 修复
59
+
60
+ - **并发(P0)**:`config set` 的候选生成(plan)移入实例锁内——两个并发变更不再
61
+ 互相静默覆盖(实测复现:后写入者覆盖前者,两边都报成功);`config edit` 引入
62
+ 锁内 CAS(编辑期间文件被并发修改 → 拒绝草稿并提示重新编辑);
63
+ `config rollback` / `config diff` 的读取同样入锁。
64
+ - **安全**:策略 JSON 的宽松转换曾是 fail-open——`"allow_unknown_user": "false"`
65
+ (字符串)被 `bool()` 判成 **True**,等于打开鉴权后门;`allow_random_port` 同理。
66
+ 现改为严格类型(类型错误报 3 并指出字段名),并修掉 `"audit": "false"` 的裸
67
+ `AttributeError`。
68
+ - `--health-timeout` 对 systemd 实例生效(此前硬编码 10s,参数被静默忽略)。
69
+ - `same_config_active` 扫描全部 active service:自建 unit 指向同一配置不再漏检
70
+ (direct 与 systemd 双起防护补洞)。
71
+ - `status` 单次探测所有权(systemd 下从 4 个子进程降为 2 个,消除两次探测间的 TOCTOU)。
72
+ - 下载在 curl 失败时回退 urllib(此前兜底代码不可达);curl 进度不再被捕获。
73
+ - `config edit` / `verify` / `config diff` 在配置缺失时给出配置错误(3),而不是
74
+ 未分类错误(1);`EDITOR="vim -u NONE"` 这类带参数写法可用(引号不配对归用法错误 2)。
75
+ - `reject_log_burst` / `reject_log_window` 真正生效:拒绝风暴期间审计限速,
76
+ 被抑制的条数在 `suppressed` 字段如实汇报(此前是零引用的死配置)。
77
+ - `write_state` 走统一原子写(fsync + 随机临时名 + 保留属主)。
78
+ - `parse_listen`:`bindPort = 0` 实测回落默认 7000,不再当作"无监听"。
79
+ - `do_GET /healthz` 的 BrokenPipe 不再把回溯打进插件 stderr。
80
+ - "是否需要 `--allow-unsafe`" 收敛为单点判定(此前四份实现)。
81
+ - 死代码清理(`restart` 的无效 try、`policy.validate` 的空分支)。
82
+ - `doctor` 重复探测去重(`frps -v` 与 `resolve_owner` 各一次)。
83
+ - 回归 review 追加修复:`doctor` 对低于门槛的二进制给出**正确诊断**
84
+ ("低于最低支持版本"而非"无法读取版本 / 可能不是官方 frps");systemd unit
85
+ 渲染统一绝对化——`--root ./instances` / `--binary ./bin/frps` 这类相对输入
86
+ 不再写出 `ExecStart=bin/frps` 的坏 unit。
87
+
88
+ - 回归 review 追加修复:展示层异常不再拖垮启动(`on_tick` 进度回调的异常被
89
+ 隔离——`start 2>&1 | head` 场景曾把刚派生的 frps 误杀);`note` / `progress` /
90
+ `warn` / `trace` 在 stderr 管道断开时静默(不再二次崩溃);tty 进度补清行尾码
91
+ (消除残影)。
92
+
93
+ ### 工程
94
+
95
+ - 新增 61 条回归用例(总计 357),覆盖率 83%。
96
+ - `init --bind-port` 加范围约束(`0` 归用法错误 2);`config set --json` 的 noop
97
+ 路径输出 JSON(此前打印人读文本)。
98
+
99
+ ## [0.2.0] - 2026-09-16
100
+
101
+ 第三轮全量迭代:安全缺口、部署守护、稳定性与发布链路。
102
+
103
+ ### 新增
104
+
105
+ - `start` / `restart` 在健康 gate(L1 ∧ L2)未通过时以**退出码 12** 收场并向
106
+ stderr 告警——此前完全静默(退出码 0,脚本会当成成功)。未过 gate 的进程
107
+ 仍被托管:`status` 可见、`stop` 可停。
108
+ - `install --mirror URL`(可重复)与 `FRPSCTL_MIRROR` 环境变量:下载镜像可配置,
109
+ 兑现"下载地址可被镜像替换"的承诺。
110
+ - `service install --user/--group`:systemd 服务账户可指定(默认 `frps`)。
111
+ - `status` 输出 `listen` 行(人读)与 `listen` 字段(JSON),对齐设计文档 §7.4。
112
+ - `status --watch --json` 改为单行 NDJSON,可被逐行消费(`jq -c` 等)。
113
+ - shell 补全(`--install-completion` / `--show-completion`)。
114
+ - PyPI 发布链路:版本号单一来源(hatchling 读 `__init__.py`)、release workflow、
115
+ CHANGELOG、`py.typed`。
116
+
117
+ ### 修复
118
+
119
+ - **安全**:`config diff` / `edit` / `set` / `rollback` 的内联表与**跨行结构**
120
+ (三引号多行字符串、多行内联表/数组)机密不再明文泄露;解析失败但形似含机密
121
+ 时保守打码;裸键名 `token`/`password`/`clientSecret` 同样命中。
122
+ - `config rollback` 每次只产生 **1** 份快照:此前 `rollback_to` 与 `apply_change`
123
+ 各存一份完全相同的内容,10 份历史实际只够 5 次操作,且快照创建在实例锁外。
124
+ - 全局选项前移支持 `--` 终止符:`config set k -- --json` 的字面值不再被搬走。
125
+ - `service install` 前置体检:服务账户不存在、二进制对服务用户不可达、
126
+ 日志目录不可写、**路径位于家目录(`ProtectHome=true`)**——这些过去都要等
127
+ `systemctl start` 才炸,且错误现场与安装动作相隔很远。
128
+ - unit 的 `ReadWritePaths` 增加**实例目录**:`ProtectSystem=strict` 下其余路径
129
+ 只读,而 frp 默认要往实例目录写 `./frps.log`。
130
+ - `config set` 重建配置时**保留原属主**:systemd 部署把配置移交给服务用户后,
131
+ 再次改配置不会再"夺回"属主导致 frps 读不到。
132
+ - `plugin serve` 的 SIGTERM handler 安装纳入 `try`:安装瞬间收到信号不再绕过
133
+ 审计刷盘(此前 `finally` 尚未生效,进程直接死亡)。
134
+ - 配额检查按用户锁重构:dashboard 查询不再持全局锁,一个用户的慢查询不会串行
135
+ 挂住所有用户的登录链路(frp 侧对插件 HTTP 客户端没有超时)。
136
+ - `install --with-frpc --only-download` 不再切换 frpc 软链,与 frps 语义统一。
137
+ - `log` 改为纯 Python tail:不依赖外部 `tail` 命令,支持日志轮转后自动重开新文件。
138
+ - `plugin init` 生成策略文件改用原子写(此前 `write_text` 存在半截文件与权限窗口)。
139
+ - 健康等待期间进程死亡时按启动失败收尾并清理 `state.json`(不再留下假 RUNNING)。
140
+ - 负数数值选项(`--interval` / `--timeout` / `-n` 等)归入用法错误(2):
141
+ 此前 `--timeout -1` 会跳过等待直接 SIGKILL——参数笔误造成不可逆动作。
142
+ - `status --watch` 重定向到文件时不再写入 ANSI 清屏码。
143
+
144
+ ### 工程
145
+
146
+ - CI 增加 `ruff check` 与 80% 覆盖率门禁;`[dev]` 补齐 `pytest-cov` / `ruff`。
147
+ - 新增 70 条回归用例(单元 / CLI / 集成 / 插件各层,总计 296 条)。
148
+
149
+ ## [0.1.0] - 2026-09-15
150
+
151
+ 首个版本。
152
+
153
+ - 生命周期:`install` / `init` / `start` / `stop` / `restart` / `status` / `log`,
154
+ 含实例锁、三重进程身份校验、启动早退检测、三层健康判定。
155
+ - 配置闭环:`config get|set|edit|diff|rollback`,tomlkit 无损补丁、官方
156
+ `frps verify` 权威校验、原子写、快照历史、失败自动回滚。
157
+ - 运维面:`doctor` 体检与安全 lint、`service install` systemd 集成、`kick`。
158
+ - 服务端插件:多用户鉴权、端口白名单、代理名/类型约束、`max_proxies` 配额、
159
+ 异步 JSONL 审计;fail-closed 与绑回环硬约束。
160
+ - 五层测试:单元 / 集成 / CLI / 契约(真二进制)/ 故障注入。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: frpsctl
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -41,13 +41,15 @@ Description-Content-Type: text/markdown
41
41
  - [五分钟上手](#五分钟上手)
42
42
  - [日常使用](#日常使用)
43
43
  - [看状态](#看状态)
44
+ - [看客户端与代理](#看客户端与代理)
44
45
  - [改配置](#改配置)
45
46
  - [看日志](#看日志)
46
47
  - [停下来](#停下来)
47
48
  - [体检](#体检)
48
- - [下线代理](#下线代理)
49
+ - [清理离线记录](#清理离线记录)
49
50
  - [多实例](#多实例)
50
51
  - [服务端插件(多用户鉴权 + 端口白名单)](#服务端插件多用户鉴权-端口白名单)
52
+ - [Web 管理台](#web-管理台)
51
53
  - [用 systemd 托管](#用-systemd-托管)
52
54
  - [升级 frps](#升级-frps)
53
55
  - [退出码(脚本化契约)](#退出码脚本化契约)
@@ -100,9 +102,8 @@ Description-Content-Type: text/markdown
100
102
  ## 安装
101
103
 
102
104
  ```bash
103
- # 1) 装 Python 侧
104
- pipx install frpsctl # 已发布到 PyPI;尚未发布时可先走下面的源码安装
105
- # 或:pip install frpsctl / pip install -U frpsctl(升级)
105
+ # 1) 装 Python 侧(已发布到 PyPI)
106
+ pipx install frpsctl # 或:pip install frpsctl;升级:pip install -U frpsctl
106
107
 
107
108
  # 2) 装 frps 二进制(从官方发布页下载,sha256 强校验)
108
109
  frpsctl install
@@ -178,7 +179,7 @@ $ ./install.sh
178
179
  注册全局命令
179
180
  ✓ 已注册:/home/u/.local/bin/frpsctl
180
181
  自检
181
- ✓ 命令可用:frpsctl 0.2.0
182
+ ✓ 命令可用:frpsctl 0.2.2
182
183
 
183
184
  frpsctl 安装完成
184
185
  ```
@@ -192,7 +193,7 @@ frpsctl 安装完成
192
193
  | `--no-verify` | 跳过安装后自检 |
193
194
 
194
195
  **反复运行即为升级**(会重新装依赖并重写命令)。源码用 `-e` 方式安装,因此改完
195
- 源码无需重装,命令立即生效。发布到 PyPI 后,用 pipx / pip 安装的版本可分别用
196
+ 源码无需重装,命令立即生效。用 pipx / pip 安装的版本可分别用
196
197
  `pipx upgrade frpsctl` / `pip install -U frpsctl` 升级。
197
198
 
198
199
  建议开启 shell 补全(`frpsctl --install-completion`,支持 bash/zsh/fish)。
@@ -235,7 +236,7 @@ dashboard 端口(0 = 不启用,将失去状态聚合能力) [7500]:
235
236
  ```
236
237
 
237
238
  **把这三项记下来**——`auth.token` 要填到每个 frpc 客户端,dashboard 口令用于
238
- `kick` 等操作。
239
+ `prune` 与 Web 管理台等操作。
239
240
 
240
241
  ```console
241
242
  $ frpsctl verify
@@ -393,6 +394,26 @@ $ frpsctl status --json
393
394
  | `systemd` | 由 systemd 托管,`start`/`stop`/`restart` 委托 systemctl |
394
395
  | `none` | 没有进程在跑,也没有 unit |
395
396
 
397
+ ### 看客户端与代理
398
+
399
+ `status` 给的是总数;"谁在线、哪个代理在跑、各跑了多少流量"用这两条
400
+ (走 v2 Admin API,自动翻页取全量,`--json` 可管道给 `jq`):
401
+
402
+ ```console
403
+ $ frpsctl clients
404
+ name user hostname online ip version
405
+ alice.f2a3e2edeef4a920 alice DESKTOP-S8A3AVK True 127.0.0.1 0.71.0
406
+
407
+ $ frpsctl proxies
408
+ name user type port phase conns traffic(in/out)
409
+ alice.alice-ssh alice tcp 6000 online 0 0 B / 0 B
410
+
411
+ $ frpsctl proxies --type http # 只看某类型
412
+ ```
413
+
414
+ `prune` 清理离线代理记录;在线代理无法从服务端强制下线(frp 没有该 API,
415
+ 见"清理离线记录"一节)。
416
+
396
417
  ### 改配置
397
418
 
398
419
  frps 没有热重载,所以"改配置"和"重启"是同一件事。`config set` 把它实现为一次
@@ -429,6 +450,9 @@ $ frpsctl config set maxPortsPerClient 30
429
450
 
430
451
  ```bash
431
452
  frpsctl config set bindPort 8000 --no-restart # 只写不重启(输出会提示"尚未生效")
453
+ frpsctl config list # 列出全部键(值自动打码)
454
+ frpsctl config list --prefix webServer # 只看某张表
455
+ frpsctl config list --tree # 按表分组缩进展示
432
456
  frpsctl config get bindPort # 读单键
433
457
  frpsctl config get auth # 读整张表(机密自动打码)
434
458
  frpsctl config get auth.token --reveal # 需要看原值时显式索取
@@ -462,11 +486,15 @@ $ frpsctl config set webServer.addr '"0.0.0.0"' # 这一步会让它变成完
462
486
  frpsctl log # 最近 100 行
463
487
  frpsctl log -n 500 # 最近 500 行
464
488
  frpsctl log -f # 持续跟踪(tail -f)
489
+ frpsctl service logs -f # systemd 模式:unit 级日志(journalctl -u)
465
490
  ```
466
491
 
467
492
  日志路径取自配置里的 `log.to`(相对路径按实例目录解析)。frpsctl **不写**这个
468
493
  文件——它由 frp 自己写并按天轮转。多一个写入者会和轮转互相破坏。
469
494
 
495
+ `service logs` 看的是 **journald** 里的 unit 级日志(启动失败、OOM、权限拒绝
496
+ 这类"frp 还没写进自己的日志文件"的问题),两者互补。
497
+
470
498
  ### 停下来
471
499
 
472
500
  ```bash
@@ -520,13 +548,16 @@ $ frpsctl doctor
520
548
 
521
549
  **有 ERROR 时退出码为 1**,可直接接进 CI 或监控。
522
550
 
523
- ### 下线代理
551
+ ### 清理离线记录
524
552
 
525
553
  ```bash
526
- frpsctl kick my-ssh # DELETE /api/proxies
554
+ frpsctl prune # 清理 dashboard 统计里的离线代理记录
527
555
  ```
528
556
 
529
- 需要 dashboard 启用(`webServer.port > 0`);未启用时退出码 7。
557
+ ⚠️ **frp 没有强制下线在线代理的 API**——`DELETE /api/proxies` 的实际语义是
558
+ `ClearOfflineProxies()`(只接受 `?status=offline`,源码与真机均已核实)。
559
+ 要断开某个客户端请停掉它的 frpc。此前版本的 `kick` 基于对该端点的误读,
560
+ 从未真正工作过,已由 `prune` 取代。
530
561
 
531
562
  ---
532
563
 
@@ -545,6 +576,17 @@ FRPSCTL_INSTANCE=web frpsctl status # 或长期用环境变量
545
576
 
546
577
  优先级:`--instance` > `FRPSCTL_INSTANCE` > `default`。
547
578
 
579
+ 一眼看全部实例(一行一个:owner / 状态 / pid / 版本 / 健康):
580
+
581
+ ```console
582
+ $ frpsctl instances
583
+ default direct RUNNING (pid 10582, up 2m10s) frps 0.71.0 L1 process ok L2 control ok L3 plugin skipped
584
+ web systemd SYSTEMD_ACTIVE (pid 20041)
585
+ ```
586
+
587
+ 默认不做网络探测(快速);加 `--health` 会对运行中的实例跑三层健康检查。
588
+ `instances --json` 输出与 `status --json` 同构的数组。
589
+
548
590
  服务端场景可把实例根目录放到 `/etc`:
549
591
 
550
592
  ```bash
@@ -645,6 +687,25 @@ localPort = 8080
645
687
  remotePort = 6005 # 必须在自己被允许的范围内
646
688
  ```
647
689
 
690
+ ### 用 systemd 守护(推荐)
691
+
692
+ `plugin serve` 是前台进程,而插件是**全部客户端登录的单点**且 fail-closed——
693
+ 生产环境必须让它随系统自启、退出即拉起。工具直接生成 unit:
694
+
695
+ ```bash
696
+ sudo frpsctl plugin service install # 体检 → 渲染 frpsctl-plugin@.service → enable
697
+ sudo frpsctl plugin service status
698
+ sudo frpsctl plugin service uninstall # 停用并移除
699
+ ```
700
+
701
+ 生成的 unit 要点:`Restart=always`、`ProtectSystem=strict`、`ReadWritePaths=<实例目录>`
702
+ (策略与审计都在那里)、`ExecStart=... --instance %i plugin serve --policy ...`。
703
+
704
+ 安装前的体检与 frps 的 `service install` 同样严格,且多一条硬约束——绑定地址
705
+ 必须是回环。另外 `frpsctl` 本身必须对服务用户可达且**不在家目录**:
706
+ `ProtectHome=true` 会挡住 `~/.local/bin`,pipx 用户请用
707
+ `sudo pipx install --global frpsctl`(或 `sudo pip install frpsctl`)。
708
+
648
709
  ### 审计
649
710
 
650
711
  每次裁决写入 JSONL(默认 `./plugin-audit.jsonl`,可用 `audit.path` 改):
@@ -676,6 +737,51 @@ $ tail -1 plugin-audit.jsonl
676
737
 
677
738
  ---
678
739
 
740
+ ## Web 管理台
741
+
742
+ 内置的浏览器界面——不是 frp 自带 dashboard 的复刻,而是**含控制面**的管理台:
743
+ 进程启停、配置编辑(预览 diff → 应用 → 失败自动回滚)、日志、7 天流量图。
744
+
745
+ ```bash
746
+ frpsctl web serve # 默认只绑 127.0.0.1:8787
747
+ frpsctl web serve --bind 127.0.0.1:9000 # 换端口
748
+ FRPSCTL_WEB_PASSWORD=my-pw frpsctl web serve # 指定口令(默认自动生成并打印一次)
749
+ ```
750
+
751
+ ```console
752
+ $ frpsctl web serve
753
+ Web 管理台:http://127.0.0.1:8787/
754
+ 登录口令(仅显示这一次):Ih2x...(24 字符)
755
+ Ctrl-C 停止。
756
+ ```
757
+
758
+ 打开浏览器即可——单文件前端(暗色主题),零外部资源(不加载任何 CDN):
759
+
760
+ | 页面 | 内容 |
761
+ |------|------|
762
+ | 仪表盘 | 实例状态 / 三层健康 / 客户端与代理列表 / 近 7 天流量柱状图 / 会话内实时流量曲线 / 日志(5 秒自动刷新) |
763
+ | 配置 | 逐字段表单(敏感值不回显,留空表示不改)→ 预览打码 diff → 确认应用(一次事务、一次重启)→ 一键回滚 |
764
+
765
+ **安全设计**(比 frp 自带 dashboard 更严——它正是本项目安全决策的来源):
766
+
767
+ - 默认只绑回环;绑非回环必须显式 `--allow-non-loopback`(建议再套反向代理 + TLS);
768
+ - **不允许空口令**:自动生成(仅打印一次)或显式指定,会话 Cookie 带 `HttpOnly` + `SameSite=Strict`;
769
+ - 一切变更请求要求 `X-CSRF-Token`(登录时下发,仅存浏览器内存);
770
+ - 登录失败按来源限速(防爆破),错误口令与限速的响应完全一致;
771
+ - **配置原文绝不回显**:界面与 API 只返回打码值;要明文用 `config get --reveal`;
772
+ - 响应带 CSP(`default-src 'none'`)与 `no-store`。
773
+
774
+ 用 systemd 托管(生成 0600 口令文件,unit 只引用路径,明文不落 unit):
775
+
776
+ ```bash
777
+ sudo frpsctl web service install # 体检 → 渲染 frpsctl-web@.service → enable
778
+ sudo frpsctl web service status
779
+ sudo frpsctl web service uninstall
780
+ ```
781
+
782
+ > `web service install` 同样需要 `frpsctl` 位于系统路径(不能被 `ProtectHome`
783
+ > 挡住)——与插件服务的部署要求一致。
784
+
679
785
  ## 用 systemd 托管
680
786
 
681
787
  ```bash
@@ -794,7 +900,7 @@ frpsctl install --version 0.71.0 && frpsctl restart
794
900
  | 4 | 二进制缺失 / 不可执行 / 版本不受支持 | 未 install,或版本 `< 0.70.0` |
795
901
  | 5 | 实例未运行 | `stop` 时无进程 |
796
902
  | 6 | 实例已在运行 | 重复 `start` |
797
- | 7 | dashboard 不可达 / 未启用 | `kick` 时 `webServer.port = 0`;v2 API 缺失 |
903
+ | 7 | dashboard 不可达 / 未启用 | `clients` / `proxies` / `prune` 时 `webServer.port = 0`;v2 API 缺失 |
798
904
  | 8 | 权限不足 | 需要 root 的操作 |
799
905
  | 9 | 变更已自动回滚 | 配置写入后启动/健康检查失败,已恢复上一版 |
800
906
  | 10 | 启动失败 / 进程停不下来 | 启动即退出(附 frp 原始报错);SIGKILL 后仍存在 |
@@ -827,6 +933,7 @@ esac
827
933
  | `FRPSCTL_ADMIN_PASSWORD` | dashboard 口令(优先于配置文件) |
828
934
  | `FRPSCTL_PLUGIN_POLICY` | 插件策略文件路径 |
829
935
  | `FRPSCTL_MIRROR` | frps 下载镜像(逗号分隔;`install --mirror` 优先于它) |
936
+ | `FRPSCTL_WEB_PASSWORD` | Web 管理台登录口令(`web serve --password` 优先于它) |
830
937
  | `FRPSCTL_TRACEBACK` | 设为 `1` 时打印完整回溯(排查未分类错误用) |
831
938
 
832
939
  全局选项(写在子命令前后都可以):
@@ -1062,9 +1169,9 @@ FRPSCTL_TRACEBACK=1 frpsctl status
1062
1169
 
1063
1170
  ```bash
1064
1171
  uv venv && uv pip install -e ".[dev]"
1065
- .venv/bin/pytest # 全部 296 条(契约层缺二进制时自动 skip)
1066
- .venv/bin/pytest -m "not contract" # 快速回归(271 条)
1067
- .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 82%)
1172
+ .venv/bin/pytest # 全部 435 条(契约层缺二进制时自动 skip)
1173
+ .venv/bin/pytest -m "not contract" # 快速回归(405 条)
1174
+ .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 83%)
1068
1175
  .venv/bin/ruff check src/ tests/ # 静态分析
1069
1176
  ```
1070
1177