#!/usr/bin/env node /** * 飞书卡片交互服务器 * * 功能: * 1. 接收飞书卡片按钮点击回调 * 2. 处理交互逻辑(上传、配置、帮助) * 3. 回复交互式消息 * * 使用方式: * node scripts/feishu-card-server.js [port] * * 默认端口:3000 */ const http = require('http'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); // ============ 配置 ============ const PORT = process.argv[2] || 3000; const CARD_TEMPLATE_PATH = path.join(__dirname, '../cards/upload-card.json'); const QINIU_CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/qiniu-config.json'); // 飞书验证令牌(在飞书开发者后台设置) const FEISHU_VERIFICATION_TOKEN = process.env.FEISHU_VERIFICATION_TOKEN || 'your_verification_token'; // ============ 工具函数 ============ function loadConfig(configPath = QINIU_CONFIG_PATH) { if (!fs.existsSync(configPath)) { throw new Error(`配置文件不存在:${configPath}`); } return JSON.parse(fs.readFileSync(configPath, 'utf-8')); } function loadCardTemplate(templatePath = CARD_TEMPLATE_PATH) { if (!fs.existsSync(templatePath)) { throw new Error(`卡片模板不存在:${templatePath}`); } return JSON.parse(fs.readFileSync(templatePath, 'utf-8')); } function renderCard(template, variables) { let cardJson = JSON.stringify(template); for (const [key, value] of Object.entries(variables)) { cardJson = cardJson.replace(new RegExp(`{{${key}}}`, 'g'), value); } return JSON.parse(cardJson); } function getRegionName(regionCode) { const regions = { 'z0': '华东', 'z1': '华北', 'z2': '华南', 'na0': '北美', 'as0': '东南亚' }; return regions[regionCode] || '未知'; } // ============ 飞书鉴权 ============ /** * 验证飞书请求签名 * 文档:https://open.feishu.cn/document/ukTMukTMukTM/uYjNwYjL2YDM14SM2ATN */ function verifyFeishuSignature(req, body) { const signature = req.headers['x-feishu-signature']; if (!signature) return false; // 简单验证,生产环境需要严格验证 return true; } // ============ 卡片交互处理 ============ /** * 处理卡片按钮点击 */ async function handleCardInteraction(req, res) { let body = ''; req.on('data', chunk => body += chunk); req.on('end', async () => { try { const data = JSON.parse(body); // 飞书挑战验证 if (data.type === 'url_verification') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ challenge: data.challenge })); return; } // 处理交互事件 if (data.type === 'interactive_card.action') { const action = data.action?.value?.action; const userId = data.user?.user_id; const openId = data.user?.open_id; const tenantKey = data.tenant_key; console.log(`收到卡片交互:${action}, 用户:${userId}`); let responseCard; switch (action) { case 'upload_select': responseCard = await handleUploadSelect(data); break; case 'config_view': responseCard = await handleConfigView(data); break; case 'help': responseCard = await handleHelp(data); break; default: responseCard = createErrorResponse('未知操作'); } // 回复卡片 res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ type: 'interactive_card.response', card: responseCard })); return; } // 未知类型 res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok' })); } catch (error) { console.error('处理交互失败:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } }); } /** * 处理"选择文件上传"按钮 */ async function handleUploadSelect(data) { const config = loadConfig(); const bucketName = data.action?.value?.bucket || 'default'; const bucketConfig = config.buckets[bucketName]; if (!bucketConfig) { return createErrorResponse(`存储桶 "${bucketName}" 不存在`); } // 回复引导用户上传文件 return { config: { wide_screen_mode: true }, header: { template: "green", title: { content: "📎 选择文件", tag: "plain_text" } }, elements: [ { tag: "div", text: { content: `请点击下方按钮选择要上传的文件,文件将上传到 **${bucketName}** 存储桶。`, tag: "lark_md" } }, { tag: "action", actions: [ { tag: "button", text: { content: "📁 选择文件", tag: "plain_text" }, type: "primary", url: "feishu://attachment/select" // 飞书内部协议,触发文件选择 } ] } ] }; } /** * 处理"查看配置"按钮 */ async function handleConfigView(data) { const config = loadConfig(); let bucketList = ''; for (const [name, bucket] of Object.entries(config.buckets)) { bucketList += `**${name}**: ${bucket.bucket} (${bucket.region})\n`; } return { config: { wide_screen_mode: true }, header: { template: "blue", title: { content: "📋 当前配置", tag: "plain_text" } }, elements: [ { tag: "div", text: { content: bucketList || '暂无配置', tag: "lark_md" } }, { tag: "hr" }, { tag: "note", elements: [ { tag: "plain_text", content: `配置文件:${QINIU_CONFIG_PATH}` } ] } ] }; } /** * 处理"帮助"按钮 */ async function handleHelp(data) { return { config: { wide_screen_mode: true }, header: { template: "grey", title: { content: "❓ 帮助", tag: "plain_text" } }, elements: [ { tag: "div", text: { content: `**七牛云上传帮助** 📤 **上传文件** - 点击"选择文件上传"按钮 - 选择要上传的文件 - 自动上传到七牛云 ⚙️ **快捷命令** - \`/u\` - 快速上传 - \`/qc\` - 查看配置 - \`/qh\` - 显示帮助 📦 **存储桶** - 支持多存储桶配置 - 上传时可指定目标桶`, tag: "lark_md" } } ] }; } /** * 创建错误响应 */ function createErrorResponse(message) { return { config: { wide_screen_mode: true }, header: { template: "red", title: { content: "❌ 错误", tag: "plain_text" } }, elements: [ { tag: "div", text: { content: message, tag: "lark_md" } } ] }; } // ============ 主页面(测试用) ============ function serveHomePage(res) { const html = ` 七牛云上传 - 飞书卡片服务器

🍙 七牛云上传 - 飞书卡片服务器

✅ 服务器运行中
端口:${PORT}

配置信息

卡片模板:${CARD_TEMPLATE_PATH}

七牛配置:${QINIU_CONFIG_PATH}

飞书开发者后台配置

  1. 请求网址:http://你的服务器IP:${PORT}/feishu/card
  2. 数据加密方式:选择"不加密"
  3. 验证令牌:在环境变量中设置 FEISHU_VERIFICATION_TOKEN

测试

使用 curl 测试:

curl -X POST http://localhost:${PORT}/feishu/card \\
  -H "Content-Type: application/json" \\
  -d '{"type":"url_verification","challenge":"test123"}'
`; res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html); } // ============ HTTP 服务器 ============ const server = http.createServer((req, res) => { console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); // CORS 头(飞书回调需要) res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Feishu-Signature'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } // 主页 if (req.url === '/' || req.url === '/health') { serveHomePage(res); return; } // 卡片交互回调 if (req.url === '/feishu/card' && req.method === 'POST') { handleCardInteraction(req, res); return; } // 404 res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); }); // ============ 启动服务器 ============ server.listen(PORT, () => { console.log(`🍙 七牛云卡片服务器已启动`); console.log(`端口:${PORT}`); console.log(`主页:http://localhost:${PORT}/`); console.log(`回调地址:http://localhost:${PORT}/feishu/card`); console.log(`\n在飞书开发者后台配置请求网址为:http://你的服务器IP:${PORT}/feishu/card`); }); // 优雅退出 process.on('SIGINT', () => { console.log('\n正在关闭服务器...'); server.close(() => { console.log('服务器已关闭'); process.exit(0); }); });