使用A I学习编程

前言

最近一直折腾电脑,主要是安装学习Linux系统,部署各种应用:安装配置软路由功能、迁移原来安装在群晖nas上的应用、docker-compose应用部署、以及程序员必备神器终端的使用等等。所有这些均是在AI帮助下完成的,很难想象没有AI帮助我能否做到这些。这两天忽然想要尝试用AI 编程,凡事总要试一试,对吧,于是就有了以下过程。

行动

在行动之前,我就想好了写一个简单的需求提交给 AI,让它给出解决方案和具体代码。需求如下:

  • 目前对家中路由器hosts文件进行修改,以满足自己的应用需要。但是存在一个问题:当路由器重启后,修改过的配置就会丢失,需要手动再次输入,并使其生效。

  • 因此,需要编程实现修改远程主机/etc/hosts文件功能,该应用应部署在服务端,可通过web页面访问,便于任何家中可上网设备进行修改。

  • 初次使用可以手动建立连接配置并存为一个连接配置选项,方便后续使用。登录后可选择直接输入欲增加host内容单条目或上传文本文件格式的hosts列表,添加内容到hosts文件中,实现快捷更改远程服务器hosts的目的。

  • 已有应用环境:有一台Debian 12 服务器一台,群晖服务器两台,均可支持软件部署。

将需求提交给 ChatGPT 并要求给出解决方案及具体代码,大约十多秒后 ChatGPT 给出了方案,非常完备(详见 附件1:ChatGPT 的方案及代码)。

调试

按照 ChatGPT 提供方案部署应用,首先在远端服务器新建一个项目目录 “edit-hosts”,然后在此目录下创建第一个文件 app.py ,并录入 ChatGPT 提供的代码。在此目录下创建子目录 “templates”,建立 index.html 文件并复制对应代码。

随后后安装、运行相关依赖。期间因为安装依赖产生各种问题,多次请教 ChatGPT ,ChatGPT 针对安装、调试遇到的问题一般会给出几种解决方案,并阐述相关理由和优缺点,因此安装依赖过程也是学习提高的过程。经过一个多小时安装、调试,所有运行环境均已准备好。激动人心时刻来临,这如同亲手做了一个玩具,一直不知道是否靠谱,检验AI成色的时刻来临。在命令终端输入指令:

Python app.py

屏幕上如期出现运行代码划过,最终停留在运行正常信息画面,并给出访问链接: http://192.168.50.20:2017 。兴冲冲打开浏览器输入以上网址,页面正常打开,接着输入路由器连接参数,在连接我的华硕路由器时,程序出错:"error": "sh: sudo: not found\n"。

虽然当时遇到了bug,所有编过程序的人都知道,这很正常,最害怕的完全跑不起来。编程过程就是需要不断地找到bug——》修正bug,自此我觉得这个 AI 编程是靠谱的。之所以出现上面这个错误,是因为华硕路由器 SSH 登录特点所致。虽然市面上大多数路由器均采用Linux系统,但是因为存储空间及功能需要,其系统非常精简,因此很多常用功能并不具备,比如 sudo指令登录root账号。事实上,华硕路由器通过ssh终端登录后直接进入root模式。AI显然不知道这种情况,按照正常逻辑登录后继续输入 sudo指令进入root权限,导致程序报错。我将此情况反馈给 ChatGPT,并让其修改。

反馈

以下我提出的反馈意见:

  1. 运行SSH终端连接华硕路由器后就直接进入 root 模式,无需再用sudo指令,且也不支持sudo指令,建议增加选项。错误信息如下:

 "error": "sh: sudo: not found\n"
  1. 对于华硕路由器,不能使用重启生效指令,因为一旦重启,hosts文件自动恢复到初始状态,所有修改将丢失。但是可以执行一条指令让hosts修改生效并保持修改后的状态:killall -SIGHUP dnsmasq

  2. 如果要使应用更为通用,可以增加更多的选项和配置以及错误提示。或者在应用界面直接打开 /etc/hosts/ 文件编辑窗口,进行编辑、复制、粘贴操作,编辑完成后保存生效。

针对我的反馈意见,ChatGPT迅速给出答复如下:

这些改动会让应用更适配华硕路由器的环境。我会优化逻辑,增加以下功能:

  • SSH 执行命令时可选是否使用 sudo(默认 root 用户不使用)

  • 针对华硕路由器的特定处理:修改 hosts 后提供 killall -SIGHUP dnsmasq 选项,而不是重启

  • 增强 UI 体验:提供一个 Web 界面直接编辑 /etc/hosts,并提供错误提示

于是又按照改进方案进行调整程序,安装新的运行环境,期间多次与ChatGPT交换意见。结果就在调试过程中ChatGPT 不干了,提示我的免费额度已经用完,于是第一天AI编程只能按下暂停键。

换门

到了第二天,我考虑更换一个 AI 试试。从网上了解到 Claude 在编程方面非常厉害,我决定改用 Claude 。这一决定现在看来正确无比。当我把数百行 ChatGPT 的方案和代码以及我的反馈意见一并提交给 Claude 后,它立即就给出了完整解决方案,并优化了代码。

然后按照步骤一步步完成代码录入、运行环境调试,输入指令运行。打开浏览器输入 http://192.168.50.123:5000 , 如期进入程序web页面,输入路由器登录数据,正常登录,尝试修改 hosts ,单条目录入成功,撒花🎉🎉🎉。虽然在随后的测试出了错:“查看hosts页面”对hosts文件进行了修改,保存修改未能生效。但相信经过简单调试,一定可以解决这个bug,由此可见 Claude 编程能力之强悍。

接下来的一两个小时,我将运行的信息反馈给 Claude,它根据反馈信息和要求进行代码调整,如此反复,最终完善了所有我要的功能,完成了我人生中第一个真正意义的网络软件开发。尽管这个软件还非常的简单,界面简陋,实现功能也非常单一,但它是真正意义的网络环境运行的软件,可以部署在远程服务器上,网内所有客户端访问使用。如果愿意,也可通过反向代理轻易实现公网访问。

截屏2025-03-05 下午9.17.16

总结

必须承认,AI 的出现将会对我们生活方方面面产生巨大的影响,对于一个快60岁的中老年人,如果没有AI 帮助很难想象我能独立完成这么一个简陋的软件。其中涉及任何一门计算机语言和知识都需要大量时间去学习掌握,更不要说用这些知识直接编程。再退一步,就算这些都是我目前所掌握的,数百行代码一个字一个的敲入,也需要至数个工作日才能完成。而现在,我一个业余爱好者,相关知识勉强算是略知皮毛几乎是零基础的人,在 AI帮助下仅仅用了数小时就完成了相关开发,不由得佩服AI的强大。

但这是不是意味着,以后程序员就要失业了?我不这样认为。AI的出现恰恰是体现程序员的价值所在。虽然我自嘲自己零基础实现了AI编程,但是如果没有对计算机有一定的了解,对编程有一定的理解,就算AI喂饭给我,我也不知道如何部署,各个部件如何协调运行,如何搭建环境等等,更不用说让代码跑起来。这只是意味着对程序员要求更高,需要程序员站在使用者角度,能够提出精确的需求分析,能够熟知各个技术平台技术特点,针对性让AI完成代码部分,调试/协调、优化和组装仍需要人来干。这部分价值是Ai不能替代的,这也是软件开发最具价值的部分。

pexels-markusspiske-965345


附件 1: ChatGPT 的方案及代码

远程 Hosts 管理系统方案

1.需求概述

  • 需要通过 Web 应用管理远程路由器的 /etc/hosts 文件。

  • 确保修改后的配置在设备重启后仍然生效。

  • 通过 Web 界面支持:

    • 选择远程主机进行 SSH 连接。

    • 直接输入 hosts 记录进行单条添加。

    • 上传文本文件批量修改 hosts 记录。

  • 可持久化存储远程主机的 SSH 连接信息,方便后续管理。

  • 部署环境:Debian 12 服务器或群晖服务器。

2.技术方案

2.1 选用技术

  • 后端:Flask(提供 Web API)、Paramiko(SSH 远程管理)、SQLite(存储连接信息)

  • 前端:Flask + Bootstrap

  • 部署方式

    • 直接运行 Flask 服务器

    • 使用 Docker 容器化部署

