fix: complete HoloLake Agent and Forgejo desktop runtime

This commit is contained in:
冰朔 2026-08-08 10:39:18 +08:00
commit 46bdd0ca73
15 changed files with 1217 additions and 638 deletions

View file

@ -59,6 +59,15 @@ export interface DiffResult {
hunks: { oldStart: number; newStart: number; lines: string[] }[];
}
export interface RepositoryStatus {
branch: string;
head: string;
clean: boolean;
ahead: number;
behind: number;
remote: { name: string; url: string } | null;
}
// ─── Git 引擎 ───
export class GitEngine {
@ -94,6 +103,69 @@ export class GitEngine {
await fs.mkdir(this.docsDir, { recursive: true });
}
// ─── Forgejo 远端仓库 ───
/** 返回本地 Git 与 Forgejo 远端的真实连接状态,不探测或修改网络。 */
async getRepositoryStatus(remoteName = 'origin'): Promise<RepositoryStatus> {
this.assertRemoteName(remoteName);
const [status, remotes, head] = await Promise.all([
this.git.status(),
this.git.getRemotes(true),
this.git.revparse(['HEAD']).catch(() => ''),
]);
const remote = remotes.find(item => item.name === remoteName);
return {
branch: status.current || '未命名分支',
head: head.trim(),
clean: status.isClean(),
ahead: status.ahead,
behind: status.behind,
remote: remote ? { name: remote.name, url: this.redactRemoteUrl(remote.refs.fetch) } : null,
};
}
/** 配置 Forgejo Git 远端。认证交给系统钥匙串或 SSH不保存凭据。 */
async configureRemote(url: string, remoteName = 'origin'): Promise<RepositoryStatus> {
this.assertRemoteName(remoteName);
const safeUrl = this.validateRemoteUrl(url);
const remotes = await this.git.getRemotes(true);
if (remotes.some(item => item.name === remoteName)) {
await this.git.remote(['set-url', remoteName, safeUrl]);
} else {
await this.git.addRemote(remoteName, safeUrl);
}
return this.getRepositoryStatus(remoteName);
}
async fetchRemote(remoteName = 'origin'): Promise<RepositoryStatus> {
this.assertRemoteName(remoteName);
await this.requireRemote(remoteName);
await this.git.fetch(remoteName, ['--prune']);
return this.getRepositoryStatus(remoteName);
}
/** 只允许快进拉取,避免客户端静默制造合并提交。 */
async pullRemote(remoteName = 'origin'): Promise<RepositoryStatus> {
this.assertRemoteName(remoteName);
await this.requireRemote(remoteName);
const status = await this.git.status();
if (!status.current) throw new Error('当前没有可拉取的分支');
if (!status.isClean()) throw new Error('本地有未提交变更,请先保存或提交后再拉取');
await this.git.pull(remoteName, status.current, { '--ff-only': null });
return this.getRepositoryStatus(remoteName);
}
/** 推送只能由显式 UI 操作调用Agent 不注册此写操作。 */
async pushRemote(remoteName = 'origin'): Promise<RepositoryStatus> {
this.assertRemoteName(remoteName);
await this.requireRemote(remoteName);
const status = await this.git.status();
if (!status.current) throw new Error('当前没有可推送的分支');
if (!status.isClean()) throw new Error('本地有未提交变更,请先保存后再推送');
await this.git.push(remoteName, status.current, ['--set-upstream']);
return this.getRepositoryStatus(remoteName);
}
// ─── 文档 CRUD ───
/** 读取文档 */
@ -360,8 +432,44 @@ export class GitEngine {
// ─── 辅助 ───
private resolvePath(docPath: string): string {
// 安全检查:防止路径穿越
const normalized = path.normalize(docPath).replace(/^(\.\.\/?)+/, '');
return path.join(this.docsDir, normalized);
const normalized = path.normalize(docPath);
const resolved = path.resolve(this.docsDir, normalized);
const prefix = `${path.resolve(this.docsDir)}${path.sep}`;
if (!resolved.startsWith(prefix) || !normalized.endsWith('.md')) {
throw new Error('文档路径无效,只允许知识库 docs 目录内的 Markdown 文件');
}
return resolved;
}
private assertRemoteName(name: string): void {
if (!/^[A-Za-z0-9._-]{1,64}$/.test(name)) throw new Error('远端名称无效');
}
private validateRemoteUrl(value: string): string {
const url = value.trim();
if (!url) throw new Error('Forgejo 仓库地址不能为空');
if (/^https?:\/\//i.test(url)) {
const parsed = new URL(url);
if (parsed.username || parsed.password) throw new Error('仓库地址不能包含账号或密钥,请使用系统钥匙串');
return parsed.toString().replace(/\/$/, '');
}
if (/^(ssh:\/\/|git@)[^\s]+$/i.test(url)) return url;
throw new Error('仅支持 HTTPS 或 SSH Forgejo 仓库地址');
}
private redactRemoteUrl(value: string): string {
try {
const parsed = new URL(value);
parsed.username = '';
parsed.password = '';
return parsed.toString().replace(/\/$/, '');
} catch {
return value.replace(/\/\/[^/@]+@/, '//***@');
}
}
private async requireRemote(name: string): Promise<void> {
const remotes = await this.git.getRemotes();
if (!remotes.includes(name)) throw new Error('尚未配置 Forgejo 仓库地址');
}
}