ollama网页AI聊天
·
这是一个使用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)
更多推荐


所有评论(0)