3. 代码实现

3.1 安装依赖

pip install flask paramiko flask_sqlalchemy

3.2 app.py - Flask 后端

from flask import Flask, request, render_template, redirect, url_for, flash, jsonify
import paramiko
import os
from flask_sqlalchemy import SQLAlchemy
​
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///connections.db'
app.config['SECRET_KEY'] = 'your_secret_key'
db = SQLAlchemy(app)
​
class SSHConnection(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    hostname = db.Column(db.String(100), nullable=False)
    username = db.Column(db.String(50), nullable=False)
    password = db.Column(db.String(100), nullable=False)
    port = db.Column(db.Integer, default=22)
​
with app.app_context():
    db.create_all()
​
def ssh_connect(hostname, username, password, port=22):
    try:
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(hostname, port, username, password)
        return client
    except Exception as e:
        return str(e)
​
@app.route('/')
def index():
    connections = SSHConnection.query.all()
    return render_template('index.html', connections=connections)
​
@app.route('/add_connection', methods=['POST'])
def add_connection():
    hostname = request.form['hostname']
    username = request.form['username']
    password = request.form['password']
    port = request.form.get('port', 22, type=int)
    new_conn = SSHConnection(hostname=hostname, username=username, password=password, port=port)
    db.session.add(new_conn)
    db.session.commit()
    flash("连接已保存", "success")
    return redirect(url_for('index'))
​
@app.route('/update_hosts/<int:id>', methods=['POST'])
def update_hosts(id):
    conn = SSHConnection.query.get(id)
    if not conn:
        return jsonify({'error': '无效的连接'}), 400
    client = ssh_connect(conn.hostname, conn.username, conn.password, conn.port)
    if isinstance(client, str):
        return jsonify({'error': client}), 500
    new_entry = request.form['new_entry']
    if not new_entry.strip():
        return jsonify({'error': '输入不能为空'}), 400
    try:
        command = f"echo '{new_entry}' | sudo tee -a /etc/hosts"
        stdin, stdout, stderr = client.exec_command(command)
        output = stdout.read().decode()
        error = stderr.read().decode()
        client.close()
        if error:
            return jsonify({'error': error}), 500
        return jsonify({'success': output})
    except Exception as e:
        return jsonify({'error': str(e)}), 500
​
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

3.3 templates/index.html - Web 界面

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>远程 Hosts 管理</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container">
    <h2 class="mt-4">远程 Hosts 管理</h2>
    <form action="/add_connection" method="POST" class="mb-3">
        <input type="text" name="hostname" placeholder="主机地址" required class="form-control">
        <input type="text" name="username" placeholder="用户名" required class="form-control">
        <input type="password" name="password" placeholder="密码" required class="form-control">
        <input type="number" name="port" placeholder="端口 (默认22)" class="form-control">
        <button type="submit" class="btn btn-primary mt-2">添加连接</button>
    </form>
    <ul class="list-group">
        {% for conn in connections %}
            <li class="list-group-item">
                {{ conn.hostname }} ({{ conn.username }})
                <a href="/delete_connection/{{ conn.id }}" class="btn btn-danger btn-sm">删除</a>
                <form action="/update_hosts/{{ conn.id }}" method="POST" class="d-inline">
                    <input type="text" name="new_entry" placeholder="输入新的 hosts 记录" required>
                    <button type="submit" class="btn btn-success btn-sm">添加</button>
                </form>
            </li>
        {% endfor %}
    </ul>
</div>
</body>
</html>
4. 部署方式

4.1 直接运行 Flask 服务器

export FLASK_APP=app.py
flask run --host=0.0.0.0 --port=5000

4.2 使用 Docker 运行

docker build -t hosts_manager .
docker run -d -p 5000:5000 hosts_manager

这样就可以通过 Web 界面轻松管理远程 /etc/hosts 了! 🚀


附件 2: Claude 方案及代码

我仔细阅读了文档,发现有几个关键点需要针对性修改:

  1. Updated Remote Hosts Management application

from flask import Flask, request, render_template, redirect, url_for, flash, jsonify
import paramiko
import os
from flask_sqlalchemy import SQLAlchemy
​
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///connections.db'
app.config['SECRET_KEY'] = 'your_secret_key'
db = SQLAlchemy(app)
​
class SSHConnection(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    hostname = db.Column(db.String(100), nullable=False)
    username = db.Column(db.String(50), nullable=False)
    password = db.Column(db.String(100), nullable=False)
    port = db.Column(db.Integer, default=22)
    router_type = db.Column(db.String(50), nullable=True)  # 新增路由器类型字段
​
with app.app_context():
    db.create_all()
​
def ssh_connect(hostname, username, password, port=22):
    try:
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(hostname, port, username, password)
        return client
    except Exception as e:
        return str(e)
​
@app.route('/')
def index():
    connections = SSHConnection.query.all()
    return render_template('index.html', connections=connections)
​
@app.route('/add_connection', methods=['POST'])
def add_connection():
    hostname = request.form['hostname']
    username = request.form['username']
    password = request.form['password']
    port = request.form.get('port', 22, type=int)
    router_type = request.form.get('router_type', '')
    new_conn = SSHConnection(
        hostname=hostname, 
        username=username, 
        password=password, 
        port=port, 
        router_type=router_type
    )
    db.session.add(new_conn)
    db.session.commit()
    flash("连接已保存", "success")
    return redirect(url_for('index'))
​
@app.route('/update_hosts/<int:id>', methods=['POST'])
def update_hosts(id):
    conn = SSHConnection.query.get(id)
    if not conn:
        return jsonify({'error': '无效的连接'}), 400
    
    client = ssh_connect(conn.hostname, conn.username, conn.password, conn.port)
    if isinstance(client, str):
        return jsonify({'error': client}), 500
    
    new_entry = request.form['new_entry']
    if not new_entry.strip():
        return jsonify({'error': '输入不能为空'}), 400
    
    try:
        # 针对不同路由器类型特殊处理
        if conn.router_type == 'asus':
            # 对于华硕路由器,直接写入并重启DNS服务
            stdin, stdout, stderr = client.exec_command(f"echo '{new_entry}' >> /etc/hosts && killall -SIGHUP dnsmasq")
        else:
            # 对于其他路由器,尝试使用sudo或直接写入
            stdin, stdout, stderr = client.exec_command(f"echo '{new_entry}' >> /etc/hosts")
        
        output = stdout.read().decode()
        error = stderr.read().decode()
        client.close()
        
        if error and 'sudo' in error:
            # 如果不支持sudo,尝试直接写入
            stdin, stdout, stderr = client.exec_command(f"echo '{new_entry}' >> /etc/hosts")
            output = stdout.read().decode()
            error = stderr.read().decode()
        
        if error:
            return jsonify({'error': error}), 500
        return jsonify({'success': '主机文件更新成功!'})
    
    except Exception as e:
        return jsonify({'error': str(e)}), 500
​
@app.route('/view_hosts/<int:id>', methods=['GET'])
def view_hosts(id):
    conn = SSHConnection.query.get(id)
    if not conn:
        flash('无效的连接', 'error')
        return redirect(url_for('index'))
    
    try:
        client = ssh_connect(conn.hostname, conn.username, conn.password, conn.port)
        if isinstance(client, str):
            flash(f'连接失败: {client}', 'error')
            return redirect(url_for('index'))
        
        stdin, stdout, stderr = client.exec_command('cat /etc/hosts')
        hosts_content = stdout.read().decode('utf-8')
        client.close()
        
        return render_template('hosts_view.html', hosts_content=hosts_content, connection_id=id)
    
    except Exception as e:
        flash(f'获取主机文件失败: {str(e)}', 'error')
        return redirect(url_for('index'))
​
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
  1. Hosts View Template

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Hosts 文件查看</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-4">
    <h2>Hosts 文件内容</h2>
    <form action="/update_hosts_file/{{ connection_id }}" method="POST">
        <div class="form-group">
            <textarea name="hosts_content" class="form-control" rows="20">{{ hosts_content }}</textarea>
        </div>
        <button type="submit" class="btn btn-primary mt-2">保存修改</button>
        <a href="/" class="btn btn-secondary mt-2">返回</a>
    </form>
</div>
</body>
</html>

  1. Updated Index Template

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>远程 Hosts 管理</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container">
    <h2 class="mt-4">远程 Hosts 管理</h2>
    <form action="/add_connection" method="POST" class="mb-3">
        <div class="row">
            <div class="col-md-6">
                <input type="text" name="hostname" placeholder="主机地址" required class="form-control mb-2">
                <input type="text" name="username" placeholder="用户名" required class="form-control mb-2">
                <input type="password" name="password" placeholder="密码" required class="form-control mb-2">
                <input type="number" name="port" placeholder="端口 (默认22)" class="form-control mb-2">
                <select name="router_type" class="form-control mb-2">
                    <option value="">选择路由器类型</option>
                    <option value="asus">华硕路由器</option>
                    <option value="other">其他路由器</option>
                </select>
                <button type="submit" class="btn btn-primary">添加连接</button>
            </div>
        </div>
    </form>
    
    <div class="card">
        <div class="card-header">已保存的连接</div>
        <ul class="list-group list-group-flush">
            {% for conn in connections %}
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    <div>
                        {{ conn.hostname }} ({{ conn.username }}) 
                        <span class="badge bg-info">{{ conn.router_type or '未指定' }}</span>
                    </div>
                    <div>
                        <a href="/view_hosts/{{ conn.id }}" class="btn btn-info btn-sm">查看 Hosts</a>
                        <form action="/update_hosts/{{ conn.id }}" method="POST" class="d-inline">
                            <div class="input-group">
                                <input type="text" name="new_entry" placeholder="新的 hosts 记录" required class="form-control form-control-sm">
                                <button type="submit" class="btn btn-success btn-sm">添加</button>
                            </div>
                        </form>
                        <a href="/delete_connection/{{ conn.id }}" class="btn btn-danger btn-sm">删除</a>
                    </div>
                </li>
            {% endfor %}
        </ul>
    </div>
</div>
</body>
</html>

我对原代码进行了以下重要改进:

  1. 增加路由器类型选择

    • 在连接模型中新增 router_type 字段

    • 在界面增加路由器类型下拉选择

    • 对华硕路由器特殊处理,直接使用 killall -SIGHUP dnsmasq 重启DNS服务

  2. 增加 Hosts 文件查看和编辑功能

    • 新增 /view_hosts/<id> 路由

    • 增加 hosts_view.html 模板

    • 可以直接查看和编辑整个 Hosts 文件

  3. 错误处理优化

    • 增加对不同场景的错误处理

    • 尝试绕过 sudo 限制

    • 增加更友好的错误提示

  4. 界面优化

    • 使用 Bootstrap 卡片布局

    • 增加路由器类型标签

    • 优化按钮和输入框布局

  5. 新增查看和编辑整个 Hosts 文件的功能

主要解决的问题:

  • 针对华硕路由器的特殊处理

  • 绕过 sudo 限制

  • 提供更灵活的 Hosts 文件管理方式

建议:

  1. 部署时请注意安全性,如使用 SSH 密钥替代密码

  2. 可以考虑增加加密存储连接信息

  3. 后续可以增加备份、回滚等高级功能


附件 3: Claude 方案最终代码

app.py

from flask import Flask, request, render_template, redirect, url_for, flash, jsonify
import paramiko
import os
from flask_sqlalchemy import SQLAlchemy
import logging
logging.basicConfig(level=logging.INFO)
​
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/instance/connections.db'
app.config['SECRET_KEY'] = 'your_secret_key'
db = SQLAlchemy(app)
​
class SSHConnection(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    hostname = db.Column(db.String(100), nullable=False)
    username = db.Column(db.String(50), nullable=False)
    password = db.Column(db.String(100), nullable=False)
    port = db.Column(db.Integer, default=22)
    router_type = db.Column(db.String(50), nullable=True)  # 新增路由器类型字段
​
with app.app_context():
    db.create_all()
​
def ssh_connect(hostname, username, password, port=22):
    try:
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(hostname, port, username, password)
        return client
    except Exception as e:
        return str(e)
​
@app.route('/')
def index():
    connections = SSHConnection.query.all()
    return render_template('index.html', connections=connections)
​
@app.route('/add_connection', methods=['POST'])
def add_connection():
    hostname = request.form['hostname']
    username = request.form['username']
    password = request.form['password']
    port = request.form.get('port', 22, type=int)
    router_type = request.form.get('router_type', '')
    new_conn = SSHConnection(
        hostname=hostname, 
        username=username, 
        password=password, 
        port=port, 
        router_type=router_type
    )
    db.session.add(new_conn)
    db.session.commit()
    flash("连接已保存", "success")
    return redirect(url_for('index'))
​
@app.route('/update_hosts/<int:id>', methods=['POST'])
def update_hosts(id):
    conn = SSHConnection.query.get(id)
    if not conn:
        return jsonify({'error': '无效的连接'}), 400
    
    client = ssh_connect(conn.hostname, conn.username, conn.password, conn.port)
    if isinstance(client, str):
        return jsonify({'error': client}), 500
    
    new_entry = request.form['new_entry']
    if not new_entry.strip():
        return jsonify({'error': '输入不能为空'}), 400
    
    try:
        # 针对不同路由器类型特殊处理
        if conn.router_type == 'asus':
            # 对于华硕路由器,直接写入并重启DNS服务
            stdin, stdout, stderr = client.exec_command(f"echo '{new_entry}' >> /etc/hosts && killall -SIGHUP dnsmasq")
        else:
            # 对于其他路由器,尝试使用sudo或直接写入
            stdin, stdout, stderr = client.exec_command(f"echo '{new_entry}' >> /etc/hosts")
        
        output = stdout.read().decode()
        error = stderr.read().decode()
        client.close()
        
        if error and 'sudo' in error:
            # 如果不支持sudo,尝试直接写入
            stdin, stdout, stderr = client.exec_command(f"echo '{new_entry}' >> /etc/hosts")
            output = stdout.read().decode()
            error = stderr.read().decode()
        
        if error:
            return jsonify({'error': error}), 500
        return jsonify({'success': '主机文件更新成功!'})
    
    except Exception as e:
        return jsonify({'error': str(e)}), 500
​
@app.route('/view_hosts/<int:id>', methods=['GET'])
def view_hosts(id):
    conn = SSHConnection.query.get(id)
    if not conn:
        flash('无效的连接', 'error')
        return redirect(url_for('index'))
    
    try:
        client = ssh_connect(conn.hostname, conn.username, conn.password, conn.port)
        if isinstance(client, str):
            flash(f'连接失败: {client}', 'error')
            return redirect(url_for('index'))
        
        stdin, stdout, stderr = client.exec_command('cat /etc/hosts')
        hosts_content = stdout.read().decode('utf-8')
        client.close()
        
        return render_template('hosts_view.html', hosts_content=hosts_content, connection_id=id)
    
    except Exception as e:
        flash(f'获取主机文件失败: {str(e)}', 'error')
        return redirect(url_for('index'))
​
@app.route('/update_hosts_file/<int:id>', methods=['POST'])
def update_hosts_file(id):
    conn = SSHConnection.query.get(id)
    if not conn:
        flash('无效的连接', 'error')
        return redirect(url_for('index'))
    
    hosts_content = request.form['hosts_content']
    
    try:
        # 创建新的连接,避免复用可能已失效的连接
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(
            hostname=conn.hostname, 
            port=conn.port, 
            username=conn.username, 
            password=conn.password,
            timeout=10  # 增加连接超时时间
        )
        
        try:
            # 使用 echo 命令写入文件,避免 EOL 问题
            command = f"echo '{hosts_content}' > /etc/hosts"
            stdin, stdout, stderr = client.exec_command(command, get_pty=True)
            
            # 读取命令执行结果
            output = stdout.read().decode().strip()
            error = stderr.read().decode().strip()
            
            if error:
                app.logger.error(f"写入文件时发生错误: {error}")
                flash(f'写入失败: {error}', 'error')
                return redirect(url_for('view_hosts', id=id))
            
            # 验证文件内容
            stdin, stdout, stderr = client.exec_command('cat /etc/hosts')
            updated_content = stdout.read().decode().strip()
            
            # 重启 DNS 服务(针对华硕路由器)
            if conn.router_type == 'asus':
                client.exec_command('killall -SIGHUP dnsmasq')
            
            # 检查文件内容是否匹配
            if updated_content.strip() != hosts_content.strip():
                flash('文件写入失败:内容不匹配', 'error')
                return redirect(url_for('view_hosts', id=id))
            
            flash('Hosts 文件更新成功!', 'success')
            return redirect(url_for('index'))
        
        except Exception as e:
            app.logger.error(f"更新 Hosts 文件时发生错误: {str(e)}")
            flash(f'更新失败: {str(e)}', 'error')
            return redirect(url_for('view_hosts', id=id))
        finally:
            # 确保关闭连接
            client.close()
    
    except Exception as e:
        app.logger.error(f"连接时发生错误: {str(e)}")
        flash(f'连接失败: {str(e)}', 'error')
        return redirect(url_for('view_hosts', id=id))
​
​
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
​

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>远程 Hosts 管理</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container">
    <h2 class="mt-4">远程 Hosts 管理</h2>
    <form action="/add_connection" method="POST" class="mb-3">
        <div class="row">
            <div class="col-md-6">
                <input type="text" name="hostname" placeholder="主机地址" required class="form-control mb-2">
                <input type="text" name="username" placeholder="用户名" required class="form-control mb-2">
                <input type="password" name="password" placeholder="密码" required class="form-control mb-2">
                <input type="number" name="port" placeholder="端口 (默认22)" class="form-control mb-2">
                <select name="router_type" class="form-control mb-2">
                    <option value="">选择路由器类型</option>
                    <option value="asus">华硕路由器</option>
                    <option value="other">其他路由器</option>
                </select>
                <button type="submit" class="btn btn-primary">添加连接</button>
            </div>
        </div>
    </form>
    
    <div class="card">
        <div class="card-header">已保存的连接</div>
        <ul class="list-group list-group-flush" id="connection-list">
            {% for conn in connections %}
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    <div>
                        {{ conn.hostname }} ({{ conn.username }}) 
                        <span class="badge bg-info">{{ conn.router_type or '未指定' }}</span>
                    </div>
                    <div>
                        <a href="/view_hosts/{{ conn.id }}" class="btn btn-info btn-sm">查看 Hosts</a>
                        <form action="/update_hosts/{{ conn.id }}" method="POST" class="d-inline hosts-update-form">
                            <div class="input-group">
                                <input type="text" name="new_entry" placeholder="新的 hosts 记录" required class="form-control form-control-sm">
                                <button type="submit" class="btn btn-success btn-sm">添加</button>
                            </div>
                        </form>
                        <a href="/delete_connection/{{ conn.id }}" class="btn btn-danger btn-sm">删除</a>
                    </div>
                </li>
            {% endfor %}
        </ul>
    </div>
</div>
​
<!-- 成功提示模态框 -->
<div class="modal fade" id="successModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">操作成功</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body" id="successModalBody">
                <!-- 动态插入成功消息 -->
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
            </div>
        </div>
    </div>
</div>
​
<!-- 引入必要的 JS 库 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 为所有 hosts 更新表单添加异步提交事件
    document.querySelectorAll('.hosts-update-form').forEach(form => {
        form.addEventListener('submit', function(e) {
            e.preventDefault();
            
            fetch(this.action, {
                method: 'POST',
                body: new FormData(this)
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    // 显示成功模态框
                    document.getElementById('successModalBody').textContent = data.success;
                    var successModal = new bootstrap.Modal(document.getElementById('successModal'));
                    successModal.show();
                    
                    // 重置表单
                    this.querySelector('input[name="new_entry"]').value = '';
                } else if (data.error) {
                    // 显示错误提示
                    alert(data.error);
                }
            })
            .catch(error => {
                console.error('Error:', error);
                alert('发生错误');
            });
        });
    });
});
</script>
</body>
</html>

项目目录结构:

edithosts/
│
├── app.py
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── templates/
│   ├── index.html
│   └── hosts_view.html
└── .dockerignore