mirror of
https://github.com/jackqqq123/luban_ui_internal.git
synced 2025-11-15 13:48:24 +08:00
feta: 新增知识库管理:
- Core新增Git项目管理,支持代理 - Knowledge新增知识库来源配置
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<RootNamespace>LubanHub.App</RootNamespace>
|
||||
<AssemblyName>LubanHub.App</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Debug模式下启用控制台输出 -->
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" 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>
|
||||
|
||||
@@ -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,58 +219,81 @@
|
||||
|
||||
<!-- 设置页面 -->
|
||||
<Grid Name="SettingsPanel" IsVisible="False">
|
||||
<StackPanel Margin="{DynamicResource LargeMargin}">
|
||||
<TextBlock Text="⚙️ 设置"
|
||||
Classes="subheader"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
FontSize="20"
|
||||
Margin="0,0,0,20"/>
|
||||
|
||||
<StackPanel Margin="0,0,0,20">
|
||||
<TextBlock Text="下载目录"
|
||||
Classes="title"
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="{DynamicResource LargeMargin}">
|
||||
<TextBlock Text="⚙️ 设置"
|
||||
Classes="subheader"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,5"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0"
|
||||
Name="DownloadDirectoryTextBox"
|
||||
Background="{DynamicResource InputBackgroundBrush}"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
Margin="0,0,10,0"/>
|
||||
<Button Grid.Column="1"
|
||||
Content="浏览..."
|
||||
Classes="primary"
|
||||
Padding="{DynamicResource ButtonPadding}"
|
||||
Click="OnBrowseDownloadDirectoryClick"/>
|
||||
</Grid>
|
||||
<Button Content="🧪 测试其他模块下载"
|
||||
Margin="0,10,0,0"
|
||||
Padding="10,5"
|
||||
Click="OnTestModuleDownloadClick"/>
|
||||
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"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,5"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0"
|
||||
Name="DownloadDirectoryTextBox"
|
||||
Background="{DynamicResource InputBackgroundBrush}"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
Margin="0,0,10,0"/>
|
||||
<Button Grid.Column="1"
|
||||
Content="浏览..."
|
||||
Classes="primary"
|
||||
Padding="{DynamicResource ButtonPadding}"
|
||||
Click="OnBrowseDownloadDirectoryClick"/>
|
||||
</Grid>
|
||||
<Button Content="🧪 测试其他模块下载"
|
||||
Margin="0,10,0,0"
|
||||
Padding="10,5"
|
||||
Click="OnTestModuleDownloadClick"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Text="主题设置"
|
||||
Classes="title"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,5"/>
|
||||
<ComboBox Name="ThemeComboBox"
|
||||
SelectedIndex="0"
|
||||
Width="200"
|
||||
HorizontalAlignment="Left"
|
||||
SelectionChanged="OnThemeComboBoxSelectionChanged">
|
||||
<ComboBoxItem Content="深色主题"/>
|
||||
<ComboBoxItem Content="浅色主题"/>
|
||||
</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>
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Text="主题设置"
|
||||
Classes="title"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,5"/>
|
||||
<ComboBox Name="ThemeComboBox"
|
||||
SelectedIndex="0"
|
||||
Width="200"
|
||||
HorizontalAlignment="Left"
|
||||
SelectionChanged="OnThemeComboBoxSelectionChanged">
|
||||
<ComboBoxItem Content="深色主题"/>
|
||||
<ComboBoxItem Content="浅色主题"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</Grid>
|
||||
|
||||
@@ -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);
|
||||
|
||||
245
src/LubanHub.App/ViewModels/ConfigurationViewModel.cs
Normal file
245
src/LubanHub.App/ViewModels/ConfigurationViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
332
src/LubanHub.App/ViewModels/KnowledgeSourceItemViewModel.cs
Normal file
332
src/LubanHub.App/ViewModels/KnowledgeSourceItemViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" };
|
||||
}
|
||||
}
|
||||
116
src/LubanHub.App/Views/ConfigurationView.axaml
Normal file
116
src/LubanHub.App/Views/ConfigurationView.axaml
Normal 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>
|
||||
82
src/LubanHub.App/Views/ConfigurationView.axaml.cs
Normal file
82
src/LubanHub.App/Views/ConfigurationView.axaml.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
315
src/LubanHub.App/Views/KnowledgeSourceManagementView.axaml
Normal file
315
src/LubanHub.App/Views/KnowledgeSourceManagementView.axaml
Normal 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
readme
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>
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/LubanHub.Core/Attributes/ConfigurationPropertyAttribute.cs
Normal file
131
src/LubanHub.Core/Attributes/ConfigurationPropertyAttribute.cs
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
159
src/LubanHub.Core/Configurations/GitConfiguration.cs
Normal file
159
src/LubanHub.Core/Configurations/GitConfiguration.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
60
src/LubanHub.Core/Interfaces/ICoreGitService.cs
Normal file
60
src/LubanHub.Core/Interfaces/ICoreGitService.cs
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
112
src/LubanHub.Core/Models/ConfigurationInfo.cs
Normal file
112
src/LubanHub.Core/Models/ConfigurationInfo.cs
Normal 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!;
|
||||
}
|
||||
139
src/LubanHub.Core/Models/GitOperationResult.cs
Normal file
139
src/LubanHub.Core/Models/GitOperationResult.cs
Normal 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;
|
||||
}
|
||||
198
src/LubanHub.Core/Services/CoreConfigurationDiscoveryService.cs
Normal file
198
src/LubanHub.Core/Services/CoreConfigurationDiscoveryService.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
398
src/LubanHub.Core/Services/CoreGitService.cs
Normal file
398
src/LubanHub.Core/Services/CoreGitService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/LubanHub.KnowledgeService/Configurations/AIConfiguration.cs
Normal file
135
src/LubanHub.KnowledgeService/Configurations/AIConfiguration.cs
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
72
src/LubanHub.KnowledgeService/Models/KnowledgeSource.cs
Normal file
72
src/LubanHub.KnowledgeService/Models/KnowledgeSource.cs
Normal 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;
|
||||
}
|
||||
137
src/LubanHub.KnowledgeService/Models/KnowledgeUpdateResult.cs
Normal file
137
src/LubanHub.KnowledgeService/Models/KnowledgeUpdateResult.cs
Normal 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;
|
||||
}
|
||||
76
src/LubanHub.KnowledgeService/ServiceCollectionExtensions.cs
Normal file
76
src/LubanHub.KnowledgeService/ServiceCollectionExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
361
src/LubanHub.KnowledgeService/Services/KnowledgeSourceService.cs
Normal file
361
src/LubanHub.KnowledgeService/Services/KnowledgeSourceService.cs
Normal 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
29
test_config.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user