功能特性: - 支持 /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
214 lines
7.1 KiB
JavaScript
214 lines
7.1 KiB
JavaScript
#!/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 };
|