fix: complete HoloLake Agent and Forgejo desktop runtime
This commit is contained in:
parent
28f68fa9ac
commit
46bdd0ca73
15 changed files with 1217 additions and 638 deletions
|
|
@ -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 仓库地址');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue