feta: 新增知识库管理:

- Core新增Git项目管理,支持代理
- Knowledge新增知识库来源配置
This commit is contained in:
jackqqq123
2025-09-24 15:38:50 +08:00
parent 369fcbf403
commit cc0fa3576d
32 changed files with 3823 additions and 50 deletions

View File

@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.App", "src\LubanHu
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.Core", "src\LubanHub.Core\LubanHub.Core.csproj", "{B8F5E9A2-1234-4567-890A-BCDEF0123456}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.KnowledgeService", "src\LubanHub.KnowledgeService\LubanHub.KnowledgeService.csproj", "{2AD79ACA-24BD-46D4-A2A4-1C5F8086B88B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -26,9 +28,14 @@ Global
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Release|Any CPU.Build.0 = Release|Any CPU
{2AD79ACA-24BD-46D4-A2A4-1C5F8086B88B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2AD79ACA-24BD-46D4-A2A4-1C5F8086B88B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2AD79ACA-24BD-46D4-A2A4-1C5F8086B88B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2AD79ACA-24BD-46D4-A2A4-1C5F8086B88B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9A66E728-EA8A-4644-9CC2-2C056479AF5A} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
{B8F5E9A2-1234-4567-890A-BCDEF0123456} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
{2AD79ACA-24BD-46D4-A2A4-1C5F8086B88B} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
EndGlobalSection
EndGlobal

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using LubanHub.App.Services;
using LubanHub.Core;
using LubanHub.KnowledgeService;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
@@ -44,9 +45,15 @@ public partial class App : Application
builder.SetMinimumLevel(LogLevel.Debug);
});
// 强制加载所有LubanHub程序集确保配置发现能找到它们
LoadLubanHubAssemblies();
// 添加Core服务
services.AddCoreServices();
// 添加知识库服务
services.AddKnowledgeServices();
// 添加App层服务
services.AddSingleton<IDownloadProgressService, DownloadProgressService>();
services.AddSingleton<IAppDownloadService, AppDownloadService>();
@@ -56,6 +63,20 @@ public partial class App : Application
// services.AddSingleton<MainWindowViewModel>();
}
private static void LoadLubanHubAssemblies()
{
try
{
// 强制加载LubanHub.KnowledgeService程序集
var knowledgeAssembly = typeof(LubanHub.KnowledgeService.Configurations.AIConfiguration).Assembly;
Console.WriteLine($"已加载程序集: {knowledgeAssembly.GetName().Name}");
}
catch (Exception ex)
{
Console.WriteLine($"加载LubanHub程序集时出错: {ex.Message}");
}
}
public override void OnFrameworkInitializationCompleted()
{
try

View File

@@ -10,6 +10,11 @@
<AssemblyName>LubanHub.App</AssemblyName>
</PropertyGroup>
<!-- Debug模式下启用控制台输出 -->
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.6" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
@@ -27,5 +32,6 @@
<ItemGroup>
<ProjectReference Include="..\LubanHub.Core\LubanHub.Core.csproj" />
<ProjectReference Include="..\LubanHub.KnowledgeService\LubanHub.KnowledgeService.csproj" />
</ItemGroup>
</Project>

View File

@@ -37,6 +37,10 @@
Content="📚 知识库"
Classes="navigation-button"
Click="OnKnowledgeButtonClick"/>
<Button Name="KnowledgeManageButton"
Content="🔧 知识库管理"
Classes="navigation-button"
Click="OnKnowledgeManageButtonClick"/>
<Button Name="ProjectButton"
Content="📁 项目"
Classes="navigation-button"
@@ -167,6 +171,11 @@
</StackPanel>
</Grid>
<!-- 知识库管理页面 -->
<Grid Name="KnowledgeManagePanel" IsVisible="False">
<views:KnowledgeSourceManagementView/>
</Grid>
<!-- 安装页面 -->
<Grid Name="InstallPanel" IsVisible="False">
<StackPanel Margin="20">
@@ -210,6 +219,7 @@
<!-- 设置页面 -->
<Grid Name="SettingsPanel" IsVisible="False">
<ScrollViewer>
<StackPanel Margin="{DynamicResource LargeMargin}">
<TextBlock Text="⚙️ 设置"
Classes="subheader"
@@ -217,6 +227,15 @@
FontSize="20"
Margin="0,0,0,20"/>
<!-- 基本设置 -->
<StackPanel Margin="0,0,0,30">
<TextBlock Text="基本设置"
Classes="title"
Foreground="{DynamicResource PrimaryTextBrush}"
FontSize="16"
FontWeight="Bold"
Margin="0,0,0,15"/>
<StackPanel Margin="0,0,0,20">
<TextBlock Text="下载目录"
Classes="title"
@@ -262,6 +281,19 @@
</ComboBox>
</StackPanel>
</StackPanel>
<!-- 模块配置 -->
<StackPanel Margin="0,0,0,20">
<TextBlock Text="模块配置"
Classes="title"
Foreground="{DynamicResource PrimaryTextBrush}"
FontSize="16"
FontWeight="Bold"
Margin="0,0,0,15"/>
<views:ConfigurationView Name="ConfigurationViewControl"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Panel>
</Grid>

View File

@@ -14,11 +14,13 @@ public partial class MainWindow : Window
private Panel? _contentPanel;
private StackPanel? _welcomePanel;
private Grid? _knowledgePanel;
private Grid? _knowledgeManagePanel;
private Grid? _projectPanel;
private Grid? _installPanel;
private Grid? _settingsPanel;
private Button? _knowledgeButton;
private Button? _knowledgeManageButton;
private Button? _projectButton;
private Button? _installButton;
private Button? _settingsButton;
@@ -72,12 +74,14 @@ public partial class MainWindow : Window
_contentPanel = this.FindControl<Panel>("ContentPanel");
_welcomePanel = this.FindControl<StackPanel>("WelcomePanel");
_knowledgePanel = this.FindControl<Grid>("KnowledgePanel");
_knowledgeManagePanel = this.FindControl<Grid>("KnowledgeManagePanel");
_projectPanel = this.FindControl<Grid>("ProjectPanel");
_installPanel = this.FindControl<Grid>("InstallPanel");
_settingsPanel = this.FindControl<Grid>("SettingsPanel");
// 获取按钮引用
_knowledgeButton = this.FindControl<Button>("KnowledgeButton");
_knowledgeManageButton = this.FindControl<Button>("KnowledgeManageButton");
_projectButton = this.FindControl<Button>("ProjectButton");
_installButton = this.FindControl<Button>("InstallButton");
_settingsButton = this.FindControl<Button>("SettingsButton");
@@ -92,6 +96,7 @@ public partial class MainWindow : Window
// 隐藏所有面板
if (_welcomePanel != null) _welcomePanel.IsVisible = false;
if (_knowledgePanel != null) _knowledgePanel.IsVisible = false;
if (_knowledgeManagePanel != null) _knowledgeManagePanel.IsVisible = false;
if (_projectPanel != null) _projectPanel.IsVisible = false;
if (_installPanel != null) _installPanel.IsVisible = false;
if (_settingsPanel != null) _settingsPanel.IsVisible = false;
@@ -110,6 +115,7 @@ public partial class MainWindow : Window
{
// 移除所有按钮的选中样式
_knowledgeButton?.Classes.Remove("selected");
_knowledgeManageButton?.Classes.Remove("selected");
_projectButton?.Classes.Remove("selected");
_installButton?.Classes.Remove("selected");
_settingsButton?.Classes.Remove("selected");
@@ -117,6 +123,8 @@ public partial class MainWindow : Window
// 根据当前显示的面板添加选中样式
if (_knowledgePanel?.IsVisible == true)
_knowledgeButton?.Classes.Add("selected");
else if (_knowledgeManagePanel?.IsVisible == true)
_knowledgeManageButton?.Classes.Add("selected");
else if (_projectPanel?.IsVisible == true)
_projectButton?.Classes.Add("selected");
else if (_installPanel?.IsVisible == true)
@@ -130,6 +138,11 @@ public partial class MainWindow : Window
ShowPanel(_knowledgePanel);
}
private void OnKnowledgeManageButtonClick(object? sender, RoutedEventArgs e)
{
ShowPanel(_knowledgeManagePanel);
}
private void OnProjectButtonClick(object? sender, RoutedEventArgs e)
{
ShowPanel(_projectPanel);

View File

@@ -0,0 +1,245 @@
using LubanHub.App.ViewModels;
using LubanHub.Core.Interfaces;
using LubanHub.Core.Models;
using LubanHub.Core.Attributes;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
namespace LubanHub.App.ViewModels;
public class ConfigurationViewModel : ViewModelBase
{
private readonly ICoreConfigurationDiscoveryService _configurationService;
private readonly ILogger<ConfigurationViewModel> _logger;
private ObservableCollection<ConfigurationGroupViewModel> _configurationGroups;
private bool _isLoading;
public ConfigurationViewModel(
ICoreConfigurationDiscoveryService configurationService,
ILogger<ConfigurationViewModel> logger)
{
_configurationService = configurationService;
_logger = logger;
_configurationGroups = new ObservableCollection<ConfigurationGroupViewModel>();
_ = LoadConfigurationGroupsAsync();
}
public ObservableCollection<ConfigurationGroupViewModel> ConfigurationGroups
{
get => _configurationGroups;
set => SetProperty(ref _configurationGroups, value);
}
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
private async Task LoadConfigurationGroupsAsync()
{
try
{
IsLoading = true;
var groups = await _configurationService.DiscoverConfigurationGroupsAsync();
ConfigurationGroups.Clear();
foreach (var group in groups)
{
var groupViewModel = new ConfigurationGroupViewModel(group, _configurationService);
await groupViewModel.LoadConfigurationAsync();
ConfigurationGroups.Add(groupViewModel);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "加载配置分组时发生错误");
}
finally
{
IsLoading = false;
}
}
public async Task SaveAllConfigurationsAsync()
{
try
{
foreach (var group in ConfigurationGroups)
{
await group.SaveConfigurationAsync();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "保存配置时发生错误");
}
}
public async Task ResetAllConfigurationsAsync()
{
await LoadConfigurationGroupsAsync();
}
}
public class ConfigurationGroupViewModel : ViewModelBase
{
private readonly ConfigurationGroupInfo _groupInfo;
private readonly ICoreConfigurationDiscoveryService _configurationService;
private ObservableCollection<ConfigurationPropertyViewModel> _properties;
private object? _configurationInstance;
public ConfigurationGroupViewModel(
ConfigurationGroupInfo groupInfo,
ICoreConfigurationDiscoveryService configurationService)
{
_groupInfo = groupInfo;
_configurationService = configurationService;
_properties = new ObservableCollection<ConfigurationPropertyViewModel>();
}
public string GroupName => _groupInfo.GroupName;
public string DisplayName => _groupInfo.DisplayName;
public string? Description => _groupInfo.Description;
public string? Icon => _groupInfo.Icon;
public bool RequiresRestart => _groupInfo.RequiresRestart;
public ObservableCollection<ConfigurationPropertyViewModel> Properties
{
get => _properties;
set => SetProperty(ref _properties, value);
}
public async Task LoadConfigurationAsync()
{
try
{
// 使用反射获取配置实例
var getConfigMethod = typeof(ICoreConfigurationDiscoveryService)
.GetMethod(nameof(ICoreConfigurationDiscoveryService.GetConfigurationAsync))
?.MakeGenericMethod(_groupInfo.ConfigurationType);
if (getConfigMethod != null)
{
var task = (Task)getConfigMethod.Invoke(_configurationService, null)!;
await task;
_configurationInstance = task.GetType().GetProperty("Result")?.GetValue(task);
}
Properties.Clear();
foreach (var propInfo in _groupInfo.Properties)
{
var propertyValue = _configurationInstance?.GetType()
.GetProperty(propInfo.PropertyName)?.GetValue(_configurationInstance);
var propertyViewModel = new ConfigurationPropertyViewModel(propInfo, propertyValue);
Properties.Add(propertyViewModel);
}
}
catch (Exception ex)
{
// Log error
}
}
public async Task SaveConfigurationAsync()
{
try
{
if (_configurationInstance == null) return;
// 更新配置实例的属性值
foreach (var propViewModel in Properties)
{
var property = _configurationInstance.GetType().GetProperty(propViewModel.PropertyName);
if (property != null && property.CanWrite)
{
var convertedValue = Convert.ChangeType(propViewModel.Value, property.PropertyType);
property.SetValue(_configurationInstance, convertedValue);
}
}
// 使用反射保存配置
var saveConfigMethod = typeof(ICoreConfigurationDiscoveryService)
.GetMethod(nameof(ICoreConfigurationDiscoveryService.SaveConfigurationAsync))
?.MakeGenericMethod(_groupInfo.ConfigurationType);
if (saveConfigMethod != null)
{
await (Task<bool>)saveConfigMethod.Invoke(_configurationService, new[] { _configurationInstance })!;
}
}
catch (Exception ex)
{
// Log error
}
}
}
public class ConfigurationPropertyViewModel : ViewModelBase
{
private object? _value;
private bool _hasValidationError;
public ConfigurationPropertyViewModel(ConfigurationPropertyInfo propertyInfo, object? value)
{
PropertyInfo = propertyInfo;
_value = value;
}
public ConfigurationPropertyInfo PropertyInfo { get; }
public string PropertyName => PropertyInfo.PropertyName;
public string DisplayName => PropertyInfo.DisplayName;
public string? Description => PropertyInfo.Description;
public Core.Attributes.ConfigInputType InputType => PropertyInfo.InputType;
public bool IsRequired => PropertyInfo.IsRequired;
public string? Placeholder => PropertyInfo.Placeholder;
public string? ValidationMessage => PropertyInfo.ValidationMessage;
public object? Value
{
get => _value;
set
{
if (SetProperty(ref _value, value))
{
ValidateValue();
}
}
}
public bool HasValidationError
{
get => _hasValidationError;
set => SetProperty(ref _hasValidationError, value);
}
private void ValidateValue()
{
HasValidationError = false;
// 必填验证
if (IsRequired && (Value == null || string.IsNullOrWhiteSpace(Value.ToString())))
{
HasValidationError = true;
return;
}
// 正则表达式验证
if (!string.IsNullOrEmpty(PropertyInfo.ValidationPattern) && Value is string strValue)
{
var regex = new System.Text.RegularExpressions.Regex(PropertyInfo.ValidationPattern);
if (!regex.IsMatch(strValue))
{
HasValidationError = true;
return;
}
}
}
}

View File

@@ -0,0 +1,332 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using LubanHub.App.Services;
using LubanHub.App.ViewModels;
using LubanHub.KnowledgeService.Models;
using LubanHub.KnowledgeService.Interfaces;
using LubanHub.Core.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace LubanHub.App.ViewModels;
/// <summary>
/// 知识库来源项ViewModel
/// </summary>
public class KnowledgeSourceItemViewModel : ViewModelBase
{
private readonly KnowledgeSourceManagementViewModel _parentViewModel;
private KnowledgeSource _source;
private string _searchDirectoriesText;
private string _fileExtensionsText;
private bool _isEditing;
public KnowledgeSourceItemViewModel(KnowledgeSource source, KnowledgeSourceManagementViewModel parentViewModel)
{
_source = source;
_parentViewModel = parentViewModel;
_searchDirectoriesText = string.Join("\n", source.SearchDirectories);
_fileExtensionsText = string.Join(", ", source.FileExtensions);
EditCommand = new RelayCommand(async () => { IsEditing = true; await Task.CompletedTask; });
SaveCommand = new RelayCommand(SaveAsync);
CancelCommand = new RelayCommand(async () => { Cancel(); await Task.CompletedTask; });
DeleteCommand = new RelayCommand(DeleteAsync);
UpdateCommand = new RelayCommand(UpdateAsync);
}
public KnowledgeSource Source => _source;
public string Name
{
get => _source.Name;
set
{
if (_source.Name != value)
{
_source.Name = value;
OnPropertyChanged();
}
}
}
public KnowledgeSourceType Type
{
get => _source.Type;
set
{
if (_source.Type != value)
{
_source.Type = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsGitHubSource));
OnPropertyChanged(nameof(TypeDisplayName));
}
}
}
public string TypeDisplayName => Type == KnowledgeSourceType.Local ? "本地知识库" : "GitHub知识库";
public bool IsGitHubSource => Type == KnowledgeSourceType.GitHub;
public string RemoteUrl
{
get => _source.RemoteUrl;
set
{
if (_source.RemoteUrl != value)
{
_source.RemoteUrl = value;
OnPropertyChanged();
// 当远端地址更新时,自动更新本地路径
if (Type == KnowledgeSourceType.GitHub && !string.IsNullOrWhiteSpace(value))
{
UpdateLocalPathFromRemoteUrl(value);
}
}
}
}
public string LocalPath
{
get => _source.LocalPath;
set
{
if (_source.LocalPath != value)
{
_source.LocalPath = value;
OnPropertyChanged();
}
}
}
public string GitHubToken
{
get => _source.GitHubToken;
set
{
if (_source.GitHubToken != value)
{
_source.GitHubToken = value;
OnPropertyChanged();
}
}
}
public bool IsEnabled
{
get => _source.IsEnabled;
set
{
if (_source.IsEnabled != value)
{
_source.IsEnabled = value;
OnPropertyChanged();
}
}
}
public string SearchDirectoriesText
{
get => _searchDirectoriesText;
set
{
if (SetProperty(ref _searchDirectoriesText, value))
{
UpdateSearchDirectories();
OnPropertyChanged();
}
}
}
public string FileExtensionsText
{
get => _fileExtensionsText;
set
{
if (SetProperty(ref _fileExtensionsText, value))
{
UpdateFileExtensions();
OnPropertyChanged();
}
}
}
public bool IsEditing
{
get => _isEditing;
set => SetProperty(ref _isEditing, value);
}
public ICommand EditCommand { get; }
public ICommand SaveCommand { get; }
public ICommand CancelCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand UpdateCommand { get; }
private void UpdateSearchDirectories()
{
_source.SearchDirectories = _searchDirectoriesText
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(d => d.Trim())
.Where(d => !string.IsNullOrWhiteSpace(d))
.ToList();
}
private void UpdateFileExtensions()
{
_source.FileExtensions = _fileExtensionsText
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(ext => ext.Trim())
.Where(ext => !string.IsNullOrWhiteSpace(ext))
.ToList();
}
private async Task SaveAsync()
{
IsEditing = false;
await _parentViewModel.SaveSourceAsync(this);
}
private void Cancel()
{
IsEditing = false;
// 重置文本框内容
_searchDirectoriesText = string.Join("\n", _source.SearchDirectories);
_fileExtensionsText = string.Join(", ", _source.FileExtensions);
OnPropertyChanged(nameof(SearchDirectoriesText));
OnPropertyChanged(nameof(FileExtensionsText));
}
private async Task DeleteAsync()
{
await _parentViewModel.RemoveSourceAsync(this);
}
private async Task UpdateAsync()
{
try
{
var knowledgeService = App.ServiceProvider?.GetService<IKnowledgeSourceService>();
if (knowledgeService == null)
{
Console.WriteLine("❌ 错误:无法获取知识库服务");
return;
}
Console.WriteLine($"🔄 开始更新知识库: {Name}");
Console.WriteLine($"📍 类型: {TypeDisplayName}");
if (Type == KnowledgeSourceType.GitHub)
{
Console.WriteLine($"🌐 远端地址: {RemoteUrl}");
}
Console.WriteLine($"📁 本地路径: {LocalPath}");
// 调用服务进行更新
var result = await knowledgeService.UpdateKnowledgeSourceAsync(_source);
// 输出详细日志
foreach (var log in result.Logs)
{
Console.WriteLine($"📝 {log}");
}
if (result.IsSuccess)
{
Console.WriteLine($"✅ 更新成功: {result.Message}");
Console.WriteLine($"📊 操作类型: {result.OperationType}");
if (result.UpdatedFilesCount > 0)
{
Console.WriteLine($"📋 文件数量: {result.UpdatedFilesCount}");
}
}
else
{
Console.WriteLine($"❌ 更新失败: {result.ErrorMessage}");
if (!string.IsNullOrEmpty(result.OperationType))
{
Console.WriteLine($"📊 失败操作: {result.OperationType}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ 更新过程中发生异常: {ex.Message}");
Console.WriteLine($"🔍 异常详情: {ex}");
}
}
/// <summary>
/// 根据远端地址自动更新本地路径
/// </summary>
private void UpdateLocalPathFromRemoteUrl(string remoteUrl)
{
try
{
// 从GitHub URL中提取仓库名称
// 例如: https://github.com/user/repo 或 https://github.com/user/repo.git
var repositoryName = ExtractRepositoryNameFromUrl(remoteUrl);
if (!string.IsNullOrWhiteSpace(repositoryName))
{
// 获取默认下载目录
var downloadService = App.ServiceProvider?.GetService<IAppDownloadService>();
var defaultDownloadPath = downloadService?.GetDownloadDirectory() ??
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LubanHub", "Downloads");
// 构建本地路径:默认下载路径 + "/" + 仓库名称
var newLocalPath = Path.Combine(defaultDownloadPath, repositoryName);
// 更新本地路径
if (_source.LocalPath != newLocalPath)
{
_source.LocalPath = newLocalPath;
OnPropertyChanged(nameof(LocalPath));
}
}
}
catch (Exception ex)
{
Console.WriteLine($"更新本地路径时出错: {ex.Message}");
}
}
/// <summary>
/// 从GitHub URL中提取仓库名称
/// </summary>
private string ExtractRepositoryNameFromUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
return string.Empty;
try
{
// 移除末尾的 .git 后缀
if (url.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
url = url.Substring(0, url.Length - 4);
// 移除末尾的斜杠
url = url.TrimEnd('/');
// 提取最后一个路径段作为仓库名称
var lastSlashIndex = url.LastIndexOf('/');
if (lastSlashIndex >= 0 && lastSlashIndex < url.Length - 1)
{
return url.Substring(lastSlashIndex + 1);
}
return string.Empty;
}
catch
{
return string.Empty;
}
}
}

View File

@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using LubanHub.App.ViewModels;
using LubanHub.Core.Interfaces;
using LubanHub.KnowledgeService.Configurations;
using LubanHub.KnowledgeService.Models;
using Microsoft.Extensions.Logging;
namespace LubanHub.App.ViewModels;
/// <summary>
/// 知识库来源管理ViewModel
/// </summary>
public class KnowledgeSourceManagementViewModel : ViewModelBase
{
private readonly ICoreConfigurationDiscoveryService _configurationService;
private readonly ILogger<KnowledgeSourceManagementViewModel> _logger;
private ObservableCollection<KnowledgeSourceItemViewModel> _knowledgeSources;
private bool _isLoading;
public KnowledgeSourceManagementViewModel(
ICoreConfigurationDiscoveryService configurationService,
ILogger<KnowledgeSourceManagementViewModel> logger)
{
_configurationService = configurationService;
_logger = logger;
_knowledgeSources = new ObservableCollection<KnowledgeSourceItemViewModel>();
AddLocalSourceCommand = new RelayCommand(AddLocalSourceAsync);
AddGitHubSourceCommand = new RelayCommand(AddGitHubSourceAsync);
RefreshCommand = new RelayCommand(async () => await LoadKnowledgeSourcesAsync());
_ = LoadKnowledgeSourcesAsync();
}
public ObservableCollection<KnowledgeSourceItemViewModel> KnowledgeSources
{
get => _knowledgeSources;
set => SetProperty(ref _knowledgeSources, value);
}
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public ICommand AddLocalSourceCommand { get; }
public ICommand AddGitHubSourceCommand { get; }
public ICommand RefreshCommand { get; }
private async Task LoadKnowledgeSourcesAsync()
{
try
{
IsLoading = true;
var config = await _configurationService.GetConfigurationAsync<KnowledgeConfiguration>();
if (config?.KnowledgeSources != null)
{
KnowledgeSources.Clear();
foreach (var source in config.KnowledgeSources)
{
var itemViewModel = new KnowledgeSourceItemViewModel(source, this);
KnowledgeSources.Add(itemViewModel);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "加载知识库来源时发生错误");
}
finally
{
IsLoading = false;
}
}
private async Task AddLocalSourceAsync()
{
var newSource = new KnowledgeSource
{
Name = "新建本地知识库",
Type = KnowledgeSourceType.Local,
SearchDirectories = GetDefaultSearchDirectories(),
FileExtensions = GetDefaultFileExtensions()
};
var itemViewModel = new KnowledgeSourceItemViewModel(newSource, this);
KnowledgeSources.Add(itemViewModel);
await Task.CompletedTask;
}
private async Task AddGitHubSourceAsync()
{
var newSource = new KnowledgeSource
{
Name = "新建GitHub知识库",
Type = KnowledgeSourceType.GitHub,
SearchDirectories = GetDefaultSearchDirectories(),
FileExtensions = GetDefaultFileExtensions()
};
var itemViewModel = new KnowledgeSourceItemViewModel(newSource, this);
KnowledgeSources.Add(itemViewModel);
await Task.CompletedTask;
}
internal async Task RemoveSourceAsync(KnowledgeSourceItemViewModel item)
{
KnowledgeSources.Remove(item);
await SaveConfigurationAsync();
}
internal async Task SaveSourceAsync(KnowledgeSourceItemViewModel item)
{
await SaveConfigurationAsync();
}
private async Task SaveConfigurationAsync()
{
try
{
var config = await _configurationService.GetConfigurationAsync<KnowledgeConfiguration>();
if (config == null)
config = new KnowledgeConfiguration();
config.KnowledgeSources = KnowledgeSources.Select(vm => vm.Source).ToList();
await _configurationService.SaveConfigurationAsync(config);
}
catch (Exception ex)
{
_logger.LogError(ex, "保存知识库配置时发生错误");
}
}
private List<string> GetDefaultSearchDirectories()
{
return new List<string> { "docs", "readme", "src" };
}
private List<string> GetDefaultFileExtensions()
{
return new List<string> { ".md", ".txt", ".rst", ".json", ".yml", ".yaml" };
}
}

View File

@@ -0,0 +1,116 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LubanHub.App.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LubanHub.App.Views.ConfigurationView">
<Grid>
<ScrollViewer>
<StackPanel Margin="20">
<TextBlock Text="配置管理" FontSize="24" FontWeight="Bold" Margin="0,0,0,20"/>
<!-- 配置分组列表 -->
<ItemsControl ItemsSource="{Binding ConfigurationGroups}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding DisplayName}" IsExpanded="True" Margin="0,0,0,10">
<Expander.HeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Icon}" FontSize="16" Margin="0,0,8,0"
IsVisible="{Binding Icon, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Text="{Binding DisplayName}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Description}" FontSize="12" Opacity="0.7" Margin="8,0,0,0"
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</DataTemplate>
</Expander.HeaderTemplate>
<Border Background="#F5F5F5" Padding="15" Margin="5">
<ItemsControl ItemsSource="{Binding Properties}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,15">
<TextBlock Text="{Binding DisplayName}" FontWeight="SemiBold" Margin="0,0,0,5"/>
<TextBlock Text="{Binding Description}" FontSize="12" Opacity="0.7" Margin="0,0,0,5"
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<!-- 根据InputType显示不同的控件 -->
<ContentControl>
<ContentControl.ContentTemplate>
<DataTemplate>
<Panel>
<!-- TextBox -->
<TextBox Text="{Binding Value}"
Watermark="{Binding Placeholder}"
IsVisible="{Binding InputType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=TextBox}"/>
<!-- PasswordBox -->
<TextBox Text="{Binding Value}"
PasswordChar="*"
Watermark="{Binding Placeholder}"
IsVisible="{Binding InputType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=PasswordBox}"/>
<!-- NumberBox -->
<NumericUpDown Value="{Binding Value}"
IsVisible="{Binding InputType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=NumberBox}"/>
<!-- CheckBox -->
<CheckBox IsChecked="{Binding Value}"
Content="{Binding DisplayName}"
IsVisible="{Binding InputType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=CheckBox}"/>
<!-- TextArea -->
<TextBox Text="{Binding Value}"
AcceptsReturn="True"
Height="100"
Watermark="{Binding Placeholder}"
IsVisible="{Binding InputType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=TextArea}"/>
<!-- FilePicker / DirectoryPicker / UrlBox 等其他类型暂时用TextBox代替 -->
<StackPanel Orientation="Horizontal"
IsVisible="{Binding InputType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=DirectoryPicker}">
<TextBox Text="{Binding Value}"
Watermark="{Binding Placeholder}"
Width="300"/>
<Button Content="浏览..." Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal"
IsVisible="{Binding InputType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=FilePicker}">
<TextBox Text="{Binding Value}"
Watermark="{Binding Placeholder}"
Width="300"/>
<Button Content="选择..." Margin="5,0,0,0"/>
</StackPanel>
</Panel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
<!-- 验证错误提示 -->
<TextBlock Text="{Binding ValidationMessage}"
Foreground="Red"
FontSize="11"
Margin="0,3,0,0"
IsVisible="{Binding HasValidationError}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 保存按钮 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
<Button Content="重置" Margin="0,0,10,0" x:Name="ResetButton"/>
<Button Content="保存配置" x:Name="SaveButton"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -0,0 +1,82 @@
using Avalonia.Controls;
using LubanHub.App.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using LubanHub.Core.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using Avalonia.Interactivity;
namespace LubanHub.App.Views;
public partial class ConfigurationView : UserControl
{
private ConfigurationViewModel? _viewModel;
public ConfigurationView()
{
InitializeComponent();
InitializeViewModel();
InitializeEventHandlers();
}
private void InitializeViewModel()
{
try
{
if (App.ServiceProvider != null)
{
var configService = App.ServiceProvider.GetRequiredService<ICoreConfigurationDiscoveryService>();
var logger = App.ServiceProvider.GetRequiredService<ILogger<ConfigurationViewModel>>();
_viewModel = new ConfigurationViewModel(configService, logger);
DataContext = _viewModel;
}
}
catch (Exception ex)
{
Console.WriteLine($"初始化ConfigurationViewModel时出错: {ex.Message}");
}
}
private void InitializeEventHandlers()
{
var saveButton = this.FindControl<Button>("SaveButton");
var resetButton = this.FindControl<Button>("ResetButton");
if (saveButton != null)
saveButton.Click += OnSaveButtonClick;
if (resetButton != null)
resetButton.Click += OnResetButtonClick;
}
private async void OnSaveButtonClick(object? sender, RoutedEventArgs e)
{
try
{
if (_viewModel != null)
{
await _viewModel.SaveAllConfigurationsAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"保存配置时出错: {ex.Message}");
}
}
private async void OnResetButtonClick(object? sender, RoutedEventArgs e)
{
try
{
if (_viewModel != null)
{
await _viewModel.ResetAllConfigurationsAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"重置配置时出错: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,315 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LubanHub.App.Views.KnowledgeSourceManagementView">
<ScrollViewer>
<StackPanel Margin="20">
<!-- 标题和操作按钮 -->
<Grid Margin="0,0,0,20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="📚 知识库来源管理"
FontSize="20"
FontWeight="Bold"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
<Button Content=" 添加本地知识库"
Classes="primary"
Command="{Binding AddLocalSourceCommand}"/>
<Button Content="🌐 添加GitHub知识库"
Classes="primary"
Command="{Binding AddGitHubSourceCommand}"/>
<Button Content="🔄 刷新"
Command="{Binding RefreshCommand}"/>
</StackPanel>
</Grid>
<!-- 知识库来源列表 -->
<ItemsControl ItemsSource="{Binding KnowledgeSources}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{DynamicResource PanelBackgroundBrush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="5"
Margin="0,0,0,15"
Padding="20">
<!-- 只读模式 -->
<Panel>
<StackPanel IsVisible="{Binding !IsEditing}">
<!-- 标题行 -->
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10">
<TextBlock Text="{Binding Name}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<Border Background="{DynamicResource AccentBrush}"
CornerRadius="10"
Padding="8,2">
<TextBlock Text="{Binding TypeDisplayName}"
FontSize="12"
Foreground="White"/>
</Border>
<CheckBox IsChecked="{Binding IsEnabled}"
Content="启用"
Foreground="{DynamicResource PrimaryTextBrush}"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="5">
<Button Content="✏️ 编辑"
Classes="secondary"
Padding="8,4"
Command="{Binding EditCommand}"/>
<Button Content="🔄 更新"
Classes="accent"
Padding="8,4"
Command="{Binding UpdateCommand}"
IsVisible="{Binding IsGitHubSource}"/>
<Button Content="🗑️ 删除"
Classes="danger"
Padding="8,4"
Command="{Binding DeleteCommand}"/>
</StackPanel>
</Grid>
<!-- 详细信息 -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- GitHub URL -->
<TextBlock Grid.Row="0" Grid.Column="0"
Text="远端地址:"
FontWeight="SemiBold"
Foreground="{DynamicResource SecondaryTextBrush}"
IsVisible="{Binding IsGitHubSource}"/>
<TextBlock Grid.Row="0" Grid.Column="1"
Text="{Binding RemoteUrl}"
Foreground="{DynamicResource PrimaryTextBrush}"
TextWrapping="Wrap"
IsVisible="{Binding IsGitHubSource}"/>
<!-- 本地路径 -->
<TextBlock Grid.Row="1" Grid.Column="0"
Text="本地路径:"
FontWeight="SemiBold"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Text="{Binding LocalPath}"
Foreground="{DynamicResource PrimaryTextBrush}"
TextWrapping="Wrap"/>
<!-- 搜索目录 -->
<TextBlock Grid.Row="2" Grid.Column="0"
Text="搜索目录:"
FontWeight="SemiBold"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<TextBlock Grid.Row="2" Grid.Column="1"
Text="{Binding SearchDirectoriesText}"
Foreground="{DynamicResource PrimaryTextBrush}"
TextWrapping="Wrap"/>
<!-- 文件扩展名 -->
<TextBlock Grid.Row="3" Grid.Column="0"
Text="文件扩展名:"
FontWeight="SemiBold"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<TextBlock Grid.Row="3" Grid.Column="1"
Text="{Binding FileExtensionsText}"
Foreground="{DynamicResource PrimaryTextBrush}"
TextWrapping="Wrap"/>
</Grid>
</StackPanel>
<!-- 编辑模式 -->
<StackPanel IsVisible="{Binding IsEditing}">
<!-- 编辑标题 -->
<Grid Margin="0,0,0,15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="编辑知识库来源"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="5">
<Button Content="💾 保存"
Classes="success"
Command="{Binding SaveCommand}"/>
<Button Content="❌ 取消"
Classes="secondary"
Command="{Binding CancelCommand}"/>
</StackPanel>
</Grid>
<!-- 编辑表单 -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 名称 -->
<TextBlock Grid.Row="0" Grid.Column="0"
Text="名称:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Text="{Binding Name}"
Margin="0,5"/>
<!-- 类型 -->
<TextBlock Grid.Row="1" Grid.Column="0"
Text="类型:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<ComboBox Grid.Row="1" Grid.Column="1"
SelectedIndex="{Binding Type}"
Margin="0,5">
<ComboBoxItem Content="本地知识库"/>
<ComboBoxItem Content="GitHub知识库"/>
</ComboBox>
<!-- GitHub URL -->
<TextBlock Grid.Row="2" Grid.Column="0"
Text="远端地址:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"
IsVisible="{Binding IsGitHubSource}"/>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding RemoteUrl}"
Watermark="https://github.com/user/repo"
Margin="0,5"
IsVisible="{Binding IsGitHubSource}"/>
<!-- 本地路径 -->
<TextBlock Grid.Row="3" Grid.Column="0"
Text="本地路径:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<Grid Grid.Row="3" Grid.Column="1" Margin="0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="{Binding LocalPath}"
Watermark="选择本地文件夹路径"/>
<Button Grid.Column="1"
Content="浏览..."
Margin="5,0,0,0"/>
</Grid>
<!-- GitHub Token -->
<TextBlock Grid.Row="4" Grid.Column="0"
Text="GitHub令牌:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"
IsVisible="{Binding IsGitHubSource}"/>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding GitHubToken}"
PasswordChar="*"
Watermark="ghp_xxxxxxxxxxxxxx"
Margin="0,5"
IsVisible="{Binding IsGitHubSource}"/>
<!-- 搜索目录 -->
<TextBlock Grid.Row="5" Grid.Column="0"
Text="搜索目录:"
VerticalAlignment="Top"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"
Margin="0,8,0,0"/>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding SearchDirectoriesText}"
AcceptsReturn="True"
Height="80"
Watermark="docs&#x0A;readme&#x0A;src"
Margin="0,5"/>
<!-- 文件扩展名 -->
<TextBlock Grid.Row="6" Grid.Column="0"
Text="文件扩展名:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<TextBox Grid.Row="6" Grid.Column="1"
Text="{Binding FileExtensionsText}"
Watermark=".md, .txt, .rst, .json, .yml, .yaml"
Margin="0,5"/>
</Grid>
</StackPanel>
</Panel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 空状态提示 -->
<StackPanel IsVisible="{Binding !KnowledgeSources.Count}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,50">
<TextBlock Text="🤷 暂无知识库来源"
FontSize="18"
HorizontalAlignment="Center"
Foreground="{DynamicResource SecondaryTextBrush}"
Margin="0,0,0,10"/>
<TextBlock Text="点击上方按钮添加您的第一个知识库来源"
FontSize="14"
HorizontalAlignment="Center"
Foreground="{DynamicResource SecondaryTextBrush}"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,36 @@
using Avalonia.Controls;
using LubanHub.App.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using LubanHub.Core.Interfaces;
using Microsoft.Extensions.Logging;
using System;
namespace LubanHub.App.Views;
public partial class KnowledgeSourceManagementView : UserControl
{
public KnowledgeSourceManagementView()
{
InitializeComponent();
InitializeViewModel();
}
private void InitializeViewModel()
{
try
{
if (App.ServiceProvider != null)
{
var configService = App.ServiceProvider.GetRequiredService<ICoreConfigurationDiscoveryService>();
var logger = App.ServiceProvider.GetRequiredService<ILogger<KnowledgeSourceManagementViewModel>>();
var viewModel = new KnowledgeSourceManagementViewModel(configService, logger);
DataContext = viewModel;
}
}
catch (Exception ex)
{
Console.WriteLine($"初始化KnowledgeSourceManagementViewModel时出错: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.ComponentModel;
namespace LubanHub.Core.Attributes;
/// <summary>
/// 配置属性装饰器用于标记配置属性并定义其在UI中的显示和编辑行为
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ConfigurationPropertyAttribute : Attribute
{
/// <summary>
/// 属性显示名称
/// </summary>
public string DisplayName { get; }
/// <summary>
/// 属性描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 输入控件类型
/// </summary>
public ConfigInputType InputType { get; set; } = ConfigInputType.TextBox;
/// <summary>
/// 排序优先级,数值越小优先级越高
/// </summary>
public int Priority { get; set; } = 0;
/// <summary>
/// 是否必填
/// </summary>
public bool IsRequired { get; set; } = false;
/// <summary>
/// 占位符文本
/// </summary>
public string? Placeholder { get; set; }
/// <summary>
/// 默认值
/// </summary>
public object? DefaultValue { get; set; }
/// <summary>
/// 验证正则表达式
/// </summary>
public string? ValidationPattern { get; set; }
/// <summary>
/// 验证错误消息
/// </summary>
public string? ValidationMessage { get; set; }
/// <summary>
/// 初始化配置属性装饰器
/// </summary>
/// <param name="displayName">属性显示名称</param>
public ConfigurationPropertyAttribute(string displayName)
{
DisplayName = displayName;
}
}
/// <summary>
/// 配置输入控件类型
/// </summary>
public enum ConfigInputType
{
/// <summary>
/// 文本框
/// </summary>
[Description("文本框")]
TextBox,
/// <summary>
/// 密码框
/// </summary>
[Description("密码框")]
PasswordBox,
/// <summary>
/// 数字输入框
/// </summary>
[Description("数字输入框")]
NumberBox,
/// <summary>
/// 复选框
/// </summary>
[Description("复选框")]
CheckBox,
/// <summary>
/// 下拉选择框
/// </summary>
[Description("下拉选择框")]
ComboBox,
/// <summary>
/// 文件选择器
/// </summary>
[Description("文件选择器")]
FilePicker,
/// <summary>
/// 目录选择器
/// </summary>
[Description("目录选择器")]
DirectoryPicker,
/// <summary>
/// 多行文本框
/// </summary>
[Description("多行文本框")]
TextArea,
/// <summary>
/// URL输入框
/// </summary>
[Description("URL输入框")]
UrlBox,
/// <summary>
/// 自定义控件
/// </summary>
[Description("自定义控件")]
Custom
}

View File

@@ -0,0 +1,51 @@
using System;
namespace LubanHub.Core.Attributes;
/// <summary>
/// 配置选项装饰器用于标记配置类并定义其在UI中的显示信息
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ConfigurationSectionAttribute : Attribute
{
/// <summary>
/// 配置分组名称(用于在设置界面中分组显示)
/// </summary>
public string GroupName { get; }
/// <summary>
/// 配置显示名称
/// </summary>
public string DisplayName { get; }
/// <summary>
/// 配置描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 配置图标Emoji
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// 排序优先级,数值越小优先级越高
/// </summary>
public int Priority { get; set; } = 0;
/// <summary>
/// 是否需要重启应用才能生效
/// </summary>
public bool RequiresRestart { get; set; } = false;
/// <summary>
/// 初始化配置选项装饰器
/// </summary>
/// <param name="groupName">配置分组名称</param>
/// <param name="displayName">配置显示名称</param>
public ConfigurationSectionAttribute(string groupName, string displayName)
{
GroupName = groupName;
DisplayName = displayName;
}
}

View File

@@ -0,0 +1,159 @@
using LubanHub.Core.Attributes;
using System.Collections.Generic;
namespace LubanHub.Core.Configurations;
/// <summary>
/// Git配置
/// </summary>
[ConfigurationSection("版本控制", "Git设置",
Description = "配置Git版本控制相关参数包括代理设置",
Icon = "🌐",
Priority = 5)]
public class GitConfiguration
{
/// <summary>
/// 是否启用代理
/// </summary>
[ConfigurationProperty("启用代理",
Description = "是否为Git操作启用HTTP/HTTPS代理",
InputType = ConfigInputType.CheckBox,
Priority = 1,
DefaultValue = false)]
public bool EnableProxy { get; set; } = false;
/// <summary>
/// 代理服务器地址
/// </summary>
[ConfigurationProperty("代理服务器地址",
Description = "代理服务器的IP地址或域名",
Priority = 2,
Placeholder = "127.0.0.1 或 proxy.company.com")]
public string ProxyHost { get; set; } = string.Empty;
/// <summary>
/// 代理服务器端口
/// </summary>
[ConfigurationProperty("代理服务器端口",
Description = "代理服务器的端口号",
InputType = ConfigInputType.NumberBox,
Priority = 3,
DefaultValue = 8080,
Placeholder = "8080")]
public int ProxyPort { get; set; } = 8080;
/// <summary>
/// 代理类型
/// </summary>
[ConfigurationProperty("代理类型",
Description = "选择代理协议类型",
InputType = ConfigInputType.ComboBox,
Priority = 4,
DefaultValue = "HTTP")]
public string ProxyType { get; set; } = "HTTP";
/// <summary>
/// 代理用户名
/// </summary>
[ConfigurationProperty("代理用户名",
Description = "代理服务器认证用户名(可选)",
Priority = 5,
Placeholder = "username")]
public string ProxyUsername { get; set; } = string.Empty;
/// <summary>
/// 代理密码
/// </summary>
[ConfigurationProperty("代理密码",
Description = "代理服务器认证密码(可选)",
InputType = ConfigInputType.PasswordBox,
Priority = 6,
Placeholder = "password")]
public string ProxyPassword { get; set; } = string.Empty;
/// <summary>
/// 不使用代理的地址
/// </summary>
[ConfigurationProperty("代理排除地址",
Description = "不使用代理的地址列表,用逗号分隔",
InputType = ConfigInputType.TextArea,
Priority = 7,
Placeholder = "localhost,127.0.0.1,*.local")]
public string NoProxy { get; set; } = string.Empty;
/// <summary>
/// Git超时时间
/// </summary>
[ConfigurationProperty("操作超时",
Description = "Git操作的超时时间",
InputType = ConfigInputType.NumberBox,
Priority = 8,
DefaultValue = 300)]
public int TimeoutSeconds { get; set; } = 300;
/// <summary>
/// 是否验证SSL证书
/// </summary>
[ConfigurationProperty("验证SSL证书",
Description = "是否验证HTTPS连接的SSL证书",
InputType = ConfigInputType.CheckBox,
Priority = 9,
DefaultValue = true)]
public bool VerifySSL { get; set; } = true;
/// <summary>
/// 获取完整的代理URL
/// </summary>
/// <returns>代理URL如果未启用代理则返回空字符串</returns>
public string GetProxyUrl()
{
if (!EnableProxy || string.IsNullOrWhiteSpace(ProxyHost))
{
return string.Empty;
}
var protocol = ProxyType.ToLower() switch
{
"socks4" => "socks4",
"socks5" => "socks5",
_ => "http"
};
if (string.IsNullOrWhiteSpace(ProxyUsername))
{
return $"{protocol}://{ProxyHost}:{ProxyPort}";
}
else
{
return $"{protocol}://{ProxyUsername}:{ProxyPassword}@{ProxyHost}:{ProxyPort}";
}
}
/// <summary>
/// 获取代理配置的Git环境变量
/// </summary>
/// <returns>Git环境变量字典</returns>
public Dictionary<string, string> GetGitEnvironmentVariables()
{
var env = new Dictionary<string, string>();
if (!EnableProxy || string.IsNullOrWhiteSpace(ProxyHost))
{
return env;
}
var proxyUrl = GetProxyUrl();
// 设置HTTP和HTTPS代理
env["http_proxy"] = proxyUrl;
env["https_proxy"] = proxyUrl;
// 设置不使用代理的地址
if (!string.IsNullOrWhiteSpace(NoProxy))
{
env["no_proxy"] = NoProxy;
}
return env;
}
}

View File

@@ -0,0 +1,39 @@
using LubanHub.Core.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LubanHub.Core.Interfaces;
/// <summary>
/// 配置发现服务接口
/// </summary>
public interface ICoreConfigurationDiscoveryService
{
/// <summary>
/// 发现所有配置分组
/// </summary>
/// <returns>配置分组信息列表</returns>
Task<List<ConfigurationGroupInfo>> DiscoverConfigurationGroupsAsync();
/// <summary>
/// 获取指定配置的实例
/// </summary>
/// <typeparam name="T">配置类型</typeparam>
/// <returns>配置实例</returns>
Task<T?> GetConfigurationAsync<T>() where T : class, new();
/// <summary>
/// 保存配置
/// </summary>
/// <typeparam name="T">配置类型</typeparam>
/// <param name="configuration">配置实例</param>
/// <returns>是否保存成功</returns>
Task<bool> SaveConfigurationAsync<T>(T configuration) where T : class;
/// <summary>
/// 获取配置文件路径
/// </summary>
/// <param name="configurationType">配置类型</param>
/// <returns>配置文件路径</returns>
string GetConfigurationFilePath(System.Type configurationType);
}

View File

@@ -0,0 +1,60 @@
using System.Threading.Tasks;
using LubanHub.Core.Models;
using LubanHub.Core.Configurations;
namespace LubanHub.Core.Interfaces;
/// <summary>
/// Git版本控制服务接口
/// </summary>
public interface ICoreGitService
{
/// <summary>
/// 克隆Git仓库
/// </summary>
/// <param name="repositoryUrl">仓库URL</param>
/// <param name="localPath">本地路径</param>
/// <param name="accessToken">访问令牌(可选)</param>
/// <returns>操作结果</returns>
Task<GitOperationResult> CloneAsync(string repositoryUrl, string localPath, string? accessToken = null);
/// <summary>
/// 更新Git仓库
/// </summary>
/// <param name="localPath">本地仓库路径</param>
/// <returns>操作结果</returns>
Task<GitOperationResult> PullAsync(string localPath);
/// <summary>
/// 检查目录是否为Git仓库
/// </summary>
/// <param name="localPath">本地路径</param>
/// <returns>是否为Git仓库</returns>
bool IsGitRepository(string localPath);
/// <summary>
/// 获取仓库状态
/// </summary>
/// <param name="localPath">本地仓库路径</param>
/// <returns>仓库状态</returns>
Task<GitRepositoryStatus> GetRepositoryStatusAsync(string localPath);
/// <summary>
/// 配置Git代理设置
/// </summary>
/// <param name="gitConfig">Git配置</param>
/// <returns>操作结果</returns>
Task<GitOperationResult> ConfigureProxyAsync(GitConfiguration gitConfig);
/// <summary>
/// 清除Git代理设置
/// </summary>
/// <returns>操作结果</returns>
Task<GitOperationResult> ClearProxyAsync();
/// <summary>
/// 获取当前Git代理设置
/// </summary>
/// <returns>代理设置信息</returns>
Task<GitProxyInfo> GetProxyInfoAsync();
}

View File

@@ -1,5 +1,6 @@
using LubanHub.Core.Models;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -35,4 +36,17 @@ public interface ICoreProcessService
/// 杀死进程
/// </summary>
void KillProcess(string processName);
/// <summary>
/// 执行进程并等待完成(支持环境变量)
/// </summary>
Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
Dictionary<string, string>? environmentVariables, CancellationToken cancellationToken = default);
/// <summary>
/// 执行进程并实时获取输出(支持环境变量)
/// </summary>
Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
Dictionary<string, string>? environmentVariables, Action<string>? onOutputReceived,
Action<string>? onErrorReceived, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using LubanHub.Core.Attributes;
namespace LubanHub.Core.Models;
/// <summary>
/// 配置分组信息
/// </summary>
public class ConfigurationGroupInfo
{
/// <summary>
/// 分组名称
/// </summary>
public string GroupName { get; set; } = string.Empty;
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// 描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 图标
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// 排序优先级
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 是否需要重启
/// </summary>
public bool RequiresRestart { get; set; }
/// <summary>
/// 配置类型
/// </summary>
public Type ConfigurationType { get; set; } = null!;
/// <summary>
/// 配置属性列表
/// </summary>
public List<ConfigurationPropertyInfo> Properties { get; set; } = new();
}
/// <summary>
/// 配置属性信息
/// </summary>
public class ConfigurationPropertyInfo
{
/// <summary>
/// 属性名称
/// </summary>
public string PropertyName { get; set; } = string.Empty;
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// 描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 输入控件类型
/// </summary>
public ConfigInputType InputType { get; set; }
/// <summary>
/// 排序优先级
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 是否必填
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// 占位符文本
/// </summary>
public string? Placeholder { get; set; }
/// <summary>
/// 默认值
/// </summary>
public object? DefaultValue { get; set; }
/// <summary>
/// 验证正则表达式
/// </summary>
public string? ValidationPattern { get; set; }
/// <summary>
/// 验证错误消息
/// </summary>
public string? ValidationMessage { get; set; }
/// <summary>
/// 属性类型
/// </summary>
public Type PropertyType { get; set; } = null!;
}

View File

@@ -0,0 +1,139 @@
using System.Collections.Generic;
namespace LubanHub.Core.Models;
/// <summary>
/// Git操作结果
/// </summary>
public class GitOperationResult
{
/// <summary>
/// 操作是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 操作消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 错误信息
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// 操作类型
/// </summary>
public string OperationType { get; set; } = string.Empty;
/// <summary>
/// 操作日志
/// </summary>
public List<string> Logs { get; set; } = new();
/// <summary>
/// 更新的文件数量
/// </summary>
public int UpdatedFilesCount { get; set; }
/// <summary>
/// 标准输出
/// </summary>
public string StandardOutput { get; set; } = string.Empty;
/// <summary>
/// 标准错误
/// </summary>
public string StandardError { get; set; } = string.Empty;
/// <summary>
/// 创建成功结果
/// </summary>
/// <param name="message">成功消息</param>
/// <param name="standardOutput">标准输出</param>
/// <returns>成功结果</returns>
public static GitOperationResult Success(string message, string standardOutput = "")
{
return new GitOperationResult
{
IsSuccess = true,
Message = message,
StandardOutput = standardOutput
};
}
/// <summary>
/// 创建失败结果
/// </summary>
/// <param name="errorMessage">错误消息</param>
/// <param name="standardError">标准错误</param>
/// <returns>失败结果</returns>
public static GitOperationResult Failure(string errorMessage, string standardError = "")
{
return new GitOperationResult
{
IsSuccess = false,
Message = "操作失败",
ErrorMessage = errorMessage,
StandardError = standardError
};
}
}
/// <summary>
/// Git仓库状态
/// </summary>
public class GitRepositoryStatus
{
/// <summary>
/// 是否为Git仓库
/// </summary>
public bool IsRepository { get; set; }
/// <summary>
/// 当前分支名称
/// </summary>
public string? CurrentBranch { get; set; }
/// <summary>
/// 是否有未提交的更改
/// </summary>
public bool HasUncommittedChanges { get; set; }
/// <summary>
/// 最后一次提交的哈希
/// </summary>
public string? LastCommitHash { get; set; }
/// <summary>
/// 远程URL
/// </summary>
public string? RemoteUrl { get; set; }
}
/// <summary>
/// Git代理信息
/// </summary>
public class GitProxyInfo
{
/// <summary>
/// HTTP代理地址
/// </summary>
public string? HttpProxy { get; set; }
/// <summary>
/// HTTPS代理地址
/// </summary>
public string? HttpsProxy { get; set; }
/// <summary>
/// 是否启用了代理
/// </summary>
public bool IsProxyEnabled => !string.IsNullOrWhiteSpace(HttpProxy) || !string.IsNullOrWhiteSpace(HttpsProxy);
/// <summary>
/// 代理验证状态
/// </summary>
public string Status { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,198 @@
using LubanHub.Core.Attributes;
using LubanHub.Core.Interfaces;
using LubanHub.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
namespace LubanHub.Core.Services;
/// <summary>
/// 配置发现服务实现
/// </summary>
[RegistService(ServiceLifetime.Singleton, typeof(ICoreConfigurationDiscoveryService))]
public class CoreConfigurationDiscoveryService : ICoreConfigurationDiscoveryService
{
private readonly ICoreFileService _fileService;
private readonly ILogger<CoreConfigurationDiscoveryService> _logger;
private readonly JsonSerializerOptions _jsonOptions;
private readonly string _configDirectory;
public CoreConfigurationDiscoveryService(
ICoreFileService fileService,
ILogger<CoreConfigurationDiscoveryService> logger)
{
_fileService = fileService;
_logger = logger;
_jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
// 配置文件存储在用户数据目录下的LubanHub/Config子目录
var userDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
_configDirectory = Path.Combine(userDataPath, "LubanHub", "Config");
// 确保配置目录存在
if (!Directory.Exists(_configDirectory))
{
Directory.CreateDirectory(_configDirectory);
}
}
public Task<List<ConfigurationGroupInfo>> DiscoverConfigurationGroupsAsync()
{
var groups = new List<ConfigurationGroupInfo>();
try
{
// 获取所有已加载的程序集
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && a.GetName().Name?.StartsWith("LubanHub") == true)
.ToList();
_logger.LogDebug("发现 {Count} 个LubanHub程序集", assemblies.Count);
foreach (var assembly in assemblies)
{
_logger.LogDebug("检查程序集: {AssemblyName}", assembly.GetName().Name);
}
foreach (var assembly in assemblies)
{
var configTypes = assembly.GetTypes()
.Where(type => type.IsClass && !type.IsAbstract &&
type.GetCustomAttribute<ConfigurationSectionAttribute>() != null)
.ToList();
_logger.LogDebug("程序集 {AssemblyName} 中发现 {Count} 个配置类型", assembly.GetName().Name, configTypes.Count);
foreach (var configType in configTypes)
{
_logger.LogDebug("处理配置类型: {TypeName}", configType.Name);
var sectionAttr = configType.GetCustomAttribute<ConfigurationSectionAttribute>()!;
var groupInfo = new ConfigurationGroupInfo
{
GroupName = sectionAttr.GroupName,
DisplayName = sectionAttr.DisplayName,
Description = sectionAttr.Description,
Icon = sectionAttr.Icon,
Priority = sectionAttr.Priority,
RequiresRestart = sectionAttr.RequiresRestart,
ConfigurationType = configType
};
// 发现配置属性
var properties = configType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(prop => prop.CanRead && prop.CanWrite)
.Select(prop =>
{
var propAttr = prop.GetCustomAttribute<ConfigurationPropertyAttribute>();
return new ConfigurationPropertyInfo
{
PropertyName = prop.Name,
DisplayName = propAttr?.DisplayName ?? prop.Name,
Description = propAttr?.Description,
InputType = propAttr?.InputType ?? GetDefaultInputType(prop.PropertyType),
Priority = propAttr?.Priority ?? 0,
IsRequired = propAttr?.IsRequired ?? false,
Placeholder = propAttr?.Placeholder,
DefaultValue = propAttr?.DefaultValue,
ValidationPattern = propAttr?.ValidationPattern,
ValidationMessage = propAttr?.ValidationMessage,
PropertyType = prop.PropertyType
};
})
.OrderBy(p => p.Priority)
.ThenBy(p => p.PropertyName)
.ToList();
groupInfo.Properties = properties;
groups.Add(groupInfo);
}
}
// 按优先级和组名排序
groups = groups.OrderBy(g => g.Priority)
.ThenBy(g => g.GroupName)
.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "发现配置分组时发生错误");
}
return Task.FromResult(groups);
}
public async Task<T?> GetConfigurationAsync<T>() where T : class, new()
{
try
{
var configPath = GetConfigurationFilePath(typeof(T));
if (!_fileService.FileExists(configPath))
{
// 配置文件不存在,返回默认实例
var defaultInstance = new T();
await SaveConfigurationAsync(defaultInstance);
return defaultInstance;
}
var jsonContent = await _fileService.ReadAllTextAsync(configPath);
var configuration = JsonSerializer.Deserialize<T>(jsonContent, _jsonOptions);
return configuration ?? new T();
}
catch (Exception ex)
{
_logger.LogError(ex, "读取配置 {ConfigType} 时发生错误", typeof(T).Name);
return new T();
}
}
public async Task<bool> SaveConfigurationAsync<T>(T configuration) where T : class
{
try
{
var configPath = GetConfigurationFilePath(typeof(T));
var jsonContent = JsonSerializer.Serialize(configuration, _jsonOptions);
await _fileService.WriteAllTextAsync(configPath, jsonContent);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "保存配置 {ConfigType} 时发生错误", typeof(T).Name);
return false;
}
}
public string GetConfigurationFilePath(Type configurationType)
{
var fileName = $"{configurationType.Name}.json";
return Path.Combine(_configDirectory, fileName);
}
private static Attributes.ConfigInputType GetDefaultInputType(Type propertyType)
{
// 获取基础类型(处理可空类型)
var underlyingType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
return underlyingType.Name switch
{
nameof(Boolean) => Attributes.ConfigInputType.CheckBox,
nameof(Int32) or nameof(Int64) or nameof(Double) or nameof(Decimal) => Attributes.ConfigInputType.NumberBox,
nameof(String) when propertyType.Name.Contains("Password", StringComparison.OrdinalIgnoreCase) => Attributes.ConfigInputType.PasswordBox,
nameof(String) when propertyType.Name.Contains("Url", StringComparison.OrdinalIgnoreCase) => Attributes.ConfigInputType.UrlBox,
nameof(String) when propertyType.Name.Contains("Path", StringComparison.OrdinalIgnoreCase) => Attributes.ConfigInputType.DirectoryPicker,
nameof(String) when propertyType.Name.Contains("File", StringComparison.OrdinalIgnoreCase) => Attributes.ConfigInputType.FilePicker,
_ => Attributes.ConfigInputType.TextBox
};
}
}

View File

@@ -0,0 +1,398 @@
using LubanHub.Core.Attributes;
using LubanHub.Core.Configurations;
using LubanHub.Core.Interfaces;
using LubanHub.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace LubanHub.Core.Services;
/// <summary>
/// Git版本控制服务实现
/// </summary>
[RegistService(ServiceLifetime.Singleton, typeof(ICoreGitService))]
public class CoreGitService : ICoreGitService
{
private readonly ICoreProcessService _processService;
private readonly ICoreFileService _fileService;
private readonly ILogger<CoreGitService> _logger;
public CoreGitService(
ICoreProcessService processService,
ICoreFileService fileService,
ILogger<CoreGitService> logger)
{
_processService = processService;
_fileService = fileService;
_logger = logger;
}
public async Task<GitOperationResult> CloneAsync(string repositoryUrl, string localPath, string? accessToken = null)
{
try
{
_logger.LogInformation("开始克隆Git仓库: {RepositoryUrl} 到 {LocalPath}", repositoryUrl, localPath);
// 验证输入参数
if (string.IsNullOrWhiteSpace(repositoryUrl))
{
return GitOperationResult.Failure("仓库URL不能为空");
}
if (string.IsNullOrWhiteSpace(localPath))
{
return GitOperationResult.Failure("本地路径不能为空");
}
// 如果目标目录已存在检查是否为Git仓库
if (_fileService.DirectoryExists(localPath))
{
if (IsGitRepository(localPath))
{
return GitOperationResult.Failure("目标目录已经是一个Git仓库");
}
else
{
return GitOperationResult.Failure("目标目录已存在且不为空");
}
}
// 确保父目录存在
var parentDir = Path.GetDirectoryName(localPath);
if (!string.IsNullOrEmpty(parentDir) && !_fileService.DirectoryExists(parentDir))
{
_fileService.CreateDirectory(parentDir);
_logger.LogDebug("创建父目录: {ParentDir}", parentDir);
}
// 构建克隆命令
string cloneUrl = repositoryUrl;
if (!string.IsNullOrWhiteSpace(accessToken))
{
// 使用访问令牌进行认证
cloneUrl = repositoryUrl.Replace("https://", $"https://{accessToken}@");
}
var command = "git";
var arguments = $"clone \"{cloneUrl}\" \"{localPath}\"";
_logger.LogDebug("执行Git克隆命令: git clone [URL] \"{LocalPath}\"", localPath);
var result = await _processService.ExecuteAsync(command, arguments);
if (result.IsSuccess)
{
_logger.LogInformation("Git仓库克隆成功: {RepositoryUrl}", repositoryUrl);
return GitOperationResult.Success("仓库克隆成功", result.StandardOutput);
}
else
{
_logger.LogError("Git仓库克隆失败: {Error}", result.StandardError);
return GitOperationResult.Failure("仓库克隆失败", result.StandardError);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "克隆Git仓库时发生异常");
return GitOperationResult.Failure($"克隆过程中发生异常: {ex.Message}");
}
}
public async Task<GitOperationResult> PullAsync(string localPath)
{
try
{
_logger.LogInformation("开始拉取Git仓库更新: {LocalPath}", localPath);
// 验证输入参数
if (string.IsNullOrWhiteSpace(localPath))
{
return GitOperationResult.Failure("本地路径不能为空");
}
// 检查目录是否存在
if (!_fileService.DirectoryExists(localPath))
{
return GitOperationResult.Failure("本地目录不存在");
}
// 检查是否为Git仓库
if (!IsGitRepository(localPath))
{
return GitOperationResult.Failure("目标目录不是Git仓库");
}
var command = "git";
var arguments = "pull";
var workingDirectory = localPath;
_logger.LogDebug("在目录 {WorkingDirectory} 中执行: git pull", workingDirectory);
var result = await _processService.ExecuteAsync(command, arguments, workingDirectory);
if (result.IsSuccess)
{
_logger.LogInformation("Git仓库更新成功: {LocalPath}", localPath);
return GitOperationResult.Success("仓库更新成功", result.StandardOutput);
}
else
{
_logger.LogError("Git仓库更新失败: {Error}", result.StandardError);
return GitOperationResult.Failure("仓库更新失败", result.StandardError);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "更新Git仓库时发生异常");
return GitOperationResult.Failure($"更新过程中发生异常: {ex.Message}");
}
}
public bool IsGitRepository(string localPath)
{
try
{
if (string.IsNullOrWhiteSpace(localPath) || !_fileService.DirectoryExists(localPath))
{
return false;
}
var gitDir = Path.Combine(localPath, ".git");
return _fileService.DirectoryExists(gitDir);
}
catch (Exception ex)
{
_logger.LogError(ex, "检查Git仓库状态时发生异常");
return false;
}
}
public async Task<GitRepositoryStatus> GetRepositoryStatusAsync(string localPath)
{
var status = new GitRepositoryStatus();
try
{
if (string.IsNullOrWhiteSpace(localPath) || !_fileService.DirectoryExists(localPath))
{
return status;
}
status.IsRepository = IsGitRepository(localPath);
if (!status.IsRepository)
{
return status;
}
// 获取当前分支
var branchResult = await _processService.ExecuteAsync("git", "branch --show-current", localPath);
if (branchResult.IsSuccess)
{
status.CurrentBranch = branchResult.StandardOutput.Trim();
}
// 检查是否有未提交的更改
var statusResult = await _processService.ExecuteAsync("git", "status --porcelain", localPath);
if (statusResult.IsSuccess)
{
status.HasUncommittedChanges = !string.IsNullOrWhiteSpace(statusResult.StandardOutput);
}
// 获取最后一次提交的哈希
var commitResult = await _processService.ExecuteAsync("git", "rev-parse HEAD", localPath);
if (commitResult.IsSuccess)
{
status.LastCommitHash = commitResult.StandardOutput.Trim();
}
// 获取远程URL
var remoteResult = await _processService.ExecuteAsync("git", "remote get-url origin", localPath);
if (remoteResult.IsSuccess)
{
status.RemoteUrl = remoteResult.StandardOutput.Trim();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "获取Git仓库状态时发生异常");
}
return status;
}
public async Task<GitOperationResult> ConfigureProxyAsync(GitConfiguration gitConfig)
{
try
{
_logger.LogInformation("开始配置Git代理设置");
var commands = new List<(string command, string arguments)>();
if (gitConfig.EnableProxy && !string.IsNullOrWhiteSpace(gitConfig.ProxyHost))
{
var proxyUrl = gitConfig.GetProxyUrl();
// 配置HTTP代理
commands.Add(("git", $"config --global http.proxy \"{proxyUrl}\""));
// 配置HTTPS代理
commands.Add(("git", $"config --global https.proxy \"{proxyUrl}\""));
// 配置SSL验证
if (!gitConfig.VerifySSL)
{
commands.Add(("git", "config --global http.sslVerify false"));
}
else
{
commands.Add(("git", "config --global --unset http.sslVerify"));
}
// 配置不使用代理的地址
if (!string.IsNullOrWhiteSpace(gitConfig.NoProxy))
{
// Git不直接支持no_proxy但我们可以通过环境变量来实现
_logger.LogInformation("代理排除地址将通过环境变量应用: {NoProxy}", gitConfig.NoProxy);
}
_logger.LogDebug("配置代理URL: {ProxyUrl}", proxyUrl.Contains("@") ? proxyUrl.Substring(0, proxyUrl.IndexOf("@")) + "@***" : proxyUrl);
}
else
{
// 清除代理配置
return await ClearProxyAsync();
}
// 执行配置命令
var results = new List<string>();
foreach (var (command, arguments) in commands)
{
var result = await _processService.ExecuteAsync(command, arguments);
if (!result.IsSuccess)
{
_logger.LogError("执行Git配置命令失败: {Command} {Arguments}, Error: {Error}",
command, arguments, result.StandardError);
return GitOperationResult.Failure($"配置Git代理失败: {result.StandardError}");
}
results.Add($"{command} {arguments}: 成功");
}
_logger.LogInformation("Git代理配置完成");
return GitOperationResult.Success("Git代理配置成功", string.Join("\n", results));
}
catch (Exception ex)
{
_logger.LogError(ex, "配置Git代理时发生异常");
return GitOperationResult.Failure($"配置Git代理时发生异常: {ex.Message}");
}
}
public async Task<GitOperationResult> ClearProxyAsync()
{
try
{
_logger.LogInformation("开始清除Git代理设置");
var commands = new List<(string command, string arguments)>
{
("git", "config --global --unset http.proxy"),
("git", "config --global --unset https.proxy"),
("git", "config --global --unset http.sslVerify")
};
var results = new List<string>();
foreach (var (command, arguments) in commands)
{
var result = await _processService.ExecuteAsync(command, arguments);
// 对于--unset命令如果配置不存在会返回错误这是正常的
if (result.IsSuccess || result.StandardError.Contains("not found"))
{
results.Add($"{command} {arguments}: 成功");
}
else
{
_logger.LogWarning("清除Git配置时出现警告: {Command} {Arguments}, Error: {Error}",
command, arguments, result.StandardError);
}
}
_logger.LogInformation("Git代理设置清除完成");
return GitOperationResult.Success("Git代理设置清除成功", string.Join("\n", results));
}
catch (Exception ex)
{
_logger.LogError(ex, "清除Git代理设置时发生异常");
return GitOperationResult.Failure($"清除Git代理设置时发生异常: {ex.Message}");
}
}
public async Task<GitProxyInfo> GetProxyInfoAsync()
{
var proxyInfo = new GitProxyInfo();
try
{
// 获取HTTP代理配置
var httpProxyResult = await _processService.ExecuteAsync("git", "config --global --get http.proxy");
if (httpProxyResult.IsSuccess && !string.IsNullOrWhiteSpace(httpProxyResult.StandardOutput))
{
proxyInfo.HttpProxy = httpProxyResult.StandardOutput.Trim();
}
// 获取HTTPS代理配置
var httpsProxyResult = await _processService.ExecuteAsync("git", "config --global --get https.proxy");
if (httpsProxyResult.IsSuccess && !string.IsNullOrWhiteSpace(httpsProxyResult.StandardOutput))
{
proxyInfo.HttpsProxy = httpsProxyResult.StandardOutput.Trim();
}
// 设置状态
if (proxyInfo.IsProxyEnabled)
{
proxyInfo.Status = "代理已启用";
_logger.LogDebug("当前Git代理状态: HTTP={HttpProxy}, HTTPS={HttpsProxy}",
proxyInfo.HttpProxy ?? "未设置", proxyInfo.HttpsProxy ?? "未设置");
}
else
{
proxyInfo.Status = "代理未启用";
}
}
catch (Exception ex)
{
_logger.LogError(ex, "获取Git代理信息时发生异常");
proxyInfo.Status = $"获取代理信息失败: {ex.Message}";
}
return proxyInfo;
}
/// <summary>
/// 为Git命令设置代理环境变量
/// </summary>
/// <param name="gitConfig">Git配置</param>
/// <param name="command">命令</param>
/// <param name="arguments">参数</param>
/// <param name="workingDirectory">工作目录</param>
/// <returns>执行结果</returns>
private async Task<ProcessResult> ExecuteGitCommandWithProxyAsync(GitConfiguration gitConfig, string command, string arguments, string? workingDirectory = null)
{
var environmentVariables = gitConfig.GetGitEnvironmentVariables();
// 如果有代理环境变量,则设置它们
if (environmentVariables.Count > 0)
{
_logger.LogDebug("为Git命令设置代理环境变量: {EnvVars}", string.Join(", ", environmentVariables.Select(kv => $"{kv.Key}={kv.Value}")));
return await _processService.ExecuteAsync(command, arguments, workingDirectory, environmentVariables);
}
return await _processService.ExecuteAsync(command, arguments, workingDirectory);
}
}

View File

@@ -4,6 +4,7 @@ using LubanHub.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
@@ -226,4 +227,183 @@ public class CoreProcessService : ICoreProcessService
throw;
}
}
public async Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
Dictionary<string, string>? environmentVariables, CancellationToken cancellationToken = default)
{
try
{
_logger.LogDebug("执行进程(带环境变量): {FileName} {Arguments}", fileName, arguments);
var processStartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments ?? string.Empty,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
// 设置环境变量
if (environmentVariables != null)
{
foreach (var env in environmentVariables)
{
processStartInfo.EnvironmentVariables[env.Key] = env.Value;
_logger.LogDebug("设置环境变量: {Key}={Value}", env.Key, env.Key.ToLower().Contains("password") ? "***" : env.Value);
}
}
using var process = new Process { StartInfo = processStartInfo };
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
outputBuilder.AppendLine(e.Data);
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data != null)
errorBuilder.AppendLine(e.Data);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken);
var exitCode = process.ExitCode;
var standardOutput = outputBuilder.ToString();
var standardError = errorBuilder.ToString();
var result = new ProcessResult
{
ExitCode = exitCode,
StandardOutput = standardOutput,
StandardError = standardError
};
if (result.IsSuccess)
{
_logger.LogDebug("进程执行成功: {FileName}, ExitCode: {ExitCode}", fileName, exitCode);
}
else
{
_logger.LogError("进程执行失败: {FileName}, ExitCode: {ExitCode}, Error: {Error}", fileName, exitCode, standardError);
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "执行进程时发生异常: {FileName} {Arguments}", fileName, arguments);
return new ProcessResult
{
ExitCode = -1,
StandardError = ex.Message
};
}
}
public async Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
Dictionary<string, string>? environmentVariables, Action<string>? onOutputReceived,
Action<string>? onErrorReceived, CancellationToken cancellationToken = default)
{
try
{
_logger.LogDebug("执行进程(带环境变量和回调): {FileName} {Arguments}", fileName, arguments);
var processStartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments ?? string.Empty,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
// 设置环境变量
if (environmentVariables != null)
{
foreach (var env in environmentVariables)
{
processStartInfo.EnvironmentVariables[env.Key] = env.Value;
_logger.LogDebug("设置环境变量: {Key}={Value}", env.Key, env.Key.ToLower().Contains("password") ? "***" : env.Value);
}
}
using var process = new Process { StartInfo = processStartInfo };
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
outputBuilder.AppendLine(e.Data);
onOutputReceived?.Invoke(e.Data);
}
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data != null)
{
errorBuilder.AppendLine(e.Data);
onErrorReceived?.Invoke(e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken);
var exitCode = process.ExitCode;
var standardOutput = outputBuilder.ToString();
var standardError = errorBuilder.ToString();
var result = new ProcessResult
{
ExitCode = exitCode,
StandardOutput = standardOutput,
StandardError = standardError
};
if (result.IsSuccess)
{
_logger.LogDebug("进程执行成功: {FileName}, ExitCode: {ExitCode}", fileName, exitCode);
}
else
{
_logger.LogError("进程执行失败: {FileName}, ExitCode: {ExitCode}, Error: {Error}", fileName, exitCode, standardError);
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "执行进程时发生异常: {FileName} {Arguments}", fileName, arguments);
return new ProcessResult
{
ExitCode = -1,
StandardError = ex.Message
};
}
}
}

View File

@@ -0,0 +1,135 @@
using LubanHub.Core.Attributes;
namespace LubanHub.KnowledgeService.Configurations;
/// <summary>
/// AI API配置
/// </summary>
[ConfigurationSection("知识库", "AI API设置",
Description = "配置AI服务提供商的API密钥和参数",
Icon = "🤖",
Priority = 11)]
public class AIConfiguration
{
/// <summary>
/// AI服务提供商
/// </summary>
[ConfigurationProperty("服务提供商",
Description = "选择AI服务提供商",
InputType = Core.Attributes.ConfigInputType.ComboBox,
Priority = 1,
IsRequired = true,
DefaultValue = "OpenAI")]
public string Provider { get; set; } = "OpenAI";
/// <summary>
/// OpenAI API密钥
/// </summary>
[ConfigurationProperty("OpenAI API Key",
Description = "OpenAI的API密钥",
InputType = Core.Attributes.ConfigInputType.PasswordBox,
Priority = 2,
Placeholder = "sk-xxxxxxxxxxxxxxxxxxxxxxxx")]
public string OpenAIApiKey { get; set; } = string.Empty;
/// <summary>
/// OpenAI API基础URL
/// </summary>
[ConfigurationProperty("OpenAI Base URL",
Description = "OpenAI API的基础URL可以用于代理或其他兼容服务",
InputType = Core.Attributes.ConfigInputType.UrlBox,
Priority = 3,
DefaultValue = "https://api.openai.com/v1",
Placeholder = "https://api.openai.com/v1")]
public string OpenAIBaseUrl { get; set; } = "https://api.openai.com/v1";
/// <summary>
/// OpenAI模型名称
/// </summary>
[ConfigurationProperty("OpenAI 模型",
Description = "使用的OpenAI模型名称",
Priority = 4,
DefaultValue = "gpt-3.5-turbo",
Placeholder = "gpt-3.5-turbo")]
public string OpenAIModel { get; set; } = "gpt-3.5-turbo";
/// <summary>
/// DeepSeek API密钥
/// </summary>
[ConfigurationProperty("DeepSeek API Key",
Description = "DeepSeek的API密钥",
InputType = Core.Attributes.ConfigInputType.PasswordBox,
Priority = 5,
Placeholder = "sk-xxxxxxxxxxxxxxxxxxxxxxxx")]
public string DeepSeekApiKey { get; set; } = string.Empty;
/// <summary>
/// DeepSeek API基础URL
/// </summary>
[ConfigurationProperty("DeepSeek Base URL",
Description = "DeepSeek API的基础URL",
InputType = Core.Attributes.ConfigInputType.UrlBox,
Priority = 6,
DefaultValue = "https://api.deepseek.com/v1",
Placeholder = "https://api.deepseek.com/v1")]
public string DeepSeekBaseUrl { get; set; } = "https://api.deepseek.com/v1";
/// <summary>
/// DeepSeek模型名称
/// </summary>
[ConfigurationProperty("DeepSeek 模型",
Description = "使用的DeepSeek模型名称",
Priority = 7,
DefaultValue = "deepseek-chat",
Placeholder = "deepseek-chat")]
public string DeepSeekModel { get; set; } = "deepseek-chat";
/// <summary>
/// 本地模型API地址
/// </summary>
[ConfigurationProperty("本地模型地址",
Description = "本地模型服务的API地址如Ollama等",
InputType = Core.Attributes.ConfigInputType.UrlBox,
Priority = 8,
Placeholder = "http://localhost:11434/v1")]
public string LocalModelUrl { get; set; } = string.Empty;
/// <summary>
/// 本地模型名称
/// </summary>
[ConfigurationProperty("本地模型名称",
Description = "本地模型的名称",
Priority = 9,
Placeholder = "llama2")]
public string LocalModelName { get; set; } = string.Empty;
/// <summary>
/// 温度参数
/// </summary>
[ConfigurationProperty("温度参数",
Description = "控制AI回复的创造性0.0-1.0之间,数值越高越有创造性",
InputType = Core.Attributes.ConfigInputType.NumberBox,
Priority = 10,
DefaultValue = 0.7)]
public double Temperature { get; set; } = 0.7;
/// <summary>
/// 最大令牌数
/// </summary>
[ConfigurationProperty("最大令牌数",
Description = "AI回复的最大令牌数量",
InputType = Core.Attributes.ConfigInputType.NumberBox,
Priority = 11,
DefaultValue = 2000)]
public int MaxTokens { get; set; } = 2000;
/// <summary>
/// 请求超时时间(秒)
/// </summary>
[ConfigurationProperty("请求超时",
Description = "API请求的超时时间",
InputType = Core.Attributes.ConfigInputType.NumberBox,
Priority = 12,
DefaultValue = 30)]
public int TimeoutSeconds { get; set; } = 30;
}

