Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/OpenDeepWiki/Endpoints/Admin/AdminGitHubImportEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using OpenDeepWiki.Models.Admin;
using OpenDeepWiki.Services.Admin;
using OpenDeepWiki.Services.Auth;
using OpenDeepWiki.Services.GitHub;

namespace OpenDeepWiki.Endpoints.Admin;

public static class AdminGitHubImportEndpoints
{
public static RouteGroupBuilder MapAdminGitHubImportEndpoints(this RouteGroupBuilder group)
{
var github = group.MapGroup("/github")
.WithTags("Admin - GitHub Import");

github.MapGet("/status", async (
IAdminGitHubImportService service,
CancellationToken cancellationToken) =>
{
var result = await service.GetStatusAsync(cancellationToken);
return Results.Ok(new { success = true, data = result });
});

github.MapGet("/config", async (
IAdminGitHubImportService service,
CancellationToken cancellationToken) =>
{
var result = await service.GetGitHubConfigAsync(cancellationToken);
return Results.Ok(new { success = true, data = result });
});

github.MapPost("/config", async (
SaveGitHubConfigRequest request,
IAdminGitHubImportService service,
CancellationToken cancellationToken) =>
{
try
{
var result = await service.SaveGitHubConfigAsync(request, cancellationToken);
return Results.Ok(new { success = true, data = result });
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { success = false, message = ex.Message });
}
});

github.MapDelete("/config", async (
IAdminGitHubImportService service,
CancellationToken cancellationToken) =>
{
try
{
await service.ResetGitHubConfigAsync(cancellationToken);
return Results.Ok(new { success = true });
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { success = false, message = ex.Message });
}
});

github.MapGet("/install-url", async (
Microsoft.Extensions.Configuration.IConfiguration configuration,
GitHubAppCredentialCache cache) =>
{
var appName = cache.AppName
?? configuration["GitHub:App:Name"]
?? Environment.GetEnvironmentVariable("GitHub__App__Name")
?? Environment.GetEnvironmentVariable("GITHUB_APP_NAME")
?? "deepwiki-keboola";
var url = $"https://github.com/apps/{appName}/installations/new";
return Results.Ok(new { success = true, data = new { url, appName } });
});

github.MapPost("/installations", async (
StoreInstallationRequest request,
IAdminGitHubImportService service,
CancellationToken cancellationToken) =>
{
try
{
var result = await service.StoreInstallationAsync(request.InstallationId, cancellationToken);
return Results.Ok(new { success = true, data = result });
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { success = false, message = ex.Message });
}
});

github.MapDelete("/installations/{installationId}", async (
string installationId,
IAdminGitHubImportService service,
CancellationToken cancellationToken) =>
{
try
{
await service.DisconnectInstallationAsync(installationId, cancellationToken);
return Results.Ok(new { success = true });
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { success = false, message = ex.Message });
}
});

github.MapGet("/installations/{installationId:long}/repos", async (
long installationId,
int page,
int perPage,
IAdminGitHubImportService service,
CancellationToken cancellationToken) =>
{
try
{
var result = await service.ListInstallationReposAsync(installationId, page, perPage, cancellationToken);
return Results.Ok(new { success = true, data = result });
}
catch (Exception ex)
{
return Results.BadRequest(new { success = false, message = ex.Message });
}
});

github.MapPost("/batch-import", async (
BatchImportRequest request,
IAdminGitHubImportService service,
IUserContext userContext,
CancellationToken cancellationToken) =>
{
try
{
var userId = userContext.UserId;
if (string.IsNullOrEmpty(userId))
return Results.Unauthorized();

var result = await service.BatchImportAsync(request, userId, cancellationToken);
return Results.Ok(new { success = true, data = result });
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { success = false, message = ex.Message });
}
});

return group;
}
}
100 changes: 100 additions & 0 deletions src/OpenDeepWiki/Models/Admin/GitHubImportModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
namespace OpenDeepWiki.Models.Admin;

public class GitHubStatusResponse
{
public bool Configured { get; set; }
public string? AppName { get; set; }
public List<GitHubInstallationDto> Installations { get; set; } = new();
}

public class GitHubInstallationDto
{
public string? Id { get; set; }
public long InstallationId { get; set; }
public string AccountLogin { get; set; } = string.Empty;
public string AccountType { get; set; } = string.Empty;
public long AccountId { get; set; }
public string? AvatarUrl { get; set; }
public string? DepartmentId { get; set; }
public string? DepartmentName { get; set; }
public DateTime CreatedAt { get; set; }
}

