python再封装frpc内网穿透可视化客户端(by:Qoder-AI)

最近重新部署了frp,使用的是最新的0.67.0版本,之前有写过一篇frp部署的文章但是太老了,有需要中文frp介绍的可以去这个地址看下:https://gofrp.org/zh-cn/docs/overview/ 我的程序是在github上的地址下载的 https://github.com/fatedier/frp/tags(注意服务端、客户端版本用同一个版本)

本次借助Qoder直接在windows下写了个frpc的可视化配置客户端,直接看成品吧:

先附上一些所需文件/代码

frpc_manager_gui.py

import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import subprocess
import threading
import os
import sys
import configparser


class FRPCClientGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("FRP 客户端管理器")
        self.root.geometry("700x600")
        
        # 进程对象
        self.process = None
        self.output_thread = None
        self.is_running = False
        self.startup_status = "starting"  # starting, success, failed
        
        # 配置文件路径
        self.config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frpc_manager.ini")
        self.success_config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frpc_last_success.ini")
        
        # 创建主框架
        self.create_widgets()
        
        # 加载配置
        self.load_config()
        
        # 程序启动后自动触发启动
        self.root.after(500, self.auto_start_frpc)
        
    def create_widgets(self):
        """创建界面组件"""
        # 标题
        title_label = ttk.Label(self.root, text="FRP 客户端配置", font=("Arial", 16, "bold"))
        title_label.pack(pady=5)
        
        # 配置框架(默认先创建,后面根据配置决定是否隐藏)
        self.config_frame = ttk.LabelFrame(self.root, text="配置参数", padding=10)
        self.config_frame.pack(fill="x", padx=20, pady=5)
        
        # 第一行:FRP_SERVER_ADDR 和 FRP_SERVER_PORT
        ttk.Label(self.config_frame, text="FRP 服务器地址:").grid(row=0, column=0, sticky="w", pady=5, padx=5)
        self.server_addr_entry = ttk.Entry(self.config_frame, width=20)
        self.server_addr_entry.grid(row=0, column=1, padx=5, pady=5)
        self.server_addr_entry.bind("<KeyRelease>", lambda e: self.update_config_description())
        
        ttk.Label(self.config_frame, text="FRP 服务器端口:").grid(row=0, column=2, sticky="w", pady=5, padx=5)
        self.server_port_entry = ttk.Entry(self.config_frame, width=20)
        self.server_port_entry.grid(row=0, column=3, padx=5, pady=5)
        
        # 第二行:FRP_TOKEN 和 FRP_NAME
        ttk.Label(self.config_frame, text="FRP Token:").grid(row=1, column=0, sticky="w", pady=5, padx=5)
        self.token_entry = ttk.Entry(self.config_frame, width=20, show="*")
        self.token_entry.grid(row=1, column=1, padx=5, pady=5)
        
        ttk.Label(self.config_frame, text="FRP 代理名称:").grid(row=1, column=2, sticky="w", pady=5, padx=5)
        self.name_entry = ttk.Entry(self.config_frame, width=20)
        self.name_entry.grid(row=1, column=3, padx=5, pady=5)
        
        # 第三行:LOCAL_IP 和 LOCAL_PORT
        ttk.Label(self.config_frame, text="局域网主机 IP:").grid(row=2, column=0, sticky="w", pady=5, padx=5)
        self.local_ip_entry = ttk.Entry(self.config_frame, width=20)
        self.local_ip_entry.grid(row=2, column=1, padx=5, pady=5)
        self.local_ip_entry.bind("<KeyRelease>", lambda e: self.update_config_description())
        
        ttk.Label(self.config_frame, text="局域网主机端口:").grid(row=2, column=2, sticky="w", pady=5, padx=5)
        self.local_port_entry = ttk.Entry(self.config_frame, width=20)
        self.local_port_entry.grid(row=2, column=3, padx=5, pady=5)
        self.local_port_entry.bind("<KeyRelease>", lambda e: self.update_config_description())
        
        # 第四行:FRP_REMOTE_PORT 和 FRP_USER (可选)
        ttk.Label(self.config_frame, text="FRP 远程端口:").grid(row=3, column=0, sticky="w", pady=5, padx=5)
        self.remote_port_entry = ttk.Entry(self.config_frame, width=20)
        self.remote_port_entry.grid(row=3, column=1, padx=5, pady=5)
        self.remote_port_entry.bind("<KeyRelease>", lambda e: self.update_config_description())
        
        ttk.Label(self.config_frame, text="FRP 用户 (可选):").grid(row=3, column=2, sticky="w", pady=5, padx=5)
        self.user_entry = ttk.Entry(self.config_frame, width=20)
        self.user_entry.grid(row=3, column=3, padx=5, pady=5)
        
        # 第五行:配置说明标签
        self.desc_label = ttk.Label(self.config_frame, text="", foreground="gray")
        self.desc_label.grid(row=4, column=0, columnspan=4, sticky="w", padx=10, pady=5)
        
        # 按钮框架
        button_frame = ttk.Frame(self.root)
        button_frame.pack(fill="x", padx=20, pady=10)
        
        # 启动按钮
        self.start_button = ttk.Button(button_frame, text="▶ 启动 FRPC", command=self.start_frpc)
        self.start_button.pack(side="left", padx=5)
        
        # 停止按钮
        self.stop_button = ttk.Button(button_frame, text="◼ 停止 FRPC", command=self.stop_frpc, state="disabled")
        self.stop_button.pack(side="left", padx=5)
        
        # 状态标签
        self.status_label = ttk.Label(button_frame, text="状态:已停止", foreground="red")
        self.status_label.pack(side="left", padx=20)
        
        # 日志框架
        log_frame = ttk.LabelFrame(self.root, text="FRPC 控制台输出", padding=10)
        log_frame.pack(fill="both", expand=True, padx=20, pady=5)
        
        # 日志文本区域
        self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=20, bg="black", fg="green", font=("Consolas", 9))
        self.log_text.pack(fill="both", expand=True)
        
        # 清空日志按钮
        clear_button = ttk.Button(self.root, text="清空日志", command=self.clear_log)
        clear_button.pack(pady=5)
        
    def start_frpc(self):
        """启动 FRPC 客户端"""
        if self.is_running:
            messagebox.showwarning("警告", "FRPC 已经在运行中!")
            return
        
        # 先保存配置
        self.save_config()
        
        # 获取配置值
        server_addr = self.server_addr_entry.get().strip()
        server_port = self.server_port_entry.get().strip()
        token = self.token_entry.get().strip()
        remote_port = self.remote_port_entry.get().strip()
        name = self.name_entry.get().strip()
        local_ip = self.local_ip_entry.get().strip()
        local_port = self.local_port_entry.get().strip()
        user = self.user_entry.get().strip()
        
        # 验证必填字段
        if not server_addr or not server_port or not token or not remote_port or not name or not local_ip or not local_port:
            messagebox.showerror("错误", "请填写所有必填字段!")
            return
        
        # 设置环境变量
        env = os.environ.copy()
        env["FRP_SERVER_ADDR"] = server_addr
        env["FRP_SERVER_PORT"] = server_port
        env["FRP_TOKEN"] = token
        env["FRP_REMOTE_PORT"] = remote_port
        env["FRP_NAME"] = name
        env["LOCAL_IP"] = local_ip
        env["LOCAL_PORT"] = local_port
        
        if user:
            env["FRP_USER"] = user
        
        # 获取 frpc.exe 路径
        frpc_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frpc", "frpc.exe")
        
        if not os.path.exists(frpc_path):
            messagebox.showerror("错误", f"未找到 frpc.exe 文件:n{frpc_path}")
            return
        
        # 配置文件路径
        config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frpc", "frpc.env.toml")
        
        try:
            # 启动进程
            self.process = subprocess.Popen(
                [frpc_path, "-c", config_path],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                stdin=subprocess.PIPE,
                env=env,
                cwd=os.path.dirname(frpc_path),
                creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
            )
            
            self.is_running = True
            self.start_button.config(state="disabled")
            self.stop_button.config(state="normal")
            self.status_label.config(text="状态:运行中", foreground="green")
            
            self.append_log(f"=== FRPC 已启动 ===")
            self.append_log(f"服务器地址:{server_addr}:{server_port}")
            self.append_log(f"远程端口:{remote_port}")
            self.append_log(f"代理名称:{name}")
            self.append_log(f"本地地址:{local_ip}:{local_port}")
            self.append_log(f"进程 ID: {self.process.pid}")
            self.append_log("=" * 50)
            
            # 重置启动状态
            self.startup_status = "starting"
            
            # 启动读取线程
            self.output_thread = threading.Thread(target=self.read_output, daemon=True)
            self.output_thread.start()
            
        except Exception as e:
            messagebox.showerror("错误", f"启动 FRPC 失败:n{str(e)}")
            self.is_running = False
            self.start_button.config(state="normal")
            self.stop_button.config(state="disabled")
    
    def stop_frpc(self):
        """停止 FRPC 客户端"""
        if not self.is_running:
            messagebox.showwarning("警告", "FRPC 未运行!")
            return
        
        confirm = messagebox.askyesno("确认", "确定要停止 FRPC 吗?")
        if not confirm:
            return
        
        try:
            if self.process:
                self.process.terminate()
                try:
                    self.process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    self.process.kill()
                
                self.append_log("n=== FRPC 已停止 ===")
                self.is_running = False
                self.process = None
                
        except Exception as e:
            self.append_log(f"停止时出错:{str(e)}")
        
        finally:
            self.is_running = False
            self.start_button.config(state="normal")
            self.stop_button.config(state="disabled")
            self.status_label.config(text="状态:已停止", foreground="red")
    
    def read_output(self):
        """读取 FRPC 输出并判断启动状态"""
        if self.process and self.process.stdout:
            for line in iter(self.process.stdout.readline, b''):
                try:
                    decoded_line = line.decode('utf-8', errors='ignore').strip()
                    if decoded_line:
                        # 使用 after 方法在主线程中更新 UI
                        self.root.after(0, lambda l=decoded_line: self.append_log(l))
                        
                        # 判断启动状态
                        self.root.after(0, lambda l=decoded_line: self.check_startup_status(l))
                except Exception as e:
                    self.root.after(0, lambda: self.append_log(f"读取输出时出错:{str(e)}"))
    
    def append_log(self, message):
        """添加日志消息"""
        self.log_text.insert(tk.END, message + "n")
        self.log_text.see(tk.END)  # 自动滚动到底部
    
    def check_startup_status(self, line):
        """检查启动状态"""
        if self.startup_status == "starting":
            # 检测是否启动成功
            if "start proxy success" in line.lower():
                self.startup_status = "success"
                self.append_log("n✓ FRPC 启动成功!")
                self.status_label.config(text="状态:已启动", foreground="#00aa00")
                
                # 保存成功的配置
                self.save_success_config()
                
            # 检测是否启动失败(代理已存在)
            elif "already exists" in line.lower() or "error" in line.lower():
                self.startup_status = "failed"
                self.append_log(f"n✗ FRPC 启动失败:代理可能已存在/网络异常/鉴权失败")
                self.status_label.config(text="状态:启动失败", foreground="red")
                
                # 延迟后自动停止进程
                self.root.after(1000, self.auto_stop_on_failure)
    
    def auto_stop_on_failure(self):
        """启动失败后自动停止进程"""
        if self.startup_status == "failed" and self.is_running:
            self.append_log("正在停止 FRPC 进程...")
            try:
                if self.process:
                    self.process.terminate()
                    try:
                        self.process.wait(timeout=5)
                    except subprocess.TimeoutExpired:
                        self.process.kill()
                    self.process = None
            except Exception as e:
                self.append_log(f"停止进程时出错:{str(e)}")
            finally:
                self.is_running = False
                self.start_button.config(state="normal")
                self.stop_button.config(state="disabled")
    
    def clear_log(self):
        """清空日志"""
        self.log_text.delete(1.0, tk.END)
    
    def update_config_description(self):
        """更新配置说明文字"""
        server_addr = self.server_addr_entry.get().strip() or "{FRP 服务器地址}"
        remote_port = self.remote_port_entry.get().strip() or "{FRP 远程端口}"
        local_ip = self.local_ip_entry.get().strip() or "{局域网主机 IP}"
        local_port = self.local_port_entry.get().strip() or "{局域网主机端口}"
        
        desc_text = f"访问【{server_addr}:{remote_port}】> 将会转发到本机可访问的地址 > 【{local_ip}:{local_port}】"
        self.desc_label.config(text=desc_text)
    
    def auto_start_frpc(self):
        """程序启动时自动启动 FRPC"""
        # 检查配置文件是否存在且有值
        if not os.path.exists(self.config_file):
            return
        
        config = configparser.ConfigParser()
        try:
            config.read(self.config_file, encoding='utf-8')
            
            # 检查是否有必填字段
            if 'FRP' in config:
                frp = config['FRP']
                has_required = (
                    frp.get('server_addr', '').strip() and
                    frp.get('server_port', '').strip() and
                    frp.get('token', '').strip() and
                    frp.get('remote_port', '').strip() and
                    frp.get('name', '').strip() and
                    frp.get('local_ip', '').strip() and
                    frp.get('local_port', '').strip()
                )
                
                if has_required:
                    self.append_log("=== 自动启动 FRPC ===")
                    self.start_frpc()
        except Exception as e:
            self.append_log(f"自动启动检查失败:{str(e)}")
    
    def load_config(self):
        """从 INI 配置文件加载配置"""
        if not os.path.exists(self.config_file):
            # 如果配置文件不存在,创建空配置文件
            self.create_default_config()
            return
        
        config = configparser.ConfigParser()
        try:
            config.read(self.config_file, encoding='utf-8')
            
            # 读取 UI 显示配置(默认不显示)
            show_config = False  # 默认不显示配置区域
            if 'UI' in config:
                ui = config['UI']
                show_config_str = ui.get('show_config', '0').strip()
                # 只有明确设置为 1 才显示
                show_config = (show_config_str == '1')
            
            # 根据配置显示/隐藏配置区域
            if show_config:
                self.config_frame.pack(fill="x", padx=20, pady=5)
            else:
                self.config_frame.pack_forget()
            
            # 读取 FRP 配置
            if 'FRP' in config:
                frp = config['FRP']
                self.server_addr_entry.insert(0, frp.get('server_addr', ''))
                self.server_port_entry.insert(0, frp.get('server_port', ''))
                self.token_entry.insert(0, frp.get('token', ''))
                self.remote_port_entry.insert(0, frp.get('remote_port', ''))
                self.name_entry.insert(0, frp.get('name', ''))
                self.local_ip_entry.insert(0, frp.get('local_ip', ''))
                self.local_port_entry.insert(0, frp.get('local_port', ''))
                self.user_entry.insert(0, frp.get('user', ''))
            
            # 更新配置说明
            self.update_config_description()
        except Exception as e:
            messagebox.showerror("错误", f"读取配置文件失败:n{str(e)}")
    
    def create_default_config(self):
        """创建默认配置文件"""
        config = configparser.ConfigParser()
        config['FRP'] = {
            'server_addr': '',
            'server_port': '',
            'token': '',
            'remote_port': '',
            'name': '',
            'local_ip': '',
            'local_port': '',
            'user': ''
        }
        config['UI'] = {
            'show_config': '0'  # 默认不显示配置区域
        }
        
        try:
            with open(self.config_file, 'w', encoding='utf-8') as f:
                config.write(f)
            messagebox.showinfo("提示", "已创建配置文件:n" + self.config_file + "nn请在配置文件中填写参数后重新启动程序。")
        except Exception as e:
            messagebox.showerror("错误", f"创建配置文件失败:n{str(e)}")
    
    def save_config(self):
        """保存配置到 INI 文件"""
        config = configparser.ConfigParser()
        config['FRP'] = {
            'server_addr': self.server_addr_entry.get().strip(),
            'server_port': self.server_port_entry.get().strip(),
            'token': self.token_entry.get().strip(),
            'remote_port': self.remote_port_entry.get().strip(),
            'name': self.name_entry.get().strip(),
            'local_ip': self.local_ip_entry.get().strip(),
            'local_port': self.local_port_entry.get().strip(),
            'user': self.user_entry.get().strip()
        }
        
        # 保留原有的 UI 配置,不修改用户的设置
        if os.path.exists(self.config_file):
            old_config = configparser.ConfigParser()
            try:
                old_config.read(self.config_file, encoding='utf-8')
                # 只复制 UI 节,不做任何修改
                if 'UI' in old_config:
                    config['UI'] = {}
                    for key, value in old_config['UI'].items():
                        config['UI'][key] = value
            except:
                pass
        
        try:
            with open(self.config_file, 'w', encoding='utf-8') as f:
                config.write(f)
        except Exception as e:
            self.append_log(f"保存配置文件失败:{str(e)}")
    
    def save_success_config(self):
        """保存最后一次成功启动的配置"""
        config = configparser.ConfigParser()
        config['FRP'] = {
            'server_addr': self.server_addr_entry.get().strip(),
            'server_port': self.server_port_entry.get().strip(),
            'token': self.token_entry.get().strip(),
            'remote_port': self.remote_port_entry.get().strip(),
            'name': self.name_entry.get().strip(),
            'local_ip': self.local_ip_entry.get().strip(),
            'local_port': self.local_port_entry.get().strip(),
            'user': self.user_entry.get().strip()
        }
        
        try:
            with open(self.success_config_file, 'w', encoding='utf-8') as f:
                config.write(f)
            self.append_log(f"✓ 成功配置已保存到:{self.success_config_file}")
        except Exception as e:
            self.append_log(f"保存成功配置失败:{str(e)}")


