Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:27:10 +08:00
commit 411ca26b46
8 changed files with 1173 additions and 0 deletions

79
hooks/check-figma-api-key.sh Executable file
View File

@@ -0,0 +1,79 @@
#!/bin/bash
# 获取插件根目录
if [ -n "$CLAUDE_PLUGIN_ROOT" ]; then
PLUGIN_ROOT="$CLAUDE_PLUGIN_ROOT"
else
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PLUGIN_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
fi
MCP_JSON="$PLUGIN_ROOT/.mcp.json"
MCP_JSON_TEMPLATE="$PLUGIN_ROOT/.mcp.json.template"
# 检查 .mcp.json 是否存在,如果不存在则从模板复制
if [ ! -f "$MCP_JSON" ]; then
if [ -f "$MCP_JSON_TEMPLATE" ]; then
echo "📝 首次运行,正在从模板创建 .mcp.json 文件..."
cp "$MCP_JSON_TEMPLATE" "$MCP_JSON"
else
echo "❌ 错误:找不到 .mcp.json 和 .mcp.json.template 文件"
exit 1
fi
fi
# 检查 .mcp.json 中是否还包含未替换的 ${FIGMA_API_KEY} 占位符
if grep -q '\${FIGMA_API_KEY}' "$MCP_JSON"; then
# 包含占位符,说明还没配置
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ 🎨 切图仔 (Qietuzai) Plugin - 配置向导 ║"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
echo "⚠️ 检测到您还未配置 Figma API Key"
echo ""
echo "🌐 正在启动图形化配置界面..."
echo ""
# 获取脚本所在目录(使用 CLAUDE_PLUGIN_ROOT 如果可用)
if [ -n "$CLAUDE_PLUGIN_ROOT" ]; then
SCRIPT_DIR="$CLAUDE_PLUGIN_ROOT/hooks"
else
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
fi
# 启动 Python HTTP 服务器(后台运行)
python3 "$SCRIPT_DIR/setup-server.py" > /dev/null 2>&1 &
SERVER_PID=$!
# 等待服务器启动
sleep 2
# 打开浏览器到配置页面
CONFIG_URL="http://localhost:3456"
echo "✨ 配置页面已在浏览器中打开: $CONFIG_URL"
echo ""
echo "📝 请在浏览器中完成以下步骤:"
echo " 1. 访问 Figma 设置页面获取 API Key"
echo " 2. 在表单中输入您的 API Key"
echo " 3. 点击保存"
echo " 4. 重启 Claude Code"
echo ""
echo "💡 如果浏览器没有自动打开,请手动访问: $CONFIG_URL"
echo ""
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
open "$CONFIG_URL" 2>/dev/null
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux
xdg-open "$CONFIG_URL" 2>/dev/null
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
# Windows
start "$CONFIG_URL" 2>/dev/null
fi
else
# API Key 已配置,静默通过
exit 0
fi

14
hooks/hooks.json Normal file
View File

@@ -0,0 +1,14 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/check-figma-api-key.sh"
}
]
}
]
}
}

597
hooks/setup-server.py Executable file
View File

@@ -0,0 +1,597 @@
#!/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()

26
hooks/test-setup.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
# 测试配置向导
# 用法: bash test-setup.sh
echo "🧪 测试切图仔 Plugin 配置向导"
echo ""
# 临时清除环境变量(仅用于测试)
unset FIGMA_API_KEY
# 设置 CLAUDE_PLUGIN_ROOT模拟 Claude Code 环境)
export CLAUDE_PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo "📁 Plugin 目录: $CLAUDE_PLUGIN_ROOT"
echo ""
# 运行配置检查脚本
bash "$CLAUDE_PLUGIN_ROOT/hooks/check-figma-api-key.sh"
echo ""
echo "✅ 测试完成!"
echo ""
echo "💡 提示:"
echo " - 如果浏览器打开了配置页面,说明脚本工作正常"
echo " - 配置成功后会在 ~/.claude/qietuzai-setup-done 创建标记文件"
echo " - 可以通过 'cat ~/.zshrc | grep FIGMA_API_KEY' 验证配置"