feat: add HoloLake personal channel module runtime
This commit is contained in:
parent
bb96d5eb89
commit
a106b23f9b
21 changed files with 786 additions and 31 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue