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
12 changes: 12 additions & 0 deletions src/OpenDeepWiki.EFCore/MasterDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public interface IContext : IDisposable
DbSet<McpProvider> McpProviders { get; set; }
DbSet<McpUsageLog> McpUsageLogs { get; set; }
DbSet<McpDailyStatistics> McpDailyStatistics { get; set; }
DbSet<GitHubAppInstallation> GitHubAppInstallations { get; set; }

Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
Expand Down Expand Up @@ -97,6 +98,7 @@ protected MasterDbContext(DbContextOptions options)
public DbSet<McpProvider> McpProviders { get; set; } = null!;
public DbSet<McpUsageLog> McpUsageLogs { get; set; } = null!;
public DbSet<McpDailyStatistics> McpDailyStatistics { get; set; } = null!;
public DbSet<GitHubAppInstallation> GitHubAppInstallations { get; set; } = null!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Expand Down Expand Up @@ -382,5 +384,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
// 日期索引
builder.HasIndex(s => s.Date);
});

// GitHubAppInstallation 表配置
modelBuilder.Entity<GitHubAppInstallation>(entity =>
{
entity.HasIndex(e => e.InstallationId).IsUnique();
entity.HasOne(e => e.Department)
.WithMany()
.HasForeignKey(e => e.DepartmentId)
.OnDelete(DeleteBehavior.SetNull);
});
}
}
62 changes: 62 additions & 0 deletions src/OpenDeepWiki.Entities/GitHub/GitHubAppInstallation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace OpenDeepWiki.Entities;

/// <summary>
/// Represents a GitHub App installation on an organization or user account.
/// </summary>
public class GitHubAppInstallation : AggregateRoot<string>
{
/// <summary>
/// GitHub's installation ID.
/// </summary>
public long InstallationId { get; set; }

/// <summary>
/// Organization or user login name (e.g., "keboola").
/// </summary>
[Required]
[StringLength(100)]
public string AccountLogin { get; set; } = string.Empty;

/// <summary>
/// Account type: "Organization" or "User".
/// </summary>
[Required]
[StringLength(20)]
public string AccountType { get; set; } = "Organization";

/// <summary>
/// GitHub account ID.
/// </summary>
public long AccountId { get; set; }

/// <summary>
/// Avatar URL of the account.
/// </summary>
[StringLength(500)]
public string? AvatarUrl { get; set; }

/// <summary>
/// Optional link to a Department for auto-assignment.
/// </summary>
[StringLength(36)]
public string? DepartmentId { get; set; }

/// <summary>
/// Cached installation access token (short-lived, ~1 hour).
/// </summary>
public string? CachedAccessToken { get; set; }

/// <summary>
/// When the cached access token expires.
/// </summary>
public DateTime? TokenExpiresAt { get; set; }

/// <summary>
/// Navigation property to the linked department.
/// </summary>
[ForeignKey("DepartmentId")]
public virtual Department? Department { get; set; }
}
1 change: 1 addition & 0 deletions src/OpenDeepWiki/Endpoints/Admin/AdminEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static IEndpointRouteBuilder MapAdminEndpoints(this IEndpointRouteBuilder
adminGroup.MapAdminSettingsEndpoints();
adminGroup.MapAdminChatAssistantEndpoints();
adminGroup.MapAdminMcpProviderEndpoints();
adminGroup.MapAdminGitHubImportEndpoints();

return app;
}
Expand Down
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";
}
22 changes: 22 additions & 0 deletions src/OpenDeepWiki/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using OpenDeepWiki.Services.Repositories;
using OpenDeepWiki.Services.Translation;
using OpenDeepWiki.Services.Mcp;
using OpenDeepWiki.Services.GitHub;
using OpenDeepWiki.Services.UserProfile;
using OpenDeepWiki.Services.Wiki;
using Scalar.AspNetCore;
Expand Down Expand Up @@ -288,6 +289,11 @@
// 注册处理日志服务(使用 Singleton,因为它内部使用 IServiceScopeFactory 创建独立 scope)
builder.Services.AddSingleton<IProcessingLogService, ProcessingLogService>();

// Register GitHub App services
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 @@ -445,6 +451,22 @@
await DbInitializer.InitializeAsync(scope.ServiceProvider);
}

// Load GitHub App credentials from DB into cache
using (var scope = app.Services.CreateScope())
{
var credCache = scope.ServiceProvider.GetRequiredService<GitHubAppCredentialCache>();
var settingsService = scope.ServiceProvider.GetRequiredService<IAdminSettingsService>();
try
{
await credCache.LoadFromDbAsync(settingsService);
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogWarning(ex, "Failed to load GitHub App credentials from database at startup");
}
}

app.Run();
}
catch (Exception ex)
Expand Down
Loading