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