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:
daoqi
2026-03-07 16:02:18 +08:00
commit 1aeae9cc51
36 changed files with 6826 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
#!/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 };