View File

@@ -0,0 +1,65 @@
using LubanHub.Core.Attributes;
using LubanHub.KnowledgeService.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LubanHub.KnowledgeService.Configurations;
/// <summary>
/// 知识库配置
/// </summary>
[ConfigurationSection("知识库", "知识库设置",
Description = "配置知识库来源支持本地知识库和GitHub知识库",
Icon = "📚",
Priority = 10)]
public class KnowledgeConfiguration
{
/// <summary>
/// 知识库来源列表
/// </summary>
public List<KnowledgeSource> KnowledgeSources { get; set; } = new List<KnowledgeSource>();
/// <summary>
/// 默认搜索目录(用于新建知识库来源时的默认值)
/// </summary>
[ConfigurationProperty("默认搜索目录",
Description = "创建新知识库来源时的默认搜索目录,一行一个",
InputType = Core.Attributes.ConfigInputType.TextArea,
Priority = 1,
DefaultValue = "docs\nreadme\nsrc",
Placeholder = "docs\nreadme\nsrc")]
public string DefaultSearchDirectories { get; set; } = "docs\nreadme\nsrc";
/// <summary>
/// 默认文件扩展名(用于新建知识库来源时的默认值)
/// </summary>
[ConfigurationProperty("默认文件扩展名",
Description = "创建新知识库来源时的默认文件扩展名,用逗号分隔",
Priority = 2,
DefaultValue = ".md,.txt,.rst,.json,.yml,.yaml",
Placeholder = ".md,.txt,.rst,.json,.yml,.yaml")]
public string DefaultFileExtensions { get; set; } = ".md,.txt,.rst,.json,.yml,.yaml";
/// <summary>
/// 获取默认搜索目录列表
/// </summary>
public List<string> GetDefaultSearchDirectoriesList()
{
return string.IsNullOrWhiteSpace(DefaultSearchDirectories)
? new List<string> { "docs", "readme", "src" }
: DefaultSearchDirectories.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(d => d.Trim()).ToList();
}
/// <summary>
/// 获取默认文件扩展名列表
/// </summary>
public List<string> GetDefaultFileExtensionsList()
{
return string.IsNullOrWhiteSpace(DefaultFileExtensions)
? new List<string> { ".md", ".txt", ".rst", ".json", ".yml", ".yaml" }
: DefaultFileExtensions.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(ext => ext.Trim()).ToList();
}
}