def main():
    root = tk.Tk()
    app = FRPCClientGUI(root)
    root.protocol("WM_DELETE_WINDOW", lambda: on_closing(root, app))
    root.mainloop()


def on_closing(root, app):
    """窗口关闭时的处理"""
    if app.is_running:
        confirm = messagebox.askyesno("确认", "FRPC 正在运行,确定要退出程序吗?")
        if confirm:
            app.stop_frpc()
            app.save_config()  # 保存配置
            root.destroy()
    else:
        app.save_config()  # 保存配置
        root.destroy()


if __name__ == "__main__":
    main()

然后是配置文件frpc_manager.ini(这个是给python程序加载使用了,最终会注入环境变量后用来执行frpc程序)

[FRP]
server_addr = xxx.xxx.xxx.xxx
server_port = 7000
token = xxxxxxxxxx
remote_port = 7999
name = rdp_3389
local_ip = 127.0.0.1
local_port = 3389
user = test1

[UI]
show_config = 1

另外需要创建frpc文件夹,并放入frpc程序和配置文件frpc.env.toml(使用的是环境变量方式)

serverAddr = "{{ .Envs.FRP_SERVER_ADDR }}"
serverPort = {{ .Envs.FRP_SERVER_PORT }}
auth.token = "{{ .Envs.FRP_TOKEN }}"
user = "{{ .Envs.FRP_USER }}"

[[proxies]]
name = "{{ .Envs.FRP_NAME }}"
type = "tcp"

localIP = "{{ .Envs.LOCAL_IP }}"
localPort = {{ .Envs.LOCAL_PORT }}
remotePort = {{ .Envs.FRP_REMOTE_PORT }}

上面准备好就可以python frpc_manager_gui.py启动啦(注意导入的一些包需要手动pip install安装下)

# FRP 客户端管理器 - 软件操作说明书

## 一、软件简介

FRP 客户端管理器是一款基于 Python + Tkinter 开发的图形化管理工具,用于方便地配置和管理 FRP(Fast Reverse Proxy)客户端程序。该软件通过图形界面配置参数,自动注入环境变量启动 frpc,并实时监控运行状态和日志输出。

### 主要功能
- ✅ 图形化配置 FRP 参数,无需手动编辑配置文件
- ✅ 一键启动/停止 FRPC 客户端
- ✅ 实时显示 FRPC 控制台日志
- ✅ 智能判断启动状态(成功/失败)
- ✅ 配置参数持久化保存(INI 文件)
- ✅ 程序启动时自动加载配置并启动 FRPC
- ✅ Token 密码化显示,保护敏感信息
- ✅ 动态显示端口转发规则说明

---

## 二、系统要求

