这是一个使用ollama的本地网页AI,它可以解决你的大部分问题,为了很好的游览,请去

  • 下载页https://ollama.com/download
  • 模型库(可浏览所有可用模型):https://ollama.com/library
  • GitHub(开源地址):https://github.com/ollama/ollama
  • 下载ollama
  • 以下是代码:
    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """
    Ollama AI Chat Server - 修复版
    """
     
    import os, sys, json, time, hashlib, sqlite3, secrets, socket, threading
    from datetime import datetime
    from functools import wraps
    from typing import Dict, List, Tuple
     
    try:
        from flask import Flask, request, jsonify, session, render_template_string, redirect
        import requests
    except ImportError:
        os.system(sys.executable + " -m pip install flask requests -q")
        from flask import Flask, request, jsonify, session, render_template_string, redirect
        import requests
     
    # 配置
    CONFIG = {
        "HOST": "0.0.0.0",
        "PORT": 5000,
        "SECRET_KEY": secrets.token_hex(32),
        "OLLAMA_BASE_URL": "http://localhost:11434",
        "OLLAMA_MODEL": "qwen2.5:0.8b",
        "ADMIN_USERNAME": "admin",
        "ADMIN_PASSWORD": "admin123",
        "DB_PATH": "chat_server.db",
        "CREDITS_PER_REQUEST": 10,
        "DAILY_FREE_CREDITS": 2000,
    }
     
    app = Flask(__name__)
    app.secret_key = CONFIG["SECRET_KEY"]
     
    # 数据库类
    class Database:
        def __init__(self, db_path):
            self.db_path = db_path
            self.init_db()
     
        def get_conn(self):
            return sqlite3.connect(self.db_path, check_same_thread=False)
     
        def init_db(self):
            conn = self.get_conn()
            c = conn.cursor()
            c.executescript("""
                CREATE TABLE IF NOT EXISTS users (
                    id INTEGER PRIMARY KEY, username TEXT UNIQUE, password_hash TEXT,
                    credits INTEGER DEFAULT 2000, is_admin INTEGER DEFAULT 0, is_kicked INTEGER DEFAULT 0,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                );
                CREATE TABLE IF NOT EXISTS credit_logs (
                    id INTEGER PRIMARY KEY, user_id INTEGER, amount INTEGER, reason TEXT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 
                );
                CREATE TABLE IF NOT EXISTS kicked_users (
                    id INTEGER PRIMARY KEY, user_id INTEGER, admin_id INTEGER, reason TEXT,
                    kicked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP
                );
            """)
            if not c.execute("SELECT 1 FROM users WHERE username=?", (CONFIG["ADMIN_USERNAME"],)).fetchone():
                c.execute("INSERT INTO users (username,password_hash,credits,is_admin) VALUES(?,?,?,1)",
                         (CONFIG["ADMIN_USERNAME"], hashlib.sha256(CONFIG["ADMIN_PASSWORD"].encode()).hexdigest(), 999999))
            conn.commit()
            conn.close()
     
        def execute(self, q, p=()):
            conn = self.get_conn()
            conn.execute(q, p)
            conn.commit()
            conn.close()
     
        def fetch_one(self, q, p=()):
            conn = self.get_conn()
            c = conn.cursor()
            c.execute(q, p)
            r = c.fetchone()
            conn.close()
            return r
     
        def fetch_all(self, q, p=()):
            conn = self.get_conn()
            c = conn.cursor()
            c.execute(q, p)
            r = c.fetchall()
            conn.close()
            return r
     
    db = Database(CONFIG["DB_PATH"])
     
    # 在线用户管理 
    class OnlineManager:
        def __init__(self):
            self._lock = threading.Lock()
            self._users = {}
     
        def add(self, uid, sid, ip):
            with self._lock:
                self._users[uid] = {"user_id": uid, "session_id": sid, "ip": ip, "time": time.time()}
     
        def remove(self, uid=None, sid=None):
            with self._lock:
                if sid:
                    for k, v in list(self._users.items()):
                        if v.get("session_id") == sid:
                            del self._users[k]
                            return True
                if uid and uid in self._users:
                    del self._users[uid]
                    return True
                return False
     
        def get_session_id(self, uid):
            with self._lock:
                for v in self._users.values():
                    if v["user_id"] == uid:
                        return v.get("session_id")
            return None
     
        def count(self):
            with self._lock:
                return len(self._users)
     
        def list(self):
            with self._lock:
                return list(self._users.values())
     
    online = OnlineManager()
     
    # 踢人服务
    class KickService:
        @staticmethod
        def kick_user(user_id, admin_id, reason="违规发言", duration_minutes=0):
            session_id = online.get_session_id(user_id)
            if session_id:
                online.remove(sid=session_id)
            
            if duration_minutes > 0:
                db.execute("""
                    INSERT INTO kicked_users (user_id, admin_id, reason, expires_at) 
                    VALUES (?, ?, ?, datetime('now', '+' || ? || ' minutes'))
                """, (user_id, admin_id, reason, duration_minutes))
            else:
                db.execute("INSERT INTO kicked_users (user_id, admin_id, reason) VALUES(?, ?, ?)",
                           (user_id, admin_id, reason))
            db.execute("UPDATE users SET is_kicked=1 WHERE id=?", (user_id,))
     
        @staticmethod
        def is_kicked(user_id):
            user = db.fetch_one("SELECT is_kicked FROM users WHERE id=?", (user_id,))
            if user and user[0]:
                kick_record = db.fetch_one("""
                    SELECT reason FROM kicked_users 
                    WHERE user_id=? AND (expires_at IS NULL OR expires_at > datetime('now'))
                    ORDER BY kicked_at DESC LIMIT 1
                """, (user_id,))
                reason = kick_record[0] if kick_record else "管理员踢出"
                return (True, reason)
            return (False, "")
     
        @staticmethod
        def pardon_user(user_id):
            db.execute("UPDATE users SET is_kicked=0 WHERE id=?", (user_id,))
            db.execute("DELETE FROM kicked_users WHERE user_id=?", (user_id,))
     
    kick_service = KickService()
     
    # Ollama
    class Ollama:
        def __init__(self, base, model):
            self.base = base.rstrip("/")
            self.model = model
     
        def generate(self, prompt):
            try:
                r = requests.post(self.base + "/api/generate",
                                 json={"model": self.model, "prompt": prompt, "stream": False},
                                 timeout=120)
                if r.ok:
                    return {"success": True, "response": r.json().get("response", "")}
                return {"success": False, "error": "请求失败"}
            except Exception as e:
                return {"success": False, "error": str(e)}
     
    ollama = Ollama(CONFIG["OLLAMA_BASE_URL"], CONFIG["OLLAMA_MODEL"])
     
    # 工具函数
    def hash_pw(p): return hashlib.sha256(p.encode()).hexdigest()
    def verify_pw(p, h): return hash_pw(p) == h
    def get_user(): 
        if "user_id" in session:
            u = db.fetch_one("SELECT id,username,credits,is_admin FROM users WHERE id=?", (session["user_id"],))
            if u:
                return {"id":u[0],"username":u[1],"credits":u[2],"is_admin":bool(u[3])}
        return None
     
    def login_required(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            if "user_id" not in session:
                return redirect("/") if not request.path.startswith("/api/") else jsonify({"success": False}), 401
            uid = session["user_id"]
            is_kicked, reason = kick_service.is_kicked(uid)
            if is_kicked:
                online.remove(sid=session.get("sid"))
                session.clear()
                return redirect("/?kicked=1") if not request.path.startswith("/api/") else jsonify({"success": False, "kicked": True}), 403
            return f(*args, **kwargs)
        return decorated
     
    def admin_required(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            if "user_id" not in session or not session.get("is_admin"):
                return redirect("/admin") if not request.path.startswith("/api/") else jsonify({"success": False}), 403
            return f(*args, **kwargs)
        return decorated
     
    def get_local_ip():
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
            s.close()
            return ip
        except:
            return "127.0.0.1"
     
    def deduct_credits(uid, amount):
        u = db.fetch_one("SELECT credits FROM users WHERE id=?", (uid,))
        if u and u[0] >= amount:
            db.execute("UPDATE users SET credits=credits-? WHERE id=?", (amount, uid))
            db.execute("INSERT INTO credit_logs(user_id,amount,reason) VALUES(?,?,?)", (uid, -amount, "AI对话"))
            return True
        return False
     
    # ====== HTML模板 ======
    HTML = """<!DOCTYPE html>
    <html lang="zh">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>{title}</title>
    <style>
    * {{ margin:0; padding:0; box-sizing:border-box; }}
    body {{ font-family:system-ui,-apple-system,sans-serif; background:linear-gradient(135deg,#667eea,#764ba2); min-height:100vh; display:flex; justify-content:center; align-items:center; padding:20px; }}
    .container {{ width:100%; max-width:900px; }}
    .card {{ background:white; border-radius:20px; box-shadow:0 20px 60px rgba(0,0,0,0.3); overflow:hidden; }}
    .nav {{ background:#343a40; color:white; padding:15px 20px; display:flex; justify-content:space-between; align-items:center; }}
    .nav a {{ color:white; text-decoration:none; padding:5px 10px; border-radius:5px; }}
    .nav a:hover {{ background:rgba(255,255,255,0.1); }}
    .header {{ background:linear-gradient(135deg,#667eea,#764ba2); color:white; padding:30px; text-align:center; }}
    .content {{ padding:40px; }}
    .form-group {{ margin-bottom:20px; }}
    .form-group label {{ display:block; margin-bottom:8px; font-weight:600; }}
    .form-group input {{ width:100%; padding:12px; border:2px solid #e0e0e0; border-radius:10px; font-size:16px; }}
    .form-group input:focus {{ border-color:#667eea; outline:none; }}
    .btn {{ width:100%; padding:15px; border:none; border-radius:10px; font-size:16px; font-weight:600; cursor:pointer; transition:transform 0.2s; }}
    .btn:hover {{ transform:translateY(-2px); }}
    .btn-primary {{ background:linear-gradient(135deg,#667eea,#764ba2); color:white; }}
    .btn-green {{ background:linear-gradient(135deg,#11998e,#38ef7d); color:white; }}
    .btn-red {{ background:#dc3545; color:white; }}
    .btn-yellow {{ background:#ffc107; color:#000; }}
    .btn-secondary {{ background:#6c757d; color:white; }}
    .btn-sm {{ padding:8px 15px; width:auto; font-size:14px; }}
    .text-center {{ text-align:center; }}
    .mt-3 {{ margin-top:15px; }}
    .credits {{ background:linear-gradient(135deg,#f093fb,#f5576c); padding:5px 12px; border-radius:15px; font-size:12px; color:white; }}
    .chat-box {{ display:flex; flex-direction:column; height:75vh; }}
    .chat-header {{ display:flex; justify-content:space-between; padding:15px 20px; background:#f8f9fa; border-bottom:1px solid #dee2e6; }}
    .chat-msgs {{ flex:1; overflow-y:auto; padding:20px; background:#f8f9fa; }}
    .msg {{ margin-bottom:15px; padding:15px; border-radius:15px; max-width:80%; word-wrap:break-word; }}
    .msg-bot {{ background:linear-gradient(135deg,#667eea,#764ba2); color:white; margin-left:auto; }}
    .msg-user {{ background:white; border:1px solid #dee2e6; margin-right:auto; }}
    .msg-time {{ font-size:11px; opacity:0.7; margin-top:5px; }}
    .chat-input {{ display:flex; gap:10px; padding:20px; background:white; border-top:1px solid #dee2e6; }}
    .chat-input textarea {{ flex:1; padding:12px; border:2px solid #e0e0e0; border-radius:10px; resize:none; font-size:16px; }}
    .chat-input textarea:focus {{ border-color:#667eea; outline:none; }}
    .send-btn {{ padding:12px 25px; background:linear-gradient(135deg,#667eea,#764ba2); color:white; border:none; border-radius:10px; font-weight:600; cursor:pointer; }}
    .send-btn:disabled {{ opacity:0.5; cursor:not-allowed; }}
    .user-item {{ display:flex; justify-content:space-between; align-items:center; padding:15px; background:white; border-radius:10px; margin-bottom:10px; box-shadow:0 2px 10px rgba(0,0,0,0.1); }}
    .stats {{ display:grid; grid-template-columns:repeat(3,1fr); gap:15px; margin:20px 0; }}
    .stat {{ background:linear-gradient(135deg,#667eea,#764ba2); color:white; padding:20px; border-radius:15px; text-align:center; }}
    .stat h3 {{ font-size:2em; margin:0; }}
    .stat p {{ margin:5px 0 0; opacity:0.8; }}
    .loading {{ display:inline-block; width:20px; height:20px; border:3px solid rgba(255,255,255,.3); border-radius:50%; border-top-color:#fff; animation:spin 1s linear infinite; }}
    @keyframes spin {{ to{{transform:rotate(360deg);}} }}
    .alert {{ padding:15px; border-radius:10px; margin-bottom:20px; text-align:center; }}
    .alert-warning {{ background:#fff3cd; border:1px solid #ffc107; color:#856404; }}
    .alert-danger {{ background:#f8d7da; border:1px solid #f5c6cb; color:#721c24; }}
    .link {{ color:#667eea; text-decoration:none; }}
    .link:hover {{ text-decoration:underline; }}
    .table {{ width:100%; border-collapse:collapse; margin-top:15px; }}
    .table th, .table td {{ padding:12px; border-bottom:1px solid #dee2e6; text-align:left; }}
    .table th {{ background:#f8f9fa; font-weight:600; }}
    .table-striped tbody tr:nth-of-type(odd) {{ background:#f8f9fa; }}
    .badge {{ padding:3px 8px; border-radius:5px; font-size:11px; color:white; }}
    .badge-success {{ background:#28a745; }}
    .badge-danger {{ background:#dc3545; }}
    .badge-warning {{ background:#ffc107; color:#000; }}
    .badge-secondary {{ background:#6c757d; }}
    .text-muted {{ color:#6c757d; font-size:12px; }}
    .d-flex {{ display:flex; }}
    .gap-2 {{ gap:10px; }}
    .mb-3 {{ margin-bottom:15px; }}
    .mb-4 {{ margin-bottom:20px; }}
    </style>
    </head>
    <body>
    <div class="container">
    <div class="card">
    {nav}
    {content}
    </div>
    </div>
    </body>
    </html>"""
     
    def page(title, content, nav=""):
        return HTML.format(title=title, content=content, nav=nav)
     
    def get_nav():
        u = get_user()
        if not u: return ""
        admin_links = '<a href="/admin">管理</a><a href="/admin/kick">踢人</a>' if u["is_admin"] else ""
        return '<div class="nav"><div style="display:flex;align-items:center;gap:10px;"><strong>' + u["username"] + '</strong><span class="credits">' + str(u["credits"]) + ' 积分</span></div><div style="display:flex;gap:10px;"><a href="/chat">聊天</a>' + admin_links + '<a href="/claim">领积分</a><a href="/logout">退出</a></div></div>'
     
    # ====== 路由 ======
    @app.route("/")
    def index():
        if "user_id" in session: return redirect("/chat")
        return page("登录", '<div class="header"><h1>🤖 Ollama AI Chat</h1></div><div class="content"><h2 class="text-center mt-3">用户登录</h2><form action="/api/login" method="POST"><div class="form-group"><label>用户名</label><input type="text" name="username" required></div><div class="form-group"><label>密码</label><input type="password" name="password" required></div><button type="submit" class="btn btn-primary">登录</button></form><p class="text-center mt-3">没有账号? <a href="/register" class="link">立即注册</a></p><p class="text-center text-muted">管理员: ' + CONFIG["ADMIN_USERNAME"] + ' / ' + CONFIG["ADMIN_PASSWORD"] + '</p></div>')
     
    @app.route("/register")
    def register():
        return page("注册", '<div class="header"><h1>📝 新用户注册</h1></div><div class="content"><form id="regForm"><div class="form-group"><label>用户名</label><input type="text" name="username" id="un" required></div><div class="form-group"><label>密码</label><input type="password" name="password" id="pw" required></div><div class="form-group"><label>确认密码</label><input type="password" name="confirm" id="cpw" required></div><div id="err" style="color:#dc3545;display:none;"></div><button type="submit" class="btn btn-primary">注册</button></form></div><script>document.getElementById("regForm").onsubmit=async function(e){e.preventDefault();var err=document.getElementById("err");if(document.getElementById("pw").value!==document.getElementById("cpw").value){err.textContent="两次密码不一致";err.style.display="block";return;}var r=await fetch("/api/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:document.getElementById("un").value,password:document.getElementById("pw").value})});var d=await r.json();if(d.success){alert("注册成功!");location.href="/";}else{err.textContent=d.message;err.style.display="block";}}</script>')
     
    @app.route("/chat")
    @login_required
    def chat():
        u = get_user()
        return page("聊天", '<div class="chat-box"><div class="chat-header"><div><strong>' + u["username"] + '</strong> <span class="credits" id="hdrCredits">' + str(u["credits"]) + ' 积分</span></div><div><span style="color:#28a745;">●</span> <span id="onlineCount">0</span> 人在线</div></div><div class="chat-msgs" id="msgs"><div class="msg msg-bot"><div>👋 你好!我是AI助手</div></div></div><div class="chat-input"><textarea id="inp" rows="2" placeholder="输入消息..."></textarea><button class="send-btn" id="snd" onclick="send()">发送</button></div></div><div id="kickModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.8);z-index:9999;justify-content:center;align-items:center;"><div style="background:white;padding:40px;border-radius:15px;text-align:center;"><h2>🚫 您已被踢出</h2><p id="kickReason"></p><button onclick="location.href=\'/\'" style="padding:10px 20px;background:#667eea;color:white;border:none;border-radius:5px;cursor:pointer;">返回登录</button></div></div><script>var tm=function(){var d=new Date();return(d.getHours()<10?"0":"")+d.getHours()+":"+(d.getMinutes()<10?"0":"")+d.getMinutes();};function addMsg(text,isBot){var div=document.createElement("div");div.className="msg "+(isBot?"msg-bot":"msg-user");div.innerHTML="<div>"+text+"</div><div class=msg-time>"+tm()+"</div>";document.getElementById("msgs").appendChild(div);document.getElementById("msgs").scrollTop=99999;}async function send(){var inp=document.getElementById("inp");var msg=inp.value.trim();if(!msg)return;addMsg(msg,false);inp.value="";var btn=document.getElementById("snd");btn.disabled=true;btn.innerHTML="<span class=loading></span>";try{var r=await fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:msg})});var d=await r.json();if(d.kicked){document.getElementById("kickModal").style.display="flex";return;}if(d.success){addMsg(d.response,true);document.getElementById("hdrCredits").textContent=d.credits+" 积分";}else{addMsg("❌ "+d.message,true);}}catch(e){addMsg("❌ 网络错误",true);}btn.disabled=false;btn.textContent="发送";}document.getElementById("inp").onkeydown=function(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();send();}};setInterval(function(){try{fetch("/api/check-kicked").then(function(r){return r.json();}).then(function(d){if(d.is_kicked){document.getElementById("kickModal").style.display="flex";}});}catch(e){}},3000);</script>', get_nav())
     
    @app.route("/claim")
    @login_required
    def claim():
        u = get_user()
        return page("领积分", '<div class="header"><h1>💰 每日积分领取</h1></div><div class="content text-center"><h2>当前: ' + str(u["credits"]) + ' 积分</h2><button class="btn btn-green mb-4" id="claimBtn" onclick="claimDaily()">立即领取 2000 积分</button></div><script>async function claimDaily(){var btn=document.getElementById("claimBtn");btn.disabled=true;var d=await(await fetch("/api/claim",{method:"POST"})).json();if(d.success){alert("领取成功!");location.reload();}else{btn.textContent=d.message;btn.disabled=false;}}</script>', get_nav())
     
    @app.route("/admin")
    @admin_required
    def admin():
        users = online.list()
        user_html = ""
        for u in users:
            udb = db.fetch_one("SELECT username,is_admin FROM users WHERE id=?", (u["user_id"],))
            if udb:
                uid = u["user_id"]
                uname = udb[0].replace("'", "\\'")
                if udb[1]:
                    user_html += '<div class="user-item"><div><strong>' + udb[0] + '</strong> <span class="badge badge-secondary">管理员</span></div><span class="text-muted">本人</span></div>'
                else:
                    user_html += '<div class="user-item"><div><strong>' + udb[0] + '</strong> <span class="badge badge-success">在线</span></div><button class="btn btn-red btn-sm" onclick="kickUser(' + str(uid) + ',\'' + uname + '\')">踢出</button></div>'
        if not user_html: user_html = '<p class="text-center text-muted">暂无在线用户</p>'
        total = db.fetch_one("SELECT COUNT(*) FROM users")[0]
        return page("管理", '<div class="header"><h1>⚙️ 管理面板</h1></div><div class="content"><div class="stats"><div class="stat"><h3>' + str(len(users)) + '</h3><p>在线</p></div><div class="stat"><h3>' + str(total) + '</h3><p>注册</p></div></div><h3>在线用户</h3>' + user_html + '<div class="d-flex gap-2 mt-4"><a href="/admin/kick" class="btn btn-secondary">踢人管理</a><a href="/admin/users" class="btn btn-secondary">用户管理</a></div></div><script>async function kickUser(uid,name){var reason=prompt("踢出原因:")||"违规";var duration=prompt("时长(分钟,0永久):","30");if(duration===null)return;var d=await(await fetch("/api/admin/kick",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:uid,reason:reason,duration:parseInt(duration)})})).json();alert(d.message);if(d.success)location.reload();}</script>', get_nav())
     
    @app.route("/admin/kick")
    @admin_required
    def admin_kick():
        online_users = []
        for u in online.list():
            user_info = db.fetch_one("SELECT username, is_admin FROM users WHERE id=?", (u["user_id"],))
            if user_info:
                online_users.append({"user_id": u["user_id"], "username": user_info[0], "is_admin": bool(user_info[1])})
        
        online_html = ""
        for u in online_users:
            uid = u["user_id"]
            uname = u["username"].replace("'", "\\'")
            if u["is_admin"]:
                online_html += '<div class="user-item"><div><strong>' + u["username"] + '</strong></div><span class="text-muted">管理员</span></div>'
            else:
                online_html += '<div class="user-item" id="user-' + str(uid) + '"><div><strong>' + u["username"] + '</strong></div><div class="d-flex gap-2"><button class="btn btn-yellow btn-sm" onclick="kickTemp(' + str(uid) + ',\'' + uname + '\')">临时踢</button><button class="btn btn-red btn-sm" onclick="kickPermanent(' + str(uid) + ',\'' + uname + '\')">永久封</button></div></div>'
        
        if not online_html: online_html = '<p class="text-center text-muted">暂无在线用户</p>'
        
        return page("踢人管理", '<div class="content"><h2>👮 踢人管理</h2><a href="/admin" class="btn btn-secondary">返回</a><h3 class="mb-3 mt-4">在线用户</h3>' + online_html + '</div><script>async function kickTemp(uid,name){var reason=prompt("临时踢出原因:")||"违规";var duration=prompt("时长(分钟):","30");if(duration===null)return;var d=await(await fetch("/api/admin/kick",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:uid,reason:reason,duration:parseInt(duration)})})).json();alert(d.message);if(d.success)document.getElementById("user-"+uid).remove();}async function kickPermanent(uid,name){if(!confirm("确定永久封禁"+name+"?"))return;var reason=prompt("封禁原因:")||"严重违规";var d=await(await fetch("/api/admin/kick",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:uid,reason:reason,duration:0})})).json();alert(d.message);if(d.success)document.getElementById("user-"+uid).remove();}</script>', get_nav())
     
    @app.route("/admin/users")
    @admin_required
    def admin_users():
        users = db.fetch_all("SELECT id, username, credits, is_admin, is_kicked FROM users")
        rows = ""
        for u in users:
            uid = u[0]; uname = u[1].replace("'", "\\'")
            status = '<span class="badge badge-danger">已封</span>' if u[4] else '<span class="badge badge-success">正常</span>'
            role = '<span class="badge badge-secondary">管理员</span>' if u[3] else ""
            rows += '<tr><td>' + str(uid) + '</td><td><strong>' + u[1] + '</strong> ' + role + '</td><td>' + str(u[2]) + '</td><td>' + status + '</td><td><button class="btn btn-sm btn-red" onclick="manage(' + str(uid) + ',\'' + uname + '\')">管理</button></td></tr>'
        return page("用户管理", '<div class="content"><h2>👥 用户管理</h2><a href="/admin" class="btn btn-secondary">返回</a><table class="table table-striped"><thead><tr><th>ID</th><th>用户</th><th>积分</th><th>状态</th><th>操作</th></tr></thead><tbody>' + rows + '</tbody></table></div><script>async function manage(uid,name){var action=prompt(name+"\\n1:临时踢 2:永久封 3:解封 4:充值","1");if(!action)return;var d;if(action==="1"){var r=prompt("原因:","违规"),t=prompt("时长:","30");d=await(await fetch("/api/admin/kick",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:uid,reason:r,duration:parseInt(t)})})).json();}else if(action==="2"){if(!confirm("永久封禁?"))return;d=await(await fetch("/api/admin/kick",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:uid,reason:"严重违规",duration:0})})).json();}else if(action==="3"){if(!confirm("解封?"))return;d=await(await fetch("/api/admin/pardon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:uid})})).json();}else if(action==="4"){var amt=prompt("充值金额:","1000");d=await(await fetch("/api/admin/recharge",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:uid,amount:parseInt(amt)})})).json();}alert(d.message);if(d.success)location.reload();}</script>', get_nav())
     
    # ====== API接口 ======
    @app.route("/api/login", methods=["POST"])
    def api_login():
        u = request.form.get("username", "").strip()
        p = request.form.get("password", "")
        user = db.fetch_one("SELECT id,password_hash,is_admin,is_kicked FROM users WHERE username=?", (u,))
        if not user or not verify_pw(p, user[1]):
            return '<html><body><script>alert("登录失败");location.href="/";</script></body></html>'
        if user[3]: db.execute("UPDATE users SET is_kicked=0 WHERE id=?", (user[0],))
        is_kicked, reason = kick_service.is_kicked(user[0])
        if is_kicked:
            return '<html><body><script>alert("账号已封禁:' + reason + '");location.href="/";</script></body></html>'
        session.permanent = True
        session["user_id"] = user[0]
        session["username"] = u
        session["is_admin"] = bool(user[2])
        sid = secrets.token_hex(16)
        session["sid"] = sid
        online.add(user[0], sid, request.remote_addr)
        return redirect("/chat")
     
    @app.route("/api/register", methods=["POST"])
    def api_register():
        data = request.get_json()
        u, p = data.get("username", "").strip(), data.get("password", "")
        if len(u) < 3 or not u.replace("_","").isalnum():
            return jsonify({"success": False, "message": "用户名需3位以上字母数字"})
        if len(p) < 6:
            return jsonify({"success": False, "message": "密码至少6位"})
        if db.fetch_one("SELECT 1 FROM users WHERE username=?", (u,)):
            return jsonify({"success": False, "message": "用户名已存在"})
        db.execute("INSERT INTO users(username,password_hash,credits) VALUES(?,?,?)", (u, hash_pw(p), CONFIG["DAILY_FREE_CREDITS"]))
        return jsonify({"success": True})
     
    @app.route("/logout")
    def logout():
        online.remove(sid=session.get("sid"))
        session.clear()
        return redirect("/")
     
    @app.route("/api/chat", methods=["POST"])
    @login_required
    def api_chat():
        u = get_user()
        msg = request.get_json().get("message", "").strip()
        if not msg or u["credits"] < CONFIG["CREDITS_PER_REQUEST"]:
            return jsonify({"success": False, "message": "积分不足"})
        result = ollama.generate(msg)
        if result["success"]:
            deduct_credits(u["id"], CONFIG["CREDITS_PER_REQUEST"])
            return jsonify({"success": True, "response": result["response"], "credits": get_user()["credits"]})
        return jsonify({"success": False, "message": result.get("error", "AI服务不可用")})
     
    @app.route("/api/claim", methods=["POST"])
    @login_required
    def api_claim():
        u = get_user()
        today = datetime.now().strftime("%Y-%m-%d")
        if db.fetch_one("SELECT 1 FROM credit_logs WHERE user_id=? AND reason='每日领取' AND date(created_at)=?", (u["id"], today)):
            return jsonify({"success": False, "message": "今日已领取"})
        db.execute("UPDATE users SET credits=credits+? WHERE id=?", (CONFIG["DAILY_FREE_CREDITS"], u["id"]))
        db.execute("INSERT INTO credit_logs(user_id,amount,reason) VALUES(?,?,?)", (u["id"], CONFIG["DAILY_FREE_CREDITS"], "每日领取"))
        return jsonify({"success": True, "credits": get_user()["credits"]})
     
    @app.route("/api/online_count")
    def api_online_count():
        return jsonify({"count": online.count()})
     
    @app.route("/api/check-kicked")
    @login_required
    def api_check_kicked():
        is_kicked, reason = kick_service.is_kicked(session["user_id"])
        if is_kicked:
            online.remove(sid=session.get("sid"))
            session.clear()
            return jsonify({"is_kicked": True, "reason": reason})
        return jsonify({"is_kicked": False})
     
    @app.route("/api/admin/kick", methods=["POST"])
    @admin_required
    def api_admin_kick():
        data = request.get_json()
        uid, reason, duration = data.get("user_id"), data.get("reason", "违规"), data.get("duration", 0)
        if not uid or uid == session["user_id"]: return jsonify({"success": False, "message": "参数错误"})
        target = db.fetch_one("SELECT username, is_admin FROM users WHERE id=?", (uid,))
        if not target or target[1]: return jsonify({"success": False, "message": "用户不存在或不能踢管理员"})
        kick_service.kick_user(uid, session["user_id"], reason, duration)
        return jsonify({"success": True, "message": "操作成功"})
     
    @app.route("/api/admin/pardon", methods=["POST"])
    @admin_required
    def api_admin_pardon():
        data = request.get_json()
        uid = data.get("user_id")
        if not uid: return jsonify({"success": False, "message": "缺少用户ID"})
        target = db.fetch_one("SELECT username FROM users WHERE id=?", (uid,))
        if not target: return jsonify({"success": False, "message": "用户不存在"})
        kick_service.pardon_user(uid)
        return jsonify({"success": True, "message": "已解除封禁"})
     
    @app.route("/api/admin/recharge", methods=["POST"])
    @admin_required
    def api_admin_recharge():
        data = request.get_json()
        uid, amt = data.get("user_id"), data.get("amount", 0)
        if not uid or amt <= 0: return jsonify({"success": False, "message": "参数错误"})
        target = db.fetch_one("SELECT username FROM users WHERE id=?", (uid,))
        if not target: return jsonify({"success": False, "message": "用户不存在"})
        db.execute("UPDATE users SET credits=credits+? WHERE id=?", (amt, uid))
        db.execute("INSERT INTO credit_logs(user_id,amount,reason) VALUES(?,?,?)", (uid, amt, "管理员充值"))
        return jsonify({"success": True, "message": "充值成功"})
     
    if __name__ == "__main__":
        ip = get_local_ip()
        print("="*50)
        print("  Ollama AI Chat Server")
        print("  地址: http://" + ip + ":" + str(CONFIG["PORT"]))
        print("  管理员: " + CONFIG["ADMIN_USERNAME"] + " / " + CONFIG["ADMIN_PASSWORD"])
        print("="*50)
        app.run(host=CONFIG["HOST"], port=CONFIG["PORT"], debug=False, threaded=True)

Logo

免费领 200 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