#!/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);