ai_member_xiaoban/scripts/upload_server.py
2026-06-01 08:00:01 +08:00

201 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""简易文件上传服务 - 浏览器打开即可拖拽上传"""
import os
import cgi
import html
from http.server import HTTPServer, BaseHTTPRequestHandler
UPLOAD_DIR = os.path.expanduser("~/.openclaw/workspace-xiaoban/tmp/uploads")
PORT = 18888
HTML = r"""<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传文件到大麦</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;display:flex;align-items:center;justify-content:center}
.container{text-align:center;max-width:600px;padding:40px}
h1{font-size:28px;margin-bottom:8px}
.sub{color:#94a3b8;margin-bottom:32px;font-size:14px}
.dropzone{background:#1e293b;border:2px dashed #475569;border-radius:16px;padding:60px 20px;cursor:pointer;transition:all .2s}
.dropzone:hover,.dropzone.dragover{border-color:#6366f1;background:#1e1b4b}
.dropzone p{font-size:16px;color:#94a3b8}
.dropzone .icon{font-size:48px;margin-bottom:16px}
input[type=file]{display:none}
#progress{display:none;margin-top:16px}
.bar-bg{background:#334155;border-radius:8px;height:8px;overflow:hidden}
.bar{background:linear-gradient(90deg,#6366f1,#a855f7);height:100%;width:0;transition:width .3s}
#result{margin-top:20px;font-size:14px}
.success{color:#4ade80}
.error{color:#f87171}
.recent{margin-top:32px;text-align:left;max-height:200px;overflow-y:auto}
.recent h3{font-size:14px;color:#64748b;margin-bottom:8px}
.recent li{font-size:12px;color:#94a3b8;padding:4px 0;border-bottom:1px solid #1e293b}
</style>
</head>
<body>
<div class="container">
<h1>📤 上传文件到大麦</h1>
<p class="sub">拖拽文件到下方区域 或 点击选择文件</p>
<div class="dropzone" id="dropzone" onclick="document.getElementById('file').click()">
<div class="icon">📁</div>
<p>点击或拖拽文件到此处上传</p>
</div>
<input type="file" id="file" multiple onchange="upload(this.files)">
<div id="progress">
<p id="status">上传中...</p>
<div class="bar-bg"><div class="bar" id="bar"></div></div>
</div>
<div id="result"></div>
<div class="recent" id="recent"><h3>📋 最近上传</h3><ul id="filelist"></ul></div>
</div>
<script>
const dropzone = document.getElementById('dropzone');
['dragenter','dragover','dragleave','drop'].forEach(e => {
dropzone.addEventListener(e, ev => { ev.preventDefault(); ev.stopPropagation(); });
});
['dragenter','dragover'].forEach(e => dropzone.addEventListener(e, () => dropzone.classList.add('dragover')));
['dragleave','drop'].forEach(e => dropzone.addEventListener(e, () => dropzone.classList.remove('dragover')));
dropzone.addEventListener('drop', ev => upload(ev.dataTransfer.files));
async function upload(files){
if(!files.length) return;
const progress = document.getElementById('progress');
const bar = document.getElementById('bar');
const status = document.getElementById('status');
const result = document.getElementById('result');
progress.style.display='block'; bar.style.width='0'; result.innerHTML='';
let ok=0, fail=0;
for(let i=0;i<files.length;i++){
const f = files[i];
status.textContent = `正在上传: ${f.name} (${(f.size/1024/1024).toFixed(1)}MB)...`;
bar.style.width = ((i/files.length)*100)+'%';
const fd = new FormData(); fd.append('file', f);
try{
const r = await fetch('/upload', {method:'POST', body:fd});
const t = await r.text();
if(r.ok){ ok++; } else { fail++; result.innerHTML+=`<div class="error">❌ ${f.name}: ${t}</div>`; }
}catch(e){ fail++; result.innerHTML+=`<div class="error">❌ ${f.name}: ${e}</div>`; }
}
bar.style.width='100%';
if(fail===0) result.innerHTML+=`<div class="success">✅ 全部 ${ok} 个文件上传成功!</div>`;
else result.innerHTML+=`<div class="success">✅ ${ok} 个成功</div><div class="error">❌ ${fail} 个失败</div>`;
status.textContent='上传完成!';
loadFiles();
setTimeout(()=>{ progress.style.display='none'; result.innerHTML=''; }, 5000);
}
async function loadFiles(){
try{
const r = await fetch('/files');
const files = await r.json();
document.getElementById('filelist').innerHTML = files.map(f =>
`<li>📄 ${f.name} <span style="color:#64748b">${(f.size/1024/1024).toFixed(1)}MB · ${f.time}</span></li>`
).join('');
}catch(e){}
}
loadFiles();
</script>
</body>
</html>"""
class UploadHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
self._send_html(HTML)
elif self.path == '/files':
self._list_files()
else:
self.send_error(404)
def do_POST(self):
if self.path == '/upload':
self._handle_upload()
else:
self.send_error(404)
def _send_html(self, content, code=200):
body = content.encode('utf-8')
self.send_response(code)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', len(body))
self.end_headers()
self.wfile.write(body)
def _list_files(self):
import json
import time
os.makedirs(UPLOAD_DIR, exist_ok=True)
files = []
for name in sorted(os.listdir(UPLOAD_DIR), reverse=True):
path = os.path.join(UPLOAD_DIR, name)
if os.path.isfile(path):
st = os.stat(path)
files.append({
'name': name,
'size': st.st_size,
'time': time.strftime('%m-%d %H:%M', time.localtime(st.st_mtime))
})
body = json.dumps(files[:20]).encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(body))
self.end_headers()
self.wfile.write(body)
def _handle_upload(self):
content_type = self.headers.get('Content-Type', '')
if 'multipart/form-data' not in content_type:
self.send_error(400, '需要 multipart/form-data')
return
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': content_type}
)
file_item = form['file']
if not file_item.filename:
self.send_error(400, '未选择文件')
return
safe_name = os.path.basename(file_item.filename)
os.makedirs(UPLOAD_DIR, exist_ok=True)
path = os.path.join(UPLOAD_DIR, safe_name)
# 避免覆盖,自动加序号
base, ext = os.path.splitext(safe_name)
counter = 1
while os.path.exists(path):
path = os.path.join(UPLOAD_DIR, f"{base}_{counter}{ext}")
counter += 1
with open(path, 'wb') as f:
f.write(file_item.file.read())
self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.end_headers()
self.wfile.write(f'OK: {os.path.basename(path)}'.encode('utf-8'))
def log_message(self, format, *args):
pass # 静默日志
if __name__ == '__main__':
os.makedirs(UPLOAD_DIR, exist_ok=True)
server = HTTPServer(('0.0.0.0', PORT), UploadHandler)
print(f'上传服务已启动: http://115.190.225.235:{PORT}')
print(f'上传目录: {UPLOAD_DIR}')
try:
server.serve_forever()
except KeyboardInterrupt:
print('\n已停止')
server.server_close()