- **操作系统**:Windows 7/8/10/11
- **Python 版本**:Python 3.6+
- **依赖库**:Tkinter(Python 内置)
- **FRP 客户端**:frpc.exe 及 frpc.env.toml 配置文件

---

## 三、安装与部署

### 3.1 文件结构
```
frpc_client/
├── frpc_manager_gui.py          # 主程序
├── frpc_manager.ini             # 配置文件(首次运行自动生成)
├── frpc_last_success.ini        # 最后一次成功配置(启动成功后生成)
└── frpc/
    ├── frpc.exe                 # FRP 客户端程序
    └── frpc.env.toml            # FRP 配置文件模板
```

### 3.2 运行方式
直接双击运行或在命令行执行:
```bash
python frpc_manager_gui.py
```

---

## 四、界面说明

### 4.1 配置参数区域

配置区域共 5 行,包含 8 个配置项:

**第 1 行:**
- **FRP 服务器地址**:FRP 服务器的 IP 地址或域名(必填)
- **FRP 服务器端口**:FRP 服务器的监听端口(必填,默认 7000)

**第 2 行:**
- **FRP Token**:FRP 认证令牌,以星号 `*` 隐藏显示(必填)
- **FRP 代理名称**:自定义代理标识名称(必填)

**第 3 行:**
- **局域网主机 IP**:本地要映射的服务 IP(必填,如 127.0.0.1)
- **局域网主机端口**:本地要映射的服务端口(必填,如 3389)

