Files
gh-fiftyk-cc-plugins-market…/hooks/setup-server.py
2025-11-29 18:27:10 +08:00

598 lines
17 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Qietuzai Plugin Setup Server
提供图形化界面配置 Figma API Key
"""
import http.server
import socketserver
import urllib.parse
import json
import os
import sys
from pathlib import Path
PORT = 3456
# HTML 页面
HTML_FORM = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>切图仔 Plugin - 配置向导</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 100%;
padding: 40px;
}
.header {
text-align: center;
margin-bottom: 32px;
}
.icon {
font-size: 64px;
margin-bottom: 16px;
}
h1 {
color: #1a202c;
font-size: 28px;
margin-bottom: 8px;
}
.subtitle {
color: #718096;
font-size: 14px;
}
.step {
background: #f7fafc;
border-left: 4px solid #667eea;
padding: 16px;
margin-bottom: 24px;
border-radius: 4px;
}
.step-title {
color: #2d3748;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.step-content {
color: #4a5568;
font-size: 14px;
line-height: 1.6;
}
.step-content a {
color: #667eea;
text-decoration: none;
}
.step-content a:hover {
text-decoration: underline;
}
.form-group {
margin-bottom: 24px;
}
label {
display: block;
color: #2d3748;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s;
font-family: 'Monaco', 'Menlo', monospace;
}
input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.hint {
color: #718096;
font-size: 12px;
margin-top: 8px;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 32px;
}
button {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.btn-secondary:hover {
background: #cbd5e0;
}
.error {
background: #fed7d7;
border: 1px solid #fc8181;
color: #c53030;
padding: 12px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
}
.loading {
display: none;
text-align: center;
margin-top: 16px;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="icon">🎨</div>
<h1>切图仔 Plugin</h1>
<p class="subtitle">配置 Figma API Key</p>
</div>
<div class="step">
<div class="step-title">
<span>📝</span>
<span>第 1 步:获取 Figma API Key</span>
</div>
<div class="step-content">
访问 <a href="https://www.figma.com/settings" target="_blank">Figma 设置页面</a>
"Personal access tokens" 部分创建一个新的 token。
</div>
</div>
<div class="step">
<div class="step-title">
<span>🔑</span>
<span>第 2 步:输入 API Key</span>
</div>
<div class="step-content">
将刚才复制的 token 粘贴到下方输入框中。
</div>
</div>
<form id="setupForm">
<div class="form-group">
<label for="apiKey">Figma API Key *</label>
<input
type="password"
id="apiKey"
name="apiKey"
placeholder="figd_xxxxxxxxxxxx"
required
autocomplete="off"
>
<div class="hint">
💡 您的 API Key 将被安全地存储在插件配置文件中
</div>
</div>
<div class="button-group">
<button type="button" class="btn-secondary" onclick="window.close()">
取消
</button>
<button type="submit" class="btn-primary">
保存配置
</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p style="margin-top: 12px; color: #718096;">正在保存配置...</p>
</div>
</form>
</div>
<script>
document.getElementById('setupForm').addEventListener('submit', async (e) => {
e.preventDefault();
const apiKey = document.getElementById('apiKey').value;
const loading = document.getElementById('loading');
const submitBtn = e.target.querySelector('button[type="submit"]');
// 显示加载状态
submitBtn.disabled = true;
loading.style.display = 'block';
try {
const response = await fetch('/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ apiKey })
});
const result = await response.json();
if (result.success) {
// 跳转到成功页面
window.location.href = '/success?shell=' + encodeURIComponent(result.shell || 'bash');
} else {
alert('保存失败: ' + result.error);
submitBtn.disabled = false;
loading.style.display = 'none';
}
} catch (error) {
alert('保存失败: ' + error.message);
submitBtn.disabled = false;
loading.style.display = 'none';
}
});
</script>
</body>
</html>
"""
SUCCESS_HTML = """
<!DOCTYPE html>
<html lang="zh-CN">
<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: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 100%;
padding: 40px;
text-align: center;
}
.success-icon {
font-size: 64px;
margin-bottom: 24px;
}
h1 {
color: #1a202c;
font-size: 28px;
margin-bottom: 16px;
}
.message {
color: #4a5568;
font-size: 16px;
line-height: 1.6;
margin-bottom: 32px;
}
.info-box {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
text-align: left;
}
.info-box h3 {
color: #2d3748;
font-size: 14px;
margin-bottom: 12px;
}
.info-box p {
color: #718096;
font-size: 14px;
line-height: 1.6;
}
.command {
background: #2d3748;
color: #48bb78;
padding: 12px 16px;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
margin: 8px 0;
text-align: left;
}
button {
background: #667eea;
color: white;
padding: 14px 32px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✅</div>
<h1>配置成功!</h1>
<p class="message">
您的 Figma API Key 已成功保存到系统环境变量中。
</p>
<div class="info-box">
<h3>📋 配置详情</h3>
<p>✅ API Key 已保存到插件配置文件 <strong>.mcp.json</strong></p>
<p>✅ 备份已添加到: <strong>{shell_config}</strong></p>
<div class="command">export FIGMA_API_KEY="****"</div>
</div>
<div class="info-box">
<h3>🔄 下一步</h3>
<p>请<strong>重启 Claude Code</strong> 使配置生效,然后即可开始使用切图仔 Plugin</p>
<p style="margin-top: 8px; font-size: 12px; color: #e53e3e;">⚠️ 注意:请勿将 <strong>.mcp.json</strong> 提交到 git以保护您的 API Key 安全</p>
</div>
<button onclick="window.close()">关闭此页面</button>
</div>
<script>
// 10 秒后自动关闭
setTimeout(() => {
window.close();
}, 10000);
</script>
</body>
</html>
"""
class SetupHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/' or self.path == '/index.html':
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
self.wfile.write(HTML_FORM.encode('utf-8'))
elif self.path.startswith('/success'):
# 解析查询参数
query = urllib.parse.urlparse(self.path).query
params = urllib.parse.parse_qs(query)
shell = params.get('shell', ['bash'])[0]
# 确定配置文件路径
shell_configs = {
'zsh': '~/.zshrc',
'bash': '~/.bashrc',
'fish': '~/.config/fish/config.fish'
}
shell_config = shell_configs.get(shell, '~/.bashrc')
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
html = SUCCESS_HTML.replace('{shell_config}', shell_config)
self.wfile.write(html.encode('utf-8'))
else:
self.send_error(404)
def do_POST(self):
if self.path == '/save':
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))
api_key = data.get('apiKey', '').strip()
if not api_key:
self.send_json_response({'success': False, 'error': 'API Key 不能为空'})
return
# 检测 shell 类型
shell = os.environ.get('SHELL', '/bin/bash')
if 'zsh' in shell:
config_file = Path.home() / '.zshrc'
shell_name = 'zsh'
elif 'fish' in shell:
config_file = Path.home() / '.config' / 'fish' / 'config.fish'
shell_name = 'fish'
else:
config_file = Path.home() / '.bashrc'
shell_name = 'bash'
try:
# 1. 更新 .mcp.json 文件(替换占位符)
# 获取插件根目录
script_dir = Path(__file__).parent
plugin_root = script_dir.parent
mcp_json_path = plugin_root / '.mcp.json'
if not mcp_json_path.exists():
raise FileNotFoundError(f'.mcp.json 文件不存在: {mcp_json_path}')
# 读取并替换占位符
mcp_content = mcp_json_path.read_text()
if '${FIGMA_API_KEY}' in mcp_content:
mcp_content = mcp_content.replace('${FIGMA_API_KEY}', api_key)
mcp_json_path.write_text(mcp_content)
# 2. 同时也保存到 shell 配置文件(作为备份)
# 读取现有配置
if config_file.exists():
content = config_file.read_text()
else:
content = ''
# 检查是否已经存在 FIGMA_API_KEY
if 'FIGMA_API_KEY' in content:
# 更新现有的
import re
pattern = r'export FIGMA_API_KEY=.*'
if re.search(pattern, content):
content = re.sub(pattern, f'export FIGMA_API_KEY="{api_key}"', content)
else:
content += f'\nexport FIGMA_API_KEY="{api_key}"\n'
else:
# 添加新的
content += f'\n# Qietuzai Plugin - Figma API Key\nexport FIGMA_API_KEY="{api_key}"\n'
# 写入配置文件
config_file.parent.mkdir(parents=True, exist_ok=True)
config_file.write_text(content)
self.send_json_response({
'success': True,
'shell': shell_name,
'config_file': str(config_file),
'mcp_json': str(mcp_json_path)
})
# 配置成功后,延迟关闭服务器
import threading
def shutdown_server():
import time
time.sleep(2)
print('\n✅ 配置已保存,服务器即将关闭...')
os._exit(0)
threading.Thread(target=shutdown_server, daemon=True).start()
except Exception as e:
self.send_json_response({
'success': False,
'error': str(e)
})
else:
self.send_error(404)
def send_json_response(self, data):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode('utf-8'))
def log_message(self, format, *args):
# 减少日志输出
pass
def main():
try:
with socketserver.TCPServer(("", PORT), SetupHandler) as httpd:
print(f"✨ 配置服务器已启动: http://localhost:{PORT}")
print(f"📝 请在浏览器中完成配置...")
print(f"⏹ 完成后服务器会自动关闭\n")
httpd.serve_forever()
except KeyboardInterrupt:
print("\n\n👋 服务器已关闭")
sys.exit(0)
except OSError as e:
if e.errno == 48: # Address already in use
print(f"❌ 端口 {PORT} 已被占用,请检查是否有其他配置向导正在运行")
sys.exit(1)
else:
raise
if __name__ == '__main__':
main()