#!/usr/bin/env node /** * 七牛云存储桶覆盖设置检查脚本 * * 用途:检查存储桶是否允许覆盖上传 * * 用法: * node check-bucket-override.js [bucket-name] */ 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 generateAccessToken(accessKey, secretKey, method, path, body = '') { const host = 'kodo.qiniu.com'; const contentType = 'application/json'; // 格式:Method Path\nHost: Host\nContent-Type: ContentType\n\nBody const signData = `${method} ${path}\nHost: ${host}\nContent-Type: ${contentType}\n\n${body}`; const signature = hmacSha1(signData, secretKey); const encodedSign = urlSafeBase64(signature); return `Qiniu ${accessKey}:${encodedSign}`; } function httpRequest(url, options, body = null) { return new Promise((resolve, reject) => { const protocol = url.startsWith('https') ? https : http; const req = protocol.request(url, options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const json = JSON.parse(data); resolve({ status: res.statusCode, data: json }); } catch (e) { resolve({ status: res.statusCode, data: data }); } }); }); req.on('error', reject); if (body) { req.write(body); } req.end(); }); } async function checkBucket(bucketName) { const config = loadConfig(); const bucketConfig = config.buckets[bucketName || 'default']; if (!bucketConfig) { throw new Error(`存储桶配置 "${bucketName || 'default'}" 不存在`); } const { accessKey, secretKey, bucket, region } = bucketConfig; console.log('🔍 检查存储桶覆盖设置...\n'); console.log(`存储桶:${bucket}`); console.log(`区域:${region}`); console.log(`AccessKey: ${accessKey.substring(0, 4)}...${accessKey.substring(accessKey.length - 4)}`); console.log(''); // 1. 获取存储桶列表 // 七牛云 API 文档:https://developer.qiniu.com/kodo/api/1314/list-buckets const listBucketsUrl = 'https://kodo.qiniu.com/v2/buckets'; const accessToken = generateAccessToken(accessKey, secretKey, 'GET', '/v2/buckets'); const listOptions = { method: 'GET', headers: { 'Host': 'kodo.qiniu.com', 'Authorization': accessToken } }; console.log('📋 获取存储桶列表...'); const listResult = await httpRequest(listBucketsUrl, listOptions); if (listResult.status !== 200) { console.error('❌ 获取存储桶列表失败:', listResult.data); return; } const buckets = listResult.data; const targetBucket = buckets.find(b => b.name === bucket); if (!targetBucket) { console.error(`❌ 未找到存储桶:${bucket}`); console.log('\n可用的存储桶:'); buckets.forEach(b => console.log(` - ${b.name}`)); return; } console.log('✅ 存储桶存在\n'); // 2. 获取存储桶详细信息 const bucketInfoUrl = `https://kodo.qiniu.com/v2/buckets/${bucket}`; const bucketInfoToken = generateAccessToken(accessKey, secretKey, 'GET', `/v2/buckets/${bucket}`); const infoOptions = { method: 'GET', headers: { 'Host': 'kodo.qiniu.com', 'Authorization': bucketInfoToken } }; console.log('📋 获取存储桶详细信息...'); const infoResult = await httpRequest(bucketInfoUrl, infoOptions); if (infoResult.status !== 200) { console.error('❌ 获取存储桶信息失败:', infoResult.data); console.log('\n⚠️ 可能是权限不足,请检查 AccessKey/SecretKey 是否有存储桶管理权限'); return; } const bucketInfo = infoResult.data; console.log('\n📊 存储桶配置信息:'); console.log('─────────────────────────────────────'); console.log(` 名称:${bucketInfo.name || 'N/A'}`); console.log(` 区域:${bucketInfo.region || bucketInfo.info?.region || 'N/A'}`); console.log(` 创建时间:${bucketInfo.createdAt || bucketInfo.info?.createdAt || 'N/A'}`); // 检查覆盖相关设置 const info = bucketInfo.info || bucketInfo; console.log('\n🔒 安全设置:'); console.log('─────────────────────────────────────'); // 防覆盖设置(关键!) const noOverwrite = info.noOverwrite !== undefined ? info.noOverwrite : '未设置'; console.log(` 防覆盖:${noOverwrite === true || noOverwrite === 1 ? '❌ 已启用(禁止覆盖)' : '✅ 未启用(允许覆盖)'}`); // 私有空间设置 const private = info.private !== undefined ? info.private : '未知'; console.log(` 空间类型:${private === true || private === 1 ? '私有空间' : '公共空间'}`); // 其他设置 if (info.maxSpace !== undefined) { console.log(` 容量限制:${info.maxSpace} bytes`); } console.log('\n💡 解决方案:'); console.log('─────────────────────────────────────'); if (noOverwrite === true || noOverwrite === 1) { console.log('⚠️ 存储桶已启用"防覆盖"设置,需要关闭才能覆盖上传同名文件。\n'); console.log('关闭方法:'); console.log('1. 登录七牛云控制台:https://portal.qiniu.com/'); console.log(`2. 进入"对象存储" → 选择存储桶 "${bucket}"`); console.log('3. 点击"设置" → "空间设置"'); console.log('4. 找到"防覆盖"选项,关闭它'); console.log('5. 保存设置后重试上传\n'); console.log('或者使用命令行关闭:'); console.log(`node scripts/update-bucket-setting.js ${bucket} noOverwrite 0`); } else { console.log('✅ 存储桶允许覆盖上传'); console.log('\n如果仍然无法覆盖,可能原因:'); console.log('1. 上传凭证 scope 指定了具体 key,但上传时使用了不同的 key'); console.log('2. 上传 API 端点不正确'); console.log('3. 文件正在被其他进程占用'); console.log('\n建议:'); console.log('- 检查上传日志中的实际上传 key 是否一致'); console.log('- 使用相同的完整路径(包括前导 /)'); } } async function main() { const bucketName = process.argv[2]; try { await checkBucket(bucketName); } catch (error) { console.error('❌ 错误:', error.message); process.exit(1); } } if (require.main === module) { main(); } module.exports = { checkBucket };