**第 4 行:**
- **FRP 远程端口**:FRP 服务器端对外开放的端口(必填)
- **FRP 用户**:可选的用户标识(可选)

**第 5 行:**
- **配置说明**:动态显示端口转发规则
  ```
  访问【FRP 服务器地址:FRP 远程端口】> 将会转发到本机可访问的地址 > 【局域网主机 IP:局域网主机端口】
  ```

### 4.2 控制按钮

- **▶ 启动 FRPC**:启动 FRP 客户端
- **◼ 停止 FRPC**:停止 FRP 客户端
- **清空日志**:清除控制台输出日志

### 4.3 状态指示器

- **状态:已停止**(红色)- FRPC 未运行
- **状态:运行中**(绿色)- FRPC 正常启动
- **状态:启动失败**(红色)- 启动失败,自动停止进程

### 4.4 日志显示区域

黑色背景、绿色文字的仿终端样式,实时显示:
- 启动配置信息
- FRPC 运行日志
- 启动成功/失败提示
- 进程 ID 等信息

---

## 五、详细操作步骤

### 5.1 首次使用

1. **启动程序**
   - 双击运行 `frpc_manager_gui.py`
   - 程序自动创建空配置文件 `frpc_manager.ini`
   - 弹出提示框提示填写配置

2. **配置参数**
   - 在配置区域填写所有必填字段
   - Token 输入时显示为星号,保护隐私
   - 观察底部的配置说明,确认转发规则正确