View File

@@ -0,0 +1,44 @@
using LubanHub.KnowledgeService.Models;
using LubanHub.Core.Models;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace LubanHub.KnowledgeService.Interfaces;
/// <summary>
/// 知识库管理服务接口
/// </summary>
public interface IKnowledgeSourceService
{
/// <summary>
/// 获取所有知识库来源
/// </summary>
/// <returns>知识库来源列表</returns>
Task<List<KnowledgeSource>> GetKnowledgeSourcesAsync();
/// <summary>
/// 保存知识库来源
/// </summary>
/// <param name="sources">知识库来源列表</param>
Task SaveKnowledgeSourcesAsync(List<KnowledgeSource> sources);
/// <summary>
/// 更新知识库来源
/// </summary>
/// <param name="source">要更新的知识库来源</param>
/// <returns>更新结果</returns>
Task<KnowledgeUpdateResult> UpdateKnowledgeSourceAsync(KnowledgeSource source);
/// <summary>
/// 验证知识库来源配置
/// </summary>
/// <param name="source">知识库来源</param>
/// <returns>验证结果</returns>
Task<ValidationResult> ValidateKnowledgeSourceAsync(KnowledgeSource source);
/// <summary>
/// 获取默认配置
/// </summary>
/// <returns>默认配置</returns>
Task<KnowledgeSourceDefaults> GetDefaultsAsync();
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LubanHub.Core\LubanHub.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,72 @@
using System.ComponentModel;
namespace LubanHub.KnowledgeService.Models;
/// <summary>
/// 知识库来源类型
/// </summary>
public enum KnowledgeSourceType
{
/// <summary>
/// 本地知识库
/// </summary>
[Description("本地知识库")]
Local,
/// <summary>
/// GitHub知识库
/// </summary>
[Description("GitHub知识库")]
GitHub
}
/// <summary>
/// 知识库来源配置
/// </summary>
public class KnowledgeSource
{
/// <summary>
/// 唯一标识符
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 显示名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 知识库类型
/// </summary>
public KnowledgeSourceType Type { get; set; } = KnowledgeSourceType.Local;
/// <summary>
/// 远端地址仅GitHub知识库
/// </summary>
public string RemoteUrl { get; set; } = string.Empty;
/// <summary>
/// 本地地址
/// </summary>
public string LocalPath { get; set; } = string.Empty;
/// <summary>
/// 搜索目录列表
/// </summary>
public List<string> SearchDirectories { get; set; } = new List<string> { "docs", "readme", "src" };
/// <summary>
/// 文件扩展名列表
/// </summary>
public List<string> FileExtensions { get; set; } = new List<string> { ".md", ".txt", ".rst", ".json", ".yml", ".yaml" };
/// <summary>
/// GitHub访问令牌仅GitHub知识库
/// </summary>
public string GitHubToken { get; set; } = string.Empty;
/// <summary>
/// 是否启用
/// </summary>
public bool IsEnabled { get; set; } = true;
}

View File

@@ -0,0 +1,137 @@
using System.Collections.Generic;
namespace LubanHub.KnowledgeService.Models;
/// <summary>
/// 知识库更新结果
/// </summary>
public class KnowledgeUpdateResult
{
/// <summary>
/// 操作是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 操作消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 错误信息
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// 操作类型Clone, Pull等
/// </summary>
public string OperationType { get; set; } = string.Empty;
/// <summary>
/// 更新的文件数量
/// </summary>
public int UpdatedFilesCount { get; set; }
/// <summary>
/// 详细日志
/// </summary>
public List<string> Logs { get; set; } = new List<string>();
/// <summary>
/// 创建成功结果
/// </summary>
/// <param name="message">成功消息</param>
/// <param name="operationType">操作类型</param>
/// <returns>成功结果</returns>
public static KnowledgeUpdateResult Success(string message, string operationType = "")
{
return new KnowledgeUpdateResult
{
IsSuccess = true,
Message = message,
OperationType = operationType
};
}
/// <summary>
/// 创建失败结果
/// </summary>
/// <param name="errorMessage">错误消息</param>
/// <param name="operationType">操作类型</param>
/// <returns>失败结果</returns>
public static KnowledgeUpdateResult Failure(string errorMessage, string operationType = "")
{
return new KnowledgeUpdateResult
{
IsSuccess = false,
Message = "操作失败",
ErrorMessage = errorMessage,
OperationType = operationType
};
}
}
/// <summary>
/// 验证结果
/// </summary>
public class ValidationResult
{
/// <summary>
/// 验证是否通过
/// </summary>
public bool IsValid { get; set; }
/// <summary>
/// 错误信息列表
/// </summary>
public List<string> Errors { get; set; } = new List<string>();
/// <summary>
/// 警告信息列表
/// </summary>
public List<string> Warnings { get; set; } = new List<string>();
/// <summary>
/// 创建成功验证结果
/// </summary>
/// <returns>成功结果</returns>
public static ValidationResult Success()
{
return new ValidationResult { IsValid = true };
}
/// <summary>
/// 创建失败验证结果
/// </summary>
/// <param name="errors">错误信息</param>
/// <returns>失败结果</returns>
public static ValidationResult Failure(params string[] errors)
{
return new ValidationResult
{
IsValid = false,
Errors = new List<string>(errors)
};
}
}
/// <summary>
/// 知识库来源默认配置
/// </summary>
public class KnowledgeSourceDefaults
{
/// <summary>
/// 默认搜索目录
/// </summary>
public List<string> SearchDirectories { get; set; } = new List<string> { "docs", "readme", "src" };
/// <summary>
/// 默认文件扩展名
/// </summary>
public List<string> FileExtensions { get; set; } = new List<string> { ".md", ".txt", ".rst", ".json", ".yml", ".yaml" };
/// <summary>
/// 默认下载路径
/// </summary>
public string DefaultDownloadPath { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,76 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Reflection;
namespace LubanHub.KnowledgeService;
/// <summary>
/// 知识库服务依赖注入扩展
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加知识库服务 - 通过反射自动发现并注册带有RegistService特性的服务
/// </summary>
public static IServiceCollection AddKnowledgeServices(this IServiceCollection services)
{
// 获取当前程序集
var assembly = Assembly.GetExecutingAssembly();
// 发现所有带有RegistService特性的类型
var serviceTypes = assembly.GetTypes()
.Where(type => type.IsClass && !type.IsAbstract &&
type.GetCustomAttribute<LubanHub.Core.Attributes.RegistServiceAttribute>() != null)
.Select(type => new
{
Type = type,
Attribute = type.GetCustomAttribute<LubanHub.Core.Attributes.RegistServiceAttribute>()!
})
.OrderBy(x => x.Attribute.Priority) // 按优先级排序
.ToList();
// 注册服务
foreach (var serviceInfo in serviceTypes)
{
var implementationType = serviceInfo.Type;
var attribute = serviceInfo.Attribute;
// 确定服务接口类型
Type serviceType;
if (attribute.ServiceType != null)
{
// 使用特性指定的服务类型
serviceType = attribute.ServiceType;
}
else
{
// 自动推断:查找实现的第一个接口
var interfaceType = implementationType.GetInterfaces().FirstOrDefault();
if (interfaceType == null)
{
throw new InvalidOperationException($"服务 {implementationType.Name} 没有实现任何接口且未在RegistService特性中指定ServiceType");
}
serviceType = interfaceType;
}
// 根据生命周期注册服务
switch (attribute.Lifetime)
{
case ServiceLifetime.Singleton:
services.AddSingleton(serviceType, implementationType);
break;
case ServiceLifetime.Scoped:
services.AddScoped(serviceType, implementationType);
break;
case ServiceLifetime.Transient:
services.AddTransient(serviceType, implementationType);
break;
default:
throw new ArgumentOutOfRangeException(nameof(attribute.Lifetime), attribute.Lifetime, "不支持的服务生命周期");
}
}
return services;
}
}

View File

@@ -0,0 +1,361 @@
using LubanHub.Core.Attributes;
using LubanHub.Core.Interfaces;
using LubanHub.KnowledgeService.Interfaces;
using LubanHub.KnowledgeService.Models;
using LubanHub.KnowledgeService.Configurations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace LubanHub.KnowledgeService.Services;
/// <summary>
/// 知识库管理服务实现
/// </summary>
[RegistService(ServiceLifetime.Singleton, typeof(IKnowledgeSourceService))]
public class KnowledgeSourceService : IKnowledgeSourceService
{
private readonly ICoreConfigurationDiscoveryService _configurationService;
private readonly ICoreGitService _gitService;
private readonly ICoreFileService _fileService;
private readonly ICoreDownloadService _downloadService;
private readonly ILogger<KnowledgeSourceService> _logger;
public KnowledgeSourceService(
ICoreConfigurationDiscoveryService configurationService,
ICoreGitService gitService,
ICoreFileService fileService,
ICoreDownloadService downloadService,
ILogger<KnowledgeSourceService> logger)
{
_configurationService = configurationService;
_gitService = gitService;
_fileService = fileService;
_downloadService = downloadService;
_logger = logger;
}
public async Task<List<KnowledgeSource>> GetKnowledgeSourcesAsync()
{
try
{
var config = await _configurationService.GetConfigurationAsync<KnowledgeConfiguration>();
return config?.KnowledgeSources ?? new List<KnowledgeSource>();
}
catch (Exception ex)
{
_logger.LogError(ex, "获取知识库来源时发生错误");
return new List<KnowledgeSource>();
}
}
public async Task SaveKnowledgeSourcesAsync(List<KnowledgeSource> sources)
{
try
{
var config = await _configurationService.GetConfigurationAsync<KnowledgeConfiguration>();
if (config == null)
{
config = new KnowledgeConfiguration();
}
config.KnowledgeSources = sources;
await _configurationService.SaveConfigurationAsync(config);
_logger.LogInformation("已保存 {Count} 个知识库来源", sources.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "保存知识库来源时发生错误");
throw;
}
}
public async Task<KnowledgeUpdateResult> UpdateKnowledgeSourceAsync(KnowledgeSource source)
{
try
{
_logger.LogInformation("开始更新知识库来源: {Name}", source.Name);
// 验证知识库来源配置
var validation = await ValidateKnowledgeSourceAsync(source);
if (!validation.IsValid)
{
return KnowledgeUpdateResult.Failure($"配置验证失败: {string.Join(", ", validation.Errors)}");
}
var result = new KnowledgeUpdateResult();
result.Logs.Add($"开始更新知识库: {source.Name}");
switch (source.Type)
{
case KnowledgeSourceType.GitHub:
result = await UpdateGitHubSourceAsync(source);
break;
case KnowledgeSourceType.Local:
result = await UpdateLocalSourceAsync(source);
break;
default:
result = KnowledgeUpdateResult.Failure($"不支持的知识库类型: {source.Type}");
break;
}
_logger.LogInformation("知识库 {Name} 更新完成,结果: {IsSuccess}", source.Name, result.IsSuccess);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "更新知识库来源时发生异常");
return KnowledgeUpdateResult.Failure($"更新过程中发生异常: {ex.Message}");
}
}
private async Task<KnowledgeUpdateResult> UpdateGitHubSourceAsync(KnowledgeSource source)
{
var result = new KnowledgeUpdateResult();
result.Logs.Add($"GitHub仓库: {source.RemoteUrl}");
result.Logs.Add($"本地路径: {source.LocalPath}");
try
{
// 检查本地路径是否存在且为Git仓库
bool isExistingRepo = _fileService.DirectoryExists(source.LocalPath) &&
_gitService.IsGitRepository(source.LocalPath);
if (isExistingRepo)
{
// 执行拉取操作
result.Logs.Add("本地仓库存在,执行拉取操作...");
var pullResult = await _gitService.PullAsync(source.LocalPath);
if (pullResult.IsSuccess)
{
result.IsSuccess = true;
result.Message = "仓库更新成功";
result.OperationType = "Pull";
result.Logs.Add($"拉取成功: {pullResult.Message}");
}
else
{
result.IsSuccess = false;
result.ErrorMessage = pullResult.ErrorMessage;
result.OperationType = "Pull";
result.Logs.Add($"拉取失败: {pullResult.ErrorMessage}");
}
}
else
{
// 执行克隆操作
result.Logs.Add("本地仓库不存在,执行克隆操作...");
var cloneResult = await _gitService.CloneAsync(source.RemoteUrl, source.LocalPath, source.GitHubToken);
if (cloneResult.IsSuccess)
{
result.IsSuccess = true;
result.Message = "仓库克隆成功";
result.OperationType = "Clone";
result.Logs.Add($"克隆成功: {cloneResult.Message}");
}
else
{
result.IsSuccess = false;
result.ErrorMessage = cloneResult.ErrorMessage;
result.OperationType = "Clone";
result.Logs.Add($"克隆失败: {cloneResult.ErrorMessage}");
}
}
// 如果操作成功,统计更新的文件数量
if (result.IsSuccess)
{
result.UpdatedFilesCount = await CountSourceFilesAsync(source);
result.Logs.Add($"发现 {result.UpdatedFilesCount} 个源文件");
}
return result;
}
catch (Exception ex)
{
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
result.Logs.Add($"异常: {ex.Message}");
return result;
}
}
private async Task<KnowledgeUpdateResult> UpdateLocalSourceAsync(KnowledgeSource source)
{
var result = new KnowledgeUpdateResult();
result.OperationType = "Scan";
try
{
result.Logs.Add($"扫描本地知识库: {source.LocalPath}");
if (!_fileService.DirectoryExists(source.LocalPath))
{
result.IsSuccess = false;
result.ErrorMessage = "本地目录不存在";
result.Logs.Add("错误: 本地目录不存在");
return result;
}
// 统计文件数量
result.UpdatedFilesCount = await CountSourceFilesAsync(source);
result.IsSuccess = true;
result.Message = "本地知识库扫描完成";
result.Logs.Add($"发现 {result.UpdatedFilesCount} 个源文件");
return result;
}
catch (Exception ex)
{
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
result.Logs.Add($"异常: {ex.Message}");
return result;
}
}
private async Task<int> CountSourceFilesAsync(KnowledgeSource source)
{
try
{
int fileCount = 0;
foreach (var searchDir in source.SearchDirectories)
{
var fullPath = Path.Combine(source.LocalPath, searchDir);
if (_fileService.DirectoryExists(fullPath))
{
var files = Directory.GetFiles(fullPath, "*", SearchOption.AllDirectories)
.Where(f => source.FileExtensions.Any(ext =>
Path.GetExtension(f).Equals(ext, StringComparison.OrdinalIgnoreCase)))
.ToList();
fileCount += files.Count;
}
}
return fileCount;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "统计源文件数量时发生错误");
return 0;
}
}
public async Task<ValidationResult> ValidateKnowledgeSourceAsync(KnowledgeSource source)
{
var errors = new List<string>();
var warnings = new List<string>();
try
{
// 基本验证
if (string.IsNullOrWhiteSpace(source.Name))
{
errors.Add("知识库名称不能为空");
}
if (string.IsNullOrWhiteSpace(source.LocalPath))
{
errors.Add("本地路径不能为空");
}
// 根据类型进行特定验证
switch (source.Type)
{
case KnowledgeSourceType.GitHub:
if (string.IsNullOrWhiteSpace(source.RemoteUrl))
{
errors.Add("GitHub远端地址不能为空");
}
else if (!IsValidGitHubUrl(source.RemoteUrl))
{
errors.Add("GitHub远端地址格式不正确");
}
break;
case KnowledgeSourceType.Local:
if (!string.IsNullOrWhiteSpace(source.LocalPath) && !_fileService.DirectoryExists(source.LocalPath))
{
warnings.Add("本地目录不存在,请确保路径正确");
}
break;
}
// 验证搜索目录
if (source.SearchDirectories == null || source.SearchDirectories.Count == 0)
{
warnings.Add("未设置搜索目录");
}
// 验证文件扩展名
if (source.FileExtensions == null || source.FileExtensions.Count == 0)
{
warnings.Add("未设置文件扩展名过滤");
}
var result = new ValidationResult
{
IsValid = errors.Count == 0,
Errors = errors,
Warnings = warnings
};
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "验证知识库来源时发生异常");
return ValidationResult.Failure($"验证过程中发生异常: {ex.Message}");
}
}
public async Task<KnowledgeSourceDefaults> GetDefaultsAsync()
{
try
{
var config = await _configurationService.GetConfigurationAsync<KnowledgeConfiguration>();
var downloadPath = _downloadService.GetDownloadDirectory();
return new KnowledgeSourceDefaults
{
SearchDirectories = config?.GetDefaultSearchDirectoriesList() ?? new List<string> { "docs", "readme", "src" },
FileExtensions = config?.GetDefaultFileExtensionsList() ?? new List<string> { ".md", ".txt", ".rst", ".json", ".yml", ".yaml" },
DefaultDownloadPath = downloadPath
};
}
catch (Exception ex)
{
_logger.LogError(ex, "获取默认配置时发生错误");
return new KnowledgeSourceDefaults();
}
}
private static bool IsValidGitHubUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
return false;
try
{
var uri = new Uri(url);
return uri.Host.Equals("github.com", StringComparison.OrdinalIgnoreCase) &&
uri.Segments.Length >= 3; // /user/repo/
}
catch
{
return false;
}
}
}

29
test_config.cs Normal file
View File

@@ -0,0 +1,29 @@
using System;
using System.Linq;
using System.Reflection;
using LubanHub.Core.Attributes;
// 简单的测试脚本来验证配置发现
Console.WriteLine("检查程序集加载情况...");
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && a.GetName().Name?.StartsWith("LubanHub") == true)
.ToList();
Console.WriteLine($"发现 {assemblies.Count} 个LubanHub程序集:");
foreach (var assembly in assemblies)
{
Console.WriteLine($" - {assembly.GetName().Name}");
var configTypes = assembly.GetTypes()
.Where(type => type.IsClass && !type.IsAbstract &&
type.GetCustomAttribute<ConfigurationSectionAttribute>() != null)
.ToList();
Console.WriteLine($" 配置类型数量: {configTypes.Count}");
foreach (var configType in configTypes)
{
var attr = configType.GetCustomAttribute<ConfigurationSectionAttribute>();
Console.WriteLine($" - {configType.Name}: {attr?.DisplayName}");
}
}