#!/usr/bin/env node /** * OpenClaw Skill - 七牛云上传处理器 * * 用途:处理 OpenClaw 转发的七牛云相关命令 * 使用方式:作为 OpenClaw 的工具脚本被调用 */ const fs = require('fs'); const path = require('path'); const { exec } = require('child_process'); const https = require('https'); const http = require('http'); // ============ 配置 ============ const CONFIG = { scriptDir: __dirname, credentials: path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials'), tempDir: path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/temp'), // 飞书 API 配置 feishu: { appId: process.env.FEISHU_APP_ID || 'cli_a92ce47b02381bcc', appSecret: process.env.FEISHU_APP_SECRET || 'WpCWhqOPKv3F5Lhn11DqubrssJnAodot' } }; // ============ 工具函数 ============ // 调试日志(生产环境可禁用) const DEBUG = process.env.QINIU_DEBUG === 'true'; function log(...args) { if (!DEBUG) return; const timestamp = new Date().toISOString(); console.error(`[${timestamp}]`, ...args); } function ensureTempDir() { if (!fs.existsSync(CONFIG.tempDir)) { fs.mkdirSync(CONFIG.tempDir, { recursive: true }); } } // ============ 飞书 API ============ async function getAccessToken() { const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'; const body = JSON.stringify({ app_id: CONFIG.feishu.appId, app_secret: CONFIG.feishu.appSecret }); return new Promise((resolve, reject) => { const req = https.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const result = JSON.parse(data); if (result.code === 0) { resolve(result.tenant_access_token); } else { reject(new Error(`获取 token 失败:${result.msg}`)); } } catch (e) { reject(e); } }); }); req.on('error', reject); req.write(body); req.end(); }); } async function downloadFeishuFile(fileKey, destPath) { const token = await getAccessToken(); const url = `https://open.feishu.cn/open-apis/im/v1/files/${fileKey}/download`; return new Promise((resolve, reject) => { const req = https.get(url, { headers: { 'Authorization': `Bearer ${token}` } }, (res) => { if (res.statusCode !== 200) { reject(new Error(`下载失败:${res.statusCode}`)); return; } const file = fs.createWriteStream(destPath); res.pipe(file); file.on('finish', () => { file.close(); resolve(destPath); }); }).on('error', reject); }); } async function sendMessageToChat(chatId, text) { const token = await getAccessToken(); const url = 'https://open.feishu.cn/open-apis/im/v1/messages'; const body = JSON.stringify({ receive_id: chatId, msg_type: 'text', content: JSON.stringify({ text }) }); return new Promise((resolve, reject) => { const req = https.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }); req.on('error', reject); req.write(body); req.end(); }); } // ============ 命令解析 ============ function parseUploadCommand(text) { // 支持 /upload 和 /u 两种命令 const match = text.match(/^\/(upload|u)(?:\s+(.+))?$/i); if (!match) return null; // match[1] = 命令名 (upload/u), match[2] = 参数 const args = (match[2] || '').trim().split(/\s+/).filter(Boolean); let targetPath = null; let useOriginal = false; let bucket = 'default'; for (const arg of args) { if (arg === '--original') { useOriginal = true; } else if (arg.startsWith('/') || arg.includes('.')) { targetPath = arg; } else { bucket = arg; } } return { targetPath, useOriginal, bucket }; } function parseConfigCommand(text) { const match = text.match(/^\/qiniu-config\s+(.+)$/i); if (!match) return null; const args = match[1].trim().split(/\s+/); return { subCommand: args[0], args: args.slice(1) }; } // ============ 命令处理 ============ async function handleUpload(message) { const content = typeof message.content === 'string' ? JSON.parse(message.content) : message.content; const text = content.text || ''; const attachments = message.attachments || []; const cmd = parseUploadCommand(text); if (!cmd) { return { handled: false }; } log('处理上传命令:', cmd); // 检查附件 if (!attachments || attachments.length === 0) { return { handled: true, reply: `❌ 请附上要上传的文件 💡 使用示例: /upload /config/test/file.txt default [附上文件] 或:/upload --original default [附上文件] (使用原文件名)` }; } const attachment = attachments[0]; const fileKey = attachment.file_key; const originalFileName = attachment.file_name; log(`下载文件:${originalFileName} (${fileKey})`); try { // 确保临时目录存在 ensureTempDir(); // 下载文件 const tempFile = path.join(CONFIG.tempDir, `upload_${Date.now()}_${originalFileName}`); await downloadFeishuFile(fileKey, tempFile); log('文件已下载:', tempFile); // 确定目标文件名 let targetKey; if (cmd.useOriginal) { targetKey = originalFileName; } else if (cmd.targetPath) { // 如果指定了路径,保留完整路径(去掉前导 /) targetKey = cmd.targetPath.startsWith('/') ? cmd.targetPath.substring(1) : cmd.targetPath; } else { // 没有指定路径时,使用原文件名 targetKey = originalFileName; } // 确保 targetKey 不为空 if (!targetKey || targetKey.trim() === '') { targetKey = originalFileName; } log('目标 key:', targetKey); log('原始文件名:', originalFileName); log('命令参数:', cmd); // 调用上传脚本 log('上传到七牛云:', targetKey); const uploadScript = path.join(CONFIG.scriptDir, 'scripts/upload-to-qiniu.js'); const uploadCmd = `node "${uploadScript}" upload --file "${tempFile}" --key "${targetKey}" --bucket "${cmd.bucket}"`; const { stdout, stderr } = await new Promise((resolve, reject) => { exec(uploadCmd, (error, stdout, stderr) => { if (error) { reject(new Error(`上传失败:${stderr || error.message}`)); return; } resolve({ stdout, stderr }); }); }); log('上传结果:', stdout); // 清理临时文件 if (fs.existsSync(tempFile)) { fs.unlinkSync(tempFile); } // 解析结果 const urlMatch = stdout.match(/🔗 URL: (.+)/); const fileUrl = urlMatch ? urlMatch[1] : 'N/A'; // 解析存储桶名称(从输出中获取实际桶名) const bucketMatch = stdout.match(/☁️ 存储桶:(.+)/); const actualBucket = bucketMatch ? bucketMatch[1].trim() : (cmd.bucket || 'default'); // 直接返回完整回复 return { handled: true, reply: `✅ 上传成功! 📦 文件:${targetKey} 🔗 链接:${fileUrl} 💾 原文件:${originalFileName} 🪣 存储桶:${actualBucket}` }; } catch (error) { log('上传失败:', error.message); // 清理临时文件 const tempFiles = fs.readdirSync(CONFIG.tempDir); tempFiles.forEach(f => { if (f.startsWith('upload_')) { try { fs.unlinkSync(path.join(CONFIG.tempDir, f)); } catch (e) {} } }); return { handled: true, reply: `❌ 上传失败:${error.message}` }; } } async function handleConfig(message) { const content = typeof message.content === 'string' ? JSON.parse(message.content) : message.content; const text = content.text || ''; const cmd = parseConfigCommand(text); if (!cmd) { return { handled: false }; } log('处理配置命令:', cmd.subCommand); try { const uploadScript = path.join(CONFIG.scriptDir, 'scripts/upload-to-qiniu.js'); const configCmd = `node "${uploadScript}" config ${cmd.subCommand} ${cmd.args.join(' ')}`; const { stdout, stderr } = await new Promise((resolve, reject) => { exec(configCmd, (error, stdout, stderr) => { if (error) { reject(new Error(stderr || error.message)); return; } resolve({ stdout, stderr }); }); }); return { handled: true, reply: '```\n' + stdout + '\n```' }; } catch (error) { return { handled: true, reply: `❌ 配置命令执行失败:${error.message}` }; } } async function handleHelp() { return { handled: true, reply: ` 🍙 七牛云上传 - 使用帮助 📤 上传文件: /upload [目标路径] [存储桶名] /upload --original [存储桶名] 示例: /upload /config/test/file.txt default /upload --original default /upload docs/report.pdf ⚙️ 配置管理: /qiniu-config list # 查看配置 /qiniu-config set # 修改配置 /qiniu-config set-bucket # 添加存储桶 /qiniu-config reset # 重置配置 示例: /qiniu-config set default.accessKey YOUR_KEY /qiniu-config set default.domain https://cdn.example.com ` }; } // ============ 主处理函数 ============ async function processMessage(message) { const content = typeof message.content === 'string' ? JSON.parse(message.content) : message.content; const text = content.text || ''; const trimmed = text.trim(); // 检查是否是七牛云命令 if (/^\/upload/i.test(trimmed)) { return await handleUpload(message); } if (/^\/qiniu-config/i.test(trimmed)) { return await handleConfig(message); } if (/^\/(qiniu-)?help/i.test(trimmed)) { return await handleHelp(); } return { handled: false }; } // ============ 命令行接口 ============ async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.log('七牛云上传 Skill 处理器'); console.log(''); console.log('用法:'); console.log(' node openclaw-processor.js --message ""'); console.log(''); console.log('示例:'); console.log(' node openclaw-processor.js --message "{\"content\":{\"text\":\"/qiniu-config list\"}}"'); process.exit(0); } if (args[0] === '--message' && args[1]) { try { const message = JSON.parse(args[1]); const result = await processMessage(message); console.log(JSON.stringify(result, null, 2)); if (result.handled && result.reply) { // 如果有 chat_id,直接发送消息 if (message.chat_id) { await sendMessageToChat(message.chat_id, result.reply); } } } catch (e) { console.error('处理失败:', e.message); process.exit(1); } } } // 导出给 OpenClaw 调用 if (require.main === module) { main(); } module.exports = { processMessage, handleUpload, handleConfig, handleHelp };