feat: add HoloLake personal channel module runtime

This commit is contained in:
冰朔 2026-08-09 14:14:07 +08:00
commit a106b23f9b
21 changed files with 786 additions and 31 deletions

View file

@ -10,8 +10,17 @@ import simpleGit, { SimpleGit, LogResult } from 'simple-git';
import matter from 'gray-matter';
import { diffLines } from 'diff';
import path from 'path';
import crypto from 'crypto';
import fs from 'fs/promises';
import fsSync from 'fs';
import {
applyChannelPatch as compileChannelPatch,
defaultChannelState,
normalizeChannelState,
type ChannelPatch,
type ChannelReceipt,
type ChannelState,
} from './channel-runtime.js';
// ─── 类型定义 ───
@ -74,15 +83,66 @@ export class GitEngine {
private git: SimpleGit;
private docsDir: string;
private repoPath: string;
private channelStatePath: string;
private channelReceiptsDir: string;
constructor(repoPath: string) {
this.repoPath = repoPath;
this.docsDir = path.join(repoPath, 'docs');
this.channelStatePath = path.join(repoPath, '.hololake', 'channel-state.json');
this.channelReceiptsDir = path.join(repoPath, '.hololake', 'receipts');
// simple-git 要求目录先存在,先同步创建
fsSync.mkdirSync(repoPath, { recursive: true });
this.git = simpleGit(repoPath);
}
async getChannelState(): Promise<ChannelState> {
try {
return normalizeChannelState(JSON.parse(await fs.readFile(this.channelStatePath, 'utf8')));
} catch {
return defaultChannelState();
}
}
async applyChannelPatch(patch: ChannelPatch): Promise<ChannelReceipt> {
const receipt = compileChannelPatch(await this.getChannelState(), patch);
await this.persistChannelReceipt(receipt, `channel: ${patch.operation} ${patch.moduleId}`);
return receipt;
}
async undoChannelPatch(receiptId: string): Promise<ChannelReceipt> {
if (!/^HL-CHANNEL-RCPT-[0-9a-f-]+$/i.test(receiptId)) throw new Error('无效的频道回执编号');
const receiptPath = path.join(this.channelReceiptsDir, `${receiptId}.json`);
const original = JSON.parse(await fs.readFile(receiptPath, 'utf8')) as ChannelReceipt;
const current = await this.getChannelState();
if (current.revision !== original.after.revision) throw new Error('频道状态已经继续变化,不能静默覆盖;请按当前状态重新操作');
const restored = {
...normalizeChannelState(original.before),
revision: current.revision + 1,
updatedAt: new Date().toISOString(),
};
const undoReceipt: ChannelReceipt = {
schema: 'hololake.channel-receipt/v1',
id: `HL-CHANNEL-RCPT-${crypto.randomUUID()}`,
operation: original.operation,
before: current,
after: restored,
createdAt: restored.updatedAt,
reversible: true,
};
await this.persistChannelReceipt(undoReceipt, `channel: undo ${receiptId}`);
return undoReceipt;
}
private async persistChannelReceipt(receipt: ChannelReceipt, message: string): Promise<void> {
await fs.mkdir(this.channelReceiptsDir, { recursive: true });
const receiptPath = path.join(this.channelReceiptsDir, `${receipt.id}.json`);
await fs.writeFile(this.channelStatePath, `${JSON.stringify(receipt.after, null, 2)}\n`, { mode: 0o600 });
await fs.writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
await this.git.add([this.channelStatePath, receiptPath]);
await this.git.commit(message, [this.channelStatePath, receiptPath]);
}
/** 初始化仓库(如果不存在则创建) */
async init(): Promise<void> {
const repoRoot = path.dirname(this.docsDir);