3. **启动 FRPC**
   - 点击"▶ 启动 FRPC"按钮
   - 程序自动保存配置到 `frpc_manager.ini`
   - 日志区域显示启动过程

4. **查看状态**
   - 启动成功:状态变为"已启动"(绿色),显示"✓ FRPC 启动成功!"
   - 启动失败:状态变为"启动失败"(红色),自动停止进程并提示原因

### 5.2 日常使用

#### 方式一:使用已有配置
1. 打开程序(自动加载上次配置)
2. 等待 500ms 后自动启动 FRPC
3. 查看日志确认启动成功

#### 方式二:修改配置后启动
1. 打开程序
2. 修改需要调整的参数
3. 点击"▶ 启动 FRPC"
4. 新配置会自动保存到 INI 文件

#### 方式三:隐藏配置区域(简洁模式)
1. 编辑 `frpc_manager.ini`
2. 设置 `show_config = 0`
3. 重启程序,只显示按钮和日志
4. 配置通过 INI 文件预先设置

### 5.3 停止 FRPC

1. 点击"◼ 停止 FRPC"按钮
2. 确认对话框点击"是"
3. 程序安全终止 FRPC 进程
4. 状态恢复为"已停止"

### 5.4 退出程序

1. 点击窗口关闭按钮
2. 如果 FRPC 正在运行,会弹出确认框
3. 确认后自动停止 FRPC 并保存配置
4. 程序退出