public class StoreInstallationRequest
{
public long InstallationId { get; set; }
}

public class GitHubRepoDto
{
public long Id { get; set; }
public string FullName { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Owner { get; set; } = string.Empty;
public bool Private { get; set; }
public string? Description { get; set; }
public string? Language { get; set; }
public int StargazersCount { get; set; }
public int ForksCount { get; set; }
public string DefaultBranch { get; set; } = "main";
public string CloneUrl { get; set; } = string.Empty;
public string HtmlUrl { get; set; } = string.Empty;
public bool AlreadyImported { get; set; }
}

public class GitHubRepoListDto
{
public int TotalCount { get; set; }
public List<GitHubRepoDto> Repositories { get; set; } = new();
public int Page { get; set; }
public int PerPage { get; set; }
}

public class BatchImportRequest
{
public long InstallationId { get; set; }
public string DepartmentId { get; set; } = string.Empty;
public string LanguageCode { get; set; } = "en";
public List<BatchImportRepo> Repos { get; set; } = new();
}

public class BatchImportRepo
{
public string FullName { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Owner { get; set; } = string.Empty;
public string CloneUrl { get; set; } = string.Empty;
public string DefaultBranch { get; set; } = "main";
public bool Private { get; set; }
public string? Language { get; set; }
public int StargazersCount { get; set; }
public int ForksCount { get; set; }
}

public class BatchImportResult
{
public int TotalRequested { get; set; }
public int Imported { get; set; }
public int Skipped { get; set; }
public List<string> SkippedRepos { get; set; } = new();
public List<string> ImportedRepos { get; set; } = new();
}

public class SaveGitHubConfigRequest
{
public string AppId { get; set; } = string.Empty;
public string AppName { get; set; } = string.Empty;
public string PrivateKey { get; set; } = string.Empty;
}

public class GitHubConfigResponse
{
public bool HasAppId { get; set; }
public bool HasPrivateKey { get; set; }
public string? AppId { get; set; }
public string? AppName { get; set; }
/// <summary>
/// Credential source: "database", "environment", or "none".
/// </summary>
public string Source { get; set; } = "none";
}
13 changes: 11 additions & 2 deletions src/OpenDeepWiki/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using OpenDeepWiki.Infrastructure;
using OpenDeepWiki.Services.Admin;
using OpenDeepWiki.Services.Auth;
using OpenDeepWiki.Services.GitHub;
using OpenDeepWiki.Services.Chat;
using OpenDeepWiki.Services.MindMap;
using OpenDeepWiki.Services.Notifications;
Expand Down Expand Up @@ -302,6 +303,11 @@
// 注册处理日志服务(使用 Singleton,因为它内部使用 IServiceScopeFactory 创建独立 scope)
builder.Services.AddSingleton<IProcessingLogService, ProcessingLogService>();

// 注册 GitHub App 服务
builder.Services.AddSingleton<GitHubAppCredentialCache>();
builder.Services.AddScoped<IGitHubAppService, GitHubAppService>();
builder.Services.AddScoped<IAdminGitHubImportService, AdminGitHubImportService>();

// 注册管理端服务
builder.Services.AddScoped<IAdminStatisticsService, AdminStatisticsService>();
builder.Services.AddScoped<IAdminRepositoryService, AdminRepositoryService>();
Expand Down Expand Up @@ -397,6 +403,10 @@
var settingsService = scope.ServiceProvider.GetRequiredService<IAdminSettingsService>();
var wikiOptions = scope.ServiceProvider.GetRequiredService<IOptions<WikiGeneratorOptions>>();
await SystemSettingDefaults.ApplyToWikiGeneratorOptions(wikiOptions.Value, settingsService);

// Load GitHub App credentials from DB into the in-memory cache
var githubCache = app.Services.GetRequiredService<GitHubAppCredentialCache>();
await githubCache.LoadFromDbAsync(settingsService);
}

// 启用 CORS
Expand Down Expand Up @@ -457,8 +467,7 @@ static void LoadEnvFile(IConfigurationBuilder configuration)
var envPaths = new[]
{
Path.Combine(Directory.GetCurrentDirectory(), ".env"),
Path.Combine(AppContext.BaseDirectory, ".env"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".env"),
Path.Combine(AppContext.BaseDirectory, ".env")
};

foreach (var envPath in envPaths)
Expand Down
Loading