- 核心配置: IDENTITY, USER, SOUL, AGENTS, TOOLS, HEARTBEAT, MEMORY - memory/: 每日总结和临时记录 - skills/: 所有已安装技能 - notes/: 语音配置笔记
324 lines
6.8 KiB
JavaScript
324 lines
6.8 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 腾讯云 COS Node.js SDK 操作脚本
|
||
* 作为 cos-mcp MCP 工具不可用时的降级方案
|
||
*
|
||
* 依赖:npm install cos-nodejs-sdk-v5
|
||
* 凭证通过环境变量读取:
|
||
* TENCENT_COS_SECRET_ID / TENCENT_COS_SECRET_KEY / TENCENT_COS_REGION / TENCENT_COS_BUCKET
|
||
*
|
||
* 用法:node cos_node.mjs <action> [options]
|
||
*/
|
||
|
||
import { createRequire } from 'module';
|
||
import { createReadStream, createWriteStream, existsSync } from 'fs';
|
||
import { basename, resolve } from 'path';
|
||
import { pipeline } from 'stream/promises';
|
||
|
||
const require = createRequire(import.meta.url);
|
||
const COS = require('cos-nodejs-sdk-v5');
|
||
|
||
// 读取环境变量
|
||
const SecretId = process.env.TENCENT_COS_SECRET_ID;
|
||
const SecretKey = process.env.TENCENT_COS_SECRET_KEY;
|
||
const Region = process.env.TENCENT_COS_REGION;
|
||
const Bucket = process.env.TENCENT_COS_BUCKET;
|
||
|
||
// 可选的自定义域名配置
|
||
const Domain = process.env.TENCENT_COS_DOMAIN;
|
||
const ServiceDomain = process.env.TENCENT_COS_SERVICE_DOMAIN;
|
||
const Protocol = process.env.TENCENT_COS_PROTOCOL;
|
||
|
||
if (!SecretId || !SecretKey || !Region || !Bucket) {
|
||
console.error(JSON.stringify({
|
||
success: false,
|
||
error: '缺少环境变量,需要:TENCENT_COS_SECRET_ID, TENCENT_COS_SECRET_KEY, TENCENT_COS_REGION, TENCENT_COS_BUCKET',
|
||
}));
|
||
process.exit(1);
|
||
}
|
||
|
||
const cosOptions = { SecretId, SecretKey };
|
||
|
||
if (Domain) {
|
||
cosOptions.Domain = Domain;
|
||
}
|
||
|
||
if (ServiceDomain) {
|
||
cosOptions.ServiceDomain = ServiceDomain;
|
||
}
|
||
|
||
if (Protocol) {
|
||
cosOptions.Protocol = Protocol;
|
||
}
|
||
|
||
const cos = new COS(cosOptions);
|
||
|
||
// 解析命令行参数
|
||
function parseArgs(args) {
|
||
const result = {};
|
||
for (let i = 0; i < args.length; i++) {
|
||
const arg = args[i];
|
||
if (arg.startsWith('--')) {
|
||
const key = arg.slice(2);
|
||
const next = args[i + 1];
|
||
if (next && !next.startsWith('--')) {
|
||
result[key] = next;
|
||
i++;
|
||
} else {
|
||
result[key] = true;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// 输出 JSON 结果
|
||
function output(data) {
|
||
console.log(JSON.stringify(data, null, 2));
|
||
}
|
||
|
||
// 封装 COS SDK 回调为 Promise
|
||
function cosPromise(method, params) {
|
||
return new Promise((resolve, reject) => {
|
||
cos[method]({ Bucket, Region, ...params }, (err, data) => {
|
||
if (err) {
|
||
reject(err);
|
||
} else {
|
||
resolve(data);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ========== 操作实现 ==========
|
||
|
||
async function upload(opts) {
|
||
const filePath = opts.file;
|
||
const key = opts.key || basename(filePath);
|
||
|
||
if (!filePath) {
|
||
throw new Error('缺少 --file 参数');
|
||
}
|
||
if (!existsSync(filePath)) {
|
||
throw new Error(`文件不存在:${filePath}`);
|
||
}
|
||
|
||
const data = await cosPromise('putObject', {
|
||
Key: key,
|
||
Body: createReadStream(filePath),
|
||
});
|
||
|
||
output({
|
||
success: true,
|
||
action: 'upload',
|
||
key,
|
||
etag: data.ETag,
|
||
location: data.Location,
|
||
statusCode: data.statusCode,
|
||
});
|
||
}
|
||
|
||
async function putString(opts) {
|
||
const content = opts.content;
|
||
const key = opts.key;
|
||
const contentType = opts['content-type'] || 'text/plain';
|
||
|
||
if (!content) {
|
||
throw new Error('缺少 --content 参数');
|
||
}
|
||
if (!key) {
|
||
throw new Error('缺少 --key 参数');
|
||
}
|
||
|
||
const data = await cosPromise('putObject', {
|
||
Key: key,
|
||
Body: content,
|
||
ContentType: contentType,
|
||
});
|
||
|
||
output({
|
||
success: true,
|
||
action: 'put-string',
|
||
key,
|
||
etag: data.ETag,
|
||
location: data.Location,
|
||
statusCode: data.statusCode,
|
||
});
|
||
}
|
||
|
||
async function download(opts) {
|
||
const key = opts.key;
|
||
const outputPath = opts.output || basename(key);
|
||
|
||
if (!key) {
|
||
throw new Error('缺少 --key 参数');
|
||
}
|
||
|
||
const data = await cosPromise('getObject', {
|
||
Key: key,
|
||
});
|
||
|
||
const resolvedPath = resolve(outputPath);
|
||
const ws = createWriteStream(resolvedPath);
|
||
|
||
if (data.Body instanceof Buffer) {
|
||
ws.write(data.Body);
|
||
ws.end();
|
||
} else if (data.Body && typeof data.Body.pipe === 'function') {
|
||
await pipeline(data.Body, ws);
|
||
} else {
|
||
ws.write(String(data.Body));
|
||
ws.end();
|
||
}
|
||
|
||
output({
|
||
success: true,
|
||
action: 'download',
|
||
key,
|
||
savedTo: resolvedPath,
|
||
contentLength: data.headers?.['content-length'],
|
||
statusCode: data.statusCode,
|
||
});
|
||
}
|
||
|
||
async function list(opts) {
|
||
const prefix = opts.prefix || '';
|
||
const maxKeys = parseInt(opts['max-keys'], 10) || 100;
|
||
|
||
const data = await cosPromise('getBucket', {
|
||
Prefix: prefix,
|
||
MaxKeys: maxKeys,
|
||
});
|
||
|
||
const files = (data.Contents || []).map(item => ({
|
||
key: item.Key,
|
||
size: parseInt(item.Size, 10),
|
||
lastModified: item.LastModified,
|
||
etag: item.ETag,
|
||
storageClass: item.StorageClass,
|
||
}));
|
||
|
||
output({
|
||
success: true,
|
||
action: 'list',
|
||
prefix,
|
||
count: files.length,
|
||
isTruncated: data.IsTruncated === 'true',
|
||
files,
|
||
});
|
||
}
|
||
|
||
async function signUrl(opts) {
|
||
const key = opts.key;
|
||
const expires = parseInt(opts.expires, 10) || 3600;
|
||
|
||
if (!key) {
|
||
throw new Error('缺少 --key 参数');
|
||
}
|
||
|
||
const url = await new Promise((resolve, reject) => {
|
||
cos.getObjectUrl({
|
||
Bucket,
|
||
Region,
|
||
Key: key,
|
||
Expires: expires,
|
||
Sign: true,
|
||
}, (err, data) => {
|
||
if (err) {
|
||
reject(err);
|
||
} else {
|
||
resolve(data.Url);
|
||
}
|
||
});
|
||
});
|
||
|
||
output({
|
||
success: true,
|
||
action: 'sign-url',
|
||
key,
|
||
expires,
|
||
url,
|
||
});
|
||
}
|
||
|
||
async function deleteObject(opts) {
|
||
const key = opts.key;
|
||
|
||
if (!key) {
|
||
throw new Error('缺少 --key 参数');
|
||
}
|
||
|
||
const data = await cosPromise('deleteObject', {
|
||
Key: key,
|
||
});
|
||
|
||
output({
|
||
success: true,
|
||
action: 'delete',
|
||
key,
|
||
statusCode: data.statusCode,
|
||
});
|
||
}
|
||
|
||
async function head(opts) {
|
||
const key = opts.key;
|
||
|
||
if (!key) {
|
||
throw new Error('缺少 --key 参数');
|
||
}
|
||
|
||
const data = await cosPromise('headObject', {
|
||
Key: key,
|
||
});
|
||
|
||
output({
|
||
success: true,
|
||
action: 'head',
|
||
key,
|
||
contentLength: parseInt(data.headers?.['content-length'], 10),
|
||
contentType: data.headers?.['content-type'],
|
||
etag: data.headers?.etag,
|
||
lastModified: data.headers?.['last-modified'],
|
||
storageClass: data.headers?.['x-cos-storage-class'] || 'STANDARD',
|
||
statusCode: data.statusCode,
|
||
});
|
||
}
|
||
|
||
// ========== 主入口 ==========
|
||
|
||
const args = process.argv.slice(2);
|
||
const action = args[0];
|
||
const opts = parseArgs(args.slice(1));
|
||
|
||
const actions = {
|
||
upload,
|
||
'put-string': putString,
|
||
download,
|
||
list,
|
||
'sign-url': signUrl,
|
||
delete: deleteObject,
|
||
head,
|
||
};
|
||
|
||
if (!action || !actions[action]) {
|
||
output({
|
||
success: false,
|
||
error: `未知操作:${action || '(空)'}`,
|
||
availableActions: Object.keys(actions),
|
||
usage: 'node cos_node.mjs <action> [--option value ...]',
|
||
});
|
||
process.exit(1);
|
||
}
|
||
|
||
try {
|
||
await actions[action](opts);
|
||
} catch (err) {
|
||
output({
|
||
success: false,
|
||
action,
|
||
error: err.message || String(err),
|
||
code: err.code,
|
||
});
|
||
process.exit(1);
|
||
}
|