feat: support personal user repos for canvas storage

This commit is contained in:
2026-04-24 01:18:27 +05:00
parent 53ae4634b9
commit eb91d95ec0
+32 -24
View File
@@ -1,6 +1,6 @@
// Gitea API integration for XFlow canvas sync // Gitea API integration for XFlow canvas sync
// Structure: // Structure:
// Personal canvases → AVTEX/user-{username} (created on first save) // Personal canvases → {username}/MM (personal user repo, created on first save)
// Group canvases → AVTEX/group-{groupId} (created on group creation) // Group canvases → AVTEX/group-{groupId} (created on group creation)
// Public canvases → AVTEX/MMAVTEX (shared org repo) // Public canvases → AVTEX/MMAVTEX (shared org repo)
@@ -27,50 +27,56 @@ const apiHeaders = (token: string) => ({
}) })
// Resolve which repo to use based on canvas ownership // Resolve which repo to use based on canvas ownership
export function resolveRepo(opts: { username?: string; groupId?: string; isPublic?: boolean }): string { // Returns { owner, repo } to support both org and personal repos
export function resolveRepo(opts: { username?: string; groupId?: string; isPublic?: boolean }): { owner: string; repo: string } {
const { org, mindmapRepo } = getConfig() const { org, mindmapRepo } = getConfig()
if (opts.groupId) return `group-${opts.groupId}` if (opts.groupId) return { owner: org, repo: `group-${opts.groupId}` }
if (opts.username && !opts.isPublic) return `user-${opts.username}` if (opts.username && !opts.isPublic) return { owner: opts.username, repo: 'MM' }
return mindmapRepo return { owner: org, repo: mindmapRepo }
} }
// Ensure repository exists in org, create if not // Ensure repository exists, create if not (supports personal and org repos)
export async function ensureRepo(repoName: string, description = ''): Promise<boolean> { export async function ensureRepo(owner: string, repoName: string, description = ''): Promise<boolean> {
const { url, token, org } = getConfig() const { url, token, org } = getConfig()
const h = apiHeaders(token) const h = apiHeaders(token)
// Check if exists // Check if exists
try { try {
await $fetch(`${url}/api/v1/repos/${org}/${repoName}`, { headers: h }) await $fetch(`${url}/api/v1/repos/${owner}/${repoName}`, { headers: h })
return true return true
} catch { } } catch { }
// Create // Create — personal repo or org repo
try { try {
await $fetch(`${url}/api/v1/orgs/${org}/repos`, { const isOrg = owner === org
const endpoint = isOrg
? `${url}/api/v1/orgs/${org}/repos`
: `${url}/api/v1/user/repos`
await $fetch(endpoint, {
method: 'POST', method: 'POST',
headers: h, headers: h,
body: { body: {
name: repoName, name: repoName,
description: description || `XFlow canvas repo: ${repoName}`, description: description || `XFlow canvas repo: ${repoName}`,
private: !repoName.startsWith('group-') && repoName !== getConfig().mindmapRepo, private: true,
auto_init: true, auto_init: true,
default_branch: 'main' default_branch: 'main'
} }
}) })
return true return true
} catch (e) { } catch (e) {
console.error(`Gitea ensureRepo(${repoName}) error:`, e) console.error(`Gitea ensureRepo(${owner}/${repoName}) error:`, e)
return false return false
} }
} }
// Get file from Gitea (returns null if not found) // Get file from Gitea (returns null if not found)
export async function giteaGetFile(path: string, repo?: string): Promise<GiteaFile | null> { export async function giteaGetFile(path: string, owner?: string, repo?: string): Promise<GiteaFile | null> {
const { url, token, org, mindmapRepo } = getConfig() const { url, token, org, mindmapRepo } = getConfig()
const repoOwner = owner || org
const repoName = repo || mindmapRepo const repoName = repo || mindmapRepo
try { try {
const res = await $fetch<any>(`${url}/api/v1/repos/${org}/${repoName}/contents/${path}`, { const res = await $fetch<any>(`${url}/api/v1/repos/${repoOwner}/${repoName}/contents/${path}`, {
headers: apiHeaders(token) headers: apiHeaders(token)
}) })
return { content: res.content, sha: res.sha } return { content: res.content, sha: res.sha }
@@ -80,13 +86,14 @@ export async function giteaGetFile(path: string, repo?: string): Promise<GiteaFi
} }
// Create or update file in Gitea // Create or update file in Gitea
export async function giteaPutFile(path: string, content: string, message: string, sha?: string, repo?: string): Promise<boolean> { export async function giteaPutFile(path: string, content: string, message: string, sha?: string, owner?: string, repo?: string): Promise<boolean> {
const { url, token, org, mindmapRepo } = getConfig() const { url, token, org, mindmapRepo } = getConfig()
const repoOwner = owner || org
const repoName = repo || mindmapRepo const repoName = repo || mindmapRepo
const encoded = Buffer.from(content).toString('base64') const encoded = Buffer.from(content).toString('base64')
try { try {
await $fetch(`${url}/api/v1/repos/${org}/${repoName}/contents/${path}`, { await $fetch(`${url}/api/v1/repos/${repoOwner}/${repoName}/contents/${path}`, {
method: sha ? 'PUT' : 'POST', method: sha ? 'PUT' : 'POST',
headers: apiHeaders(token), headers: apiHeaders(token),
body: { body: {
@@ -109,14 +116,14 @@ export async function saveCanvasToGitea(
opts: { username?: string; groupId?: string; isPublic?: boolean } = {}, opts: { username?: string; groupId?: string; isPublic?: boolean } = {},
commitMessage?: string commitMessage?: string
): Promise<boolean> { ): Promise<boolean> {
const repo = resolveRepo(opts) const { owner, repo } = resolveRepo(opts)
const path = `${canvasId}.canvas` const path = `${canvasId}.canvas`
const content = JSON.stringify(canvasData, null, 2) const content = JSON.stringify(canvasData, null, 2)
const message = commitMessage || `Update ${canvasId} at ${new Date().toLocaleString('ru-RU')}` const message = commitMessage || `Update ${canvasId} at ${new Date().toLocaleString('ru-RU')}`
await ensureRepo(repo) await ensureRepo(owner, repo)
const existing = await giteaGetFile(path, repo) const existing = await giteaGetFile(path, owner, repo)
return giteaPutFile(path, content, message, existing?.sha, repo) return giteaPutFile(path, content, message, existing?.sha, owner, repo)
} }
// Load canvas from Gitea // Load canvas from Gitea
@@ -124,8 +131,8 @@ export async function loadCanvasFromGitea(
canvasId: string, canvasId: string,
opts: { username?: string; groupId?: string; isPublic?: boolean } = {} opts: { username?: string; groupId?: string; isPublic?: boolean } = {}
): Promise<any | null> { ): Promise<any | null> {
const repo = resolveRepo(opts) const { owner, repo } = resolveRepo(opts)
const file = await giteaGetFile(`${canvasId}.canvas`, repo) const file = await giteaGetFile(`${canvasId}.canvas`, owner, repo)
if (!file) return null if (!file) return null
try { try {
@@ -136,11 +143,12 @@ export async function loadCanvasFromGitea(
} }
// List canvases in a repo // List canvases in a repo
export async function listCanvasesFromGitea(repo?: string): Promise<string[]> { export async function listCanvasesFromGitea(owner?: string, repo?: string): Promise<string[]> {
const { url, token, org, mindmapRepo } = getConfig() const { url, token, org, mindmapRepo } = getConfig()
const repoOwner = owner || org
const repoName = repo || mindmapRepo const repoName = repo || mindmapRepo
try { try {
const res = await $fetch<any[]>(`${url}/api/v1/repos/${org}/${repoName}/contents/`, { const res = await $fetch<any[]>(`${url}/api/v1/repos/${repoOwner}/${repoName}/contents/`, {
headers: apiHeaders(token) headers: apiHeaders(token)
}) })
return res return res