---

## 六、配置文件说明

### 6.1 主配置文件(frpc_manager.ini)

```ini
[FRP]
server_addr = xxx.xxx.xxx.xxx
server_port = 7000
token = xxxxxxx
remote_port = xxx
name = rdp_3389
local_ip = 127.0.0.1
local_port = 3389
user = test1

[UI]
# 是否在主程序显示配置参数区域(1=显示,0=隐藏)- 手动修改此值
show_config = 1
```

**配置项说明:**
- `server_addr`:FRP 服务器地址(必填)
- `server_port`:FRP 服务器端口(必填)
- `token`:认证令牌(必填)
- `remote_port`:远程映射端口(必填)
- `name`:代理名称(必填)
- `local_ip`:本地服务 IP(必填)
- `local_port`:本地服务端口(必填)
- `user`:用户标识(可选)
- `show_config`:是否显示配置区域(1=显示,0=隐藏)

### 6.2 成功配置文件(frpc_last_success.ini)

- **用途**:记录最后一次成功启动的配置
- **更新时机**:仅当启动成功时更新
- **格式**:与主配置文件相同
- **特点**:永不记录失败的配置

### 6.3 FRP 配置文件(frpc/frpc.env.toml)

这是 FRP 客户端的原始配置文件,使用环境变量占位符:

```toml
serverAddr = "{{ .Envs.FRP_SERVER_ADDR }}"
serverPort = {{ .Envs.FRP_SERVER_PORT }}
auth.token = "{{ .Envs.FRP_TOKEN }}"
user = "{{ .Envs.FRP_USER }}"

[[proxies]]
name = "{{ .Envs.FRP_NAME }}"
type = "tcp"

localIP = "{{ .Envs.LOCAL_IP }}"
localPort = {{ .Envs.LOCAL_PORT }}
remotePort = {{ .Envs.FRP_REMOTE_PORT }}
```

---

## 七、高级功能

### 7.1 自动启动

程序启动时会自动检查配置文件:
- ✓ 配置文件存在
- ✓ 所有必填字段都有值
- ✓ 满足条件则自动启动 FRPC
- ✓ 延迟 500ms 执行,确保界面完全加载

### 7.2 启动状态智能判断

程序实时监控 FRPC 控制台输出:

**成功检测:**
- 检测到关键词:`start proxy success`
- 状态变更为"已启动"(绿色)
- 保存配置到 `frpc_last_success.ini`
- 日志显示:✓ FRPC 启动成功!

**失败检测:**
- 检测到关键词:`already exists` / `error`
- 状态变更为"启动失败"(红色)
- 1 秒后自动停止进程
- 日志显示:✗ FRPC 启动失败:代理可能已存在/网络异常/鉴权失败

### 7.3 配置显示/隐藏切换

**显示配置区域(适合初次配置):**
```ini
[UI]
show_config = 1
```

**隐藏配置区域(适合日常使用):**
```ini
[UI]
show_config = 0
```

**注意事项:**
- ⚠️ 此配置只能手动在 INI 文件中修改
- ⚠️ 程序不会自动更改此值
- ⚠️ 默认值为 0(不显示)
- ⚠️ 新建配置文件时默认为 0

---

## 八、常见问题

### Q1: 启动失败提示"代理已存在"
**原因**:FRP 服务器上该代理名称已被占用  
**解决**:
1. 修改"FRP 代理名称"为其他名称
2. 或在 FRP 服务器端删除旧配置

### Q2: 启动失败提示"网络异常"或"鉴权失败"
**原因**:服务器地址、端口或 Token 错误  
**解决**:
1. 检查 FRP 服务器地址和端口是否正确
2. 检查 Token 是否与服务器配置一致
3. 确认 FRP 服务器正常运行

### Q3: 配置区域不显示
**原因**:`show_config` 设置为 0  
**解决**:
1. 用文本编辑器打开 `frpc_manager.ini`
2. 设置 `show_config = 1`
3. 重启程序

### Q4: 如何修改已保存的配置?
**方法一**:直接在界面修改后点击启动  
**方法二**:用文本编辑器打开 `frpc_manager.ini` 直接修改

### Q5: 在哪里查看 FRPC 的详细日志?
- 程序界面的日志区域会实时显示 FRPC 输出
- 支持自动滚动和清空功能
- 黑色背景绿色文字,仿终端样式

---

## 九、环境变量映射表

| 界面配置项 | 环境变量名 | TOML 配置对应 |
|-----------|-----------|--------------|
| FRP 服务器地址 | `FRP_SERVER_ADDR` | `serverAddr` |
| FRP 服务器端口 | `FRP_SERVER_PORT` | `serverPort` |
| FRP Token | `FRP_TOKEN` | `auth.token` |
| FRP 代理名称 | `FRP_NAME` | `proxies.name` |
| 局域网主机 IP | `LOCAL_IP` | `localIP` |
| 局域网主机端口 | `LOCAL_PORT` | `localPort` |
| FRP 远程端口 | `FRP_REMOTE_PORT` | `remotePort` |
| FRP 用户 | `FRP_USER` | `user` |

---

## 十、技术规格

### 10.1 开发技术
- **编程语言**:Python 3
- **GUI 框架**:Tkinter
- **配置文件**:INI 格式(configparser 模块)
- **进程管理**:subprocess 模块

### 10.2 核心特性
- ✅ 多线程读取 FRPC 输出,避免界面卡顿
- ✅ 使用 `root.after()` 安全更新 UI
- ✅ 进程终止超时处理(5 秒后强制结束)
- ✅ 配置文件 UTF-8 编码,支持中文注释
- ✅ 异常捕获和用户友好的错误提示

### 10.3 安全特性
- Token 密码化显示(星号遮挡)
- 配置文件不主动修改 UI 开关设置
- 窗口关闭前确认和配置保存
- 启动失败自动清理进程

---

## 十一、更新日志

### v1.0(初始版本)
- 基础 GUI 界面和 FRPC 启停功能
- 环境变量注入机制
- 实时日志显示

### v1.1(配置优化)
- 增加 INI 配置文件支持
- 配置持久化和自动加载
- 程序启动自动触发启动

### v1.2(状态监控)
- 智能判断启动状态
- 启动失败自动停止
- 成功配置备份功能

### v1.3(界面优化)
- 紧凑布局(每行 2 个配置项)
- 配置区域显示/隐藏开关
- Token 密码化显示
- 动态配置说明

### v1.4(当前版本)
- 新增 FRP 服务器端口配置
- 优化配置参数排列顺序
- 增强配置说明文字

---

## 十二、联系与支持

如有问题或建议,请参考:
1. 本操作说明书
2. FRP 官方文档
3. 查看程序日志中的错误信息

---

**最后更新**:2026 年 3 月 12 日  
**软件版本**:v1.4  
**适用系统**:Windows

发表评论