131 lines
4.4 KiB
Python
131 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
theme-server.py — 主题选择器本地服务
|
|
Agent 在主题选择步骤启动此 server,serve theme-picker.html 并接收用户选择。
|
|
|
|
用法(Agent 执行):
|
|
python3 theme-server.py [--port PORT] [--dir DIR]
|
|
|
|
默认行为:
|
|
- 在 references/ 目录下启动 HTTP 服务
|
|
- 监听随机可用端口(避免冲突)
|
|
- serve 静态文件 + /api/theme-choice POST 端点
|
|
- 收到选择后写入 .theme-choice 文件并自动关闭 server
|
|
|
|
Agent 读取结果:
|
|
读取工作目录下的 .theme-choice 文件,内容为主题 id(如 "cyber-dark")
|
|
"""
|
|
|
|
import http.server
|
|
import json
|
|
import os
|
|
import signal
|
|
import socket
|
|
import sys
|
|
import threading
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
def find_free_port():
|
|
"""找一个可用端口"""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(('', 0))
|
|
return s.getsockname()[1]
|
|
|
|
|
|
class ThemeHandler(http.server.SimpleHTTPRequestHandler):
|
|
"""处理静态文件 serve + POST /api/theme-choice"""
|
|
|
|
def do_POST(self):
|
|
parsed = urlparse(self.path)
|
|
if parsed.path == '/api/theme-choice':
|
|
content_length = int(self.headers.get('Content-Length', 0))
|
|
body = self.rfile.read(content_length)
|
|
try:
|
|
data = json.loads(body)
|
|
theme = data.get('theme', '')
|
|
if not theme:
|
|
self.send_error(400, 'Missing theme')
|
|
return
|
|
|
|
# 写入选择结果到工作目录
|
|
choice_path = Path(self.server.output_dir) / '.theme-choice'
|
|
choice_path.write_text(theme, encoding='utf-8')
|
|
print(f'\n✓ 用户选择了主题: {theme}')
|
|
print(f' 结果已写入: {choice_path}')
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({'ok': True, 'theme': theme}).encode())
|
|
|
|
# 延迟关闭 server(让响应先发出去)
|
|
threading.Timer(0.5, self.server.shutdown).start()
|
|
|
|
except json.JSONDecodeError:
|
|
self.send_error(400, 'Invalid JSON')
|
|
else:
|
|
self.send_error(404, 'Not found')
|
|
|
|
def do_OPTIONS(self):
|
|
"""处理 CORS 预检"""
|
|
self.send_response(200)
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
self.end_headers()
|
|
|
|
def log_message(self, format, *args):
|
|
"""静默普通请求日志,只显示关键信息"""
|
|
if '/api/' in str(args[0]) if args else False:
|
|
super().log_message(format, *args)
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description='Theme Picker Server')
|
|
parser.add_argument('--port', type=int, default=0, help='端口号(默认自动分配)')
|
|
parser.add_argument('--dir', type=str, default='.', help='静态文件目录(默认当前目录)')
|
|
parser.add_argument('--output', type=str, default='.', help='结果文件写入目录')
|
|
args = parser.parse_args()
|
|
|
|
port = args.port or find_free_port()
|
|
serve_dir = os.path.abspath(args.dir)
|
|
output_dir = os.path.abspath(args.output)
|
|
|
|
os.chdir(serve_dir)
|
|
|
|
# 清理上一次遗留的选择结果(防止误读旧主题)
|
|
old_choice = Path(output_dir) / '.theme-choice'
|
|
if old_choice.exists():
|
|
old_choice.unlink()
|
|
print(f'🧹 已清理旧的 .theme-choice 文件')
|
|
|
|
server = http.server.HTTPServer(('127.0.0.1', port), ThemeHandler)
|
|
server.output_dir = output_dir
|
|
|
|
url = f'http://localhost:{port}/theme-picker.html'
|
|
print(f'🎨 主题选择器已启动')
|
|
print(f' 地址: {url}')
|
|
print(f' 静态目录: {serve_dir}')
|
|
print(f' 结果写入: {output_dir}/.theme-choice')
|
|
print(f' 等待用户选择...\n')
|
|
|
|
# 输出 URL 供 Agent 读取(Agent 可以 grep 这行来获取 URL)
|
|
print(f'THEME_PICKER_URL={url}')
|
|
sys.stdout.flush()
|
|
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
server.server_close()
|
|
print('\n🛑 Server 已关闭')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|