jianzhihuixiang/skills/tencent-cos-skill/scripts/cos_node.mjs
小虾米 f8894dd3f3 fix: 修复上一章/下一章导航对小数章节(如107.5)的支持
- updateNavButtons: 改用数组索引查找,而非简单的+1/-1
- updateSidebarHighlight: 使用parseFloat比较章节ID
- updateMobileTOCHighlight: 使用parseFloat比较章节ID
- 更新版本号强制刷新缓存
2026-03-27 17:42:41 +08:00

324 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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