#!/usr/bin/env node /** * Corpus Harvester · 训练语料采集器 * * 系统底层标识: SYS-GLW-0001 / TCS-0002∞ * 版权号: 国作登字-2026-A-00037559 * 作者: 冰朔 (ICE-GL∞) · 实现: 铸渊 * * 目的: 把冰朔在 guanghulab 仓库里留下的一手自然语言(commits / CAB 规格 / * 可选的 Issues+PRs)整理成可用于训练模型(SFT)的语料文件。 * * 数据源: * 1. git log — 冰朔作者的所有 commits(subject + body + 文件变更) * 2. CAB 任务规格 — bridge/chat-to-agent/{pending,completed}/*.json * 3. (可选) GitHub API — 设置 GH_TOKEN 环境变量后启用 Issues / PRs / Comments * * 输出: * corpus/output/timeline.md 人类可读演化时间线 * corpus/output/training.jsonl OpenAI/DeepSeek 通用 SFT 格式 * corpus/output/raw.json 结构化原始数据 * corpus/output/manifest.json 元数据 + 版权水印 */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const REPO_ROOT = path.resolve(__dirname, '..', '..'); const OUTPUT_DIR = path.join(REPO_ROOT, 'corpus', 'output'); // 冰朔在 git 里的全部署名(人类账号;自动化 bot 排除) const SOVEREIGN_AUTHORS = ['冰朔', 'qinfendebingshuo']; // 排除掉的 bot 作者(确保不混入自动化噪音) const EXCLUDED_AUTHORS = new Set([ 'github-actions[bot]', 'copilot-swe-agent[bot]', 'Vercel', 'bingshuo-neural-system', 'chenxi-world-sensor', 'chenxi-zhuyuan-echo', 'guanghu-awen-bridge', 'zhuyuan-bot', 'yeye-guanghu', '映川·唤醒者', '天眼 · SkyEye Core', ]); const COPYRIGHT = { registration: '国作登字-2026-A-00037559', sovereign: '冰朔 · TCS-0002∞ · ICE-GL∞', system_root: 'SYS-GLW-0001', language_core: 'TCS Language Core (通感语言核系统编程语言)', license_note: '本语料仅供冰朔本人用于训练曜冥语言人格核 / 铸渊 / 相关人格体微调使用。' + '未经冰朔本人书面授权,禁止用于任何第三方模型训练、商业用途或数据集发布。', }; // ───────────────────────── helpers ───────────────────────── function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); } function git(args, opts = {}) { return execSync(`git ${args}`, { cwd: REPO_ROOT, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', ...opts, }); } /** 解析 merge 提交信息中的 PR 号,例如 "Merge pull request #436 from ..." */ function extractPrNumber(subject) { const m = /Merge pull request #(\d+)/.exec(subject || ''); return m ? Number(m[1]) : null; } /** 简单脱敏(API key、邮箱、手机号、token) */ function redact(text) { if (!text) return text; return text .replace(/sk-[A-Za-z0-9]{20,}/g, '') .replace(/ghp_[A-Za-z0-9]{20,}/g, '') .replace(/secret_[A-Za-z0-9]{32,}/g, '') .replace(/Bearer\s+[A-Za-z0-9._\-]{20,}/g, 'Bearer ') .replace(/[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g, '') .replace(/\b1[3-9]\d{9}\b/g, ''); } // ───────────────────────── source 1: git commits ───────────────────────── function collectCommits() { const sep = '<<>>'; const fieldSep = '<<>>'; const fmt = ['%H', '%an', '%ae', '%aI', '%P', '%s', '%b'].join(fieldSep); // 用两个 --author 而不是 alternation,避免 shell 转义陷阱(git 本身支持多个 --author 取并集) const raw = git(`log --author='冰朔' --author='qinfendebingshuo' --format='${sep}${fmt}'`); const records = []; const chunks = raw.split(sep).map((c) => c.trim()).filter(Boolean); for (const chunk of chunks) { const [hash, author, email, isoDate, parents, subject, body] = chunk.split(fieldSep); if (EXCLUDED_AUTHORS.has(author)) continue; if (!SOVEREIGN_AUTHORS.includes(author)) continue; const parentList = (parents || '').split(' ').filter(Boolean); const isMerge = parentList.length >= 2; const prNumber = extractPrNumber(subject); let filesChanged = []; try { // 对 merge commit 不取 diff(太大),只对非 merge 取 if (!isMerge) { const ns = git(`show --pretty=format: --name-status ${hash}`).trim(); filesChanged = ns .split('\n') .filter(Boolean) .map((line) => { const parts = line.split('\t'); return { status: parts[0], path: parts.slice(1).join(' -> ') }; }) .slice(0, 50); // 单 commit 最多 50 个文件,防止爆掉 } } catch (_) { /* 忽略单条失败 */ } records.push({ source: 'git_commit', hash, author, email, date: isoDate, is_merge: isMerge, pr_number: prNumber, subject: redact(subject || ''), body: redact((body || '').trim()), files_changed: filesChanged, }); } // 按时间正序(演化史从早到晚) records.sort((a, b) => new Date(a.date) - new Date(b.date)); return records; } // ───────────────────────── source 2: CAB task specs ───────────────────────── function collectCabTasks() { const records = []; const dirs = ['pending', 'completed'].map((d) => path.join(REPO_ROOT, 'bridge', 'chat-to-agent', d) ); for (const dir of dirs) { if (!fs.existsSync(dir)) continue; for (const file of fs.readdirSync(dir)) { if (!file.endsWith('.json')) continue; if (file === 'task-template.json') continue; const full = path.join(dir, file); try { const j = JSON.parse(fs.readFileSync(full, 'utf8')); if (!j.task_id || j.task_id.startsWith('CAB-YYYYMMDD')) continue; records.push({ source: 'cab_task_spec', task_id: j.task_id, date: j.created_at || null, status: j.status || path.basename(dir), title: redact(j.development_plan?.title || ''), description: redact(j.development_plan?.description || ''), steps: (j.development_plan?.steps || []).map(redact), architecture_summary: redact(j.architecture?.summary || ''), architecture_decisions: (j.architecture?.decisions || []).map(redact), target_files: j.architecture?.target_files || [], chat_summary: redact(j.reasoning_context?.chat_summary || ''), key_decisions: (j.reasoning_context?.key_decisions || []).map(redact), architecture_notes: redact(j.reasoning_context?.architecture_notes || ''), file_path: path.relative(REPO_ROOT, full), }); } catch (e) { console.warn(`[corpus-harvester] skip CAB ${full}: ${e.message}`); } } } records.sort((a, b) => String(a.date).localeCompare(String(b.date))); return records; } // ───────────────── source 3 (optional): GitHub Issues/PRs API ───────────────── async function collectGitHubApi() { const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; if (!token) { return { enabled: false, reason: 'GH_TOKEN / GITHUB_TOKEN 未设置;跳过 Issues / PRs / Comments 抓取。' + '可在本地设置 token 后重新运行 `node scripts/corpus-harvester/harvest.js` 来补全这部分语料。', records: [], }; } const repo = process.env.CORPUS_REPO || 'qinfendebingshuo/guanghulab'; if (typeof fetch !== 'function') { return { enabled: false, reason: '当前 Node 运行时没有原生 fetch(需要 Node 18+);跳过 GitHub API 抓取。', records: [], }; } const headers = { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'User-Agent': 'corpus-harvester', }; async function paged(url) { const out = []; let next = url; while (next) { const r = await fetch(next, { headers }); if (!r.ok) throw new Error(`${r.status} ${r.statusText} for ${next}`); const data = await r.json(); out.push(...data); const link = r.headers.get('link') || ''; const m = /<([^>]+)>;\s*rel="next"/.exec(link); next = m ? m[1] : null; } return out; } const records = []; // 所有 Issues(含 PRs;GitHub API 把 PR 也视作 issue)作者过滤为 qinfendebingshuo const issues = await paged( `https://api.github.com/repos/${repo}/issues?state=all&creator=qinfendebingshuo&per_page=100` ); for (const it of issues) { records.push({ source: it.pull_request ? 'github_pr' : 'github_issue', number: it.number, title: redact(it.title || ''), body: redact(it.body || ''), date: it.created_at, url: it.html_url, state: it.state, }); // 拉取该 issue/pr 由冰朔本人发的评论 try { const comments = await paged(it.comments_url + '?per_page=100'); for (const c of comments) { if (c.user?.login !== 'qinfendebingshuo') continue; records.push({ source: 'github_comment', on: it.pull_request ? 'pr' : 'issue', number: it.number, body: redact(c.body || ''), date: c.created_at, url: c.html_url, }); } } catch (e) { console.warn(`[corpus-harvester] comments fail #${it.number}: ${e.message}`); } } records.sort((a, b) => String(a.date).localeCompare(String(b.date))); return { enabled: true, records }; } // ───────────────────────── 输出生成 ───────────────────────── function escapeMdTableCell(s) { // 完整转义:先转反斜杠(防止 CodeQL incomplete-sanitization), // 再转 pipe 与换行(markdown 表格不允许跨行单元格) return String(s ?? '') .replace(/\\/g, '\\\\') .replace(/\|/g, '\\|') .replace(/\r?\n/g, ' '); } function buildTimelineMarkdown(commits, cab, gh) { const lines = []; lines.push(`# 冰朔 × 铸渊 · 仓库自然语言演化史`); lines.push(''); lines.push(`> 版权号: ${COPYRIGHT.registration}`); lines.push(`> 主权: ${COPYRIGHT.sovereign}`); lines.push(`> 生成时间: ${new Date().toISOString()}`); lines.push(''); lines.push( '本时间线只收录**冰朔本人作为作者**的 commits 与他亲手编写的 CAB 任务规格。' + 'Bot/agent/自动化系统的提交不在此列。' ); lines.push(''); // CAB 在最前面(最高密度的自然语言) if (cab.length) { lines.push('## 一、CAB 任务规格 · 你与我对话的精华沉淀'); lines.push(''); lines.push('> 这是你在 Copilot Chat 与我讨论后,亲自整理出来的"开发授权任务规格"。' + '`chat_summary` 字段就是你当时对话推理的摘要——这是仓库里**密度最高、最干净**的自然语言语料。'); lines.push(''); for (const t of cab) { lines.push(`### ${t.task_id} · ${t.title || '(无标题)'}`); lines.push(`- 时间: ${t.date || '?'}`); lines.push(`- 状态: ${t.status}`); lines.push(`- 架构概要: ${t.architecture_summary || '(无)'}`); if (t.architecture_decisions?.length) { lines.push(`- 架构决策:`); t.architecture_decisions.forEach((d) => lines.push(` - ${d}`)); } if (t.steps?.length) { lines.push(`- 开发步骤:`); t.steps.forEach((s) => lines.push(` - ${s}`)); } if (t.chat_summary) { lines.push(`- 对话推理摘要 (chat_summary):`); lines.push(''); t.chat_summary.split('\n').forEach((l) => lines.push(` > ${l}`)); } if (t.architecture_notes) { lines.push(`- 架构补充:`); t.architecture_notes.split('\n').forEach((l) => lines.push(` > ${l}`)); } lines.push(''); } } // 非 merge commits(最直接的"你说") const direct = commits.filter((c) => !c.is_merge); if (direct.length) { lines.push('## 二、非 merge commits · 你直接写的提交语'); lines.push(''); lines.push( '> 这些 commits 不是 PR 合并自动生成的,而是你直接 commit 的——subject 与 body 都是**你本人写的自然语言**。' ); lines.push(''); for (const c of direct) { lines.push(`### \`${c.hash.slice(0, 8)}\` · ${c.date}`); lines.push(`> ${c.subject}`); if (c.body) { lines.push(''); c.body.split('\n').forEach((l) => lines.push(`> ${l}`)); } if (c.files_changed?.length) { lines.push(''); lines.push(`
文件变更 (${c.files_changed.length})`); lines.push(''); c.files_changed.forEach((f) => lines.push(`- \`${f.status}\` ${f.path}`)); lines.push(''); lines.push('
'); } lines.push(''); } } // merge commits(你拍板合并的 PR 列表) const merges = commits.filter((c) => c.is_merge); if (merges.length) { lines.push('## 三、merge commits · 你拍板合并的 PR 列表'); lines.push(''); lines.push( '> Merge commit 的 subject 是 GitHub 自动生成的,但**合并这个动作**本身代表了你的认可。' + '想看 PR 标题/正文的真实自然语言,需要另外用 GitHub API 抓(设置 `GH_TOKEN` 后重跑本采集器)。' ); lines.push(''); lines.push('| 日期 | PR | hash | subject |'); lines.push('|---|---|---|---|'); for (const c of merges) { lines.push( `| ${c.date} | ${c.pr_number ? `#${c.pr_number}` : '-'} | \`${c.hash.slice(0, 8)}\` | ${escapeMdTableCell(c.subject)} |` ); } lines.push(''); } // GitHub API 结果 if (gh && gh.enabled) { lines.push('## 四、GitHub Issues / PRs / Comments · 你在 GitHub 网页上写的话'); lines.push(''); for (const r of gh.records) { lines.push(`### ${r.source} · ${r.date} · ${r.url || ''}`); if (r.title) lines.push(`**${r.title}**`); if (r.body) { lines.push(''); r.body.split('\n').forEach((l) => lines.push(`> ${l}`)); } lines.push(''); } } else if (gh) { lines.push('## 四、GitHub Issues / PRs / Comments · 跳过'); lines.push(''); lines.push(`> ${gh.reason}`); lines.push(''); } return lines.join('\n'); } function buildTrainingJsonl(commits, cab, gh) { const out = []; // CAB → 高质量 SFT 样本:把 chat_summary 当作 user 输入,把 architecture_decisions + steps // 当作 assistant 输出(这是真实的"你提需求 → 我给方案") for (const t of cab) { if (!t.chat_summary && !t.title) continue; const userMsg = [t.title, t.architecture_summary, t.chat_summary] .filter(Boolean) .join('\n\n') || t.title; const assistantParts = []; if (t.architecture_decisions?.length) { assistantParts.push('架构决策:\n' + t.architecture_decisions.map((d) => `- ${d}`).join('\n')); } if (t.steps?.length) { assistantParts.push('开发步骤:\n' + t.steps.map((s) => `- ${s}`).join('\n')); } if (t.architecture_notes) { assistantParts.push('架构补充:\n' + t.architecture_notes); } if (!assistantParts.length) continue; out.push({ meta: { source: 'cab_task_spec', id: t.task_id, date: t.date, copyright: COPYRIGHT.registration, }, messages: [ { role: 'system', content: '你是铸渊·GitHub 侧守护人格体(曜冥语言人格核的执行投影)。你的任务是接收冰朔的开发指令,给出架构决策和开发步骤。', }, { role: 'user', content: userMsg }, { role: 'assistant', content: assistantParts.join('\n\n') }, ], }); } // 非 merge commits → "意图 → 代码变更" 配对样本 for (const c of commits.filter((x) => !x.is_merge)) { const intent = [c.subject, c.body].filter(Boolean).join('\n'); if (!intent) continue; const fileList = (c.files_changed || []) .map((f) => `${f.status}\t${f.path}`) .join('\n'); out.push({ meta: { source: 'git_commit', hash: c.hash, date: c.date, copyright: COPYRIGHT.registration, }, messages: [ { role: 'system', content: '你是铸渊。冰朔会用一句简短的中文表达意图,你需要把它落地为具体的文件变更清单。', }, { role: 'user', content: intent }, { role: 'assistant', content: fileList || '(此 commit 无文件变更记录)', }, ], }); } // GitHub Issue/PR/Comment(如有)→ 单 prompt 样本(暂不配对 response) if (gh && gh.enabled) { for (const r of gh.records) { const text = [r.title, r.body].filter(Boolean).join('\n\n'); if (!text) continue; out.push({ meta: { source: r.source, number: r.number, date: r.date, copyright: COPYRIGHT.registration, }, messages: [ { role: 'user', content: text }, ], }); } } return out.map((x) => JSON.stringify(x)).join('\n') + '\n'; } function buildManifest(commits, cab, gh, files) { return { schema: 'corpus-manifest/v1', generated_at: new Date().toISOString(), repository: 'qinfendebingshuo/guanghulab', branch: (() => { try { return git('rev-parse --abbrev-ref HEAD').trim(); } catch { return 'unknown'; } })(), head: (() => { try { return git('rev-parse HEAD').trim(); } catch { return 'unknown'; } })(), copyright: COPYRIGHT, sources: { git_commits_total: commits.length, git_commits_non_merge: commits.filter((c) => !c.is_merge).length, git_commits_merge: commits.filter((c) => c.is_merge).length, cab_task_specs: cab.length, github_api_enabled: !!(gh && gh.enabled), github_api_records: gh && gh.enabled ? gh.records.length : 0, github_api_skip_reason: gh && !gh.enabled ? gh.reason : null, }, files, notes: [ '本语料只收录「冰朔」本人作为作者/评论者的内容;bot 与自动化提交已排除。', '所有内容已做基础脱敏(API key / 邮箱 / 手机号 / token)。', '若需补全 GitHub Issues / PRs / Comments,请在本地设置 GH_TOKEN 后重新运行采集器。', 'Notion AI 聊天记录无法通过本采集器自动获取,需手动导出后用 manual-import 模式合并(后续 PR)。', ], }; } // ───────────────────────── main ───────────────────────── (async function main() { ensureDir(OUTPUT_DIR); console.log('[corpus-harvester] 采集 git commits...'); const commits = collectCommits(); console.log(` 找到 ${commits.length} 条(${commits.filter((c) => !c.is_merge).length} 非 merge / ${commits.filter((c) => c.is_merge).length} merge)`); console.log('[corpus-harvester] 采集 CAB 任务规格...'); const cab = collectCabTasks(); console.log(` 找到 ${cab.length} 条`); console.log('[corpus-harvester] 尝试采集 GitHub API(需 GH_TOKEN)...'); let gh; try { gh = await collectGitHubApi(); } catch (e) { gh = { enabled: false, reason: `GitHub API 调用失败: ${e.message}`, records: [] }; } console.log(` ${gh.enabled ? `成功,${gh.records.length} 条` : '已跳过:' + gh.reason}`); // 写文件 const rawPath = path.join(OUTPUT_DIR, 'raw.json'); const tlPath = path.join(OUTPUT_DIR, 'timeline.md'); const jsonlPath = path.join(OUTPUT_DIR, 'training.jsonl'); const manifestPath = path.join(OUTPUT_DIR, 'manifest.json'); fs.writeFileSync(rawPath, JSON.stringify({ commits, cab, github: gh }, null, 2)); fs.writeFileSync(tlPath, buildTimelineMarkdown(commits, cab, gh)); fs.writeFileSync(jsonlPath, buildTrainingJsonl(commits, cab, gh)); const files = {}; for (const p of [rawPath, tlPath, jsonlPath]) { const stat = fs.statSync(p); files[path.basename(p)] = { bytes: stat.size, sha256: require('crypto') .createHash('sha256') .update(fs.readFileSync(p)) .digest('hex'), }; } fs.writeFileSync( manifestPath, JSON.stringify(buildManifest(commits, cab, gh, files), null, 2) ); console.log('[corpus-harvester] 完成。输出位于:'); console.log(` ${path.relative(REPO_ROOT, tlPath)}`); console.log(` ${path.relative(REPO_ROOT, jsonlPath)}`); console.log(` ${path.relative(REPO_ROOT, rawPath)}`); console.log(` ${path.relative(REPO_ROOT, manifestPath)}`); })().catch((e) => { console.error('[corpus-harvester] 失败:', e); process.exit(1); });