Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

586 lines
21 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
/**
* Corpus Harvester · 训练语料采集器
*
* 系统底层标识: SYS-GLW-0001 / TCS-0002∞
* 版权号: 国作登字-2026-A-00037559
* 作者: 冰朔 (ICE-GL∞) · 实现: 铸渊
*
* 目的: 把冰朔在 guanghulab 仓库里留下的一手自然语言commits / CAB 规格 /
* 可选的 Issues+PRs整理成可用于训练模型SFT的语料文件。
*
* 数据源:
* 1. git log — 冰朔作者的所有 commitssubject + 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, '<REDACTED_API_KEY>')
.replace(/ghp_[A-Za-z0-9]{20,}/g, '<REDACTED_GH_TOKEN>')
.replace(/secret_[A-Za-z0-9]{32,}/g, '<REDACTED_NOTION_SECRET>')
.replace(/Bearer\s+[A-Za-z0-9._\-]{20,}/g, 'Bearer <REDACTED>')
.replace(/[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g, '<REDACTED_EMAIL>')
.replace(/\b1[3-9]\d{9}\b/g, '<REDACTED_PHONE>');
}
// ───────────────────────── source 1: git commits ─────────────────────────
function collectCommits() {
const sep = '<<<COMMIT_SEP>>>';
const fieldSep = '<<<F>>>';
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含 PRsGitHub 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(`<details><summary>文件变更 (${c.files_changed.length})</summary>`);
lines.push('');
c.files_changed.forEach((f) => lines.push(`- \`${f.status}\` ${f.path}`));
lines.push('');
lines.push('</details>');
}
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);
});