Files
openclaw-skill-qiniu/scripts/debug-upload.js
daoqi 1aeae9cc51 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
2026-03-07 16:02:18 +08:00

276 lines
9.0 KiB
JavaScript
Raw 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
/**
* 七牛云上传调试脚本
*
* 用途:测试上传并显示详细错误信息
*
* 用法:
* 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);