feat: 首次推送到Gitea - 完整项目代码 + 安全加固 + 知识库
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* update-knowledge.mjs — 开发完成后自动记录与迭代脚本
|
||||
*
|
||||
* 捕获三类信息并沉淀知识库(幂等,可重复运行):
|
||||
* 1. 能力特征 → .trae/knowledge/capabilities.json (代码结构 / 依赖 / 任务 / 角色 全貌快照)
|
||||
* 2. 开发过程 → .trae/knowledge/dev_process.jsonl (按提交追加,不覆盖历史)
|
||||
* 3. 模型成果 → .trae/knowledge/artifacts.jsonl (本次交付物:迁移 / 脚本 / 提交统计)
|
||||
* 并把上述事实增量 upsert 进知识图谱:
|
||||
* - .trae/knowledge_graph.json (渲染版,权威)
|
||||
* - .trae/knowledge_graph.jsonl (MCP Knowledge Graph Memory 读取的源文件)
|
||||
*
|
||||
* 知识目录解析顺序:
|
||||
* 1. <repo>/.trae (所有 .trae 内容均在代码仓库内,跟着 git 走)
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/update-knowledge.mjs # 正常写入
|
||||
* node scripts/update-knowledge.mjs --dry-run # 只预览,不写文件
|
||||
* node scripts/update-knowledge.mjs --quiet # 静默(post-commit 钩子用)
|
||||
*
|
||||
* 设计约束:
|
||||
* - 永不删除实体:同名实体整条替换(其 observations 是派生数据),未出现的旧实体原样保留
|
||||
* - 任何扫描失败只告警不中断,最终仍写出文件
|
||||
*/
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { execFileSync } from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { createLogger } from "./lib/logger.mjs";
|
||||
|
||||
const log = createLogger("update-knowledge");
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const DRY_RUN = process.argv.includes("--dry-run");
|
||||
const QUIET = process.argv.includes("--quiet");
|
||||
|
||||
const PROJECT_NAME = "追光AI";
|
||||
const CAP_ENTITY = "追光AI:能力快照";
|
||||
const TODAY = new Date().toISOString().slice(0, 10);
|
||||
const NOW = new Date().toISOString().slice(0, 19);
|
||||
const DEV_RECORD_ENTITY = `开发记录:${TODAY}`;
|
||||
|
||||
/* ------------------------------------------------------------------ 路径 */
|
||||
|
||||
const TRAE_DIR = path.join(REPO_ROOT, ".trae");
|
||||
const KNOW_DIR = path.join(TRAE_DIR, "knowledge");
|
||||
const KG_JSON = path.join(TRAE_DIR, "knowledge_graph.json");
|
||||
const KG_JSONL = path.join(TRAE_DIR, "knowledge_graph.jsonl");
|
||||
const CAP_FILE = path.join(KNOW_DIR, "capabilities.json");
|
||||
const PROC_FILE = path.join(KNOW_DIR, "dev_process.jsonl");
|
||||
const ART_FILE = path.join(KNOW_DIR, "artifacts.jsonl");
|
||||
|
||||
/* ------------------------------------------------------------- 基础工具 */
|
||||
|
||||
function readJson(file, fallback = null) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch (err) {
|
||||
if (fs.existsSync(file)) log.warn("读取 JSON 失败", { file, error: err.message });
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(file, data) {
|
||||
if (DRY_RUN) return;
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
function appendJsonl(file, rows) {
|
||||
if (DRY_RUN || rows.length === 0) return;
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.appendFileSync(file, rows.map((r) => JSON.stringify(r)).join("\n") + "\n", "utf8");
|
||||
}
|
||||
|
||||
function readJsonl(file) {
|
||||
if (!fs.existsSync(file)) return [];
|
||||
return fs
|
||||
.readFileSync(file, "utf8")
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.map((l) => {
|
||||
try {
|
||||
return JSON.parse(l);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function walk(dir, filter) {
|
||||
const out = [];
|
||||
if (!fs.existsSync(dir)) return out;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...walk(full, filter));
|
||||
else if (filter(full)) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function rel(p) {
|
||||
return path.relative(REPO_ROOT, p).replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function git(args) {
|
||||
try {
|
||||
return execFileSync("git", args, {
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 15000,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ 1. 能力特征扫描 */
|
||||
|
||||
function scanCapabilities() {
|
||||
const apiRoutes = walk(path.join(REPO_ROOT, "src", "app", "api"), (f) => f.endsWith("route.ts"));
|
||||
const pages = walk(path.join(REPO_ROOT, "src", "app"), (f) => f.endsWith("page.tsx"));
|
||||
const components = walk(path.join(REPO_ROOT, "src", "components"), (f) => f.endsWith(".tsx"));
|
||||
const hooks = walk(path.join(REPO_ROOT, "src", "hooks"), (f) => /\.(ts|tsx)$/.test(f));
|
||||
const libFiles = walk(path.join(REPO_ROOT, "src", "lib"), (f) => /\.(ts|tsx)$/.test(f));
|
||||
const scripts = walk(path.join(REPO_ROOT, "scripts"), (f) => /\.(mjs|sh)$/.test(f));
|
||||
const migrations = fs.existsSync(path.join(REPO_ROOT, "prisma", "migrations"))
|
||||
? fs
|
||||
.readdirSync(path.join(REPO_ROOT, "prisma", "migrations"), { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name)
|
||||
.sort()
|
||||
: [];
|
||||
|
||||
const schema = fs.existsSync(path.join(REPO_ROOT, "prisma", "schema.prisma"))
|
||||
? fs.readFileSync(path.join(REPO_ROOT, "prisma", "schema.prisma"), "utf8")
|
||||
: "";
|
||||
const models = [...schema.matchAll(/^model\s+(\w+)/gm)].map((m) => m[1]);
|
||||
|
||||
const crontabPath = path.join(REPO_ROOT, "crontab.txt");
|
||||
const crontabLines = fs.existsSync(crontabPath) ? fs.readFileSync(crontabPath, "utf8").split("\n") : [];
|
||||
const activeCron = crontabLines.filter((l) => /^\s*\d/.test(l));
|
||||
const activeCronKeys = activeCron
|
||||
.map((l) => (l.match(/scripts\/([\w-]+)\.mjs/) || [])[1])
|
||||
.filter(Boolean);
|
||||
|
||||
const botFile = path.join(REPO_ROOT, "data", "bot-characters.json");
|
||||
const botData = readJson(botFile, null);
|
||||
const botCharacters = Array.isArray(botData?.characters) ? botData.characters.map((c) => c.key) : [];
|
||||
|
||||
const pkg = readJson(path.join(REPO_ROOT, "package.json"), {}) || {};
|
||||
|
||||
return {
|
||||
scanned_at: NOW,
|
||||
version: pkg.version || "unknown",
|
||||
api_routes: apiRoutes.length,
|
||||
pages: pages.length,
|
||||
components: components.length,
|
||||
hooks: hooks.length,
|
||||
lib_modules: libFiles.length,
|
||||
scripts: scripts.length,
|
||||
prisma_models: models.length,
|
||||
prisma_model_names: models,
|
||||
migrations: migrations.length,
|
||||
latest_migrations: migrations.slice(-5),
|
||||
cron_active_tasks: activeCron.length,
|
||||
cron_active_scripts: activeCronKeys,
|
||||
bot_characters: botCharacters,
|
||||
tech_stack: {
|
||||
dependencies: Object.keys(pkg.dependencies || {}).length,
|
||||
dev_dependencies: Object.keys(pkg.devDependencies || {}).length,
|
||||
next: (pkg.dependencies || {}).next || "-",
|
||||
prisma: (pkg.dependencies || {}).prisma || "-",
|
||||
react: (pkg.dependencies || {}).react || "-",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ 2. 开发过程提取 */
|
||||
|
||||
function collectDevProcess() {
|
||||
const raw = git(["log", "-n", "5", "--pretty=format:%H%x1f%cI%x1f%s"]);
|
||||
if (!raw) return [];
|
||||
const rows = [];
|
||||
for (const line of raw.split("\n")) {
|
||||
const [hash, date, subject] = line.split("\x1f");
|
||||
if (!hash) continue;
|
||||
const files = git(["diff-tree", "--no-commit-id", "--name-only", "-r", "-M", hash])
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
const shortstat = git(["show", "--shortstat", "--oneline", "--format=", hash]).trim();
|
||||
rows.push({
|
||||
ts: NOW,
|
||||
kind: "commit",
|
||||
hash: hash.slice(0, 8),
|
||||
committed_at: date,
|
||||
subject,
|
||||
files_changed: files.length,
|
||||
areas: [...new Set(files.map((f) => f.split("/").slice(0, 2).join("/")))].slice(0, 12),
|
||||
shortstat,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ 3. 模型成果捕获 */
|
||||
|
||||
function collectArtifacts(cap, commits) {
|
||||
const head = commits[0] || {};
|
||||
const artifacts = {
|
||||
ts: NOW,
|
||||
date: TODAY,
|
||||
kind: "delivery",
|
||||
head_commit: head.hash || "-",
|
||||
head_subject: head.subject || "-",
|
||||
files_changed: head.files_changed ?? 0,
|
||||
shortstat: head.shortstat || "",
|
||||
capabilities_delta: {
|
||||
api_routes: cap.api_routes,
|
||||
pages: cap.pages,
|
||||
components: cap.components,
|
||||
prisma_models: cap.prisma_models,
|
||||
migrations: cap.migrations,
|
||||
scripts: cap.scripts,
|
||||
cron_active_tasks: cap.cron_active_tasks,
|
||||
},
|
||||
latest_migrations: cap.latest_migrations,
|
||||
version: cap.version,
|
||||
recorded_by: "scripts/update-knowledge.mjs",
|
||||
};
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- 知识图谱 upsert */
|
||||
|
||||
function buildEntities(cap, commits, artifacts) {
|
||||
const capObs = [
|
||||
`版本: ${cap.version}`,
|
||||
`代码规模: API 路由 ${cap.api_routes} / 页面 ${cap.pages} / 组件 ${cap.components} / hooks ${cap.hooks} / lib ${cap.lib_modules}`,
|
||||
`数据层: Prisma 模型 ${cap.prisma_models} 个、迁移 ${cap.migrations} 个(最新 ${cap.latest_migrations.join("、") || "无"})`,
|
||||
`脚本: ${cap.scripts} 个(含定时任务与运维脚本)`,
|
||||
`定时任务: 启用 ${cap.cron_active_tasks} 个 —— ${cap.cron_active_scripts.join("、")}`,
|
||||
`数字人角色: ${cap.bot_characters.length} 个(${cap.bot_characters.join("、") || "无"})`,
|
||||
`依赖: dependencies ${cap.tech_stack.dependencies} / devDependencies ${cap.tech_stack.dev_dependencies}(next ${cap.tech_stack.next}、prisma ${cap.tech_stack.prisma}、react ${cap.tech_stack.react})`,
|
||||
`快照时间: ${cap.scanned_at}(由 scripts/update-knowledge.mjs 自动生成,勿手工编辑)`,
|
||||
];
|
||||
|
||||
const devObs = [
|
||||
`记录时间: ${NOW}`,
|
||||
`本次交付: ${artifacts.head_commit} ${artifacts.head_subject}(改动文件 ${artifacts.files_changed} 个${artifacts.shortstat ? "、" + artifacts.shortstat : ""})`,
|
||||
`能力基线: API ${cap.api_routes} / 页面 ${cap.pages} / 组件 ${cap.components} / 模型 ${cap.prisma_models} / 迁移 ${cap.migrations} / 脚本 ${cap.scripts} / 定时任务 ${cap.cron_active_tasks}`,
|
||||
...commits.slice(0, 5).map((c) => `${c.hash} ${c.committed_at?.slice(0, 10) || "-"} ${c.subject}`),
|
||||
"维护: 运行 npm run knowledge:update 刷新本记录",
|
||||
];
|
||||
|
||||
return [
|
||||
{ type: "entity", entityType: "Capability", name: CAP_ENTITY, observations: capObs },
|
||||
{ type: "entity", entityType: "fact", name: DEV_RECORD_ENTITY, observations: devObs },
|
||||
];
|
||||
}
|
||||
|
||||
function upsertKnowledgeGraph(entities, relations) {
|
||||
const kg = readJson(KG_JSON, null) || {
|
||||
project: PROJECT_NAME,
|
||||
version: "V2.1.2",
|
||||
updated_at: NOW,
|
||||
entities: [],
|
||||
relations: [],
|
||||
};
|
||||
|
||||
const fresh = new Map(entities.map((e) => [e.name, e]));
|
||||
const kept = (kg.entities || []).filter((e) => e.name && !fresh.has(e.name));
|
||||
|
||||
const relSeen = new Set();
|
||||
const mergedRelations = [];
|
||||
for (const r of [...(kg.relations || []), ...relations]) {
|
||||
if (!r?.from || !r?.to) continue;
|
||||
const key = `${r.relationType}|${r.from}|${r.to}`;
|
||||
if (relSeen.has(key)) continue;
|
||||
relSeen.add(key);
|
||||
mergedRelations.push(r);
|
||||
}
|
||||
|
||||
const mergedEntities = [...kept, ...entities];
|
||||
const nextKg = {
|
||||
project: kg.project || PROJECT_NAME,
|
||||
version: kg.version || "V2.1.2",
|
||||
updated_at: NOW,
|
||||
entities: mergedEntities,
|
||||
relations: mergedRelations,
|
||||
};
|
||||
writeJson(KG_JSON, nextKg);
|
||||
|
||||
// 同步 JSONL(MCP 记忆服务读取的源文件):同名实体整条替换,其余原样保留
|
||||
const oldLines = readJsonl(KG_JSONL);
|
||||
const keptLines = oldLines.filter((o) => o.type === "entity" && o.name && !fresh.has(o.name));
|
||||
const jsonlRows = [...keptLines, ...entities, ...mergedRelations];
|
||||
if (!DRY_RUN) {
|
||||
fs.mkdirSync(path.dirname(KG_JSONL), { recursive: true });
|
||||
fs.writeFileSync(KG_JSONL, jsonlRows.map((o) => JSON.stringify(o)).join("\n") + "\n", "utf8");
|
||||
}
|
||||
|
||||
return { entities: mergedEntities.length, relations: mergedRelations.length, jsonl: jsonlRows.length };
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ main */
|
||||
|
||||
function main() {
|
||||
const cap = scanCapabilities();
|
||||
const commits = collectDevProcess();
|
||||
const artifacts = collectArtifacts(cap, commits);
|
||||
|
||||
writeJson(CAP_FILE, cap);
|
||||
|
||||
// 模型成果:按 (日期 + head 提交) 去重,避免同日多次运行产生重复记录
|
||||
const existingArtifacts = readJsonl(ART_FILE);
|
||||
const alreadyRecorded = existingArtifacts.some(
|
||||
(a) => a.date === artifacts.date && a.head_commit === artifacts.head_commit
|
||||
);
|
||||
if (!alreadyRecorded) appendJsonl(ART_FILE, [artifacts]);
|
||||
|
||||
// 开发过程:按提交 hash 去重(追加,不覆盖历史)
|
||||
const existingProc = readJsonl(PROC_FILE);
|
||||
const seenHashes = new Set(existingProc.map((r) => r.hash));
|
||||
const newCommits = commits.filter((c) => !seenHashes.has(c.hash));
|
||||
appendJsonl(PROC_FILE, newCommits);
|
||||
|
||||
const entities = buildEntities(cap, commits, artifacts);
|
||||
const relations = [
|
||||
{ type: "relation", relationType: "HAS_CAPABILITY", from: PROJECT_NAME, to: CAP_ENTITY },
|
||||
{ type: "relation", relationType: "HAS_DEV_RECORD", from: PROJECT_NAME, to: DEV_RECORD_ENTITY },
|
||||
];
|
||||
const stats = upsertKnowledgeGraph(entities, relations);
|
||||
|
||||
if (!QUIET) {
|
||||
log.info("知识库已更新", {
|
||||
mode: DRY_RUN ? "dry-run" : "write",
|
||||
trae_dir: TRAE_DIR,
|
||||
capabilities: {
|
||||
api_routes: cap.api_routes,
|
||||
pages: cap.pages,
|
||||
components: cap.components,
|
||||
models: cap.prisma_models,
|
||||
migrations: cap.migrations,
|
||||
cron_active: cap.cron_active_tasks,
|
||||
},
|
||||
new_dev_records: newCommits.length,
|
||||
artifacts_written: !alreadyRecorded,
|
||||
graph: stats,
|
||||
});
|
||||
if (DRY_RUN) {
|
||||
log.info("dry-run:未写入任何文件(含 capabilities/artifacts/knowledge_graph)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
log.error("更新知识库失败", { error: err.message, stack: err.stack });
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user