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:
275
scripts/debug-upload.js
Normal file
275
scripts/debug-upload.js
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 七牛云上传调试脚本
|
||||
*
|
||||
* 用途:测试上传并显示详细错误信息
|
||||
*
|
||||
* 用法:
|
||||
* node debug-upload.js --file <文件路径> --key <目标路径>
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
const DEFAULT_CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/qiniu-config.json');
|
||||
|
||||
function loadConfig() {
|
||||
if (!fs.existsSync(DEFAULT_CONFIG_PATH)) {
|
||||
throw new Error(`配置文件不存在:${DEFAULT_CONFIG_PATH}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf-8'));
|
||||
}
|
||||
|
||||
function hmacSha1(data, secret) {
|
||||
return crypto.createHmac('sha1', secret).update(data).digest();
|
||||
}
|
||||
|
||||
function urlSafeBase64(data) {
|
||||
return Buffer.from(data).toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function generateUploadToken(accessKey, secretKey, bucket, key = null, expires = 3600) {
|
||||
const deadline = Math.floor(Date.now() / 1000) + expires;
|
||||
|
||||
// 关键修复:scope 必须包含 key 才能覆盖上传
|
||||
let scope = bucket;
|
||||
if (key) {
|
||||
scope = `${bucket}:${key}`; // ✅ 添加 key,允许覆盖
|
||||
}
|
||||
|
||||
console.log('📝 上传凭证参数:');
|
||||
console.log(` scope: ${scope} (包含 key 才能覆盖)`);
|
||||
console.log(` deadline: ${deadline}`);
|
||||
console.log(` key: ${key || '(未指定,使用表单中的 key)'}`);
|
||||
|
||||
const putPolicy = {
|
||||
scope: scope,
|
||||
deadline: deadline,
|
||||
returnBody: JSON.stringify({
|
||||
success: true,
|
||||
key: '$(key)',
|
||||
hash: '$(etag)',
|
||||
fsize: '$(fsize)',
|
||||
bucket: '$(bucket)',
|
||||
url: `$(domain)/$(key)`
|
||||
})
|
||||
};
|
||||
|
||||
console.log('\n📋 上传凭证策略:');
|
||||
console.log(JSON.stringify(putPolicy, null, 2));
|
||||
|
||||
const encodedPolicy = urlSafeBase64(JSON.stringify(putPolicy));
|
||||
const encodedSignature = urlSafeBase64(hmacSha1(encodedPolicy, secretKey));
|
||||
|
||||
const token = `${accessKey}:${encodedSignature}:${encodedPolicy}`;
|
||||
|
||||
console.log('\n🔑 生成的上传凭证:');
|
||||
console.log(` ${accessKey}:${encodedSignature.substring(0, 20)}...`);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
function httpRequest(url, options, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
|
||||
console.log(`\n📤 发送请求:`);
|
||||
console.log(` URL: ${url}`);
|
||||
console.log(` Method: ${options.method}`);
|
||||
console.log(` Headers:`, JSON.stringify(options.headers, null, 2));
|
||||
|
||||
const req = protocol.request(url, options, (res) => {
|
||||
console.log(`\n📥 收到响应:`);
|
||||
console.log(` Status: ${res.statusCode}`);
|
||||
console.log(` Headers:`, JSON.stringify(res.headers, null, 2));
|
||||
|
||||
let data = '';
|
||||
res.on('data', chunk => {
|
||||
data += chunk;
|
||||
console.log(` 接收数据块:${chunk.length} bytes`);
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log(`\n📦 完整响应数据:`);
|
||||
console.log(data);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
resolve({ status: res.statusCode, data: json });
|
||||
} catch (e) {
|
||||
resolve({ status: res.statusCode, data: data, raw: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('❌ 请求错误:', e);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
if (body) {
|
||||
console.log(`\n📤 请求体大小:${body.length} bytes`);
|
||||
req.write(body);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function debugUpload() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
let filePath = null;
|
||||
let key = null;
|
||||
let bucketName = 'default';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--file' && args[i + 1]) {
|
||||
filePath = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--key' && args[i + 1]) {
|
||||
key = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--bucket' && args[i + 1]) {
|
||||
bucketName = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
console.error('❌ 缺少必需参数 --file');
|
||||
console.error('用法:node debug-upload.js --file <文件路径> [--key <目标路径>] [--bucket <存储桶名>]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`❌ 文件不存在:${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const bucketConfig = config.buckets[bucketName];
|
||||
|
||||
if (!bucketConfig) {
|
||||
console.error(`❌ 存储桶配置 "${bucketName}" 不存在`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { accessKey, secretKey, bucket, region, domain } = bucketConfig;
|
||||
|
||||
// 确定目标 key
|
||||
if (!key) {
|
||||
key = path.basename(filePath);
|
||||
} else if (key.startsWith('/')) {
|
||||
key = key.substring(1);
|
||||
}
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log('🔍 七牛云上传调试');
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log(`\n📁 文件信息:`);
|
||||
console.log(` 本地路径:${filePath}`);
|
||||
console.log(` 文件大小:${fs.statSync(filePath).size} bytes`);
|
||||
console.log(` 目标 key: ${key}`);
|
||||
console.log(` 存储桶:${bucket}`);
|
||||
console.log(` 区域:${region}`);
|
||||
console.log(` 域名:${domain}`);
|
||||
|
||||
// 生成上传凭证
|
||||
console.log('\n═══════════════════════════════════════════════════════════');
|
||||
const uploadToken = generateUploadToken(accessKey, secretKey, bucket, key);
|
||||
|
||||
// 构建上传请求
|
||||
const regionEndpoint = getUploadEndpoint(region);
|
||||
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
const bodyParts = [
|
||||
`------${boundary}`,
|
||||
'Content-Disposition: form-data; name="token"',
|
||||
'',
|
||||
uploadToken,
|
||||
`------${boundary}`,
|
||||
'Content-Disposition: form-data; name="key"',
|
||||
'',
|
||||
key,
|
||||
`------${boundary}`,
|
||||
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
|
||||
'Content-Type: application/octet-stream',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
|
||||
const bodyBuffer = Buffer.concat([
|
||||
Buffer.from(bodyParts.join('\r\n'), 'utf-8'),
|
||||
fileContent,
|
||||
Buffer.from(`\r\n------${boundary}--\r\n`, 'utf-8')
|
||||
]);
|
||||
|
||||
const uploadUrl = `${regionEndpoint}/`;
|
||||
|
||||
const uploadOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': `multipart/form-data; boundary=----${boundary}`,
|
||||
'Content-Length': bodyBuffer.length
|
||||
}
|
||||
};
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════════');
|
||||
console.log('📤 开始上传...');
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
|
||||
try {
|
||||
const result = await httpRequest(uploadUrl, uploadOptions, bodyBuffer);
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════════');
|
||||
console.log('📊 上传结果:');
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
|
||||
if (result.status === 200) {
|
||||
console.log('✅ 上传成功!');
|
||||
console.log(` key: ${result.data.key}`);
|
||||
console.log(` hash: ${result.data.hash}`);
|
||||
console.log(` url: ${domain}/${result.data.key}`);
|
||||
} else {
|
||||
console.log('❌ 上传失败!');
|
||||
console.log(` HTTP Status: ${result.status}`);
|
||||
console.log(` 错误信息:`, JSON.stringify(result.data, null, 2));
|
||||
|
||||
// 解析常见错误
|
||||
if (result.data.error) {
|
||||
console.log('\n🔍 错误分析:');
|
||||
if (result.data.error.includes('file exists')) {
|
||||
console.log(' ⚠️ 文件已存在,存储桶可能禁止覆盖');
|
||||
} else if (result.data.error.includes('invalid token')) {
|
||||
console.log(' ⚠️ 上传凭证无效,检查 AccessKey/SecretKey');
|
||||
} else if (result.data.error.includes('bucket')) {
|
||||
console.log(' ⚠️ 存储桶配置问题');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 上传过程出错:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function getUploadEndpoint(region) {
|
||||
const endpoints = {
|
||||
'z0': 'https://up.qiniup.com',
|
||||
'z1': 'https://up-z1.qiniup.com',
|
||||
'z2': 'https://up-z2.qiniup.com',
|
||||
'na0': 'https://up-na0.qiniup.com',
|
||||
'as0': 'https://up-as0.qiniup.com'
|
||||
};
|
||||
return endpoints[region] || endpoints['z0'];
|
||||
}
|
||||
|
||||
debugUpload().catch(console.error);
|
||||
Reference in New Issue
Block a user