initial: 七牛云上传 OpenClaw Skill
功能特性: - 支持 /upload, /u 命令上传文件到七牛云 - 支持 /qiniu-config 配置管理 - 支持飞书卡片交互 - 支持指定上传路径和存储桶 - 自动刷新 CDN 缓存 - 支持文件覆盖上传 包含组件: - OpenClaw 处理器 (openclaw-processor.js) - 独立监听器 (scripts/feishu-listener.js) - 核心上传脚本 (scripts/upload-to-qiniu.js) - 部署脚本 (deploy.sh) - 完整文档 部署方式: 1. 复制 skill 到 ~/.openclaw/workspace/skills/ 2. 配置 ~/.openclaw/credentials/qiniu-config.json 3. 重启 OpenClaw Gateway
This commit is contained in:
410
scripts/feishu-card-server.js
Normal file
410
scripts/feishu-card-server.js
Normal file
@@ -0,0 +1,410 @@
|
||||
#!/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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>七牛云上传 - 飞书卡片服务器</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
|
||||
h1 { color: #333; }
|
||||
.status { padding: 10px; background: #e8f5e9; border-radius: 4px; margin: 20px 0; }
|
||||
.config { background: #f5f5f5; padding: 15px; border-radius: 4px; }
|
||||
code { background: #eee; padding: 2px 6px; border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🍙 七牛云上传 - 飞书卡片服务器</h1>
|
||||
|
||||
<div class="status">
|
||||
✅ 服务器运行中
|
||||
<br>端口:<code>${PORT}</code>
|
||||
</div>
|
||||
|
||||
<div class="config">
|
||||
<h3>配置信息</h3>
|
||||
<p>卡片模板:<code>${CARD_TEMPLATE_PATH}</code></p>
|
||||
<p>七牛配置:<code>${QINIU_CONFIG_PATH}</code></p>
|
||||
</div>
|
||||
|
||||
<h3>飞书开发者后台配置</h3>
|
||||
<ol>
|
||||
<li>请求网址:<code>http://你的服务器IP:${PORT}/feishu/card</code></li>
|
||||
<li>数据加密方式:选择"不加密"</li>
|
||||
<li>验证令牌:在环境变量中设置 <code>FEISHU_VERIFICATION_TOKEN</code></li>
|
||||
</ol>
|
||||
|
||||
<h3>测试</h3>
|
||||
<p>使用 curl 测试:</p>
|
||||
<pre><code>curl -X POST http://localhost:${PORT}/feishu/card \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"type":"url_verification","challenge":"test123"}'</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user