180 lines
5.9 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* Corpus COS Fetcher · 从腾讯云对象存储拉取冰朔上传的语料
*
* 系统底层标识: SYS-GLW-0001 / TCS-0002
* 版权号: 国作登字-2026-A-00037559
* 作者: 冰朔 (ICE-GL) · 实现: 铸渊 (ICE-GL-ZY001)
*
* 架构引用: HLDP-ARCH-002 § · factory/training/README.md
*
* 设计:
* 冰朔把所有语料GPT 聊天记录 / Notion 导出 / 其他放在 COS 存储桶
* 训练机/本地从 COS prefix 拉取到 ./corpus/raw/{source}/ 目录
* 后续由 harvest.js + manual-import 接力处理 切分 训练就绪
*
* 用法:
* node scripts/corpus-harvester/cos-fetch.js \
* --bucket guanghu-corpus-1300000000 \
* --region ap-shanghai \
* --prefix gpt-export/ \
* --dest ./corpus/raw/gpt
*
* # 或者从环境变量读
* COS_BUCKET=... COS_REGION=... COS_PREFIX=gpt-export/ \
* node scripts/corpus-harvester/cos-fetch.js --dest ./corpus/raw/gpt
*
* 凭据:
* 通过环境变量读取 (TENCENT_COS_SECRET_ID / TENCENT_COS_SECRET_KEY)
* 不写在仓库里也可走临时密钥 (STS) 注入
*
* 状态: 骨架本地无 SDK / 无凭据时进入 dry-run仅打印将要拉取的内容
*/
'use strict';
const fs = require('fs');
const path = require('path');
const ARGS = parseArgs(process.argv.slice(2));
const CONFIG = {
bucket: ARGS.bucket || process.env.COS_BUCKET,
region: ARGS.region || process.env.COS_REGION,
prefix: ARGS.prefix || process.env.COS_PREFIX || '',
dest: ARGS.dest || process.env.COS_DEST || './corpus/raw',
secretId: process.env.TENCENT_COS_SECRET_ID,
secretKey: process.env.TENCENT_COS_SECRET_KEY,
dryRun: ARGS['dry-run'] === true || !process.env.TENCENT_COS_SECRET_ID,
};
const COPYRIGHT = {
registration: '国作登字-2026-A-00037559',
sovereign: '冰朔 · TCS-0002∞',
system_root: 'SYS-GLW-0001',
arch_ref: 'HLDP-ARCH-002',
};
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (!a.startsWith('--')) continue;
const key = a.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith('--')) {
out[key] = next;
i++;
} else {
out[key] = true;
}
}
return out;
}
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
function logBanner() {
console.log('═'.repeat(64));
console.log('Corpus COS Fetcher · 光湖语料拉取器');
console.log('灵魂印记:', JSON.stringify(COPYRIGHT, null, 0));
console.log('═'.repeat(64));
console.log(`bucket: ${CONFIG.bucket || '<未设置>'}`);
console.log(`region: ${CONFIG.region || '<未设置>'}`);
console.log(`prefix: ${CONFIG.prefix || '<空>'}`);
console.log(`dest: ${CONFIG.dest}`);
console.log(`dryRun: ${CONFIG.dryRun}`);
console.log('─'.repeat(64));
}
function validateConfig() {
if (!CONFIG.bucket || !CONFIG.region) {
console.error('❌ 缺少 --bucket / --region或环境变量 COS_BUCKET / COS_REGION');
process.exit(2);
}
if (CONFIG.dryRun) {
console.warn(
'⚠️ dry-run 模式(未提供 TENCENT_COS_SECRET_ID/KEY 或显式 --dry-run' +
'\n 仅打印将要执行的操作,不真正下载。'
);
}
}
/**
* 真实下载实现骨架
* 当前不引入 cos-nodejs-sdk-v5 依赖避免本仓多一份重依赖
* 训练机上线时铸渊会:
* 1) `npm i --save-optional cos-nodejs-sdk-v5`
* 2) 把下面的 TODO 替换为实际 listObjects + getObject 循环
*/
async function fetchFromCOS() {
if (CONFIG.dryRun) {
console.log('[dry-run] 将列出 prefix 下所有对象 → 拉取到本地 dest');
console.log('[dry-run] 实际启用需安装 cos-nodejs-sdk-v5 + 注入凭据。');
return { downloaded: 0, dryRun: true };
}
// TODO: 训练机上线后启用
// const COS = require('cos-nodejs-sdk-v5');
// const cos = new COS({ SecretId: CONFIG.secretId, SecretKey: CONFIG.secretKey });
// const list = await new Promise((res, rej) =>
// cos.getBucket({ Bucket: CONFIG.bucket, Region: CONFIG.region, Prefix: CONFIG.prefix, MaxKeys: 1000 },
// (err, data) => (err ? rej(err) : res(data))));
// for (const obj of list.Contents) {
// if (obj.Key.endsWith('/')) continue; // 跳过目录占位
// const localPath = path.join(CONFIG.dest, obj.Key.replace(CONFIG.prefix, ''));
// ensureDir(path.dirname(localPath));
// await new Promise((res, rej) =>
// cos.getObject({ Bucket: CONFIG.bucket, Region: CONFIG.region, Key: obj.Key, Output: fs.createWriteStream(localPath) },
// (err) => (err ? rej(err) : res())));
// console.log(` ✓ ${obj.Key} → ${localPath} (${obj.Size} bytes)`);
// }
// return { downloaded: list.Contents.length, dryRun: false };
throw new Error(
'真实下载尚未启用 · 见 cos-fetch.js 中的 TODO需安装 cos-nodejs-sdk-v5'
);
}
function writeFetchManifest(result) {
const manifestPath = path.join(CONFIG.dest, '_fetch-manifest.json');
ensureDir(CONFIG.dest);
const manifest = {
schema: 'cos-fetch-manifest/v1',
fetched_at: new Date().toISOString(),
config: {
bucket: CONFIG.bucket,
region: CONFIG.region,
prefix: CONFIG.prefix,
dest: CONFIG.dest,
dryRun: CONFIG.dryRun,
},
result,
soul_marker: COPYRIGHT,
};
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
console.log(`📄 manifest 写入: ${manifestPath}`);
}
async function main() {
logBanner();
validateConfig();
ensureDir(CONFIG.dest);
let result;
try {
result = await fetchFromCOS();
} catch (err) {
console.error('❌ 拉取失败:', err.message);
result = { error: err.message };
process.exitCode = 1;
}
writeFetchManifest(result);
console.log('─'.repeat(64));
console.log('完成。下一步: 运行 manual-import 模式把 raw 数据切分成训练就绪格式。');
}
main();