Files
openclaw-skill-qiniu/openclaw-processor.js
daoqi 9b584cdad4 refactor: 清理调试代码和废弃文件
删除的废弃文件:
- test-feishu-upload.js (测试文件)
- debug-upload.js (调试工具)
- check-bucket-override.js (诊断工具)
- feishu-card-server.js (废弃的卡片服务器)
- feishu-websocket-listener.js (废弃的 WebSocket 监听器)
- openclaw-bridge.js (废弃的桥接代码)
- setup.sh, start-listener.sh, verify-url.js (废弃脚本)
- cards/ 目录 (未使用的卡片模板)
- ARCHITECTURE.md, INTEGRATION.md 等废弃文档

优化:
- openclaw-processor.js: 添加 DEBUG 环境变量控制日志输出
- 移除生产环境不必要的调试日志

清理后核心文件:
- openclaw-processor.js (OpenClaw 处理器)
- openclaw-handler.js (HTTP 处理器)
- scripts/upload-to-qiniu.js (核心上传脚本)
- scripts/feishu-listener.js (独立监听器)
- scripts/update-bucket-setting.js (存储桶设置工具)
- deploy.sh (部署脚本)
2026-03-07 16:08:47 +08:00

438 lines
11 KiB
JavaScript
Executable File
Raw Permalink 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 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 <key> <value> # 修改配置
/qiniu-config set-bucket <name> <json> # 添加存储桶
/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 "<JSON 消息>"');
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 };