guanghulab/scripts/cos-training-trigger.js

493 lines
19 KiB
JavaScript
Raw Normal View History

/**
*
* 🧠 COS训练触发器 · 端到端训练管线
*
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 扫描COS桶中的新语料 解压/转换TCS格式 启动训练会话
*
* 设计:
* 1. 扫描cold桶列出所有文件含非压缩文件和文件夹
* 2. 对比tcs-structured/目录找出未处理的语料
* 3. 自动提取/转换为TCS结构化格式
* 4. 启动训练会话用LLM分析和分类
* 5. 输出处理结果写入日志
*
* 运行方式:
* node scripts/cos-training-trigger.js [scan|extract|train|full]
*
* scan 仅扫描输出未处理语料列表
* extract 扫描并解压/转换为TCS格式
* train 对已有TCS语料启动训练
* full 完整流程: 扫描 提取 训练
*/
'use strict';
const path = require('path');
const fs = require('fs');
// ─── 路径 ───
const ROOT = path.resolve(__dirname, '..');
const COS_MODULE = path.join(ROOT, 'server', 'age-os', 'mcp-server', 'cos');
const EXTRACTOR_MODULE = path.join(ROOT, 'server', 'age-os', 'mcp-server', 'tools', 'corpus-extractor-ops');
const TRAINING_MODULE = path.join(ROOT, 'server', 'age-os', 'mcp-server', 'tools', 'training-agent-ops');
// ─── 延迟加载模块允许在CI中跳过数据库依赖 ───
let cos, extractor, trainer;
function loadModules() {
cos = require(COS_MODULE);
extractor = require(EXTRACTOR_MODULE);
trainer = require(TRAINING_MODULE);
}
// ─── 配置 ───
const DEFAULT_BUCKET = 'cold';
const DEFAULT_PERSONA = 'zhuyuan';
const PROCESSED_PREFIX = 'tcs-structured/';
const MAX_EXTRACT_PER_RUN = 20; // 每次最多处理文件数
const MAX_TRAIN_PER_RUN = 5; // 每次最多训练文件数
const MAX_EXTRACT_FILE_SIZE = 200 * 1024 * 1024; // 200MB — 超过此阈值的文件使用分块策略
// ─── 排除路径(不视为语料的目录/文件) ───
const EXCLUDED_PREFIXES = [
'tcs-structured/',
'training-sessions/',
'training-results/',
'training-memory/',
];
// ─── 支持的语料文件扩展名(含非压缩格式) ───
const CORPUS_EXTENSIONS = [
'.zip', '.gz', '.tar.gz', '.tgz', '.json.gz', // 压缩格式
'.json', '.jsonl', '.md', '.txt', '.csv', // 非压缩格式
];
/**
* 判断文件是否为语料文件
*/
function isCorpusFile(key) {
// 排除处理结果目录
for (const prefix of EXCLUDED_PREFIXES) {
if (key.startsWith(prefix)) return false;
}
// 匹配扩展名
const lower = key.toLowerCase();
return CORPUS_EXTENSIONS.some(ext => lower.endsWith(ext));
}
/**
* 判断是否为语料目录 repo-archive/
*/
function isCorpusDirectory(key) {
for (const prefix of EXCLUDED_PREFIXES) {
if (key.startsWith(prefix)) return false;
}
return key.endsWith('/');
}
/**
* 从已处理列表中判断某文件是否已处理
*/
function isProcessed(rawKey, processedFiles) {
// 从rawKey提取基础文件名处理多重扩展名如.tar.gz
let baseName = rawKey.split('/').pop();
// 移除所有已知的语料文件扩展名
for (const ext of ['.tar.gz', '.json.gz', '.tgz', '.zip', '.gz', '.jsonl', '.json', '.md', '.txt', '.csv']) {
if (baseName.toLowerCase().endsWith(ext)) {
baseName = baseName.slice(0, -ext.length);
break;
}
}
return processedFiles.some(f => f.key.includes(baseName));
}
// ═══════════════════════════════════════════
// 命令: scan — 扫描未处理语料
// ═══════════════════════════════════════════
async function cmdScan(bucket) {
console.log('═══ COS训练触发器 · 语料扫描 ═══\n');
const bucketName = bucket || DEFAULT_BUCKET;
// 列出所有文件
const allFiles = await cos.list(bucketName, '', 500);
// 列出已处理文件
const processed = await cos.list(bucketName, PROCESSED_PREFIX, 500);
const processedFiles = processed.files.filter(f => f.key.endsWith('.tcs.json'));
// 分类
const corpusFiles = [];
const corpusDirs = [];
for (const file of allFiles.files) {
if (isCorpusFile(file.key)) {
const alreadyProcessed = isProcessed(file.key, processedFiles);
corpusFiles.push({
key: file.key,
size_bytes: file.size_bytes,
processed: alreadyProcessed
});
} else if (isCorpusDirectory(file.key)) {
corpusDirs.push({ key: file.key });
}
}
const pending = corpusFiles.filter(f => !f.processed);
console.log(`桶: ${bucketName}`);
console.log(`总文件数: ${allFiles.files.length}`);
console.log(`语料文件: ${corpusFiles.length}`);
console.log(`语料目录: ${corpusDirs.length}`);
console.log(`已处理: ${corpusFiles.length - pending.length}`);
console.log(`待处理: ${pending.length}`);
console.log(`已生成TCS: ${processedFiles.length}`);
if (pending.length > 0) {
console.log('\n📋 待处理语料:');
for (const f of pending) {
console.log(` 📄 ${f.key} (${formatBytes(f.size_bytes)})`);
}
}
if (corpusDirs.length > 0) {
console.log('\n📁 语料目录:');
for (const d of corpusDirs) {
console.log(` 📂 ${d.key}`);
}
// 扫描目录内的文件
for (const dir of corpusDirs) {
try {
const dirFiles = await cos.list(bucketName, dir.key, 100);
const dirCorpus = dirFiles.files.filter(f => isCorpusFile(f.key));
if (dirCorpus.length > 0) {
console.log(` └── ${dir.key} 内含 ${dirCorpus.length} 个语料文件`);
for (const f of dirCorpus) {
const alreadyProcessed = isProcessed(f.key, processedFiles);
if (!alreadyProcessed) {
pending.push({
key: f.key,
size_bytes: f.size_bytes,
processed: false
});
console.log(` 📄 ${f.key} (${formatBytes(f.size_bytes)}) [待处理]`);
}
}
}
} catch (err) {
console.log(` └── ${dir.key} 扫描失败: ${err.message}`);
}
}
}
// 写入GitHub Actions输出
if (process.env.GITHUB_OUTPUT) {
const outputLines = [
`pending=${pending.length}`,
`total_corpus=${corpusFiles.length}`,
`processed=${processedFiles.length}`,
`has_new_corpus=${pending.length > 0 ? 'true' : 'false'}`,
`pending_files=${pending.map(f => f.key).join(',')}`
];
fs.appendFileSync(process.env.GITHUB_OUTPUT, outputLines.join('\n') + '\n');
}
return { pending, processedFiles, corpusDirs };
}
// ═══════════════════════════════════════════
// 命令: extract — 提取/转换语料为TCS格式
// ═══════════════════════════════════════════
async function cmdExtract(bucket) {
console.log('═══ COS训练触发器 · 语料提取 ═══\n');
const bucketName = bucket || DEFAULT_BUCKET;
const { pending } = await cmdScan(bucketName);
if (pending.length === 0) {
console.log('\n✅ 无待处理语料');
writeGitHubOutput('extracted=0', 'extract_status=skipped');
return { extracted: 0, errors: 0 };
}
const toProcess = pending.slice(0, MAX_EXTRACT_PER_RUN);
console.log(`\n🔄 开始提取 ${toProcess.length}/${pending.length} 个文件...\n`);
let extracted = 0;
let errors = 0;
let skipped = 0;
const results = [];
for (const file of toProcess) {
try {
// 大文件预警
const sizeMB = (file.size_bytes / 1024 / 1024).toFixed(1);
if (file.size_bytes > MAX_EXTRACT_FILE_SIZE) {
console.log(` 📦 处理: ${file.key} (${sizeMB}MB · 超大文件,使用分块策略)...`);
} else {
console.log(` 📦 处理: ${file.key} (${sizeMB}MB)...`);
}
const result = await extractor.cosExtractCorpus({
bucket: bucketName,
key: file.key,
output_bucket: bucketName,
output_prefix: PROCESSED_PREFIX
});
// 根据返回状态分类计数
if (result.status === 'zip_detected') {
skipped++;
results.push({ key: file.key, status: 'skipped', reason: 'zip_needs_special_tool' });
console.log(` ⏭️ 跳过: ${file.key} — ZIP文件需要专用工具处理`);
} else if (result.status === 'skipped_too_large') {
skipped++;
results.push({ key: file.key, status: 'skipped', reason: 'too_large', size_mb: result.size_mb });
console.log(` ⏭️ 跳过: ${file.key}${result.message}`);
} else if (result.status === 'partial_extract') {
extracted++;
results.push({ key: file.key, status: 'partial', output: result.output?.key, message: result.message });
console.log(` 🔶 部分提取: ${result.message}`);
} else {
extracted++;
results.push({ key: file.key, status: 'success', output: result.output?.key });
console.log(` ✅ 完成: ${result.output?.key || '已处理'} (${result.entries || 0} 条目)`);
}
} catch (err) {
errors++;
results.push({ key: file.key, status: 'error', error: err.message });
console.log(` ❌ 失败: ${file.key}${err.message}`);
}
}
console.log(`\n═══ 提取完毕 ═══`);
console.log(`✅ 成功: ${extracted}`);
console.log(`⏭️ 跳过: ${skipped}`);
console.log(`❌ 失败: ${errors}`);
console.log(`⏳ 剩余: ${pending.length - toProcess.length}`);
writeGitHubOutput(
`extracted=${extracted}`,
`extract_skipped=${skipped}`,
`extract_errors=${errors}`,
`extract_status=${errors > 0 ? 'partial' : 'success'}`
);
return { extracted, errors, skipped, results };
}
// ═══════════════════════════════════════════
// 命令: train — 对TCS语料启动训练
// ═══════════════════════════════════════════
async function cmdTrain(bucket, personaId) {
console.log('═══ COS训练触发器 · 训练处理 ═══\n');
const bucketName = bucket || DEFAULT_BUCKET;
const persona = personaId || DEFAULT_PERSONA;
// 列出可用的TCS语料
const processed = await cos.list(bucketName, PROCESSED_PREFIX, 500);
const tcsFiles = processed.files.filter(f => f.key.endsWith('.tcs.json'));
if (tcsFiles.length === 0) {
console.log('⚠️ 无TCS结构化语料可训练。请先运行 extract 命令。');
writeGitHubOutput('trained=0', 'train_status=no_corpus');
return { trained: 0, errors: 0 };
}
console.log(`📚 找到 ${tcsFiles.length} 个TCS语料文件`);
// 检查已有训练结果,避免重复处理
let existingResults = [];
try {
const existing = await cos.list(bucketName, `training-results/${persona}/`, 100);
existingResults = existing.files.filter(f => f.key.endsWith('.json'));
} catch (err) {
console.log(`⚠️ 无法读取已有训练结果: ${err.message}`);
}
// 启动训练会话
console.log(`\n🧠 启动训练会话 · 人格体: ${persona}`);
let session;
try {
session = await trainer.trainingStartSession({
persona_id: persona,
corpus_bucket: bucketName,
corpus_prefix: PROCESSED_PREFIX,
session_name: `自动训练-${new Date().toISOString().slice(0, 10)}`
});
console.log(`✅ 会话已启动: ${session.session_id}`);
console.log(` 可用模型: ${session.models.available.map(m => m.name).join(', ') || '无'}`);
} catch (err) {
console.log(`❌ 训练会话启动失败: ${err.message}`);
writeGitHubOutput('trained=0', `train_status=session_error`, `train_error=${err.message}`);
return { trained: 0, errors: 1, error: err.message };
}
// 检查是否有可用的LLM模型
if (!session.models.available || session.models.available.length === 0) {
console.log('⚠️ 无可用LLM模型需要配置 ZY_DEEPSEEK_API_KEY 等密钥)');
console.log(' 训练会话已记录等待LLM密钥配置后再次运行。');
writeGitHubOutput('trained=0', 'train_status=no_llm_keys');
return { trained: 0, errors: 0, note: '无LLM密钥' };
}
// 处理语料
const toTrain = tcsFiles.slice(0, MAX_TRAIN_PER_RUN);
let trained = 0;
let trainErrors = 0;
const trainResults = [];
for (const tcsFile of toTrain) {
try {
console.log(` 🔬 训练处理: ${tcsFile.key}...`);
const result = await trainer.trainingProcessCorpus({
corpus_bucket: bucketName,
corpus_key: tcsFile.key,
persona_id: persona,
max_entries: 10
});
trained++;
trainResults.push({ key: tcsFile.key, status: 'success', ...result });
console.log(` ✅ 完成: ${result.classified}/${result.total} 分类成功`);
} catch (err) {
trainErrors++;
trainResults.push({ key: tcsFile.key, status: 'error', error: err.message });
console.log(` ❌ 失败: ${tcsFile.key}${err.message}`);
}
}
console.log(`\n═══ 训练完毕 ═══`);
console.log(`✅ 成功: ${trained}`);
console.log(`❌ 失败: ${trainErrors}`);
writeGitHubOutput(
`trained=${trained}`,
`train_errors=${trainErrors}`,
`train_status=${trainErrors > 0 ? 'partial' : 'success'}`
);
return { trained, errors: trainErrors, results: trainResults, session_id: session.session_id };
}
// ═══════════════════════════════════════════
// 命令: full — 完整流程
// ═══════════════════════════════════════════
async function cmdFull(bucket, personaId) {
console.log('╔═══════════════════════════════════════════╗');
console.log('║ COS训练触发器 · 完整训练管线 ║');
console.log('║ 铸渊 · ICE-GL-ZY001 ║');
console.log('╚═══════════════════════════════════════════╝\n');
const bucketName = bucket || DEFAULT_BUCKET;
const persona = personaId || DEFAULT_PERSONA;
const startTime = Date.now();
// 第一步: 提取语料
console.log('📍 第一步: 提取语料\n');
const extractResult = await cmdExtract(bucketName);
// 第二步: 训练处理
console.log('\n📍 第二步: 训练处理\n');
const trainResult = await cmdTrain(bucketName, persona);
// 汇总
const duration = Date.now() - startTime;
console.log('\n╔═══════════════════════════════════════════╗');
console.log('║ 完整训练管线 · 运行完毕 ║');
console.log('╚═══════════════════════════════════════════╝');
console.log(` 提取: ${extractResult.extracted} 成功 / ${extractResult.skipped || 0} 跳过 / ${extractResult.errors} 失败`);
console.log(` 训练: ${trainResult.trained} 成功 / ${trainResult.errors} 失败`);
console.log(` 耗时: ${(duration / 1000).toFixed(1)}s`);
writeGitHubOutput(
`pipeline_status=${(extractResult.errors + trainResult.errors) > 0 ? 'partial' : 'success'}`,
`pipeline_duration_ms=${duration}`
);
return { extract: extractResult, train: trainResult, duration_ms: duration };
}
// ═══════════════════════════════════════════
// 辅助函数
// ═══════════════════════════════════════════
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
function writeGitHubOutput(...lines) {
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, lines.join('\n') + '\n');
}
}
// ═══════════════════════════════════════════
// CLI 入口
// ═══════════════════════════════════════════
async function main() {
const args = process.argv.slice(2);
const command = args[0] || 'scan';
const bucket = args.find(a => a.startsWith('--bucket='))?.split('=')[1] || DEFAULT_BUCKET;
const persona = args.find(a => a.startsWith('--persona='))?.split('=')[1] || DEFAULT_PERSONA;
// 加载模块
try {
loadModules();
} catch (err) {
console.error(`❌ 模块加载失败: ${err.message}`);
console.error(' 请确保 server/age-os/mcp-server/ 依赖已安装');
process.exit(1);
}
switch (command) {
case 'scan':
await cmdScan(bucket);
break;
case 'extract':
await cmdExtract(bucket);
break;
case 'train':
await cmdTrain(bucket, persona);
break;
case 'full':
await cmdFull(bucket, persona);
break;
default:
console.log('COS训练触发器 · 铸渊 · ICE-GL-ZY001');
console.log('');
console.log('用法:');
console.log(' node scripts/cos-training-trigger.js scan — 扫描未处理语料');
console.log(' node scripts/cos-training-trigger.js extract — 提取/转换为TCS格式');
console.log(' node scripts/cos-training-trigger.js train — 启动训练处理');
console.log(' node scripts/cos-training-trigger.js full — 完整流程');
console.log('');
console.log('选项:');
console.log(' --bucket=cold|hot|team — 指定COS桶默认cold');
console.log(' --persona=zhuyuan — 指定人格体默认zhuyuan');
break;
}
}
main().catch(err => {
console.error('COS训练触发器异常:', err.message);
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'pipeline_status=error\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, `pipeline_error=${err.message}\n`);
}
process.exit(1);
});