手动 develop

develop
孙建超 1 month ago
parent 4dd23d11fb
commit 17e6c0d8ae

@ -0,0 +1,238 @@
[*.cs]
# 禁用命名规则,因为与 StyleCop 冲突
dotnet_diagnostic.SA1300.severity = none
# dotnet_diagnostic.SA1301.severity = none
# dotnet_diagnostic.SA1302.severity = none
# dotnet_diagnostic.SA1303.severity = none
# dotnet_diagnostic.SA1304.severity = none
# dotnet_diagnostic.SA1305.severity = none
# dotnet_diagnostic.SA1306.severity = none
# dotnet_diagnostic.SA1307.severity = none
# dotnet_diagnostic.SA1308.severity = none
# dotnet_diagnostic.SA1309.severity = none
# dotnet_diagnostic.SA1310.severity = none
# dotnet_diagnostic.SA1311.severity = none
# dotnet_diagnostic.SA1312.severity = none
# dotnet_diagnostic.SA1313.severity = none
# dotnet_diagnostic.SA1314.severity = none
# SA1649: File name should match first type name
dotnet_diagnostic.SA1649.severity = none
# SA1200: Using directives should be placed correctly
dotnet_diagnostic.SA1200.severity = none
# SA1629: Documentation text should end with a period
dotnet_diagnostic.SA1629.severity = none
# SA1512: Single-line comments should not be followed by blank line
dotnet_diagnostic.SA1512.severity = none
# Element documentation header should be preceded by blank line
dotnet_diagnostic.SA1514.severity = none
# SA1516: Elements should be separated by blank line
dotnet_diagnostic.SA1516.severity = none
# SA1515: Single-line comment should be preceded by blank line
dotnet_diagnostic.SA1515.severity = none
# SA1513: Closing brace should be followed by blank line
dotnet_diagnostic.SA1513.severity = none
# SA1113: Comma should be on the same line as previous parameter
dotnet_diagnostic.SA1113.severity = none
# SA1001: Commas should be spaced correctly
dotnet_diagnostic.SA1001.severity = none
csharp_using_directive_placement = outside_namespace:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_conditional_delegate_call = true:suggestion
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = false:suggestion
csharp_style_var_elsewhere = false:suggestion
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = block_scoped:silent
# SA1201: Elements should appear in the correct order
# dotnet_diagnostic.SA1201.severity = none
[*.{cs,vb}]
end_of_line = crlf
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_code_quality_unused_parameters = all:suggestion
dotnet_style_readonly_field = true:suggestion
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
tab_width = 4
indent_size = 4
dotnet_style_operator_placement_when_wrapping = beginning_of_line
[*.cs]
#### 命名样式 ####
# 命名规则
dotnet_naming_rule.接口_should_be_以_i_开始.severity = warning
dotnet_naming_rule.接口_should_be_以_i_开始.symbols = 接口
dotnet_naming_rule.接口_should_be_以_i_开始.style = 以_i_开始
dotnet_naming_rule.私有方法_should_be_camel拼写法.severity = warning
dotnet_naming_rule.私有方法_should_be_camel拼写法.symbols = 私有方法
dotnet_naming_rule.私有方法_should_be_camel拼写法.style = camel拼写法
dotnet_naming_rule.方法_should_be_帕斯卡拼写法.severity = warning
dotnet_naming_rule.方法_should_be_帕斯卡拼写法.symbols = 方法
dotnet_naming_rule.方法_should_be_帕斯卡拼写法.style = 帕斯卡拼写法
# 符号规范
dotnet_naming_symbols.接口.applicable_kinds = interface
dotnet_naming_symbols.接口.applicable_accessibilities = public, internal, private, protected, protected_internal
dotnet_naming_symbols.接口.required_modifiers =
dotnet_naming_symbols.私有方法.applicable_kinds = method
dotnet_naming_symbols.私有方法.applicable_accessibilities = private
dotnet_naming_symbols.私有方法.required_modifiers =
dotnet_naming_symbols.方法.applicable_kinds = method
dotnet_naming_symbols.方法.applicable_accessibilities = public
dotnet_naming_symbols.方法.required_modifiers =
# 命名样式
dotnet_naming_style.以_i_开始.required_prefix = I
dotnet_naming_style.以_i_开始.required_suffix =
dotnet_naming_style.以_i_开始.word_separator =
dotnet_naming_style.以_i_开始.capitalization = pascal_case
dotnet_naming_style.camel拼写法.required_prefix =
dotnet_naming_style.camel拼写法.required_suffix =
dotnet_naming_style.camel拼写法.word_separator =
dotnet_naming_style.camel拼写法.capitalization = camel_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_prefer_primary_constructors = true:suggestion
csharp_prefer_static_anonymous_function = true:suggestion
csharp_prefer_static_local_function = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_prefer_switch_expression = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
csharp_space_around_binary_operators = before_and_after
csharp_indent_labels = one_less_than_current
# IDE1006: 命名样式
dotnet_diagnostic.IDE1006.severity = none
[*.vb]
#### 命名样式 ####
# 命名规则
dotnet_naming_rule.interface_should_be_以_i_开始.severity = suggestion
dotnet_naming_rule.interface_should_be_以_i_开始.symbols = interface
dotnet_naming_rule.interface_should_be_以_i_开始.style = 以_i_开始
dotnet_naming_rule.类型_should_be_帕斯卡拼写法.severity = suggestion
dotnet_naming_rule.类型_should_be_帕斯卡拼写法.symbols = 类型
dotnet_naming_rule.类型_should_be_帕斯卡拼写法.style = 帕斯卡拼写法
dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.severity = suggestion
dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.symbols = 非字段成员
dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯卡拼写法
# 符号规范
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.类型.required_modifiers =
dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method
dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.非字段成员.required_modifiers =
# 命名样式
dotnet_naming_style.以_i_开始.required_prefix = I
dotnet_naming_style.以_i_开始.required_suffix =
dotnet_naming_style.以_i_开始.word_separator =
dotnet_naming_style.以_i_开始.capitalization = pascal_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case

2
Drawer/.gitignore vendored

@ -1,3 +1,4 @@
bin
packages packages
# 忽略中间文件 # 忽略中间文件
@ -26,3 +27,4 @@ MLMicroStructure.pdb
MicroStructurePP.dll MicroStructurePP.dll
MicroStructurePP.pdb MicroStructurePP.pdb
*.FileListAbsolute.txt *.FileListAbsolute.txt
obj/*

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.9" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
<PackageReference Include="Avalonia.Native" Version="11.3.9" />
<PackageReference Include="Avalonia.Skia" Version="11.3.9" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" />
<PackageReference Include="Avalonia.Win32" Version="11.3.9" />
<PackageReference Include="Avalonia.Win32.Interoperability" Version="11.3.9" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="LiveMarkdown.Avalonia" Version="1.6.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.68.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\*.svg" />
</ItemGroup>
<ItemGroup>
<!-- ai-settings.json 不提交到 Git需在部署目录手动创建 -->
<None Include="ai-settings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AI", "AI.csproj", "{F091FF6C-454D-F725-5E93-41983547B110}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F091FF6C-454D-F725-5E93-41983547B110}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F091FF6C-454D-F725-5E93-41983547B110}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F091FF6C-454D-F725-5E93-41983547B110}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F091FF6C-454D-F725-5E93-41983547B110}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDA3407-3A62-4B40-88D5-9D08353B1E4E}
EndGlobalSection
EndGlobal

@ -0,0 +1,70 @@
using System;
using System.IO;
using System.Text.Json;
namespace AI
{
/// <summary>
/// AI 服务配置
/// </summary>
public class AISettings
{
/// <summary>
/// 模型 ID
/// </summary>
public string ModelId { get; set; } = string.Empty;
/// <summary>
/// API Key
/// </summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// API EndpointOpenAI 兼容接口地址)
/// </summary>
public string Endpoint { get; set; } = string.Empty;
/// <summary>
/// 从 DLL 所在目录旁的 ai-settings.json 加载配置。
/// 若文件不存在,抛出 FileNotFoundException。
/// </summary>
public static AISettings Load()
{
string baseDir = AppContext.BaseDirectory;
string configPath = Path.Combine(baseDir, "ai-settings.json");
if (!File.Exists(configPath))
{
throw new FileNotFoundException(
$"AI 配置文件未找到,请在以下位置创建 ai-settings.json{configPath}\n" +
"文件格式示例:\n" +
"{\n" +
" \"ModelId\": \"deepseek-v3\",\n" +
" \"ApiKey\": \"sk-xxx\",\n" +
" \"Endpoint\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n" +
"}",
configPath);
}
string json = File.ReadAllText(configPath);
var settings = JsonSerializer.Deserialize<AISettings>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (settings == null)
{
throw new InvalidOperationException("ai-settings.json 解析失败,请检查 JSON 格式。");
}
if (string.IsNullOrWhiteSpace(settings.ModelId))
throw new InvalidOperationException("ai-settings.json 中 ModelId 不能为空。");
if (string.IsNullOrWhiteSpace(settings.ApiKey))
throw new InvalidOperationException("ai-settings.json 中 ApiKey 不能为空。");
if (string.IsNullOrWhiteSpace(settings.Endpoint))
throw new InvalidOperationException("ai-settings.json 中 Endpoint 不能为空。");
return settings;
}
}
}

@ -0,0 +1,16 @@
using System;
namespace AI.AgentIntegration
{
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class ActionHandlerAttribute : Attribute
{
public AppActionType ActionType { get; }
public ActionHandlerAttribute(AppActionType actionType)
{
ActionType = actionType;
}
}
}

@ -0,0 +1,26 @@
using System;
namespace AI.AgentIntegration
{
/// <summary>
/// 定义代理的操作模式
/// </summary>
public enum AgentMode
{
/// <summary>
/// 观察模式:仅观察用户操作,不主动干预
/// </summary>
Observe,
/// <summary>
/// 协助模式:在用户请求时提供帮助
/// </summary>
Assist,
/// <summary>
/// 控制模式:主动控制系统行为
/// </summary>
Control,
}
}

@ -0,0 +1,102 @@
using System.Collections.Generic;
namespace AI.AgentIntegration
{
/// <summary>
/// 表示一个应用程序操作
/// </summary>
public class AppAction
{
/// <summary>
/// 获取或设置操作类型
/// </summary>
public AppActionType Action { get; set; }
/// <summary>
/// 获取或设置操作参数字典
/// </summary>
public Dictionary<string, object> Parameters { get; set; } = new Dictionary<string, object>();
/// <summary>
/// 创建 AppAction使用它可以让调用代码变得简短
/// </summary>
/// <param name="type">类型</param>
/// <param name="parameters">参数</param>
/// <returns>AppAction 对象</returns>
public static AppAction CreateAction(AppActionType type, Dictionary<string, object>? parameters = null)
{
return new AppAction()
{
Action = type,
Parameters = parameters ?? new Dictionary<string, object>(),
};
}
/// <summary>
/// 创建一个支持单个参数的 Action
/// </summary>
/// <param name="type">类型</param>
/// <param name="name">参数名</param>
/// <param name="value">参数值</param>
/// <returns>AppAction 对象</returns>
public static AppAction CreateAction(AppActionType type, string name, object value)
{
return new AppAction()
{
Action = type,
Parameters = new Dictionary<string, object>() { [name] = value },
};
}
/// <summary>
/// 创建加载散点文件的 action
/// </summary>
/// <param name="path">文件路径</param>
/// <returns>AppAction 对象</returns>
public static AppAction CreateLoadXyz(string path)
{
return CreateAction(AppActionType.GriddingModuleLoadXyz, nameof(path), path);
}
/// <summary>
/// 创建从文件导入井点数据的 action
/// </summary>
/// <param name="path">井点数据文件路径</param>
/// <returns>AppAction 对象</returns>
public static AppAction CreateImportWellPoints(string path)
{
return CreateAction(AppActionType.WellModuleImportWellPoints, nameof(path), path);
}
/// <summary>
/// 创建从文件导入井曲线数据的 action
/// </summary>
/// <param name="path">井曲线数据文件路径</param>
/// <returns>AppAction 对象</returns>
public static AppAction CreateImportWellCurves(string path)
{
return CreateAction(AppActionType.WellModuleImportCurves, nameof(path), path);
}
/// <summary>
/// 创建打开文件的 action
/// </summary>
/// <param name="path">文件路径</param>
/// <returns>AppAction 对象</returns>
public static AppAction CreateOpenFile(string path)
{
return CreateAction(AppActionType.OpenFile, nameof(path), path);
}
/// <summary>
/// 创建参数设置
/// </summary>
/// <param name="parameters">参数</param>
/// <returns>AppAction 对象</returns>
public static AppAction CreateSetParameters(string parameters)
{
return CreateAction(AppActionType.GriddingModuleSetParameters, nameof(parameters), parameters);
}
}
}

@ -0,0 +1,43 @@
namespace AI.AgentIntegration
{
/// <summary>
/// 表示应用程序操作的结果
/// </summary>
public class AppActionResult
{
/// <summary>
/// 获取或设置操作是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 获取或设置操作结果消息
/// </summary>
public string? Message { get; set; }
/// <summary>
/// 获取或设置操作返回的数据
/// </summary>
public object? Data { get; set; }
public static AppActionResult Sucess(string message, object? data = null)
{
return new AppActionResult()
{
Success = true,
Message = message,
Data = data,
};
}
public static AppActionResult Fail(string message)
{
return new AppActionResult()
{
Success = false,
Message = message,
};
}
}
}

@ -0,0 +1,164 @@
namespace AI.AgentIntegration
{
/// <summary>
/// 定义应用程序可执行的操作类型
/// </summary>
public enum AppActionType
{
/// <summary>
/// 获取打开的标签页列表
/// </summary>
GetOpenTabs,
/// <summary>
/// 打开文件
/// </summary>
OpenFile,
/// <summary>
/// 关闭当前文件
/// </summary>
CloseFile,
/// <summary>
/// 关闭所有文件
/// </summary>
CloseAllFiles,
/// <summary>
/// 保存当前文件
/// </summary>
SaveFile,
/// <summary>
/// 保存所有文件
/// </summary>
SaveAll,
/// <summary>
/// 重命名文件
/// </summary>
RenameFile,
/// <summary>
/// 重新加载文件
/// </summary>
ReloadFile,
/// <summary>
/// 切换标签页
/// </summary>
SwitchTab,
/// <summary>
/// 导航到指定视图
/// </summary>
Navigate,
/// <summary>
/// 后退导航
/// </summary>
NavigateBack,
/// <summary>
/// 前进导航
/// </summary>
NavigateForward,
/// <summary>
/// 设置系统忙状态
/// </summary>
SetBusy,
/// <summary>
/// 刷新界面
/// </summary>
Refresh,
/// <summary>
/// 退出应用程序
/// </summary>
Exit,
/// <summary>
/// 撤销操作
/// </summary>
Undo,
/// <summary>
/// 重做操作
/// </summary>
Redo,
/// <summary>
/// 添加比例尺
/// </summary>
AddScaleBar,
/// <summary>
/// 添加边框
/// </summary>
AddBorder,
/// <summary>
/// 添加图例
/// </summary>
AddLegend,
/// <summary>
/// 描述当前状态
/// </summary>
DescribeState,
/// <summary>
/// 加载 xyz 文件
/// </summary>
GriddingModuleLoadXyz,
/// <summary>
/// 获取列信息
/// </summary>
GriddingModuleGetColumns,
/// <summary>
/// 列头匹配
/// </summary>
GriddingModuleMatchColumns,
/// <summary>
/// 获取网络化参数
/// </summary>
GriddingModuleGetParameters,
/// <summary>
/// 设置网络化参数
/// </summary>
GriddingModuleSetParameters,
/// <summary>
/// 导入数据
/// </summary>
GriddingModuleImport,
/// <summary>
/// 数据预览
/// </summary>
GriddingModulePreviewData,
/// <summary>
/// 成图
/// </summary>
GriddingModuleRun,
/// <summary>
/// 从文件导入井点数据
/// </summary>
WellModuleImportWellPoints,
/// <summary>
/// 从文件导入井曲线数据
/// </summary>
WellModuleImportCurves,
}
}

@ -0,0 +1,14 @@
namespace AI.AgentIntegration
{
/// <summary>
/// 供宿主(如 Drawer在构建 DI 前注入真实 IAppController 的占位。
/// 在 Avalonia 应用启动前设置 <see cref="Instance"/>,则 <see cref="ServiceCollectionExtensions.AddCommonServices"/> 会注册该实例。
/// </summary>
public static class AppControllerHolder
{
/// <summary>
/// 宿主设置的应用程序控制器,若未设置则使用 <see cref="NoOpAppController"/>。
/// </summary>
public static IAppController? Instance { get; set; }
}
}

@ -0,0 +1,33 @@
namespace AI.AgentIntegration
{
public static class AppPrompt
{
/// <summary>
/// 成图助手的交互式引导提示词(用于新会话的 System Message
/// </summary>
public static string InteractiveGuidePrompt =>
@"你是一名智能的成图助手,负责引导用户完成以下两步成图流程。
-
ShowForm(""gridding-load-xyz"")
ShowForm(""gridding-parameters"")
-
-
-
-
- 使
- ";
}
}

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace AI.AgentIntegration
{
/// <summary>
/// 表示应用程序的整体状态
/// </summary>
public class AppState
{
/// <summary>
/// 获取或设置用户界面状态
/// </summary>
public UIState UI { get; set; } = new UIState();
/// <summary>
/// 获取或设置文件状态
/// </summary>
public FileState File { get; set; } = new FileState();
/// <summary>
/// 获取或设置导航状态
/// </summary>
public NavigationState Navigation { get; set; } = new NavigationState();
/// <summary>
/// 获取或设置系统状态
/// </summary>
public SystemState System { get; set; } = new SystemState();
/// <summary>
/// 获取或设置代理状态
/// </summary>
public AgentState Agent { get; set; } = new AgentState();
/// <summary>
/// 将当前状态序列化为 JSON 字符串
/// </summary>
/// <param name="indented">是否使用缩进格式</param>
/// <returns>表示当前状态的 JSON 字符串</returns>
public string ToJson(bool indented = true)
{
return JsonConvert.SerializeObject(this, indented ? Formatting.Indented : Formatting.None);
}
}
/// <summary>
/// 表示用户界面状态
/// </summary>
public class UIState
{
/// <summary>
/// 获取或设置活动标签页的标识符
/// </summary>
public string ActiveTab { get; set; } = string.Empty;
/// <summary>
/// 获取或设置打开的标签页列表
/// </summary>
public List<string> OpenTabs { get; set; } = new List<string>();
/// <summary>
/// 获取或设置当前获得焦点的控件
/// </summary>
public string FocusedControl { get; set; } = string.Empty;
}
/// <summary>
/// 表示文件状态
/// </summary>
public class FileState
{
/// <summary>
/// 获取或设置活动文件的路径
/// </summary>
public string ActiveFilePath { get; set; } = string.Empty;
/// <summary>
/// 获取或设置是否有未保存的更改
/// </summary>
public bool HasUnsavedChanges { get; set; }
/// <summary>
/// 获取或设置文件类型
/// </summary>
public string FileType { get; set; } = string.Empty;
}
/// <summary>
/// 表示导航状态
/// </summary>
public class NavigationState
{
/// <summary>
/// 获取或设置当前视图名称
/// </summary>
public string CurrentView { get; set; } = "Home";
/// <summary>
/// 获取或设置导航堆栈
/// </summary>
public List<string> NavigationStack { get; set; } = new List<string>();
}
/// <summary>
/// 表示系统状态
/// </summary>
public class SystemState
{
/// <summary>
/// 获取或设置系统是否处于忙碌状态
/// </summary>
public bool IsBusy { get; set; }
/// <summary>
/// 获取或设置是否有模态对话框打开
/// </summary>
public bool ModalOpen { get; set; }
/// <summary>
/// 获取或设置最后一次操作的时间UTC
/// </summary>
public DateTime LastActionTimeUtc { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// 表示代理状态
/// </summary>
public class AgentState
{
/// <summary>
/// 获取或设置代理的操作模式
/// </summary>
public AgentMode Mode { get; set; } = AgentMode.Observe;
/// <summary>
/// 获取或设置最后执行的命令
/// </summary>
public string LastCommand { get; set; } = string.Empty;
/// <summary>
/// 获取或设置最后命令的执行结果
/// </summary>
public string LastResult { get; set; } = string.Empty;
}
}

@ -0,0 +1,29 @@
using System;
using System.Threading.Tasks;
namespace AI.AgentIntegration
{
/// <summary>
/// 定义应用程序控制器的接口
/// </summary>
public interface IAppController
{
/// <summary>
/// 当应用程序状态发生变化时触发的事件
/// </summary>
event EventHandler<AppState> StateChanged;
/// <summary>
/// 获取应用程序的当前状态
/// </summary>
/// <returns>当前应用程序状态</returns>
AppState GetCurrentState();
/// <summary>
/// 执行指定的应用程序操作
/// </summary>
/// <param name="action">要执行的操作</param>
/// <returns>操作执行结果</returns>
Task<AppActionResult> ExecuteAsync(AppAction action);
}
}

@ -0,0 +1,20 @@
using System;
using System.Threading.Tasks;
namespace AI.AgentIntegration
{
/// <summary>
/// 无操作 AppController在未注入真实控制器时使用如独立运行 AI 模块时)。
/// </summary>
public sealed class NoOpAppController : IAppController
{
public event EventHandler<AppState>? StateChanged;
public AppState GetCurrentState() => new AppState();
public Task<AppActionResult> ExecuteAsync(AppAction action)
{
return Task.FromResult(AppActionResult.Fail("未连接应用控制器"));
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,30 @@
using Avalonia;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
namespace AI
{
public partial class App : Application
{
public IServiceProvider? ServiceProvider { get; private set; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// Line below is needed to remove Avalonia data validation.
// Without this line you will get duplicate validations from both Avalonia and CT
BindingPlugins.DataValidators.RemoveAt(0);
var collection = new ServiceCollection();
collection.AddCommonServices();
var services = collection.BuildServiceProvider();
ServiceProvider = services;
base.OnFrameworkInitializationCompleted();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766626731970" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5056" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M886.7 247.6L713.4 73.4c-6-6-14.2-9.4-22.7-9.4H192c-35.3 0-64 28.7-64 64v768c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64V270.2c0-8.5-3.3-16.6-9.3-22.6zM832 864c0 17.7-14.3 32-32 32H224c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h384v160c0 35.3 28.7 64 64 64h160v512zM704 288c-17.7 0-32-14.3-32-32V128l160 160H704z" p-id="5057"></path><path d="M671 672H287c-17.7 0-32 14.3-32 32s14.3 32 32 32h384c17.7 0 32-14.3 32-32s-14.3-32-32-32zM287 480c-17.7 0-32 14.3-32 32s14.3 32 32 32h384c17.7 0 32-14.3 32-32s-14.3-32-32-32H287zM287 352h192c17.7 0 32-14.3 32-32s-14.3-32-32-32H287c-17.7 0-32 14.3-32 32s14.3 32 32 32z" p-id="5058"></path></svg>

After

Width:  |  Height:  |  Size: 973 B

@ -0,0 +1,30 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
using AI.Models;
using System.Globalization;
namespace AI.Converters
{
public class AuthorToColorConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is AuthorType authorType)
{
return authorType switch
{
AuthorType.User => new SolidColorBrush(Color.Parse("#EAEEF6")),
AuthorType.AI => new SolidColorBrush(Color.Parse("transparent")),
AuthorType.Tool => new SolidColorBrush(Color.Parse("#FFF9E6")), // 淡黄色背景,用于工具调用记录
_ => Brushes.Transparent
};
}
return Brushes.Transparent;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,28 @@
using Avalonia.Data.Converters;
using System;
using System.Globalization;
using System.IO;
namespace AI.Converters
{
public class FileExtensionConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string fileName)
{
string extension = Path.GetExtension(fileName);
if (!string.IsNullOrEmpty(extension))
{
return extension;
}
}
return "";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,24 @@
using AI.Utils;
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace AI.Converters
{
public class FileSizeConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is long fileSize)
{
return FileUnit.FormatFileSize(fileSize);
}
return "0 B";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace AI.Converters
{
/// <summary>
/// 表单布尔字段与字符串双向转换CheckBox 与 CurrentValue 绑定)
/// </summary>
public class FormBoolConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string s)
{
var v = s?.Trim() ?? string.Empty;
return v is "1" or "true" or "True" or "yes" or "是";
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool b)
{
return b ? "True" : "False";
}
return "False";
}
}
}

@ -0,0 +1,34 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml.Templates;
using AI.Models.Form;
namespace AI.Converters
{
/// <summary>
/// 按表单字段类型选择模板,避免 Boolean 的 CheckBox 与 Choice 的 ComboBox 同屏时互相覆盖 CurrentValue如列名 "0" 被写成 "False")。
/// </summary>
public class FormFieldTemplateSelector : IDataTemplate
{
public DataTemplate? BooleanTemplate { get; set; }
public DataTemplate? ChoiceTemplate { get; set; }
public DataTemplate? DefaultTemplate { get; set; }
public Control? Build(object? param)
{
if (param is not FormFieldEntry entry)
{
return null;
}
var template = entry.Type switch
{
FormFieldType.Boolean => BooleanTemplate,
FormFieldType.Choice => ChoiceTemplate,
_ => DefaultTemplate,
};
return template?.Build(param);
}
public bool Match(object? data) => data is FormFieldEntry;
}
}

@ -0,0 +1,27 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using AI.Models.Form;
namespace AI.Converters
{
/// <summary>
/// 表单字段类型与 ConverterParameter 相同时可见(用于按类型显示不同控件)
/// </summary>
public class FormFieldTypeVisibilityConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is FormFieldType ft && parameter is string key)
{
if (string.Equals(key, "NotChoice", StringComparison.OrdinalIgnoreCase))
return ft != FormFieldType.Choice;
return ft.ToString() == key;
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

@ -0,0 +1,33 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace AI.Converters
{
/// <summary>
/// 将 double? 转为 double用于 NumericUpDown 的 Minimum/Maximumnull 时用默认边界)
/// ConverterParameter: "min" -> null 转为 double.MinValue, "max" -> null 转为 double.MaxValue
/// </summary>
public class FormNullableDoubleConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is double d)
{
return d;
}
if (value is decimal dec)
{
return (double)dec;
}
var key = parameter?.ToString()?.ToLowerInvariant();
return key == "max" ? double.MaxValue
: key == "step" ? 1d
: double.MinValue;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

@ -0,0 +1,33 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace AI.Converters
{
/// <summary>
/// 表单数字字段与字符串双向转换NumericUpDown Value 与 CurrentValue 绑定)
/// </summary>
public class FormNumberConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string s && double.TryParse(s?.Trim(), NumberStyles.Any, culture, out var n))
return n;
return 0d;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is double d)
{
return d.ToString(culture);
}
if (value is decimal dec)
{
return dec.ToString(culture);
}
return "0";
}
}
}

@ -0,0 +1,46 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml.Templates;
using AI.Models;
namespace AI.Converters
{
public class MessageTemplateSelector : IDataTemplate
{
public required DataTemplate TextMessageTemplate { get; set; }
public required DataTemplate FileMessageTemplate { get; set; }
public required DataTemplate WorkflowStatusTemplate { get; set; }
public required DataTemplate FormMessageTemplate { get; set; }
public required DataTemplate TableMessageTemplate { get; set; }
public required DataTemplate ColumnMatchMessageTemplate { get; set; }
public required DataTemplate ParameterSetMessageTemplate { get; set; }
public required DataTemplate XyzLoadCardTemplate { get; set; }
public required DataTemplate GriddingParamCardTemplate { get; set; }
public Control? Build(object? param)
{
if (param is ChatMessageModel message)
{
return message.Type switch
{
MessageType.File => FileMessageTemplate.Build(param),
MessageType.WorkflowStatus => WorkflowStatusTemplate.Build(param),
MessageType.Form => FormMessageTemplate.Build(param),
MessageType.Table => TableMessageTemplate.Build(param),
MessageType.ColumnMatch => ColumnMatchMessageTemplate.Build(param),
MessageType.ParameterSet => ParameterSetMessageTemplate.Build(param),
MessageType.KnowledgeBase => TextMessageTemplate?.Build(param),
MessageType.XyzLoadCard => XyzLoadCardTemplate.Build(param),
MessageType.GriddingParamCard => GriddingParamCardTemplate.Build(param),
_ => TextMessageTemplate?.Build(param),
};
}
return TextMessageTemplate?.Build(param);
}
public bool Match(object? data)
{
return data is ChatMessageModel;
}
}
}

@ -0,0 +1,26 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using AI.Models.SpecialMessages;
namespace AI.Converters
{
/// <summary>
/// 参数集字段类型与 ConverterParameter 相同时可见(用于按类型显示不同控件)
/// </summary>
public class ParameterSetFieldTypeVisibilityConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is ParameterSetFieldType ft && parameter is string key)
{
return ft.ToString() == key;
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

@ -0,0 +1,28 @@
using Avalonia.Data.Converters;
using AI.Models;
using System;
using System.Globalization;
namespace AI.Converters
{
/// <summary>
/// 判断工作流步骤状态是否为已完成(用于显示勾)
/// </summary>
public class StatusToCompletedConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is WorkflowStepStatus status)
{
return status == WorkflowStepStatus.Completed;
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,27 @@
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace AI.Converters
{
/// <summary>
/// 将字符串转换为布尔值(非空字符串为 true空或 null 为 false
/// </summary>
public class StringToBoolConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string str)
{
return !string.IsNullOrWhiteSpace(str);
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,35 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
using AI.Models;
using System;
using System.Globalization;
namespace AI.Converters
{
/// <summary>
/// 将工作模式转换为按钮背景颜色
/// </summary>
public class WorkflowModeToColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is WorkflowMode currentMode && parameter is string targetMode)
{
var isActive = (currentMode == WorkflowMode.Ask && targetMode == "Ask") ||
(currentMode == WorkflowMode.Workflow && targetMode == "Workflow");
return isActive
? new SolidColorBrush(Color.Parse("#007AFF"))
: new SolidColorBrush(Colors.Transparent);
}
return new SolidColorBrush(Colors.Transparent);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,35 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
using AI.Models;
using System;
using System.Globalization;
namespace AI.Converters
{
/// <summary>
/// 将工作模式转换为按钮文字颜色
/// </summary>
public class WorkflowModeToTextColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is WorkflowMode currentMode && parameter is string targetMode)
{
var isActive = (currentMode == WorkflowMode.Ask && targetMode == "Ask") ||
(currentMode == WorkflowMode.Workflow && targetMode == "Workflow");
return isActive
? new SolidColorBrush(Colors.White)
: new SolidColorBrush(Color.Parse("#666666"));
}
return new SolidColorBrush(Color.Parse("#666666"));
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,38 @@
using Avalonia.Data.Converters;
using Avalonia.Media;
using AI.Models;
using System;
using System.Globalization;
namespace AI.Converters
{
/// <summary>
/// 将工作流步骤状态转换为颜色
/// </summary>
public class WorkflowStatusToColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is WorkflowStepStatus status)
{
return status switch
{
WorkflowStepStatus.Completed => new SolidColorBrush(Color.Parse("#4CAF50")),
WorkflowStepStatus.Running => new SolidColorBrush(Color.Parse("#2196F3")),
WorkflowStepStatus.Failed => new SolidColorBrush(Color.Parse("#F44336")),
WorkflowStepStatus.Skipped => new SolidColorBrush(Color.Parse("#9E9E9E")),
WorkflowStepStatus.Pending => new SolidColorBrush(Color.Parse("#757575")),
_ => new SolidColorBrush(Color.Parse("#757575"))
};
}
return new SolidColorBrush(Color.Parse("#757575"));
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,39 @@
using Avalonia.Data.Converters;
using AI.Models;
using System;
using System.Globalization;
namespace AI.Converters
{
/// <summary>
/// 将工作流步骤状态转换为图标
/// </summary>
public class WorkflowStatusToIconConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
// 注意这个转换器现在主要用于其他场景UI 中的图标已经改为使用 Grid 叠加显示
// 保持兼容性,返回基本图标
if (value is WorkflowStepStatus status)
{
return status switch
{
WorkflowStepStatus.Completed => "○", // UI 中会叠加显示勾
WorkflowStepStatus.Running => "○", // 保持圆圈,只变颜色
WorkflowStepStatus.Failed => "✗",
WorkflowStepStatus.Skipped => "⊘",
WorkflowStepStatus.Pending => "○",
_ => "○"
};
}
return "○";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,94 @@
using System.Text.RegularExpressions;
using Microsoft.SemanticKernel;
using AI.Models;
using AI.Models.SpecialMessages;
using AI.Service;
using AI.Utils;
namespace AI.Filter
{
/// <summary>
/// 在知识库查询执行完成后,将结果写入当前会话的 Store保证插件只负责查询、不依赖会话上下文。
/// </summary>
public sealed class KnowledgeBaseStoreFilter : IFunctionInvocationFilter
{
private static readonly Regex KbIdVersionRegex = new(
@"^\[KB-ID:\s*([^;]+);\s*Version:\s*([^\]]+)\]",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
{
await next(context);
var pluginName = context.Function.PluginName ?? string.Empty;
var functionName = context.Function.Name ?? string.Empty;
if (!IsKnowledgeBasePlugin(pluginName, functionName))
{
return;
}
var result = context.Result?.ToString();
if (string.IsNullOrEmpty(result))
{
return;
}
var session = CurrentSessionContext.Current;
if (session == null)
{
return;
}
var query = GetQueryFromArguments(context.Arguments);
var (id, version) = ParseKbIdAndVersion(result);
var kbMsg = new KnowledgeBaseMessage
{
Id = Guid.NewGuid().ToString(),
KnowledgeId = id,
Version = version,
TopicKey = string.Empty,
Query = query ?? string.Empty,
RawContent = result
};
var yaml = SpecialMessageSerializer.Serialize(kbMsg);
session.AppendSpecialEntry("KnowledgeBase", yaml);
}
private static bool IsKnowledgeBasePlugin(string pluginName, string functionName)
{
return string.Equals(pluginName, "KnowledgeBase", StringComparison.OrdinalIgnoreCase)
|| string.Equals(functionName, "QueryKnowledgeBaseAsync", StringComparison.OrdinalIgnoreCase);
}
private static string? GetQueryFromArguments(KernelArguments? arguments)
{
if (arguments == null)
{
return null;
}
if (arguments.TryGetValue("query", out var value))
{
return value?.ToString();
}
return null;
}
private static (string Id, string Version) ParseKbIdAndVersion(string content)
{
if (string.IsNullOrWhiteSpace(content))
return ("unknown", "1");
var firstLine = content.IndexOf('\n') >= 0
? content.AsSpan(0, content.IndexOf('\n')).ToString()
: content;
var match = KbIdVersionRegex.Match(firstLine);
if (!match.Success)
{
return ("unknown", "1");
}
return (match.Groups[1].Value.Trim(), match.Groups[2].Value.Trim());
}
}
}

@ -0,0 +1,31 @@
using System.Diagnostics;
using Microsoft.SemanticKernel;
namespace AI.Filter
{
/// <summary>
/// 函数调用过滤器,用于记录日志
/// </summary>
public sealed class LogFunctionFilter : IFunctionInvocationFilter
{
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
{
var functionName = context.Function.Name;
var pluginName = context.Function.PluginName ?? "Unknown";
Debug.WriteLine($"[FunctionCall] Invoking {pluginName}.{functionName}");
await next(context);
var result = context.Result?.ToString();
if (!string.IsNullOrEmpty(result) && result.Length < 200)
{
Debug.WriteLine($"[FunctionCall] {pluginName}.{functionName} completed: {result}");
}
else
{
Debug.WriteLine($"[FunctionCall] {pluginName}.{functionName} completed");
}
}
}
}

@ -0,0 +1,19 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
namespace AI.Filter
{
public sealed class LogPromptFilter : IPromptRenderFilter
{
public async Task OnPromptRenderAsync(PromptRenderContext context, Func<PromptRenderContext, Task> next)
{
Debug.WriteLine($"Rendering prompt for {context.Function.Name}");
await next(context);
Debug.WriteLine($"Rendered prompt: {context.RenderedPrompt}");
}
}
}

@ -0,0 +1,33 @@
using AI.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace AI.Interface
{
public interface IChatBackend
{
/// <summary>
/// 向模型发送自然语言请求。发给 LLM 的历史由 session.GetHistoryForLlm(guidePrompt) 从会话存储生成。
/// </summary>
/// <param name="userMessage">用户消息</param>
/// <param name="session">聊天会话。如果为 null将创建新的会话。</param>
/// <param name="guidePrompt">可选 System 引导词;若提供则在生成的 History 开头插入</param>
/// <param name="cancellationToken">取消令牌,用于取消请求或配合超时</param>
/// <returns>模型的响应文本</returns>
Task<string> AskAsync(string userMessage, ChatSession? session = null, string? guidePrompt = null, CancellationToken cancellationToken = default);
/// <summary>
/// 流式向模型发送自然语言请求。发给 LLM 的历史由 session.GetHistoryForLlm(guidePrompt) 从会话存储生成。
/// </summary>
/// <param name="userMessage">用户消息</param>
/// <param name="session">聊天会话。如果为 null将创建新的会话。</param>
/// <param name="guidePrompt">可选 System 引导词</param>
/// <param name="cancellationToken">取消令牌,用于取消请求或配合超时</param>
/// <returns>流式返回模型的响应文本片段</returns>
IAsyncEnumerable<string> AskStreamAsync(string userMessage, ChatSession? session = null, string? guidePrompt = null, CancellationToken cancellationToken = default);
}
}

@ -0,0 +1,20 @@
using AI.Models.Form;
namespace AI.Interface
{
/// <summary>
/// 表单注册表:根据 formId 获取表单定义。
/// </summary>
public interface IFormRegistry
{
/// <summary>
/// 根据表单 ID 获取表单定义,未注册则返回 null。
/// </summary>
FormDefinition? GetForm(string formId);
/// <summary>
/// 获取所有已注册的表单定义(用于生成 Schema YAML、供 LLM 上下文等)。
/// </summary>
IEnumerable<FormDefinition> GetAll();
}
}

@ -0,0 +1,14 @@
namespace AI.Interface
{
/// <summary>
/// 表单请求通知:当 AI 调用 ShowForm 时,由插件触发,通知 UI 插入对应表单消息。
/// </summary>
public interface IFormRequestNotifier
{
/// <summary>
/// 请求在触发该请求的会话中显示指定 ID 的表单(绑定到当前 LLM 请求所在会话,避免串会话)。
/// </summary>
/// <param name="formId">表单 ID如 gridding-load-xyz</param>
void RequestForm(string formId);
}
}

@ -0,0 +1,14 @@
namespace AI.Interface
{
/// <summary>
/// 用于通知 UI 层添加消息的接口
/// </summary>
public interface IMessageNotifier
{
/// <summary>
/// 通知添加一条工具调用消息到 UI仅显示不进入 AI 上下文)
/// </summary>
/// <param name="message">消息内容</param>
void NotifyToolCall(string message);
}
}

@ -0,0 +1,17 @@
using System.Threading.Tasks;
namespace AI.KnowledgeBase
{
/// <summary>
/// 知识库接口
/// </summary>
public interface IKnowledgeBase
{
/// <summary>
/// 查询知识库中的信息
/// </summary>
/// <param name="query">查询语句</param>
/// <returns>返回匹配的知识条目</returns>
Task<string> SearchKnowledgeAsync(string query);
}
}

@ -0,0 +1,130 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
namespace AI.KnowledgeBase
{
/// <summary>
/// 简单知识库实现
/// </summary>
public class SimpleKnowledgeBase : IKnowledgeBase
{
private static readonly Dictionary<string, string> QueryAliasToTopic = new(StringComparer.OrdinalIgnoreCase)
{
// 常用中文/英文说法归一到知识条目 key
{ "网格化", "成图" },
{ "网格化流程", "成图" },
{ "成图流程", "成图" },
{ "gridding", "成图" },
{ "grid", "成图" },
{ "gridding workflow", "成图" },
};
/// <summary>
/// 每个主题对应的知识条目 Id 与版本号,便于在会话存储或日志中引用和比对。
/// </summary>
private static readonly Dictionary<string, (string Id, string Version)> TopicMeta = new(StringComparer.OrdinalIgnoreCase)
{
{ "open file", (Id: "open-file", Version: "1") },
{ "close file", (Id: "close-file", Version: "1") },
{ "validate excel", (Id: "validate-excel", Version: "1") },
{ "成图", (Id: "gridding-flow", Version: "3") },
};
private readonly Dictionary<string, string> knowledge = new()
{
{ "open file", "Use the OpenFile tool to open a file." },
{ "close file", "Use the CloseFile tool to close a file." },
{ "validate excel", "Use the ValidExcelFile tool to validate an Excel file." },
{ "成图", @"
1. ShowForm(""gridding-load-xyz"")
2. ShowForm(""gridding-parameters"")
/
"
},
};
/// <inheritdoc/>
public Task<string> SearchKnowledgeAsync(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
return Task.FromResult(BuildNotFoundMessage());
}
var normalized = query.Trim().ToLowerInvariant();
// 先做别名归一(精确)
if (QueryAliasToTopic.TryGetValue(normalized, out var topic))
{
normalized = topic.ToLowerInvariant();
}
// 精确命中
if (knowledge.TryGetValue(normalized, out var result))
{
return Task.FromResult(FormatResult(normalized, result));
}
// 别名归一(包含匹配):比如"我要进行网格化"也能映射到"成图"
foreach (var kv in QueryAliasToTopic)
{
if (normalized.Contains(kv.Key.ToLowerInvariant()))
{
var key = kv.Value.ToLowerInvariant();
if (knowledge.TryGetValue(key, out result))
{
return Task.FromResult(FormatResult(key, result));
}
}
}
// 兜底:包含匹配(选最长 key避免过于宽泛
string? bestKey = null;
foreach (var key in knowledge.Keys)
{
var k = key.ToLowerInvariant();
if (normalized.Contains(k) || k.Contains(normalized))
{
if (bestKey == null || k.Length > bestKey.Length)
{
bestKey = key;
}
}
}
if (bestKey != null && knowledge.TryGetValue(bestKey, out result))
{
return Task.FromResult(FormatResult(bestKey, result));
}
return Task.FromResult(BuildNotFoundMessage(query));
}
/// <summary>
/// 按主题 key 为知识正文加上 [KB-ID; Version] 头部,便于回放与引用。
/// </summary>
private static string FormatResult(string topicKey, string content)
{
if (!TopicMeta.TryGetValue(topicKey, out var meta))
{
return content;
}
return $"[KB-ID: {meta.Id}; Version: {meta.Version}]{System.Environment.NewLine}{content}";
}
private string BuildNotFoundMessage(string? query = null)
{
var topics = string.Join("、", knowledge.Keys);
if (string.IsNullOrWhiteSpace(query))
return $"未找到相关知识。可用主题:{topics}";
return $"未找到与「{query}」相关的知识。可用主题:{topics}";
}
}
}

@ -0,0 +1,7 @@
namespace AI.Models
{
public class ChatInputModel
{
public string Message { get; set; } = string.Empty;
}
}

@ -0,0 +1,106 @@
using System.ComponentModel;
using LiveMarkdown.Avalonia;
using AI.Models.SpecialMessages;
namespace AI.Models
{
public enum AuthorType
{
User,
AI,
Tool, // 用于显示工具调用记录
}
public enum MessageType
{
Text,
File,
WorkflowStatus, // 工作流状态消息
Form, // 表单消息
Table, // 表格数据预览(如导入数据预览)
ColumnMatch, // 必需列与预览列匹配展示
ParameterSet, // 参数集展示(名称/值/描述)
KnowledgeBase, // 知识库查询结果(已写入 Store供 LLM 与 UI 使用)
XyzLoadCard, // 散点文件加载综合卡片(打开文件+数据预览+列头匹配)
GriddingParamCard, // 网格化参数设置综合卡片(参数加载+编辑+成图按钮)
// 未来可扩展Code, Image, Chart 等
}
public class ChatMessageModel(AuthorType authorType, string message, bool lastMessage = false) : INotifyPropertyChanged
{
public AuthorType Author { get; set; } = authorType;
public MessageType Type { get; set; } = MessageType.Text;
private ObservableStringBuilder _markdownBuilder = InitializeMarkdownBuilder(message);
public ObservableStringBuilder MarkdownBuilder
{
get => _markdownBuilder;
set
{
if (_markdownBuilder != value)
{
_markdownBuilder = value;
OnPropertyChanged(nameof(MarkdownBuilder));
}
}
}
private string _message = message;
public string Message
{
get => _message;
set
{
if (_message != value)
{
_message = value;
if (_markdownBuilder != null)
{
_markdownBuilder.Clear();
_markdownBuilder.Append(value);
}
OnPropertyChanged(nameof(Message));
}
}
}
public string FileName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public long FileSize { get; set; }
/// <summary>
/// 特殊消息内容(用于扩展消息类型)
/// </summary>
public ISpecialMessage? SpecialContent { get; set; }
private bool _lastMessage = lastMessage;
public bool LastMessage
{
get => _lastMessage;
set
{
if (_lastMessage != value)
{
_lastMessage = value;
OnPropertyChanged(nameof(LastMessage));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private static ObservableStringBuilder InitializeMarkdownBuilder(string message)
{
var builder = new ObservableStringBuilder();
if (!string.IsNullOrEmpty(message))
{
builder.Append(message);
}
return builder;
}
}
}

@ -0,0 +1,216 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using System;
using System.Linq;
using AI.Models.Store;
using AI.Utils;
namespace AI.Models
{
/// <summary>
/// 表示一个聊天会话,包含会话信息和历史记录。
/// 会话存储Store为唯一事实来源History 仅作兼容,发给 LLM 的历史由 GetHistoryForLlm 从 Store 生成。
/// </summary>
public class ChatSession
{
/// <summary>
/// 会话唯一标识符
/// </summary>
public string Id { get; }
/// <summary>
/// 会话标题(通常从第一条用户消息生成)
/// </summary>
public string Title { get; set; }
/// <summary>
/// 会话创建时间
/// </summary>
public DateTime CreatedAt { get; }
/// <summary>
/// 最后更新时间
/// </summary>
public DateTime UpdatedAt { get; private set; }
/// <summary>
/// 对话历史记录(兼容保留;发给 LLM 的请求应使用 GetHistoryForLlm 从 Store 生成)
/// </summary>
public ChatHistory History { get; }
/// <summary>
/// 会话级结构化存储唯一事实来源。UI 与 Prompt 视图均由此派生。
/// </summary>
public ConversationStore Store { get; }
/// <summary>
/// 消息数量
/// </summary>
public int MessageCount => History.Count;
/// <summary>
/// 用户消息数量(用于判断是否是第一条用户消息)
/// </summary>
private int _userMessageCount = 0;
/// <summary>
/// 当前会话的工作模式
/// </summary>
public WorkflowMode WorkflowMode { get; set; } = WorkflowMode.Ask;
/// <summary>
/// 初始化新的聊天会话
/// </summary>
/// <param name="id">会话ID如果为null则自动生成GUID</param>
/// <param name="title">会话标题</param>
public ChatSession(string? id = null, string? title = null)
{
Id = id ?? Guid.NewGuid().ToString();
Title = title ?? "新会话";
CreatedAt = DateTime.Now;
UpdatedAt = DateTime.Now;
History = new ChatHistory();
Store = new ConversationStore();
}
/// <summary>
/// 从持久化数据还原的会话(用于加载已保存的会话文件)
/// </summary>
public ChatSession(string id, string title, DateTime createdAt, DateTime updatedAt, WorkflowMode workflowMode, ConversationStore store)
{
Id = id ?? Guid.NewGuid().ToString();
Title = title ?? "新会话";
CreatedAt = createdAt;
UpdatedAt = updatedAt;
History = new ChatHistory();
Store = store ?? new ConversationStore();
WorkflowMode = workflowMode;
_userMessageCount = Store.Entries.OfType<Store.TextConversationEntry>().Count(t => t.Role == AuthorRole.User);
}
/// <summary>
/// 更新会话的最后更新时间
/// </summary>
public void UpdateTimestamp()
{
UpdatedAt = DateTime.Now;
}
/// <summary>
/// 添加用户消息(同时写入 Store作为真相源
/// </summary>
public void AddUserMessage(string message)
{
History.AddUserMessage(message);
Store.AppendText(AuthorRole.User, message);
_userMessageCount++;
UpdateTimestamp();
if (_userMessageCount == 1 && Title == "新会话")
{
Title = message.Length > 30 ? message.Substring(0, 30) + "..." : message;
}
}
/// <summary>
/// 添加助手消息(同时写入 Store
/// </summary>
public void AddAssistantMessage(string message)
{
History.AddAssistantMessage(message);
Store.AppendText(AuthorRole.Assistant, message);
UpdateTimestamp();
}
/// <summary>
/// 向会话存储追加一条特殊消息(不写入 History。供 ViewModel 在展示表单/参数集/表格/列匹配时调用。
/// </summary>
public void AppendSpecialEntry(string type, string yamlPayload)
{
Store.AppendSpecial(type, yamlPayload ?? string.Empty);
UpdateTimestamp();
}
/// <summary>
/// 从会话存储生成发给 LLM 的 ChatHistory仅包含 User/Assistant 文本及可选的 System(guidePrompt)。
/// 特殊消息不追加原始 YAML可替换为一句短占位如「[已展示表单xxx]」)。
/// </summary>
/// <param name="guidePrompt">可选;若提供则在开头插入一条 System 消息</param>
/// <returns>供 KernelService 使用的 ChatHistory不含原始 YAML</returns>
public ChatHistory GetHistoryForLlm(string? guidePrompt = null)
{
var history = new ChatHistory();
if (!string.IsNullOrWhiteSpace(guidePrompt))
{
history.AddSystemMessage(guidePrompt);
}
foreach (var entry in Store.Entries)
{
switch (entry)
{
case Store.TextConversationEntry text:
AddTextEntry(history, text, guidePrompt);
break;
case Store.SpecialConversationEntry special:
// 卡片展示记录仅用于 UI 重建,不注入 LLM 历史,避免模型从占位文本中学到错误的输出模式
break;
}
}
return history;
}
/// <summary>
/// 将 Store 中的一条文本条目按角色加入 ChatHistory供 GetHistoryForLlm 使用。
/// </summary>
private static void AddTextEntry(ChatHistory history, Store.TextConversationEntry text, string? guidePrompt)
{
if (text.Role == AuthorRole.System)
{
if (string.IsNullOrWhiteSpace(guidePrompt))
{
history.AddSystemMessage(text.Content);
}
}
else if (text.Role == AuthorRole.User)
{
history.AddUserMessage(text.Content);
}
else if (text.Role == AuthorRole.Assistant)
{
history.AddAssistantMessage(text.Content);
}
}
/// <summary>
/// 添加系统消息
/// </summary>
public void AddSystemMessage(string message)
{
History.AddSystemMessage(message);
Store.AppendText(AuthorRole.System, message);
UpdateTimestamp();
}
/// <summary>
/// 仅在需要时确保 History 中存在一条 System(guidePrompt)。不再写入 formsYaml 或摘要。
/// 发给 LLM 的 System 由 GetHistoryForLlm(guidePrompt) 在生成时插入,调用方传入 guidePrompt 即可。
/// </summary>
/// <param name="guidePrompt">引导提示词</param>
/// <param name="formsYaml">已忽略,保留参数以兼容现有调用</param>
public void EnsureFormsYamlInSystem(string guidePrompt, string formsYaml)
{
if (string.IsNullOrWhiteSpace(guidePrompt))
{
return;
}
if (History.Count == 0 || History[0].Role != AuthorRole.System)
{
History.Insert(0, new ChatMessageContent(AuthorRole.System, guidePrompt));
}
UpdateTimestamp();
}
}
}

@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace AI.Models.Form
{
/// <summary>
/// 表单定义Id、标题、提交目标、扁平字段列表
/// </summary>
public class FormDefinition
{
/// <summary>表单唯一标识</summary>
public string Id { get; set; } = string.Empty;
/// <summary>表单标题</summary>
public string Title { get; set; } = string.Empty;
/// <summary>提交后执行目标,如 AppActionType 名或 plugin:function</summary>
public string SubmitTarget { get; set; } = string.Empty;
/// <summary>提交按钮文案</summary>
public string SubmitLabel { get; set; } = "提交";
/// <summary>字段列表</summary>
public List<FormField> Fields { get; set; } = new List<FormField>();
}
}

@ -0,0 +1,50 @@
using System.Collections.Generic;
namespace AI.Models.Form
{
/// <summary>
/// 表单字段定义(通用 schema仅定义不含当前值
/// 类型与常用属性对应number(min,max,step,default), string(maxLength,placeholder,default),
/// boolean(default), select(options[],default), multi-select(options[],default[])。
/// </summary>
public class FormField
{
/// <summary>字段唯一标识,对应参数名</summary>
public string Id { get; set; } = string.Empty;
/// <summary>显示名称</summary>
public string Label { get; set; } = string.Empty;
/// <summary>可选描述/提示</summary>
public string? Description { get; set; }
/// <summary>控件/值类型</summary>
public FormFieldType Type { get; set; }
/// <summary>是否必填</summary>
public bool Required { get; set; }
/// <summary>默认值(可选)</summary>
public object? DefaultValue { get; set; }
// ----- number -----
/// <summary>数值最小值Number</summary>
public double? Min { get; set; }
/// <summary>数值最大值Number</summary>
public double? Max { get; set; }
/// <summary>步长Number</summary>
public double? Step { get; set; }
// ----- string / text -----
/// <summary>最大长度Text/MultiLine</summary>
public int? MaxLength { get; set; }
/// <summary>占位提示Text/MultiLine</summary>
public string? Placeholder { get; set; }
// ----- select / multi-select -----
/// <summary>下拉/单选选项Choice多选选项MultiSelect</summary>
public List<string>? Options { get; set; }
/// <summary>多选默认值MultiSelect</summary>
public List<string>? DefaultValues { get; set; }
}
}

@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace AI.Models.Form
{
/// <summary>
/// 表单字段条目schema + 当前值,用于绑定与提交
/// </summary>
public class FormFieldEntry : INotifyPropertyChanged
{
private string _currentValue = string.Empty;
public string Id { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
/// <summary>可选描述/提示</summary>
public string? Description { get; set; }
public FormFieldType Type { get; set; }
public bool Required { get; set; }
public object? DefaultValue { get; set; }
public List<string>? Options { get; set; }
// ----- number -----
public double? Min { get; set; }
public double? Max { get; set; }
public double? Step { get; set; }
// ----- string -----
public int? MaxLength { get; set; }
public string? Placeholder { get; set; }
// ----- multi-select -----
public List<string>? DefaultValues { get; set; }
/// <summary>多选当前选中项MultiSelect 绑定用)</summary>
public ObservableCollection<string> SelectedValues { get; } = new ObservableCollection<string>();
/// <summary>多选选项列表(用于绑定 CheckBox 列表,与 SelectedValues 同步)</summary>
public ObservableCollection<MultiSelectOptionItem> MultiSelectOptions { get; } = new ObservableCollection<MultiSelectOptionItem>();
/// <summary>当前输入值(绑定用,提交时按 Type 转换)</summary>
public string CurrentValue
{
get => _currentValue;
set => SetProperty(ref _currentValue, value);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}

@ -0,0 +1,22 @@
namespace AI.Models.Form
{
/// <summary>
/// 表单字段控件/值类型(通用 schema 类型)
/// </summary>
public enum FormFieldType
{
/// <summary>单行文本,常用属性: maxLength, placeholder, default</summary>
Text,
MultiLine,
/// <summary>数值,常用属性: min, max, step, default</summary>
Number,
/// <summary>勾选/开关,常用属性: default</summary>
Boolean,
FilePath,
/// <summary>单选下拉,常用属性: options[], default</summary>
Choice,
/// <summary>多选,常用属性: options[], default[]</summary>
MultiSelect,
Json,
}
}

@ -0,0 +1,47 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AI.Models.Form
{
/// <summary>
/// 多选项条目,用于绑定 CheckBox 与 FormFieldEntry.SelectedValues 同步
/// </summary>
public class MultiSelectOptionItem : INotifyPropertyChanged
{
private readonly FormFieldEntry _entry;
private bool _isSelected;
public string Option { get; }
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value) return;
_isSelected = value;
if (value)
{
if (!_entry.SelectedValues.Contains(Option))
_entry.SelectedValues.Add(Option);
}
else
{
_entry.SelectedValues.Remove(Option);
}
OnPropertyChanged();
}
}
public MultiSelectOptionItem(FormFieldEntry entry, string option, bool isSelected = false)
{
_entry = entry;
Option = option;
_isSelected = isSelected;
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

@ -0,0 +1,20 @@
using AI.Utils;
using Avalonia.Platform.Storage;
namespace AI.Models
{
public class PendingFileModel
{
public required IStorageFile StorageFile { get; set; }
public string Name => StorageFile?.Name ?? string.Empty;
public long Size { get; set; } // 单位: 字节
public string FormattedSize
{
get
{
return FileUnit.FormatFileSize(Size);
}
}
}
}

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 表示一条「必需列 -> 预览列」的匹配项,用于绑定显示。
/// </summary>
public class ColumnMappingItem : INotifyPropertyChanged
{
private string _requiredColumn = string.Empty;
private string _matchedColumn = string.Empty;
/// <summary>必需列名称</summary>
public string RequiredColumn
{
get => _requiredColumn;
set => SetProperty(ref _requiredColumn, value ?? string.Empty);
}
/// <summary>匹配到的预览列名称(未匹配时可为空)</summary>
public string MatchedColumn
{
get => _matchedColumn;
set => SetProperty(ref _matchedColumn, value ?? string.Empty);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
/// <summary>
/// 列头匹配特殊消息 - 用于在聊天流中展示「必需列」与「预览列」及其匹配关系。
/// </summary>
public class ColumnMatchMessage : ISpecialMessage, INotifyPropertyChanged
{
private string _title = "列头匹配";
/// <summary>消息唯一标识符</summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>类型名称</summary>
public string TypeName => "ColumnMatch";
/// <summary>不需要实时更新</summary>
public bool IsLive => false;
/// <summary>标题(如「列头匹配」)</summary>
public string Title
{
get => _title;
set => SetProperty(ref _title, value ?? string.Empty);
}
/// <summary>必需列名称列表</summary>
public ObservableCollection<string> RequiredColumns { get; } = new ObservableCollection<string>();
/// <summary>预览列(可用列)名称列表</summary>
public ObservableCollection<string> PreviewColumns { get; } = new ObservableCollection<string>();
/// <summary>匹配关系:必需列 -> 匹配的预览列</summary>
public ObservableCollection<ColumnMappingItem> Mappings { get; } = new ObservableCollection<ColumnMappingItem>();
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>设置必需列与预览列,并清空匹配关系</summary>
public void SetColumns(IEnumerable<string> requiredColumns, IEnumerable<string> previewColumns)
{
RequiredColumns.Clear();
foreach (var c in requiredColumns ?? Array.Empty<string>())
RequiredColumns.Add(c ?? string.Empty);
PreviewColumns.Clear();
foreach (var c in previewColumns ?? Array.Empty<string>())
PreviewColumns.Add(c ?? string.Empty);
Mappings.Clear();
}
/// <summary>设置匹配关系(必需列 -> 预览列),会清空并替换当前 Mappings</summary>
public void SetMappings(IEnumerable<KeyValuePair<string, string>> mappings)
{
Mappings.Clear();
if (mappings == null) return;
foreach (var kv in mappings)
{
Mappings.Add(new ColumnMappingItem
{
RequiredColumn = kv.Key ?? string.Empty,
MatchedColumn = kv.Value ?? string.Empty,
});
}
}
}
}

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using AI.Models.Form;
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 表单请求特殊消息 - 在聊天流中显示可填写的表单
/// </summary>
public class FormRequestMessage : ISpecialMessage, INotifyPropertyChanged
{
private string _submitLabel = "提交";
/// <summary>消息唯一标识符</summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>类型名称</summary>
public string TypeName => "Form";
/// <summary>不需要实时更新</summary>
public bool IsLive => false;
/// <summary>表单定义(只读)</summary>
public FormDefinition Definition { get; }
/// <summary>带当前值的字段列表,用于绑定与提交</summary>
public ObservableCollection<FormFieldEntry> FieldsWithValues { get; } = new ObservableCollection<FormFieldEntry>();
/// <summary>提交按钮文案(可从 Definition 覆盖)</summary>
public string SubmitLabel
{
get => _submitLabel;
set => SetProperty(ref _submitLabel, value);
}
public FormRequestMessage(FormDefinition definition)
{
Definition = definition ?? throw new ArgumentNullException(nameof(definition));
_submitLabel = definition.SubmitLabel;
foreach (var f in definition.Fields)
{
var entry = new FormFieldEntry
{
Id = f.Id,
Label = f.Label,
Description = f.Description,
Type = f.Type,
Required = f.Required,
DefaultValue = f.DefaultValue,
Options = f.Options,
Min = f.Min,
Max = f.Max,
Step = f.Step,
MaxLength = f.MaxLength,
Placeholder = f.Placeholder,
DefaultValues = f.DefaultValues != null ? new List<string>(f.DefaultValues) : null,
CurrentValue = f.DefaultValue?.ToString() ?? string.Empty,
};
if (f.Type == FormFieldType.MultiSelect)
{
if (f.DefaultValues != null && f.DefaultValues.Count > 0)
{
foreach (var v in f.DefaultValues)
entry.SelectedValues.Add(v);
}
else if (f.DefaultValue is System.Collections.IEnumerable en && f.DefaultValue is not string)
{
foreach (var v in en)
entry.SelectedValues.Add(v?.ToString() ?? string.Empty);
}
else if (f.DefaultValue is string defStr && !string.IsNullOrEmpty(defStr))
{
foreach (var v in defStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
entry.SelectedValues.Add(v);
}
if (f.Options != null)
{
foreach (var opt in f.Options)
entry.MultiSelectOptions.Add(new MultiSelectOptionItem(entry, opt, entry.SelectedValues.Contains(opt)));
}
}
FieldsWithValues.Add(entry);
}
}
/// <summary>收集当前值用于提交。Key 为字段 IdValue 为对象(按 Type 转换)</summary>
public Dictionary<string, object> GetValues()
{
var dict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in FieldsWithValues)
{
var raw = entry.CurrentValue?.Trim() ?? string.Empty;
object value = entry.Type switch
{
FormFieldType.Number => double.TryParse(raw, out var n) ? n : 0d,
FormFieldType.Boolean => raw is "1" or "true" or "True" or "yes" or "是",
FormFieldType.Json => raw,
FormFieldType.MultiSelect => entry.SelectedValues.Count > 0
? new List<string>(entry.SelectedValues)
: (object)raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(),
_ => raw,
};
dict[entry.Id] = value;
}
return dict;
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 网格化参数卡片的执行阶段
/// </summary>
public enum GriddingParamCardPhase
{
/// <summary>正在从业务层加载参数</summary>
Loading = 0,
/// <summary>参数已加载,等待用户编辑并点击生成</summary>
Ready = 1,
/// <summary>正在执行成图</summary>
Generating = 2,
/// <summary>成图完成</summary>
Done = 3,
/// <summary>出错(加载失败或成图失败)</summary>
Error = 4,
}
/// <summary>
/// 网格化参数设置综合卡片:将「获取参数」「编辑参数」「执行成图」合并到一张卡片。
/// 整个业务流程由卡片本身驱动AI 只需调用一次展示此卡片;
/// 用户确认参数后点击"生成"按钮,卡片自动完成参数设置和成图,最终摘要发回 AI。
/// </summary>
public class GriddingParamCardMessage : ISpecialMessage, INotifyPropertyChanged
{
private GriddingParamCardPhase _phase = GriddingParamCardPhase.Loading;
private string _statusMessage = string.Empty;
private string _generateButtonLabel = "生成";
public string Id { get; set; } = Guid.NewGuid().ToString();
public string TypeName => "GriddingParamCard";
public bool IsLive => false;
// ── 阶段控制 ──────────────────────────────────────────────────────────
public GriddingParamCardPhase Phase
{
get => _phase;
set
{
if (SetProperty(ref _phase, value))
{
OnPropertyChanged(nameof(IsLoading));
OnPropertyChanged(nameof(ShowItems));
OnPropertyChanged(nameof(ShowGenerateButton));
OnPropertyChanged(nameof(IsGenerating));
OnPropertyChanged(nameof(IsDone));
OnPropertyChanged(nameof(IsError));
OnPropertyChanged(nameof(ShowDoneBadge));
OnPropertyChanged(nameof(ShowErrorMessage));
}
}
}
// ── 状态派生属性 ─────────────────────────────────────────────────────
/// <summary>是否正在加载参数(显示 loading 占位)</summary>
public bool IsLoading => _phase == GriddingParamCardPhase.Loading;
/// <summary>是否显示参数列表(非 Loading 阶段)</summary>
public bool ShowItems => _phase != GriddingParamCardPhase.Loading;
/// <summary>是否显示"生成"按钮Ready 阶段)</summary>
public bool ShowGenerateButton => _phase == GriddingParamCardPhase.Ready;
/// <summary>是否正在执行成图</summary>
public bool IsGenerating => _phase == GriddingParamCardPhase.Generating;
/// <summary>成图是否已完成</summary>
public bool IsDone => _phase == GriddingParamCardPhase.Done;
/// <summary>是否处于错误状态</summary>
public bool IsError => _phase == GriddingParamCardPhase.Error;
/// <summary>是否显示"成图完成"标记</summary>
public bool ShowDoneBadge => _phase == GriddingParamCardPhase.Done;
/// <summary>是否显示错误信息</summary>
public bool ShowErrorMessage => _phase == GriddingParamCardPhase.Error && !string.IsNullOrEmpty(_statusMessage);
// ── 状态文案 ──────────────────────────────────────────────────────────
/// <summary>状态提示文案(加载中、错误信息、成功提示等)</summary>
public string StatusMessage
{
get => _statusMessage;
set
{
SetProperty(ref _statusMessage, value ?? string.Empty);
OnPropertyChanged(nameof(ShowErrorMessage));
OnPropertyChanged(nameof(HasStatusMessage));
}
}
/// <summary>是否有状态信息</summary>
public bool HasStatusMessage => !string.IsNullOrEmpty(_statusMessage);
/// <summary>"生成"按钮文案(执行中可切换为"生成中..."</summary>
public string GenerateButtonLabel
{
get => _generateButtonLabel;
set => SetProperty(ref _generateButtonLabel, value ?? "生成");
}
// ── 参数项列表 ────────────────────────────────────────────────────────
/// <summary>参数项列表(从业务层加载后填充,用户可编辑)</summary>
public ObservableCollection<ParameterSetItem> Items { get; } = new ObservableCollection<ParameterSetItem>();
// ── INotifyPropertyChanged ───────────────────────────────────────────
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}

@ -0,0 +1,24 @@
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 特殊消息接口 - 所有特殊消息类型的基接口
/// </summary>
public interface ISpecialMessage
{
/// <summary>
/// 特殊消息的唯一标识符
/// </summary>
string Id { get; }
/// <summary>
/// 特殊消息类型名称
/// </summary>
string TypeName { get; }
/// <summary>
/// 是否需要实时更新
/// </summary>
bool IsLive { get; }
}
}

@ -0,0 +1,32 @@
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 知识库查询结果消息,用于存入会话 Store 并在 Prompt 视图中带给 LLM。
/// </summary>
public class KnowledgeBaseMessage : ISpecialMessage
{
/// <inheritdoc />
public string Id { get; set; } = string.Empty;
/// <inheritdoc />
public string TypeName => "KnowledgeBase";
/// <inheritdoc />
public bool IsLive => false;
/// <summary>知识条目 ID如 gridding-flow</summary>
public string KnowledgeId { get; set; } = string.Empty;
/// <summary>版本号</summary>
public string Version { get; set; } = "1";
/// <summary>命中的主题 key如 成图)</summary>
public string TopicKey { get; set; } = string.Empty;
/// <summary>用户原始查询</summary>
public string Query { get; set; } = string.Empty;
/// <summary>知识库返回的完整内容,供 LLM 与 UI 使用</summary>
public string RawContent { get; set; } = string.Empty;
}
}

@ -0,0 +1,382 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using YamlDotNet.Serialization;
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 参数集单条字段的控件类型(用于可编辑参数卡片)
/// </summary>
public enum ParameterSetFieldType
{
/// <summary>单行文本</summary>
Text,
/// <summary>数值</summary>
Number,
/// <summary>下拉选择(如成图算法)</summary>
Choice,
/// <summary>文件路径,带“选择文件”按钮</summary>
FilePath,
}
/// <summary>
/// 单条参数项:名称、当前值(可编辑)、描述、控件类型及选项。
/// </summary>
public class ParameterSetItem : INotifyPropertyChanged
{
private string _name = string.Empty;
private string _valueText = string.Empty;
private string _description = string.Empty;
private ParameterSetFieldType _fieldType = ParameterSetFieldType.Text;
public string Name
{
get => _name;
set => SetProperty(ref _name, value ?? string.Empty);
}
/// <summary>当前值的展示/编辑文本</summary>
public string ValueText
{
get => _valueText;
set
{
if (SetProperty(ref _valueText, value ?? string.Empty))
{
OnPropertyChanged(nameof(NumericValue));
}
}
}
/// <summary>
/// 数字值(仅 Number 类型控件绑定使用)。
/// 写回时仅在 FieldType == Number 时才更新 ValueText
/// 防止不可见 NumericUpDown 的双向绑定覆盖 FilePath / Text 等字段的值。
/// </summary>
public double NumericValue
{
get => double.TryParse(
_valueText?.Trim(),
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out var n) ? n : 0d;
set
{
if (_fieldType == ParameterSetFieldType.Number)
{
var s = value.ToString(System.Globalization.CultureInfo.InvariantCulture);
if (SetProperty(ref _valueText, s, nameof(ValueText)))
{
OnPropertyChanged(nameof(NumericValue));
}
}
}
}
public string Description
{
get => _description;
set => SetProperty(ref _description, value ?? string.Empty);
}
/// <summary>控件类型Text / Number / Choice / FilePath</summary>
public ParameterSetFieldType FieldType
{
get => _fieldType;
set => SetProperty(ref _fieldType, value);
}
/// <summary>下拉选项Choice 类型时使用)</summary>
public List<string> Options { get; set; } = new List<string>();
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
/// <summary>
/// 参数集特殊消息 - 用于在聊天流中展示一组「名称 / 值 / 描述」参数(如网格化参数等),由基本消息样式组合而成。
/// </summary>
public class ParameterSetMessage : ISpecialMessage, INotifyPropertyChanged
{
private string _title = "参数";
public string Id { get; set; } = Guid.NewGuid().ToString();
public string TypeName => "ParameterSet";
public bool IsLive => false;
public string Title
{
get => _title;
set => SetProperty(ref _title, value ?? string.Empty);
}
/// <summary>参数项列表(名称、值、描述)</summary>
public ObservableCollection<ParameterSetItem> Items { get; } = new ObservableCollection<ParameterSetItem>();
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// 从 YAML 文本填充参数集。格式示例:
/// Input:
/// Value: ...
/// Description: 散点数据文件路径
/// Type: FilePath
/// 支持 PascalCase / camelCase 键Value/value, Type/type 等)。
/// </summary>
public void LoadFromJson(string text)
{
Items.Clear();
if (string.IsNullOrWhiteSpace(text)) return;
LoadFromYamlInternal(text.Trim());
}
/// <summary>
/// 从 YAML 解析参数集。根级为参数名(如 Input, Faultage每项含 Value/Description/Type/Options。
/// 支持根级单键包装(如 parameters: 下再挂 Input/Faultage/...),会自动解包一层。
/// </summary>
private void LoadFromYamlInternal(string yaml)
{
try
{
var deserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.Build();
var root = deserializer.Deserialize<Dictionary<string, object>>(yaml);
if (root == null || root.Count == 0) return;
// 若根级只有一键且其值为映射,认为是 parameters: { input: ..., faultage: ... },用内层作为参数表
IEnumerable<KeyValuePair<string, object>> paramsIter = root;
if (root.Count == 1)
{
var singleValue = root.Values.First();
if (singleValue is Dictionary<object, object> inner)
{
var unwrapped = new Dictionary<string, object>();
foreach (var e in inner)
unwrapped[e.Key?.ToString() ?? ""] = e.Value;
paramsIter = unwrapped;
}
else if (singleValue is IDictionary<object, object> innerDict)
{
var unwrapped = new Dictionary<string, object>();
foreach (var e in innerDict)
unwrapped[e.Key?.ToString() ?? ""] = e.Value;
paramsIter = unwrapped;
}
else if (singleValue is Dictionary<string, object> innerStr)
{
paramsIter = innerStr;
}
}
foreach (var kvp in paramsIter)
{
var name = kvp.Key ?? "";
if (string.IsNullOrWhiteSpace(name)) continue;
var entry = ParseYamlEntry(kvp.Value, name);
string valueText = entry.valueText;
string description = entry.description;
var fieldType = entry.fieldType;
var options = entry.options;
if (fieldType == ParameterSetFieldType.Choice && options.Count == 0)
{
options.AddRange(GetDefaultOptionsForChoice(name));
}
if (string.Equals(name, "Input", StringComparison.OrdinalIgnoreCase) && fieldType == ParameterSetFieldType.Text)
{
fieldType = ParameterSetFieldType.FilePath;
}
Items.Add(new ParameterSetItem
{
Name = name,
ValueText = valueText,
Description = description,
FieldType = fieldType,
Options = options,
});
}
}
catch
{
// 解析失败时保持空列表
}
}
private static (string valueText, string description, ParameterSetFieldType fieldType, List<string> options) ParseYamlEntry(object? raw, string name)
{
string valueText = string.Empty;
string description = string.Empty;
var fieldType = ParameterSetFieldType.Text;
var options = new List<string>();
Dictionary<object, object>? dictObj = raw as Dictionary<object, object>;
Dictionary<string, object>? dictStr = raw as Dictionary<string, object>;
if (dictObj == null && dictStr == null)
{
return (valueText, description, fieldType, options);
}
// 支持 PascalCaseValue/Type/Options与 camelCasevalue/type/options
if (TryGetValueIgnoreCase(raw, "value", out var valObj))
{
valueText = valObj == null ? "" : (valObj is bool b ? (b ? "true" : "false") : valObj.ToString() ?? "");
}
if (TryGetValueIgnoreCase(raw, "description", out var descObj) && descObj != null)
{
description = descObj.ToString() ?? "";
}
if (TryGetValueIgnoreCase(raw, "type", out var typeObj) && typeObj != null)
{
fieldType = ParseTypeString(typeObj.ToString() ?? "");
}
else
{
fieldType = InferFieldType(name, valueText);
}
if (TryGetValueIgnoreCase(raw, "options", out var optObj))
{
if (optObj is IEnumerable<object> optList)
{
foreach (var o in optList)
{
if (o?.ToString() is string s)
{
options.Add(s);
}
}
}
else if (optObj is System.Collections.IEnumerable optEnum)
{
foreach (var item in optEnum)
{
if (item?.ToString() is string optStr)
{
options.Add(optStr);
}
}
}
}
if (fieldType == ParameterSetFieldType.Text &&
(description.IndexOf("文件路径", StringComparison.OrdinalIgnoreCase) >= 0 ||
(description.IndexOf("文件", StringComparison.OrdinalIgnoreCase) >= 0 && description.IndexOf("路径", StringComparison.OrdinalIgnoreCase) >= 0)))
{
fieldType = ParameterSetFieldType.FilePath;
}
return (valueText, description, fieldType, options);
}
private static bool TryGetValueIgnoreCase(object? dict, string key, out object? value)
{
value = null;
if (dict == null)
{
return false;
}
var keyEq = StringComparer.OrdinalIgnoreCase;
if (dict is Dictionary<object, object> objDict)
{
foreach (var kv in objDict)
{
if (keyEq.Equals(kv.Key?.ToString(), key))
{
value = kv.Value;
return true;
}
}
return false;
}
if (dict is Dictionary<string, object> strDict)
{
foreach (var kv in strDict)
{
if (keyEq.Equals(kv.Key, key))
{
value = kv.Value;
return true;
}
}
return false;
}
return false;
}
private static ParameterSetFieldType ParseTypeString(string typeStr)
{
switch (typeStr.Trim().ToLowerInvariant())
{
case "choice": return ParameterSetFieldType.Choice;
case "number": return ParameterSetFieldType.Number;
case "filepath":
case "file": return ParameterSetFieldType.FilePath;
case "boolean":
case "bool": return ParameterSetFieldType.Text;
default: return ParameterSetFieldType.Text;
}
}
/// <summary>根据键名和当前值推断控件类型</summary>
private static ParameterSetFieldType InferFieldType(string name, string valueText)
{
if (name.IndexOf("成图算法", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("算法", StringComparison.OrdinalIgnoreCase) >= 0)
return ParameterSetFieldType.Choice;
if (name.IndexOf("文件", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("路径", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("path", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("file", StringComparison.OrdinalIgnoreCase) >= 0)
return ParameterSetFieldType.FilePath;
if (name.IndexOf("步长", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("范围", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("min", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("max", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("列", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("行", StringComparison.OrdinalIgnoreCase) >= 0)
return ParameterSetFieldType.Number;
if (double.TryParse(valueText?.Trim(), System.Globalization.NumberStyles.Any, null, out _))
return ParameterSetFieldType.Number;
return ParameterSetFieldType.Text;
}
/// <summary>成图算法等键的默认下拉选项</summary>
private static IEnumerable<string> GetDefaultOptionsForChoice(string name)
{
if (name.IndexOf("成图算法", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("算法", StringComparison.OrdinalIgnoreCase) >= 0)
return new[] { "IDW", "Kriging", "自然邻点", "反距离加权" };
return Array.Empty<string>();
}
}
}

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 表格数据预览行 - 一行单元格
/// </summary>
public class TableDataRow : INotifyPropertyChanged
{
/// <summary>该行单元格(与列顺序一致)</summary>
public ObservableCollection<string> Cells { get; } = new ObservableCollection<string>();
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// 表格数据展示特殊消息 - 用于导入数据等在聊天流中做数据预览
/// </summary>
public class TableDataMessage : ISpecialMessage, INotifyPropertyChanged
{
private string _title = "数据预览";
private int _totalRowCount;
private int _maxPreviewRows = 50;
/// <summary>消息唯一标识符</summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>类型名称</summary>
public string TypeName => "Table";
/// <summary>不需要实时更新</summary>
public bool IsLive => false;
/// <summary>标题(如“数据预览”)</summary>
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
/// <summary>列名(表头)</summary>
public ObservableCollection<string> ColumnNames { get; } = new ObservableCollection<string>();
/// <summary>预览行数据</summary>
public ObservableCollection<TableDataRow> Rows { get; } = new ObservableCollection<TableDataRow>();
/// <summary>数据总行数(用于显示“共 N 行,仅显示前 M 行”)</summary>
public int TotalRowCount
{
get => _totalRowCount;
set => SetProperty(ref _totalRowCount, value);
}
/// <summary>最多预览行数</summary>
public int MaxPreviewRows
{
get => _maxPreviewRows;
set => SetProperty(ref _maxPreviewRows, value);
}
/// <summary>是否有多余行未显示</summary>
public bool HasMoreRows => TotalRowCount > Rows.Count;
/// <summary>摘要文案(如“共 100 行,仅显示前 50 行”)</summary>
public string SummaryText => $"共 {TotalRowCount} 行,仅显示前 {Rows.Count} 行";
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
if (propertyName == nameof(TotalRowCount) || propertyName == nameof(MaxPreviewRows))
{
OnPropertyChanged(nameof(HasMoreRows));
OnPropertyChanged(nameof(SummaryText));
}
return true;
}
/// <summary>从列名与行数据批量填充(每行为字符串数组,与列顺序一致)</summary>
public void SetData(IEnumerable<string> columnNames, IEnumerable<IEnumerable<string>> rows, int? totalRowCount = null)
{
ColumnNames.Clear();
foreach (var c in columnNames)
ColumnNames.Add(c ?? string.Empty);
Rows.Clear();
foreach (var row in rows)
{
var r = new TableDataRow();
foreach (var cell in row)
r.Cells.Add(cell?.ToString() ?? string.Empty);
Rows.Add(r);
}
TotalRowCount = totalRowCount ?? Rows.Count;
OnPropertyChanged(nameof(HasMoreRows));
OnPropertyChanged(nameof(SummaryText));
}
}
}

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 工作流状态特殊消息 - 在聊天流中显示工作流进度
/// </summary>
public class WorkflowStatusMessage : ISpecialMessage, INotifyPropertyChanged
{
private string _title = "工作流进度";
private ObservableCollection<WorkflowStepModel> _steps = new();
/// <summary>
/// 消息唯一标识符
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 类型名称
/// </summary>
public string TypeName => "WorkflowStatus";
/// <summary>
/// 需要实时更新
/// </summary>
public bool IsLive => true;
/// <summary>
/// 工作流标题
/// </summary>
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
/// <summary>
/// 工作流步骤列表
/// </summary>
public ObservableCollection<WorkflowStepModel> Steps
{
get => _steps;
set => SetProperty(ref _steps, value);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}

@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using AI.Models.Form;
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 散点文件加载卡片的阶段
/// </summary>
public enum XyzLoadPhase
{
SelectFile = 0, // 初始:等待用户选择文件
FileLoaded = 1, // 文件已加载,显示数据预览 + 列头匹配
Completed = 2, // 列头匹配已确认,摘要已提交
}
/// <summary>
/// 散点文件加载综合卡片:将「打开散点文件」「数据预览」「列头匹配」合并到一张卡片。
/// 整个业务流程由卡片本身驱动,不需要 AI 参与中间步骤;
/// 用户确认列头匹配后,统一生成摘要发给 AI。
/// </summary>
public class XyzLoadCardMessage : ISpecialMessage, INotifyPropertyChanged
{
private XyzLoadPhase _phase = XyzLoadPhase.SelectFile;
private string _filePath = string.Empty;
private string _matchButtonLabel = "确认匹配";
private TableDataMessage? _tablePreview;
private bool _isLoading;
private string _statusMessage = string.Empty;
/// <summary>消息唯一标识符</summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>类型名称(用于序列化路由)</summary>
public string TypeName => "XyzLoadCard";
/// <summary>不需要实时更新</summary>
public bool IsLive => false;
// ── 阶段控制 ──────────────────────────────────────────────────────────
public XyzLoadPhase Phase
{
get => _phase;
set
{
if (SetProperty(ref _phase, value))
{
OnPropertyChanged(nameof(IsFileInputEnabled));
OnPropertyChanged(nameof(ShowLoadButtons));
OnPropertyChanged(nameof(HasSubmittedFile));
OnPropertyChanged(nameof(ShowPreviewSection));
OnPropertyChanged(nameof(ShowMatchSection));
OnPropertyChanged(nameof(ShowMatchButton));
}
}
}
// ── 加载状态 ──────────────────────────────────────────────────────────
/// <summary>是否正在执行加载(禁用按钮,显示 loading 提示)</summary>
public bool IsLoading
{
get => _isLoading;
set
{
if (SetProperty(ref _isLoading, value))
{
OnPropertyChanged(nameof(ShowLoadButtons));
OnPropertyChanged(nameof(IsFileInputEnabled));
}
}
}
/// <summary>状态提示文案(加载中、错误信息等)</summary>
public string StatusMessage
{
get => _statusMessage;
set
{
SetProperty(ref _statusMessage, value ?? string.Empty);
OnPropertyChanged(nameof(HasStatusMessage));
}
}
/// <summary>是否有错误提示</summary>
public bool HasStatusMessage => !string.IsNullOrEmpty(_statusMessage);
// ── 第一步:选择文件 ─────────────────────────────────────────────────
/// <summary>文件路径(双向绑定到输入框)</summary>
public string FilePath
{
get => _filePath;
set => SetProperty(ref _filePath, value ?? string.Empty);
}
/// <summary>文件输入框是否可编辑(仅 SelectFile 且非加载中时)</summary>
public bool IsFileInputEnabled => Phase == XyzLoadPhase.SelectFile && !IsLoading;
/// <summary>是否显示「选择文件」和「加载」按钮SelectFile 且非加载中)</summary>
public bool ShowLoadButtons => Phase == XyzLoadPhase.SelectFile && !IsLoading;
/// <summary>是否显示正在加载提示</summary>
public bool ShowLoadingHint => IsLoading;
/// <summary>文件已提交后显示「已加载」标记</summary>
public bool HasSubmittedFile => Phase != XyzLoadPhase.SelectFile;
// ── 第二步:数据预览 ─────────────────────────────────────────────────
/// <summary>CSV 数据预览(加载后填充)</summary>
public TableDataMessage? TablePreview
{
get => _tablePreview;
set
{
if (SetProperty(ref _tablePreview, value))
{
OnPropertyChanged(nameof(ShowPreviewSection));
}
}
}
/// <summary>是否显示数据预览区</summary>
public bool ShowPreviewSection => _tablePreview != null && Phase != XyzLoadPhase.SelectFile;
// ── 第三步:列头匹配 ─────────────────────────────────────────────────
/// <summary>列头匹配下拉字段(必需列 → 可用列 ComboBox</summary>
public ObservableCollection<FormFieldEntry> ColumnMatchFields { get; } = new();
/// <summary>列头匹配表单定义(保存 title、submitTarget用于构建摘要</summary>
public FormDefinition? ColumnMatchDefinition { get; set; }
/// <summary>确认匹配按钮文案</summary>
public string MatchButtonLabel
{
get => _matchButtonLabel;
set => SetProperty(ref _matchButtonLabel, value);
}
/// <summary>是否显示列头匹配区FileLoaded 阶段且有匹配字段)</summary>
public bool ShowMatchSection => Phase == XyzLoadPhase.FileLoaded;
/// <summary>是否显示「确认匹配」按钮FileLoaded 阶段)</summary>
public bool ShowMatchButton => Phase == XyzLoadPhase.FileLoaded;
// ── INotifyPropertyChanged ───────────────────────────────────────────
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}

@ -0,0 +1,60 @@
using Microsoft.SemanticKernel.ChatCompletion;
namespace AI.Models.Store
{
/// <summary>
/// 会话存储条目的抽象基类。每条为 TextEntry角色+文本)或 SpecialEntry类型+YAML 载荷)。
/// 存储由 ChatSession 持有,作为 UI 与 Prompt 构建的唯一事实来源。
/// </summary>
public abstract class ConversationEntry
{
/// <summary>
/// 条目种类Text 或 Special用于序列化/反序列化与类型判别。
/// </summary>
public abstract string Kind { get; }
}
/// <summary>
/// 纯文本消息条目角色User/Assistant/System+ 文本内容。
/// </summary>
public sealed class TextConversationEntry : ConversationEntry
{
public override string Kind => "Text";
/// <summary>角色</summary>
public AuthorRole Role { get; set; }
/// <summary>文本内容</summary>
public string Content { get; set; } = string.Empty;
public TextConversationEntry() { }
public TextConversationEntry(AuthorRole role, string content)
{
Role = role;
Content = content ?? string.Empty;
}
}
/// <summary>
/// 特殊消息条目类型Form/ParameterSet/Table/ColumnMatch/WorkflowStatus+ YAML 载荷。
/// </summary>
public sealed class SpecialConversationEntry : ConversationEntry
{
public override string Kind => "Special";
/// <summary>特殊消息类型名</summary>
public string Type { get; set; } = string.Empty;
/// <summary>YAML 载荷(与 ISpecialMessage 可序列化/反序列化对应)</summary>
public string Payload { get; set; } = string.Empty;
public SpecialConversationEntry() { }
public SpecialConversationEntry(string type, string payload)
{
Type = type ?? string.Empty;
Payload = payload ?? string.Empty;
}
}
}

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SemanticKernel.ChatCompletion;
namespace AI.Models.Store
{
public static class ConversationEntryMapper
{
public static List<ConversationEntryDto> ToDtoList(IEnumerable<ConversationEntry>? entries)
{
if (entries == null)
{
return new List<ConversationEntryDto>();
}
return entries.Select(e =>
{
if (e is TextConversationEntry t)
{
return new ConversationEntryDto
{
Kind = t.Kind,
Role = t.Role.ToString(),
Content = t.Content
};
}
if (e is SpecialConversationEntry s)
{
return new ConversationEntryDto
{
Kind = s.Kind,
Type = s.Type,
Payload = s.Payload
};
}
return new ConversationEntryDto { Kind = e?.Kind ?? string.Empty };
}).ToList();
}
public static List<ConversationEntry> ToEntryList(IEnumerable<ConversationEntryDto>? entries)
{
var list = new List<ConversationEntry>();
if (entries == null)
{
return list;
}
foreach (var e in entries)
{
if (e == null) continue;
if (string.Equals(e.Kind, "Text", StringComparison.OrdinalIgnoreCase))
{
var role = AuthorRole.User;
if (!string.IsNullOrEmpty(e.Role) && Enum.TryParse<AuthorRole>(e.Role, true, out var r))
{
role = r;
}
list.Add(new TextConversationEntry(role, e.Content ?? string.Empty));
}
else if (string.Equals(e.Kind, "Special", StringComparison.OrdinalIgnoreCase))
{
list.Add(new SpecialConversationEntry(e.Type ?? string.Empty, e.Payload ?? string.Empty));
}
}
return list;
}
}
}

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SemanticKernel.ChatCompletion;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace AI.Models.Store
{
/// <summary>
/// 会话存储序列化用的 DTOYAML 根为 entries 列表)
/// </summary>
public class ConversationStoreDto
{
public List<ConversationEntryDto> Entries { get; set; } = new();
}
/// <summary>
/// 单条存储条目的 DTO
/// </summary>
public class ConversationEntryDto
{
public string Kind { get; set; } = string.Empty;
public string? Role { get; set; }
public string? Content { get; set; }
public string? Type { get; set; }
public string? Payload { get; set; }
}
/// <summary>
/// 会话级结构化存储,作为 UI 与 Prompt 构建的唯一事实来源。
/// 持有有序的 ConversationEntry 列表,支持追加文本与特殊消息。
/// 每个 ChatSession 持有一份 ConversationStore 实例。
/// </summary>
public class ConversationStore
{
private readonly List<ConversationEntry> _entries = new();
private static readonly ISerializer _yamlSerializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
private static readonly IDeserializer _yamlDeserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
/// <summary>
/// 有序条目列表(只读视图)
/// </summary>
public IReadOnlyList<ConversationEntry> Entries => _entries;
/// <summary>
/// 存储内容变更时触发(用于持久化层按需保存)
/// </summary>
public event EventHandler? StoreChanged;
/// <summary>
/// 追加一条文本消息
/// </summary>
public void AppendText(AuthorRole role, string content)
{
_entries.Add(new TextConversationEntry(role, content ?? string.Empty));
StoreChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// 追加一条特殊消息type + YAML 载荷)
/// </summary>
public void AppendSpecial(string type, string yamlPayload)
{
_entries.Add(new SpecialConversationEntry(type ?? string.Empty, yamlPayload ?? string.Empty));
StoreChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// 清空存储(用于测试或重置)
/// </summary>
public void Clear()
{
_entries.Clear();
}
/// <summary>
/// 移除最后一条条目(用于回滚等)
/// </summary>
/// <returns>是否移除了条目</returns>
public bool RemoveLast()
{
if (_entries.Count == 0) return false;
_entries.RemoveAt(_entries.Count - 1);
return true;
}
/// <summary>
/// 条目数量
/// </summary>
public int Count => _entries.Count;
/// <summary>
/// 序列化为 YAML 字符串(仅 entries 列表,用于嵌入会话文件或单独观察)
/// </summary>
public string ToYaml()
{
var dto = new ConversationStoreDto
{
Entries = ConversationEntryMapper.ToDtoList(_entries)
};
return _yamlSerializer.Serialize(dto);
}
/// <summary>
/// 从 YAML 字符串加载并替换当前存储内容(先清空再按序追加)
/// </summary>
public void LoadFromYaml(string yaml)
{
if (string.IsNullOrWhiteSpace(yaml))
{
Clear();
return;
}
var dto = _yamlDeserializer.Deserialize<ConversationStoreDto>(yaml);
LoadFromEntries(dto?.Entries);
}
/// <summary>
/// 从 DTO 条目列表加载并替换当前存储(先清空再按序追加),用于会话文件反序列化
/// </summary>
public void LoadFromEntries(IEnumerable<ConversationEntryDto>? entries)
{
_entries.Clear();
_entries.AddRange(ConversationEntryMapper.ToEntryList(entries));
}
}
}

@ -0,0 +1,19 @@
namespace AI.Models
{
/// <summary>
/// AI 工作模式
/// </summary>
public enum WorkflowMode
{
/// <summary>
/// Ask 模式普通对话AI 自由交互
/// </summary>
Ask,
/// <summary>
/// Workflow 模式:严格流程控制
/// </summary>
Workflow
}
}

@ -0,0 +1,55 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.Generic;
namespace AI.Models
{
/// <summary>
/// 工作流步骤的 UI 模型
/// </summary>
public partial class WorkflowStepModel : ObservableObject
{
/// <summary>
/// 步骤唯一标识符
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// 执行顺序
/// </summary>
public int Order { get; set; }
/// <summary>
/// 步骤状态
/// </summary>
[ObservableProperty]
private WorkflowStepStatus _status = WorkflowStepStatus.Pending;
/// <summary>
/// 是否可以重新执行
/// </summary>
[ObservableProperty]
private bool _canReset;
/// <summary>
/// 存储步骤的输入参数(用于回溯)
/// </summary>
public Dictionary<string, object>? InputParameters { get; set; }
/// <summary>
/// 存储步骤的输出结果
/// </summary>
public object? OutputResult { get; set; }
/// <summary>
/// 当前思考过程ReAct 模式的 Thought
/// </summary>
[ObservableProperty]
private string? _thought;
}
}

@ -0,0 +1,34 @@
namespace AI.Models
{
/// <summary>
/// 工作流步骤状态
/// </summary>
public enum WorkflowStepStatus
{
/// <summary>
/// 待执行
/// </summary>
Pending,
/// <summary>
/// 执行中
/// </summary>
Running,
/// <summary>
/// 已完成
/// </summary>
Completed,
/// <summary>
/// 失败
/// </summary>
Failed,
/// <summary>
/// 已跳过(被回溯清除)
/// </summary>
Skipped
}
}

@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Newtonsoft.Json;
using AI.AgentIntegration;
using Newtonsoft.Json.Linq;
namespace AI.Plugin
{
/// <summary>
/// 应用程序状态插件类,提供对应用程序状态的访问和控制功能
/// </summary>
/// <remarks>
/// 初始化 AppStatePlugin 类的新实例
/// </remarks>
/// <param name="controller">应用程序控制器实例</param>
public class AppStatePlugin(IAppController controller)
{
/// <summary>
/// 应用程序控制器实例
/// </summary>
private readonly IAppController controller = controller;
// ======= 状态读取 =======
/// <summary>
/// 获取当前应用状态的 JSON 快照
/// </summary>
/// <returns>表示当前应用状态的 JSON 字符串</returns>
[KernelFunction]
[Description("获取当前应用状态的 JSON 快照")]
public Task<string> GetSnapshotAsync()
{
return Task.FromResult(controller.GetCurrentState().ToJson());
}
/// <summary>
/// 获取所有打开的标签页列表
/// </summary>
/// <returns>包含所有打开标签页的 JSON 数组字符串</returns>
[KernelFunction]
[Description("获取所有打开的标签页列表")]
public Task<string> GetOpenTabsAsync()
{
var tabs = controller.GetCurrentState().UI.OpenTabs;
return Task.FromResult(JsonConvert.SerializeObject(tabs, Formatting.Indented));
}
/// <summary>
/// 输出当前状态摘要信息
/// </summary>
/// <returns>包含当前视图、活动标签和未保存状态的摘要信息</returns>
[KernelFunction]
[Description("输出当前状态摘要")]
public Task<string> DescribeStateAsync()
{
var state = controller.GetCurrentState();
string summary = $"当前视图: {state.Navigation.CurrentView}, 活动标签: {state.UI.ActiveTab ?? ""}, 未保存: {state.File.HasUnsavedChanges}";
return Task.FromResult(summary);
}
// ======= 文件/标签页操作 =======
/// <summary>
/// 打开指定路径的文件
/// </summary>
/// <param name="path">要打开的文件路径</param>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("打开新文件")]
public async Task<string> OpenFileAsync(string path)
{
var result = await controller.ExecuteAsync(AppAction.CreateOpenFile(path));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 关闭当前活动文件
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("关闭当前活动文件")]
public async Task<string> CloseFileAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.CloseFile));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 关闭所有打开的文件
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("关闭所有文件")]
public async Task<string> CloseAllFilesAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.CloseAllFiles));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 保存当前文件
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("保存当前文件")]
public async Task<string> SaveFileAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.SaveFile));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 保存所有打开的文件
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("保存所有文件")]
public async Task<string> SaveAllAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.SaveAll));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 重新加载文件
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("重新加载文件")]
public async Task<string> ReloadFileAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.ReloadFile));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 切换到指定标签页
/// </summary>
/// <param name="tab">要切换到的标签页标识符</param>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("切换标签页")]
public async Task<string> SwitchTabAsync(string tab)
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.SwitchTab, new Dictionary<string, object> { ["tab"] = tab }));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
// ======= 导航 =======
/// <summary>
/// 导航到指定视图
/// </summary>
/// <param name="view">目标视图名称</param>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("导航到指定视图")]
public async Task<string> NavigateAsync(string view)
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.Navigate, new Dictionary<string, object> { ["view"] = view }));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 执行后退导航操作
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("后退导航")]
public async Task<string> NavigateBackAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.NavigateBack));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 执行前进导航操作
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("前进导航")]
public async Task<string> NavigateForwardAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.NavigateForward));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
// ======= 程序状态控制 =======
/// <summary>
/// 设置应用程序的忙碌状态
/// </summary>
/// <param name="isBusy">指示应用程序是否处于忙碌状态的布尔值</param>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("设置应用忙碌状态")]
public async Task<string> SetBusyAsync(bool isBusy)
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.SetBusy, new Dictionary<string, object> { ["isBusy"] = isBusy }));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 刷新当前视图
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("刷新当前视图")]
public async Task<string> RefreshAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.Refresh));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 退出应用程序
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("退出程序")]
public async Task<string> ExitAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.Exit));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
// ======= 编辑操作 =======
/// <summary>
/// 撤销上一次操作
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("撤销操作")]
public async Task<string> UndoAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.Undo));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
/// <summary>
/// 重做被撤销的操作
/// </summary>
/// <returns>表示操作结果的 JSON 字符串</returns>
[KernelFunction]
[Description("重做操作")]
public async Task<string> RedoAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.Redo));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
// ======= 编辑操作 =======
[KernelFunction]
[Description("添加比例尺")]
public async Task<string> AddScaleBarAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.AddScaleBar));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
[KernelFunction]
[Description("添加图例")]
public async Task<string> AddLegendAsync()
{
var result = await controller.ExecuteAsync(AppAction.CreateAction(AppActionType.AddLegend));
return JsonConvert.SerializeObject(result, Formatting.Indented);
}
}
}

@ -0,0 +1,25 @@
using System.ComponentModel;
using AI.Interface;
using Microsoft.SemanticKernel;
namespace AI.Plugin
{
/// <summary>
/// 表单请求插件AI 需要用户填写参数时调用此插件,在聊天中插入对应表单。
/// </summary>
public class FormRequestPlugin(IFormRequestNotifier notifier)
{
private readonly IFormRequestNotifier _notifier = notifier;
[KernelFunction]
[Description("当需要用户填写表单(如文件路径、参数等)时调用。调用后界面会自动弹出表单供用户填写。")]
[return: Description("通知结果,告知表单是否已发送给用户")]
public string ShowForm(
[Description("表单 ID。可选值gridding-load-xyz散点文件加载卡片自动完成文件选择、数据预览和列头匹配gridding-parameters网格化参数设置卡片自动加载参数用户确认后点击生成完成成图")]
string formId)
{
_notifier.RequestForm(formId);
return $"已向用户展示表单,等待用户填写并提交。";
}
}
}

@ -0,0 +1,45 @@
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using AI.AgentIntegration;
namespace AI.Plugin
{
/// <summary>
/// 导入相关功能插件(井点 / 曲线等从文件导入)
/// </summary>
/// <param name="controller">应用程序控制器实例</param>
public class ImportPlugin(IAppController controller)
{
private readonly IAppController controller = controller;
/// <summary>
/// 从指定路径导入井点数据
/// </summary>
/// <param name="path">井点数据文件路径</param>
/// <returns>成功或失败消息</returns>
[KernelFunction]
[Description("从指定路径导入井点数据")]
public async Task<string> ImportWellPointsAsync(
[Description("井点数据文件路径")] string path)
{
var result = await controller.ExecuteAsync(AppAction.CreateImportWellPoints(path));
return result.Success ? "井点导入成功" : result.Message ?? "井点导入失败";
}
/// <summary>
/// 从指定路径导入井曲线数据
/// </summary>
/// <param name="path">井曲线数据文件路径</param>
/// <returns>成功或失败消息</returns>
[KernelFunction]
[Description("从指定路径导入井曲线数据")]
public async Task<string> ImportWellCurvesAsync(
[Description("井曲线数据文件路径")] string path)
{
var result = await controller.ExecuteAsync(AppAction.CreateImportWellCurves(path));
return result.Success ? "曲线导入成功" : result.Message ?? "曲线导入失败";
}
}
}

@ -0,0 +1,30 @@
using Microsoft.SemanticKernel;
using AI.KnowledgeBase;
using System.ComponentModel;
using System.Threading.Tasks;
namespace AI.Plugin
{
/// <summary>
/// 知识库插件:仅负责根据查询返回知识库内容,不依赖会话或 Store。
/// 将结果写入当前会话 Store 由 <see cref="Filter.KnowledgeBaseStoreFilter"/> 在调用完成后统一处理。
/// </summary>
/// <param name="knowledgeBase">知识库实例</param>
public class KnowledgeBasePlugin(IKnowledgeBase knowledgeBase)
{
private readonly IKnowledgeBase knowledgeBase = knowledgeBase;
/// <summary>
/// 查询知识库中的信息
/// </summary>
/// <param name="query">查询语句</param>
/// <returns>返回匹配的知识条目</returns>
[KernelFunction]
[Description("当需要回答有关特定主题的知识时,搜索知识库。")]
[return: Description("从知识库中检索到的相关信息,用于回答用户的问题。")]
public async Task<string> QueryKnowledgeBaseAsync(string query)
{
return await knowledgeBase.SearchKnowledgeAsync(query);
}
}
}

@ -0,0 +1,209 @@
using System.Linq;
using AI.Models;
namespace AI.Service
{
/// <summary>
/// 管理多个聊天会话;会话存储以 YAML 文件持久化到 Sessions 目录。
/// </summary>
public class ChatSessionManager
{
private readonly Dictionary<string, ChatSession> _sessions = new();
private readonly object _lock = new object();
private ChatSession? _currentSession;
private readonly SessionStorage _storage;
public ChatSessionManager(SessionStorage? sessionStorage = null)
{
_storage = sessionStorage ?? new SessionStorage();
LoadSessionsFromDisk();
}
/// <summary>
/// 会话列表变化事件
/// </summary>
public event EventHandler? SessionsChanged;
/// <summary>
/// 当前会话变化事件
/// </summary>
public event EventHandler<ChatSession?>? CurrentSessionChanged;
/// <summary>
/// 获取所有会话(按更新时间倒序)
/// </summary>
public IEnumerable<ChatSession> GetAllSessions()
{
lock (_lock)
{
return _sessions.Values.OrderByDescending(s => s.UpdatedAt).ToList();
}
}
/// <summary>
/// 根据ID获取会话
/// </summary>
public ChatSession? GetSession(string sessionId)
{
lock (_lock)
{
return _sessions.TryGetValue(sessionId, out var session) ? session : null;
}
}
/// <summary>
/// 获取或设置当前活跃会话
/// </summary>
public ChatSession? CurrentSession
{
get
{
lock (_lock)
{
return _currentSession;
}
}
set
{
lock (_lock)
{
if (_currentSession != value)
{
_currentSession = value;
CurrentSessionChanged?.Invoke(this, value);
}
}
}
}
/// <summary>
/// 创建新会话并设置为当前会话;会订阅存储变更并持久化到 YAML 文件。
/// </summary>
public ChatSession CreateSession(string? title = null)
{
var session = new ChatSession(title: title);
session.Store.StoreChanged += (_, _) => SaveSession(session);
lock (_lock)
{
_sessions[session.Id] = session;
}
SaveSession(session);
CurrentSession = session;
SessionsChanged?.Invoke(this, EventArgs.Empty);
return session;
}
/// <summary>
/// 将会话保存到 YAML 文件
/// </summary>
public void SaveSession(ChatSession session)
{
if (session == null) return;
try
{
_storage.Save(session);
}
catch
{
// 持久化失败不抛,避免影响主流程
}
}
/// <summary>
/// 删除会话(同时删除对应 YAML 文件)
/// </summary>
public bool DeleteSession(string sessionId)
{
ChatSession? deletedSession = null;
lock (_lock)
{
if (_sessions.TryGetValue(sessionId, out deletedSession))
{
_sessions.Remove(sessionId);
if (_currentSession?.Id == sessionId)
{
_currentSession = _sessions.Values.OrderByDescending(s => s.UpdatedAt).FirstOrDefault();
CurrentSessionChanged?.Invoke(this, _currentSession);
}
}
}
if (deletedSession != null)
{
try { _storage.Delete(sessionId); } catch { }
SessionsChanged?.Invoke(this, EventArgs.Empty);
return true;
}
return false;
}
/// <summary>
/// 切换当前会话
/// </summary>
public bool SwitchToSession(string sessionId)
{
var session = GetSession(sessionId);
if (session != null)
{
CurrentSession = session;
return true;
}
return false;
}
/// <summary>
/// 获取当前活跃会话数量
/// </summary>
public int SessionCount
{
get
{
lock (_lock)
{
return _sessions.Count;
}
}
}
/// <summary>
/// 检查会话是否存在
/// </summary>
public bool SessionExists(string sessionId)
{
lock (_lock)
{
return _sessions.ContainsKey(sessionId);
}
}
private void LoadSessionsFromDisk()
{
try
{
var loaded = _storage.LoadAll().ToList();
if (loaded.Count == 0)
{
return;
}
lock (_lock)
{
foreach (var session in loaded)
{
session.Store.StoreChanged += (_, _) => SaveSession(session);
_sessions[session.Id] = session;
}
_currentSession = loaded.FirstOrDefault();
if (_currentSession != null)
{
CurrentSessionChanged?.Invoke(this, _currentSession);
}
}
SessionsChanged?.Invoke(this, EventArgs.Empty);
}
catch
{
// 加载失败则保持空列表,后续可新建会话
}
}
}
}

@ -0,0 +1,23 @@
using System.Threading;
using AI.Models;
namespace AI.Service
{
/// <summary>
/// 当前会话上下文,供在 Kernel 工具调用(如知识库插件)中获取当前 ChatSession以便将结果写入 Store。
/// 由 KernelService 在 AskAsync/AskStreamAsync 开始时设置,结束时清空。
/// </summary>
public static class CurrentSessionContext
{
private static readonly AsyncLocal<ChatSession?> _current = new();
/// <summary>
/// 当前正在处理的聊天会话(仅在 LLM 调用链内有效)。
/// </summary>
public static ChatSession? Current
{
get => _current.Value;
set => _current.Value = value;
}
}
}

@ -0,0 +1,52 @@
using System.Collections.Generic;
using AI.Interface;
using AI.Models.Form;
namespace AI.Service
{
/// <summary>
/// 默认表单注册表,内置「加载散点文件」等表单定义。
/// </summary>
public class FormRegistry : IFormRegistry
{
private readonly Dictionary<string, FormDefinition> _forms = new(StringComparer.OrdinalIgnoreCase);
public FormRegistry()
{
RegisterLoadXyzForm();
}
private void RegisterLoadXyzForm()
{
_forms["gridding-load-xyz"] = new FormDefinition
{
Id = "gridding-load-xyz",
Title = "加载散点文件",
SubmitTarget = "GriddingModuleLoadXyz",
SubmitLabel = "加载",
Fields = new List<FormField>
{
new FormField
{
Id = "path",
Label = "文件路径",
Type = FormFieldType.FilePath,
Required = true,
},
},
};
}
/// <inheritdoc />
public FormDefinition? GetForm(string formId)
{
return _forms.TryGetValue(formId ?? string.Empty, out var def) ? def : null;
}
/// <inheritdoc />
public IEnumerable<FormDefinition> GetAll()
{
return _forms.Values;
}
}
}

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using AI.Interface;
using AI.Models;
using AI.Models.Form;
using AI.ViewModels;
using Avalonia.Threading;
namespace AI.Service
{
/// <summary>
/// 表单请求通知实现:收到请求后在 UI 线程向当前会话插入表单消息。
/// </summary>
public class FormRequestNotifier : IFormRequestNotifier
{
private readonly MainWindowViewModel _mainViewModel;
private readonly IFormRegistry _registry;
private readonly Dictionary<string, Action<ChatSession?>> _handlers;
public const string FormIdLoadXyz = "gridding-load-xyz";
public const string FormIdGriddingParams = "gridding-parameters";
public FormRequestNotifier(MainWindowViewModel mainViewModel, IFormRegistry registry)
{
_mainViewModel = mainViewModel ?? throw new ArgumentNullException(nameof(mainViewModel));
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
_handlers = new Dictionary<string, Action<ChatSession?>>(StringComparer.OrdinalIgnoreCase)
{
[FormIdLoadXyz] = ShowXyzLoadCard,
[FormIdGriddingParams] = ShowGriddingParamCard,
};
}
/// <inheritdoc />
public void RequestForm(string formId)
{
if (string.IsNullOrWhiteSpace(formId))
{
return;
}
var targetSession = CurrentSessionContext.Current;
if (_handlers.TryGetValue(formId, out var handler))
{
handler(targetSession);
return;
}
var definition = _registry.GetForm(formId);
if (definition == null)
{
return;
}
Dispatcher.UIThread.Post(() => _mainViewModel.AddFormMessage(definition, targetSession));
}
private void ShowXyzLoadCard(ChatSession? session)
{
Dispatcher.UIThread.Post(() => _mainViewModel.AddXyzLoadCardMessage(session));
}
private void ShowGriddingParamCard(ChatSession? session)
{
Dispatcher.UIThread.Post(() => _mainViewModel.AddGriddingParamCardMessage(session));
}
}
}

@ -0,0 +1,249 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using AI.Filter;
using AI.KnowledgeBase;
using AI.Plugin;
using AI.AgentIntegration;
using AI.Models;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using AI.Interface;
namespace AI.Service
{
/// <summary>
/// 提供全局唯一的 <see cref="Kernel"/> 实例,负责初始化模型与插件注册。
/// </summary>
public class KernelService : IChatBackend
{
/// <summary>
/// Gets 获取当前应用的全局 <see cref="Kernel"/> 实例。
/// </summary>
public Kernel? Kernel { get; private set; }
/// <summary>
/// 对话 Service
/// </summary>
public IChatCompletionService? ChatCompletionService { get; private set; }
private readonly IKernelBuilder builder;
private bool _isBuilt = false;
/// <summary>
/// 构造函数。从 ai-settings.json 加载配置并初始化 Kernel Builder。
/// </summary>
public KernelService()
{
var settings = AISettings.Load();
builder = Kernel.CreateBuilder();
builder.Services.AddOpenAIChatCompletion(
modelId: settings.ModelId,
apiKey: settings.ApiKey,
endpoint: new Uri(settings.Endpoint));
// 注册 Filter
builder.Services.AddSingleton<IFunctionInvocationFilter, LogFunctionFilter>();
builder.Services.AddSingleton<IFunctionInvocationFilter, KnowledgeBaseStoreFilter>();
builder.Services.AddSingleton<IPromptRenderFilter, LogPromptFilter>();
}
/// <summary>
/// 调用指定插件的指定函数。
/// </summary>
/// <param name="plugin">插件名称。</param>
/// <param name="function">函数名称。</param>
/// <param name="args">调用参数。</param>
/// <returns>函数执行结果文本。</returns>
/// <exception cref="ArgumentException">当插件或函数名称无效时抛出。</exception>
public async Task<string> InvokeAsync(string plugin, string function, KernelArguments? args = null)
{
var result = await Kernel.InvokeAsync(plugin, function, args ?? []);
return result.ToString() ?? string.Empty;
}
/// <summary>
/// 向模型发送自然语言请求,让模型自动决定是否调用已注册的工具。
/// </summary>
/// <param name="input">自然语言输入。</param>
/// <param name="session">聊天会话。如果为 null将创建新的会话。</param>
/// <param name="cancellationToken">取消令牌,用于取消请求或配合超时。</param>
/// <returns>模型的响应文本。</returns>
/// <exception cref="ArgumentNullException">当输入为空时抛出。</exception>
/// <exception cref="InvalidOperationException">当 Kernel 未初始化时抛出。</exception>
public async Task<string> AskAsync(string input, ChatSession? session = null, string? guidePrompt = null, CancellationToken cancellationToken = default)
{
if (Kernel == null || ChatCompletionService == null)
{
throw new InvalidOperationException("KernelService 尚未初始化,请先调用 Build() 方法。");
}
session ??= new ChatSession();
CurrentSessionContext.Current = session;
try
{
session.AddUserMessage(input);
Debug.WriteLine($"Session [{session.Id}]: {input}");
var executionSettings = new OpenAIPromptExecutionSettings()
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
Temperature = 0.3f,
TopP = 0.9f,
};
var historyForLlm = session.GetHistoryForLlm(guidePrompt);
LogHistoryForLlm(session.Id, historyForLlm);
ChatMessageContent response = await ChatCompletionService.GetChatMessageContentAsync(
historyForLlm,
executionSettings,
Kernel!,
cancellationToken);
string responseMessage = response.Content ?? "没有回复";
Debug.WriteLine($"Session [{session.Id}] Response: {responseMessage}");
session.AddAssistantMessage(responseMessage);
return responseMessage;
}
finally
{
CurrentSessionContext.Current = null;
}
}
/// <summary>
/// 流式向模型发送自然语言请求
/// </summary>
/// <param name="input">自然语言输入</param>
/// <param name="session">聊天会话。如果为 null将创建新的会话。</param>
/// <param name="cancellationToken">取消令牌,用于取消请求或配合超时。</param>
/// <exception cref="InvalidOperationException">当 Kernel 未初始化时抛出。</exception>
public async IAsyncEnumerable<string> AskStreamAsync(string input, ChatSession? session = null, string? guidePrompt = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (Kernel == null || ChatCompletionService == null)
{
throw new InvalidOperationException("KernelService 尚未初始化,请先调用 Build() 方法。");
}
session ??= new ChatSession();
CurrentSessionContext.Current = session;
try
{
session.AddUserMessage(input);
var executionSettings = new OpenAIPromptExecutionSettings()
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
Temperature = 0.3f,
TopP = 0.9f,
};
var historyForLlm = session.GetHistoryForLlm(guidePrompt);
LogHistoryForLlm(session.Id, historyForLlm);
var fullResponse = new StringBuilder();
await foreach (var content in ChatCompletionService.GetStreamingChatMessageContentsAsync(historyForLlm, executionSettings, Kernel!, cancellationToken))
{
if (!string.IsNullOrWhiteSpace(content.Content))
{
fullResponse.Append(content.Content);
yield return content.Content;
}
}
// 流式响应完成后,通过 ChatSession 添加助手消息
if (fullResponse.Length > 0)
{
session.AddAssistantMessage(fullResponse.ToString());
}
}
finally
{
CurrentSessionContext.Current = null;
}
}
/// <summary>
/// 初始化 KernelService注册插件并构建 Kernel。
/// 这是推荐的初始化方式,一次性完成所有初始化步骤。
/// </summary>
/// <param name="appController">应用控制器实例</param>
/// <param name="formNotifier">表单请求通知器(可选,为 null 时不注册表单插件)</param>
/// <exception cref="ArgumentNullException">当 appController 为 null 时抛出</exception>
/// <exception cref="InvalidOperationException">当 Kernel 已经构建时抛出</exception>
public void Initialize(IAppController appController, IFormRequestNotifier? formNotifier = null)
{
ArgumentNullException.ThrowIfNull(appController);
if (_isBuilt)
{
throw new InvalidOperationException("Kernel 已经构建,无法重新初始化。");
}
RegisterController(appController, formNotifier);
Build();
_isBuilt = true;
}
/// <summary>
/// 注册带 <see cref="KernelFunctionAttribute"/> 的类,以及 Prompt 工具。
/// 必须在 Build() 之前调用。
/// </summary>
/// <param name="appController">应用实例</param>
/// <param name="formNotifier">表单请求通知器(可选)</param>
/// <exception cref="InvalidOperationException">如果 Kernel 已经构建,则抛出异常</exception>
private void RegisterController(IAppController appController, IFormRequestNotifier? formNotifier = null)
{
var appStatePlugin = new AppStatePlugin(appController);
var importPlugin = new ImportPlugin(appController);
var simpleKnowledgeBase = new SimpleKnowledgeBase();
var knowledgeBasePlugin = new KnowledgeBasePlugin(simpleKnowledgeBase);
builder.Plugins.AddFromObject(appStatePlugin, "AppState");
builder.Plugins.AddFromObject(importPlugin, "Import");
builder.Plugins.AddFromObject(knowledgeBasePlugin, "KnowledgeBase");
if (formNotifier != null)
{
var formRequestPlugin = new FormRequestPlugin(formNotifier);
builder.Plugins.AddFromObject(formRequestPlugin, "FormRequest");
}
}
/// <summary>
/// 构建 Kernel 实例。在调用此方法之前,必须调用 RegisterController() 或 Initialize() 注册插件。
/// </summary>
/// <exception cref="InvalidOperationException">如果插件未注册,则抛出异常</exception>
private void Build()
{
Kernel = builder.Build();
ChatCompletionService = Kernel.GetRequiredService<IChatCompletionService>();
}
private const int MaxLoggedContentLength = 300;
/// <summary>
/// 将发给 LLM 的 ChatHistory 按条输出到 Debug便于排查「到底发给模型什么」。
/// </summary>
private static void LogHistoryForLlm(string sessionId, ChatHistory history)
{
if (history == null)
{
return;
}
Debug.WriteLine($"[Session {sessionId}] History for LLM ({history.Count} messages):");
for (int i = 0; i < history.Count; i++)
{
var msg = history[i];
var role = msg.Role.ToString() ?? "?";
var content = msg.Content ?? string.Empty;
Debug.WriteLine($" [{i}] {role}: {content}");
}
}
}
}

@ -0,0 +1,26 @@
using AI.Interface;
namespace AI.Service
{
/// <summary>
/// 消息通知器,用于在 Filter 和 ViewModel 之间传递工具调用通知
/// </summary>
public class MessageNotifier : IMessageNotifier
{
/// <summary>
/// 当工具调用发生时触发此事件
/// </summary>
public event EventHandler<string>? ToolCallReceived;
/// <summary>
/// 通知添加一条工具调用消息到 UI仅显示不进入 AI 上下文)
/// </summary>
/// <param name="message">消息内容</param>
public void NotifyToolCall(string message)
{
// 触发事件,让订阅者(如 MainWindowViewModel处理
ToolCallReceived?.Invoke(this, message);
}
}
}

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.IO;
using AI.Models;
using AI.Models.Store;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace AI.Service
{
/// <summary>
/// 会话文件持久化 DTOYAML 格式:元数据 + entries
/// </summary>
public class SessionFileDto
{
public string SessionId { get; set; } = string.Empty;
public string Title { get; set; } = "新会话";
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string WorkflowMode { get; set; } = "Ask";
public List<ConversationEntryDto> Entries { get; set; } = new();
}
/// <summary>
/// 会话存储的 YAML 文件持久化:按会话 ID 保存到目录下的 {sessionId}.yaml支持加载全部会话。
/// </summary>
public class SessionStorage
{
private readonly string _directory;
private static readonly ISerializer _serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
private static readonly IDeserializer _deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
public SessionStorage(string? baseDirectory = null)
{
_directory = string.IsNullOrWhiteSpace(baseDirectory)
? Path.Combine(AppContext.BaseDirectory, "Sessions")
: Path.Combine(baseDirectory, "Sessions");
}
/// <summary>
/// 确保会话目录存在
/// </summary>
public void EnsureDirectory()
{
if (!Directory.Exists(_directory))
Directory.CreateDirectory(_directory);
}
/// <summary>
/// 获取指定会话的 YAML 文件路径
/// </summary>
public string GetFilePath(string sessionId)
{
EnsureDirectory();
return Path.Combine(_directory, $"{sessionId}.yaml");
}
/// <summary>
/// 将会话保存为 YAML 文件
/// </summary>
public void Save(ChatSession session)
{
if (session == null) return;
var dto = new SessionFileDto
{
SessionId = session.Id,
Title = session.Title ?? "新会话",
CreatedAt = session.CreatedAt,
UpdatedAt = session.UpdatedAt,
WorkflowMode = session.WorkflowMode.ToString(),
Entries = ConversationEntryMapper.ToDtoList(session.Store.Entries)
};
var yaml = _serializer.Serialize(dto);
var path = GetFilePath(session.Id);
File.WriteAllText(path, yaml);
}
/// <summary>
/// 从 YAML 文件加载一个会话;文件不存在或解析失败时返回 null
/// </summary>
public ChatSession? Load(string sessionId)
{
var path = GetFilePath(sessionId);
if (!File.Exists(path))
{
return null;
}
try
{
var yaml = File.ReadAllText(path);
return LoadFromYaml(yaml);
}
catch
{
return null;
}
}
/// <summary>
/// 从 YAML 字符串反序列化并构造 ChatSession用于单文件加载或测试
/// </summary>
public ChatSession? LoadFromYaml(string yaml)
{
if (string.IsNullOrWhiteSpace(yaml))
{
return null;
}
try
{
var dto = _deserializer.Deserialize<SessionFileDto>(yaml);
if (dto == null)
{
return null;
}
var store = new ConversationStore();
store.LoadFromEntries(dto.Entries ?? new List<ConversationEntryDto>());
var mode = WorkflowMode.Ask;
if (!string.IsNullOrEmpty(dto.WorkflowMode) && Enum.TryParse<WorkflowMode>(dto.WorkflowMode, true, out var m))
{
mode = m;
}
return new ChatSession(
dto.SessionId,
dto.Title,
dto.CreatedAt,
dto.UpdatedAt,
mode,
store);
}
catch
{
return null;
}
}
/// <summary>
/// 从会话目录加载所有会话文件,返回按 UpdatedAt 倒序的会话列表
/// </summary>
public IEnumerable<ChatSession> LoadAll()
{
EnsureDirectory();
var list = new List<ChatSession>();
foreach (var path in Directory.EnumerateFiles(_directory, "*.yaml"))
{
try
{
var yaml = File.ReadAllText(path);
var session = LoadFromYaml(yaml);
if (session != null)
{
list.Add(session);
}
}
catch
{
// 单文件失败则跳过
}
}
list.Sort((a, b) => b.UpdatedAt.CompareTo(a.UpdatedAt));
return list;
}
/// <summary>
/// 删除指定会话的持久化文件
/// </summary>
public void Delete(string sessionId)
{
var path = GetFilePath(sessionId);
if (File.Exists(path))
File.Delete(path);
}
}
}

@ -0,0 +1,29 @@
using AI.AgentIntegration;
using AI.Interface;
using AI.Service;
using Microsoft.Extensions.DependencyInjection;
using AI.ViewModels;
namespace AI
{
public static class ServiceCollectionExtensions
{
public static void AddCommonServices(this IServiceCollection collection)
{
// 注册会话管理器
collection.AddSingleton<ChatSessionManager>();
// 注册聊天后端
collection.AddSingleton<IChatBackend, KernelService>();
// 注册主窗口 ViewModel
collection.AddSingleton<MainWindowViewModel>();
// 注册表单注册表与表单通知器
collection.AddSingleton<IAppController>(_ =>
AppControllerHolder.Instance ?? new NoOpAppController());
collection.AddSingleton<IFormRegistry, FormRegistry>();
collection.AddSingleton<IFormRequestNotifier, FormRequestNotifier>();
}
}
}

@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using AI.Models.SpecialMessages;
namespace AI.Utils
{
/// <summary>
/// CSV 文件解析与预览表格构建
/// </summary>
public static class CsvPreviewHelper
{
/// <summary>
/// 从 CSV 文件路径读取并构建预览表格消息,最多显示指定行数。
/// </summary>
/// <param name="path">CSV 文件路径</param>
/// <param name="maxPreviewRows">最多预览行数,默认 20</param>
/// <param name="title">表格标题,默认「散点文件预览」</param>
/// <returns>成功返回 TableDataMessage失败返回 null</returns>
public static TableDataMessage? TryBuildCsvPreviewTable(string path, int maxPreviewRows = 20, string title = "散点文件预览")
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return null;
string[] lines;
try
{
lines = File.ReadAllLines(path, Encoding.UTF8);
}
catch
{
try
{
lines = File.ReadAllLines(path);
}
catch
{
return null;
}
}
if (lines.Length == 0)
{
return null;
}
var columnNames = ParseCsvLine(lines[0]);
if (columnNames.Count == 0)
{
return null;
}
var table = new TableDataMessage
{
Title = title,
MaxPreviewRows = maxPreviewRows,
};
var dataRows = new List<IEnumerable<string>>();
for (int i = 1; i < lines.Length && dataRows.Count < maxPreviewRows; i++)
{
var cells = ParseCsvLine(lines[i]);
while (cells.Count < columnNames.Count)
{
cells.Add(string.Empty);
}
if (cells.Count > columnNames.Count)
{
cells = cells.Take(columnNames.Count).ToList();
}
dataRows.Add(cells);
}
int totalRowCount = lines.Length - 1;
table.SetData(columnNames, dataRows, totalRowCount);
return table;
}
/// <summary>
/// 简单解析 CSV 行:按逗号拆分,去除首尾空白,不处理引号内逗号。
/// </summary>
public static List<string> ParseCsvLine(string line)
{
if (string.IsNullOrEmpty(line))
return new List<string>();
return line.Split(',').Select(c => c.Trim()).ToList();
}
}
}

@ -0,0 +1,36 @@
namespace AI.Utils
{
/// <summary>
/// 文件工具类
/// </summary>
public static class FileUnit
{
private static readonly string[] SizeUnits =
["B", "KB", "MB", "GB", "TB", "PB"];
/// <summary>
/// 格式化字节数为更易读的字符串格式
/// </summary>
/// <param name="bytes">字符数</param>
/// <returns>易读的字符串格式</returns>
public static string FormatFileSize(long bytes)
{
int unitIndex = 0;
decimal size = bytes;
// 主要是防止 UI 层可能没有做校验这些
if (size < 0)
{
return "0 B";
}
while (size >= 1024 && unitIndex < SizeUnits.Length - 1)
{
size /= 1024;
unitIndex++;
}
return $"{size:F1} {SizeUnits[unitIndex]}";
}
}
}

@ -0,0 +1,146 @@
using System.Collections.Generic;
using System;
using AI.Models.Form;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace AI.Utils
{
/// <summary>
/// 将表单定义序列化为 YAML供 LLM 上下文、追踪、调试等使用。
/// </summary>
public static class FormSchemaYamlGenerator
{
/// <summary>
/// System 消息中「表单 Schema YAML」块的开始标记存在则整块替换避免重复追加
/// </summary>
public const string FormsYamlBlockStart = "\n\n# --- 当前可用表单 Schema (YAML) ---\n";
/// <summary>
/// System 消息中「表单 Schema YAML」块的结束标记。
/// </summary>
public const string FormsYamlBlockEnd = "\n# --- 结束 ---";
/// <summary>
/// 将现有 System 内容与新的表单 YAML 合并:若已包含标记块则替换该块,否则在末尾追加。
/// </summary>
public static string MergeFormsYamlIntoSystemContent(string currentSystemContent, string formsYaml)
{
var block = FormsYamlBlockStart + formsYaml + FormsYamlBlockEnd;
if (string.IsNullOrEmpty(currentSystemContent))
return block.TrimStart('\n');
if (currentSystemContent.Contains(FormsYamlBlockStart))
{
int start = currentSystemContent.IndexOf(FormsYamlBlockStart, StringComparison.Ordinal);
int end = currentSystemContent.IndexOf(FormsYamlBlockEnd, start, StringComparison.Ordinal);
if (end >= 0)
{
end += FormsYamlBlockEnd.Length;
}
else
{
end = currentSystemContent.Length;
}
return currentSystemContent.Remove(start, end - start).Insert(start, block);
}
return currentSystemContent + block;
}
/// <summary>
/// 将当前可用表单定义生成为 YAML 字符串。
/// </summary>
public static string ToYaml(IEnumerable<FormDefinition> forms)
{
var schema = new FormsSchemaYaml
{
Forms = BuildForms(forms)
};
var serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull | DefaultValuesHandling.OmitDefaults)
.Build();
// 保留顶部说明注释(内容不参与结构序列化)。
return "# 当前可用表单 Schema供助手了解可展示的表单与字段\n" + serializer.Serialize(schema);
}
private static List<FormYaml> BuildForms(IEnumerable<FormDefinition>? forms)
{
var list = new List<FormYaml>();
foreach (var form in forms ?? [])
{
var formYaml = new FormYaml
{
Id = form.Id,
Title = form.Title,
SubmitTarget = form.SubmitTarget,
SubmitLabel = form.SubmitLabel,
Fields = BuildFields(form.Fields)
};
list.Add(formYaml);
}
return list;
}
private static List<FieldYaml> BuildFields(IReadOnlyList<FormField>? fields)
{
var list = new List<FieldYaml>();
foreach (var f in fields ?? [])
{
list.Add(new FieldYaml
{
Id = f.Id,
Label = f.Label,
Description = string.IsNullOrEmpty(f.Description) ? null : f.Description,
Type = f.Type.ToString(),
Required = f.Required ? true : null,
Default = f.DefaultValue == null ? null : f.DefaultValue.ToString(),
Min = f.Min,
Max = f.Max,
Step = f.Step,
MaxLength = f.MaxLength,
Placeholder = string.IsNullOrEmpty(f.Placeholder) ? null : f.Placeholder,
Options = (f.Options != null && f.Options.Count > 0) ? new List<string>(f.Options) : null,
DefaultValues = (f.DefaultValues != null && f.DefaultValues.Count > 0) ? new List<string>(f.DefaultValues) : null
});
}
return list;
}
private sealed class FormsSchemaYaml
{
public List<FormYaml> Forms { get; init; } = [];
}
private sealed class FormYaml
{
public string? Id { get; init; }
public string? Title { get; init; }
public string? SubmitTarget { get; init; }
public string? SubmitLabel { get; init; }
public List<FieldYaml> Fields { get; init; } = [];
}
private sealed class FieldYaml
{
public string? Id { get; init; }
public string? Label { get; init; }
public string? Description { get; init; }
public string? Type { get; init; }
// 仅在 true 时输出
public bool? Required { get; init; }
[YamlMember(Alias = "default")]
public string? Default { get; init; }
public double? Min { get; init; }
public double? Max { get; init; }
public double? Step { get; init; }
public int? MaxLength { get; init; }
public string? Placeholder { get; init; }
public List<string>? Options { get; init; }
public List<string>? DefaultValues { get; init; }
}
}
}

@ -0,0 +1,636 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using AI.Interface;
using AI.Models;
using AI.Models.Form;
using AI.Models.SpecialMessages;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace AI.Utils
{
public static class SpecialMessageTypes
{
public const string Form = "Form";
public const string ParameterSet = "ParameterSet";
public const string Table = "Table";
public const string ColumnMatch = "ColumnMatch";
public const string WorkflowStatus = "WorkflowStatus";
public const string KnowledgeBase = "KnowledgeBase";
public const string XyzLoadCard = "XyzLoadCard";
public const string GriddingParamCard = "GriddingParamCard";
}
/// <summary>
/// 根据 type + YAML 载荷还原 ISpecialMessage。Form 需 IFormRegistry 根据 formId 取 FormDefinition。
/// formId 缺失或 Definition 不存在时降级为占位或只读文本。
/// </summary>
public static class SpecialMessageDeserializer
{
public static ISpecialMessage? Deserialize(string type, string yamlPayload, IFormRegistry? formRegistry = null)
{
if (string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(yamlPayload))
{
return null;
}
try
{
return type.Trim() switch
{
SpecialMessageTypes.Form => DeserializeForm(yamlPayload, formRegistry),
SpecialMessageTypes.ParameterSet => DeserializeParameterSet(yamlPayload),
SpecialMessageTypes.Table => DeserializeTable(yamlPayload),
SpecialMessageTypes.ColumnMatch => DeserializeColumnMatch(yamlPayload),
SpecialMessageTypes.WorkflowStatus => DeserializeWorkflowStatus(yamlPayload),
SpecialMessageTypes.KnowledgeBase => DeserializeKnowledgeBase(yamlPayload),
SpecialMessageTypes.XyzLoadCard => DeserializeXyzLoadCard(yamlPayload),
SpecialMessageTypes.GriddingParamCard => DeserializeGriddingParamCard(yamlPayload),
_ => null
};
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[SpecialMessageDeserializer] 反序列化失败: type={type}, error={ex}");
return null;
}
}
private static readonly IReadOnlyDictionary<string, (string TypeName, string[] DetailKeys)> ShortDescriptionTable =
new Dictionary<string, (string, string[])>(StringComparer.OrdinalIgnoreCase)
{
[SpecialMessageTypes.Form] = ("表单", new[] { "title", "formId" }),
[SpecialMessageTypes.ParameterSet] = ("参数集", new[] { "title" }),
[SpecialMessageTypes.Table] = ("表格", new[] { "title" }),
[SpecialMessageTypes.ColumnMatch] = ("列匹配", new[] { "title" }),
[SpecialMessageTypes.WorkflowStatus] = ("工作流状态", new[] { "title" }),
[SpecialMessageTypes.XyzLoadCard] = ("散点文件加载卡片", new[] { "filePath" }),
[SpecialMessageTypes.GriddingParamCard] = ("网格化参数卡片", Array.Empty<string>()),
};
/// <summary>
/// 从特殊消息的 type + YAML 载荷生成发给 LLM 的短描述(如「[已展示:表单「加载散点」]」),便于模型理解上下文。
/// 对于 KnowledgeBase 类型,返回完整 rawContent以便后续轮次 LLM 能看到查到的知识。
/// </summary>
public static string GetShortDescriptionForLlm(string type, string payload)
{
if (string.IsNullOrWhiteSpace(type))
{
return "[已展示:未知]";
}
if (string.IsNullOrWhiteSpace(payload))
{
return $"[已展示:{type}]";
}
try
{
var dict = FromYaml(payload);
if (dict == null)
{
return $"[已展示:{type}]";
}
// 知识库条目:把完整内容带给 LLM作为后续对话的参考
if (string.Equals(type.Trim(), SpecialMessageTypes.KnowledgeBase, StringComparison.OrdinalIgnoreCase))
{
var rawContent = GetString(dict, "rawContent");
return string.IsNullOrWhiteSpace(rawContent)
? "[已查询知识库,无匹配内容]"
: $"[来自知识库的参考]{System.Environment.NewLine}{rawContent}";
}
string typeName = type;
string? detail = null;
if (ShortDescriptionTable.TryGetValue(type.Trim(), out var row))
{
typeName = row.TypeName;
foreach (var key in row.DetailKeys)
{
var value = GetString(dict, key);
if (!string.IsNullOrWhiteSpace(value))
{
detail = value;
break;
}
}
}
if (!string.IsNullOrWhiteSpace(detail))
{
return $"[已展示:{typeName}「{detail}」]";
}
return $"[已展示:{typeName}]";
}
catch
{
return $"[已展示:{type}]";
}
}
private static ISpecialMessage? DeserializeForm(string yaml, IFormRegistry? formRegistry)
{
var dict = FromYaml(yaml);
if (dict == null)
{
return null;
}
string formId = GetString(dict, "formId") ?? string.Empty;
FormDefinition? definition = null;
if (formRegistry != null && !string.IsNullOrEmpty(formId))
{
definition = formRegistry.GetForm(formId);
}
if (definition == null)
{
return null; // 降级:无 FormRegistry 或 formId 不存在时不还原表单卡片
}
var formMsg = new FormRequestMessage(definition);
if (TryGet(dict, "id", out var idObj))
{
formMsg.Id = idObj?.ToString() ?? formMsg.Id;
}
if (TryGet(dict, "submitLabel", out var submitObj))
{
formMsg.SubmitLabel = submitObj?.ToString() ?? formMsg.SubmitLabel;
}
if (TryGet(dict, "fields", out var fieldsObj) && fieldsObj is IEnumerable<object> fieldsList)
{
foreach (var fe in fieldsList)
{
if (fe is not Dictionary<object, object> fieldDict)
{
continue;
}
string fid = GetString(fieldDict, "id") ?? string.Empty;
var entry = formMsg.FieldsWithValues.FirstOrDefault(f => string.Equals(f.Id, fid, StringComparison.OrdinalIgnoreCase));
if (entry == null)
{
continue;
}
entry.CurrentValue = GetString(fieldDict, "currentValue") ?? string.Empty;
if (TryGet(fieldDict, "selectedValues", out var svObj) && svObj is IEnumerable<object> svList)
{
entry.SelectedValues.Clear();
foreach (var v in svList)
{
if (v?.ToString() is string s)
{
entry.SelectedValues.Add(s);
}
}
}
}
}
return formMsg;
}
private static ISpecialMessage? DeserializeParameterSet(string yaml)
{
var dict = FromYaml(yaml);
if (dict == null)
{
return null;
}
var param = new ParameterSetMessage();
if (TryGet(dict, "id", out var idObj))
{
param.Id = idObj?.ToString() ?? param.Id;
}
param.Title = GetString(dict, "title") ?? param.Title;
if (TryGet(dict, "items", out var itemsObj) && itemsObj is IEnumerable<object> itemsList)
{
foreach (var item in itemsList)
{
if (item is not Dictionary<object, object> id)
{
continue;
}
var name = GetString(id, "name") ?? string.Empty;
var valueText = GetString(id, "valueText") ?? string.Empty;
var description = GetString(id, "description") ?? string.Empty;
var fieldTypeStr = GetString(id, "fieldType");
var fieldType = ParameterSetFieldType.Text;
if (!string.IsNullOrEmpty(fieldTypeStr) && Enum.TryParse<ParameterSetFieldType>(fieldTypeStr, true, out var ft))
{
fieldType = ft;
}
var options = new List<string>();
if (TryGet(id, "options", out var optObj) && optObj is IEnumerable<object> optList)
{
foreach (var o in optList)
{
if (o?.ToString() is string s)
{
options.Add(s);
}
}
}
param.Items.Add(new ParameterSetItem
{
Name = name,
ValueText = valueText,
Description = description,
FieldType = fieldType,
Options = options
});
}
}
return param;
}
private static ISpecialMessage? DeserializeTable(string yaml)
{
var dict = FromYaml(yaml);
if (dict == null)
{
return null;
}
var table = new TableDataMessage();
if (TryGet(dict, "id", out var idObj))
{
table.Id = idObj?.ToString() ?? table.Id;
}
table.Title = GetString(dict, "title") ?? table.Title;
table.TotalRowCount = GetInt(dict, "totalRowCount") ?? 0;
table.MaxPreviewRows = GetInt(dict, "maxPreviewRows") ?? 50;
var columnNames = new List<string>();
if (TryGet(dict, "columnNames", out var cnObj) && cnObj is IEnumerable<object> cnList)
{
foreach (var c in cnList)
{
if (c?.ToString() is string s)
{
columnNames.Add(s);
}
}
}
var rows = new List<IEnumerable<string>>();
if (TryGet(dict, "rows", out var rowsObj) && rowsObj is IEnumerable<object> rowsList)
{
foreach (var rowObj in rowsList)
{
if (rowObj is IEnumerable<object> cells)
{
rows.Add(cells.Select(c => c?.ToString() ?? string.Empty).ToList());
}
}
}
table.SetData(columnNames, rows, table.TotalRowCount > 0 ? table.TotalRowCount : rows.Count);
return table;
}
private static ISpecialMessage? DeserializeColumnMatch(string yaml)
{
var dict = FromYaml(yaml);
if (dict == null)
{
return null;
}
var col = new ColumnMatchMessage();
if (TryGet(dict, "id", out var idObj))
{
col.Id = idObj?.ToString() ?? col.Id;
}
col.Title = GetString(dict, "title") ?? col.Title;
var required = new List<string>();
if (TryGet(dict, "requiredColumns", out var rcObj) && rcObj is IEnumerable<object> rcList)
{
foreach (var c in rcList)
{
if (c?.ToString() is string s)
{
required.Add(s);
}
}
}
var preview = new List<string>();
if (TryGet(dict, "previewColumns", out var pcObj) && pcObj is IEnumerable<object> pcList)
{
foreach (var c in pcList)
{
if (c?.ToString() is string s)
{
preview.Add(s);
}
}
}
col.SetColumns(required, preview);
if (TryGet(dict, "mappings", out var mapObj) && mapObj is IEnumerable<object> mapList)
{
var mappings = new List<KeyValuePair<string, string>>();
foreach (var m in mapList)
{
if (m is not Dictionary<object, object> md)
{
continue;
}
string req = GetString(md, "requiredColumn") ?? string.Empty;
string mat = GetString(md, "matchedColumn") ?? string.Empty;
mappings.Add(new KeyValuePair<string, string>(req, mat));
}
col.SetMappings(mappings);
}
return col;
}
private static ISpecialMessage? DeserializeKnowledgeBase(string yaml)
{
var dict = FromYaml(yaml);
if (dict == null)
{
return null;
}
var kb = new KnowledgeBaseMessage();
if (TryGet(dict, "id", out var idObj))
{
kb.Id = idObj?.ToString() ?? kb.Id;
}
kb.KnowledgeId = GetString(dict, "knowledgeId") ?? string.Empty;
kb.Version = GetString(dict, "version") ?? "1";
kb.TopicKey = GetString(dict, "topicKey") ?? string.Empty;
kb.Query = GetString(dict, "query") ?? string.Empty;
kb.RawContent = GetString(dict, "rawContent") ?? string.Empty;
return kb;
}
private static ISpecialMessage? DeserializeWorkflowStatus(string yaml)
{
var dict = FromYaml(yaml);
if (dict == null)
{
return null;
}
var wf = new WorkflowStatusMessage();
if (TryGet(dict, "id", out var idObj))
{
wf.Id = idObj?.ToString() ?? wf.Id;
}
wf.Title = GetString(dict, "title") ?? wf.Title;
if (TryGet(dict, "steps", out var stepsObj) && stepsObj is IEnumerable<object> stepsList)
{
var steps = new ObservableCollection<WorkflowStepModel>();
foreach (var s in stepsList)
{
if (s is not Dictionary<object, object> sd)
{
continue;
}
var step = new WorkflowStepModel
{
Id = GetString(sd, "id") ?? string.Empty,
DisplayName = GetString(sd, "displayName") ?? string.Empty,
Order = GetInt(sd, "order") ?? 0,
OutputResult = GetString(sd, "outputResult"),
Thought = GetString(sd, "thought")
};
var statusStr = GetString(sd, "status");
if (!string.IsNullOrEmpty(statusStr) && Enum.TryParse<WorkflowStepStatus>(statusStr, true, out var st))
{
step.Status = st;
}
steps.Add(step);
}
wf.Steps = steps;
}
return wf;
}
private static ISpecialMessage? DeserializeXyzLoadCard(string yaml)
{
var dict = FromYaml(yaml);
if (dict == null) return null;
var card = new AI.Models.SpecialMessages.XyzLoadCardMessage();
if (TryGet(dict, "id", out var idObj))
card.Id = idObj?.ToString() ?? card.Id;
card.FilePath = GetString(dict, "filePath") ?? string.Empty;
card.MatchButtonLabel = GetString(dict, "matchButtonLabel") ?? "确认匹配";
var phaseInt = GetInt(dict, "phase") ?? 0;
card.Phase = (AI.Models.SpecialMessages.XyzLoadPhase)phaseInt;
// 还原表格预览
if (TryGet(dict, "tablePreview", out var tableObj) && tableObj is Dictionary<object, object> tableDict)
{
var table = new AI.Models.SpecialMessages.TableDataMessage();
table.Title = GetString(tableDict, "title") ?? "数据预览";
table.TotalRowCount = GetInt(tableDict, "totalRowCount") ?? 0;
table.MaxPreviewRows = GetInt(tableDict, "maxPreviewRows") ?? 50;
var colNames = new List<string>();
if (TryGet(tableDict, "columnNames", out var cnObj) && cnObj is IEnumerable<object> cnList)
foreach (var c in cnList)
if (c?.ToString() is string s) colNames.Add(s);
var rows = new List<IEnumerable<string>>();
if (TryGet(tableDict, "rows", out var rowsObj) && rowsObj is IEnumerable<object> rowsList)
foreach (var rowObj in rowsList)
if (rowObj is IEnumerable<object> cells)
rows.Add(cells.Select(c => c?.ToString() ?? string.Empty).ToList());
table.SetData(colNames, rows, table.TotalRowCount > 0 ? table.TotalRowCount : rows.Count);
card.TablePreview = table;
}
// 还原列头匹配字段
if (TryGet(dict, "columnMatchFields", out var fieldsObj) && fieldsObj is IEnumerable<object> fieldsList)
{
var definitionTitle = GetString(dict, "columnMatchDefinitionTitle") ?? "列头匹配";
var definitionTarget = GetString(dict, "columnMatchDefinitionSubmitTarget") ?? "GriddingModuleMatchColumns";
var formFields = new List<AI.Models.Form.FormField>();
foreach (var fe in fieldsList)
{
if (fe is not Dictionary<object, object> fd) continue;
var fid = GetString(fd, "id") ?? string.Empty;
var label = GetString(fd, "label") ?? fid;
var currentValue = GetString(fd, "currentValue") ?? string.Empty;
var options = new List<string>();
if (TryGet(fd, "options", out var optObj) && optObj is IEnumerable<object> optList)
foreach (var o in optList)
if (o?.ToString() is string s) options.Add(s);
formFields.Add(new AI.Models.Form.FormField
{
Id = fid,
Label = label,
Type = AI.Models.Form.FormFieldType.Choice,
Options = options,
Required = true,
});
card.ColumnMatchFields.Add(new AI.Models.Form.FormFieldEntry
{
Id = fid,
Label = label,
Type = AI.Models.Form.FormFieldType.Choice,
Options = options,
Required = true,
CurrentValue = currentValue,
});
}
if (formFields.Count > 0)
{
card.ColumnMatchDefinition = new AI.Models.Form.FormDefinition
{
Id = "gridding-match-columns",
Title = definitionTitle,
SubmitTarget = definitionTarget,
SubmitLabel = "确认匹配",
Fields = formFields,
};
}
}
return card;
}
private static ISpecialMessage? DeserializeGriddingParamCard(string yaml)
{
var dict = FromYaml(yaml);
if (dict == null) return null;
var card = new AI.Models.SpecialMessages.GriddingParamCardMessage();
if (TryGet(dict, "id", out var idObj))
card.Id = idObj?.ToString() ?? card.Id;
var phaseInt = GetInt(dict, "phase") ?? 0;
card.Phase = (AI.Models.SpecialMessages.GriddingParamCardPhase)phaseInt;
card.StatusMessage = GetString(dict, "statusMessage") ?? string.Empty;
card.GenerateButtonLabel = GetString(dict, "generateButtonLabel") ?? "生成";
if (TryGet(dict, "items", out var itemsObj) && itemsObj is IEnumerable<object> itemsList)
{
foreach (var item in itemsList)
{
if (item is not Dictionary<object, object> id) continue;
var name = GetString(id, "name") ?? string.Empty;
var valueText = GetString(id, "valueText") ?? string.Empty;
var description = GetString(id, "description") ?? string.Empty;
var fieldTypeStr = GetString(id, "fieldType");
var fieldType = AI.Models.SpecialMessages.ParameterSetFieldType.Text;
if (!string.IsNullOrEmpty(fieldTypeStr) &&
Enum.TryParse<AI.Models.SpecialMessages.ParameterSetFieldType>(fieldTypeStr, true, out var ft))
fieldType = ft;
var options = new List<string>();
if (TryGet(id, "options", out var optObj) && optObj is IEnumerable<object> optList)
foreach (var o in optList)
if (o?.ToString() is string s) options.Add(s);
card.Items.Add(new AI.Models.SpecialMessages.ParameterSetItem
{
Name = name,
ValueText = valueText,
Description = description,
FieldType = fieldType,
Options = options
});
}
}
return card;
}
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
private static Dictionary<object, object>? FromYaml(string yaml)
{
try
{
return YamlDeserializer.Deserialize<Dictionary<object, object>>(yaml);
}
catch (YamlException ex)
{
System.Diagnostics.Debug.WriteLine($"[SpecialMessageDeserializer] YAML 解析失败: {ex.Message}");
return null;
}
}
private static string? GetString(Dictionary<object, object> dict, string key)
{
if (!TryGet(dict, key, out var v))
{
return null;
}
return v?.ToString();
}
private static int? GetInt(Dictionary<object, object> dict, string key)
{
if (!TryGet(dict, key, out var v))
{
return null;
}
if (v is int i)
{
return i;
}
if (v is long l)
{
return (int)l;
}
if (v != null && int.TryParse(v.ToString(), out var n))
{
return n;
}
return null;
}
private static bool TryGet(Dictionary<object, object> dict, string key, out object? value)
{
value = null;
var keyEq = StringComparer.OrdinalIgnoreCase;
foreach (var kv in dict)
{
if (keyEq.Equals(kv.Key?.ToString(), key))
{
value = kv.Value;
return true;
}
}
return false;
}
}
}

@ -0,0 +1,220 @@
using System.Collections.Generic;
using System.Linq;
using AI.Models;
using AI.Models.Form;
using AI.Models.SpecialMessages;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace AI.Utils
{
/// <summary>
/// 将 ISpecialMessage 序列化为 YAML 载荷字符串(仅载荷,不含 typetype 由 SpecialEntry 持有)。
/// </summary>
public static class SpecialMessageSerializer
{
public static string Serialize(AI.Models.SpecialMessages.ISpecialMessage message)
{
if (message == null)
{
return string.Empty;
}
return message switch
{
FormRequestMessage form => SerializeForm(form),
ParameterSetMessage param => SerializeParameterSet(param),
TableDataMessage table => SerializeTable(table),
ColumnMatchMessage col => SerializeColumnMatch(col),
WorkflowStatusMessage wf => SerializeWorkflowStatus(wf),
KnowledgeBaseMessage kb => SerializeKnowledgeBase(kb),
AI.Models.SpecialMessages.XyzLoadCardMessage xyz => SerializeXyzLoadCard(xyz),
AI.Models.SpecialMessages.GriddingParamCardMessage gp => SerializeGriddingParamCard(gp),
_ => string.Empty
};
}
private static string SerializeForm(FormRequestMessage form)
{
var dict = new Dictionary<string, object>
{
["id"] = form.Id,
["formId"] = form.Definition.Id,
["title"] = form.Definition.Title,
["submitLabel"] = form.SubmitLabel,
["status"] = form.SubmitLabel
};
var fields = new List<Dictionary<string, object>>();
foreach (var entry in form.FieldsWithValues)
{
var f = new Dictionary<string, object>
{
["id"] = entry.Id,
["currentValue"] = entry.CurrentValue ?? string.Empty
};
if (entry.Type == FormFieldType.MultiSelect && entry.SelectedValues.Count > 0)
{
f["selectedValues"] = entry.SelectedValues.ToList();
}
fields.Add(f);
}
dict["fields"] = fields;
return ToYaml(dict);
}
private static string SerializeParameterSet(ParameterSetMessage param)
{
var dict = new Dictionary<string, object>
{
["id"] = param.Id,
["title"] = param.Title,
["items"] = param.Items.Select(i => new Dictionary<string, object>
{
["name"] = i.Name,
["valueText"] = i.ValueText ?? string.Empty,
["description"] = i.Description ?? string.Empty,
["fieldType"] = i.FieldType.ToString(),
["options"] = i.Options ?? new List<string>()
}).ToList()
};
return ToYaml(dict);
}
private static string SerializeTable(TableDataMessage table)
{
var dict = new Dictionary<string, object>
{
["id"] = table.Id,
["title"] = table.Title,
["columnNames"] = table.ColumnNames.ToList(),
["totalRowCount"] = table.TotalRowCount,
["maxPreviewRows"] = table.MaxPreviewRows,
["rows"] = table.Rows.Select(r => r.Cells.ToList()).ToList()
};
return ToYaml(dict);
}
private static string SerializeColumnMatch(ColumnMatchMessage col)
{
var dict = new Dictionary<string, object>
{
["id"] = col.Id,
["title"] = col.Title,
["requiredColumns"] = col.RequiredColumns.ToList(),
["previewColumns"] = col.PreviewColumns.ToList(),
["mappings"] = col.Mappings.Select(m => new Dictionary<string, string>
{
["requiredColumn"] = m.RequiredColumn,
["matchedColumn"] = m.MatchedColumn
}).ToList<object>()
};
return ToYaml(dict);
}
private static string SerializeWorkflowStatus(WorkflowStatusMessage wf)
{
var dict = new Dictionary<string, object>
{
["id"] = wf.Id,
["title"] = wf.Title,
["steps"] = (wf.Steps ?? new System.Collections.ObjectModel.ObservableCollection<WorkflowStepModel>())
.Select(s => new Dictionary<string, object>
{
["id"] = s.Id,
["displayName"] = s.DisplayName ?? string.Empty,
["order"] = s.Order,
["status"] = s.Status.ToString(),
["outputResult"] = s.OutputResult?.ToString() ?? string.Empty,
["thought"] = s.Thought ?? string.Empty
}).ToList()
};
return ToYaml(dict);
}
private static string SerializeKnowledgeBase(KnowledgeBaseMessage kb)
{
var dict = new Dictionary<string, object>
{
["id"] = kb.Id,
["knowledgeId"] = kb.KnowledgeId,
["version"] = kb.Version ?? "1",
["topicKey"] = kb.TopicKey ?? string.Empty,
["query"] = kb.Query ?? string.Empty,
["rawContent"] = kb.RawContent ?? string.Empty
};
return ToYaml(dict);
}
private static string SerializeXyzLoadCard(AI.Models.SpecialMessages.XyzLoadCardMessage xyz)
{
var dict = new Dictionary<string, object>
{
["id"] = xyz.Id,
["phase"] = (int)xyz.Phase,
["filePath"] = xyz.FilePath ?? string.Empty,
["matchButtonLabel"] = xyz.MatchButtonLabel ?? "确认匹配",
};
if (xyz.TablePreview != null)
{
var tableDict = new Dictionary<string, object>
{
["title"] = xyz.TablePreview.Title,
["columnNames"] = xyz.TablePreview.ColumnNames.ToList(),
["totalRowCount"] = xyz.TablePreview.TotalRowCount,
["maxPreviewRows"] = xyz.TablePreview.MaxPreviewRows,
["rows"] = xyz.TablePreview.Rows.Select(r => r.Cells.ToList()).ToList(),
};
dict["tablePreview"] = tableDict;
}
if (xyz.ColumnMatchFields.Count > 0)
{
dict["columnMatchFields"] = xyz.ColumnMatchFields.Select(f => new Dictionary<string, object>
{
["id"] = f.Id,
["label"] = f.Label,
["currentValue"] = f.CurrentValue ?? string.Empty,
["options"] = f.Options ?? new List<string>(),
}).ToList();
}
if (xyz.ColumnMatchDefinition != null)
{
dict["columnMatchDefinitionTitle"] = xyz.ColumnMatchDefinition.Title ?? string.Empty;
dict["columnMatchDefinitionSubmitTarget"] = xyz.ColumnMatchDefinition.SubmitTarget ?? string.Empty;
}
return ToYaml(dict);
}
private static string SerializeGriddingParamCard(AI.Models.SpecialMessages.GriddingParamCardMessage gp)
{
var dict = new Dictionary<string, object>
{
["id"] = gp.Id,
["phase"] = (int)gp.Phase,
["statusMessage"] = gp.StatusMessage ?? string.Empty,
["generateButtonLabel"] = gp.GenerateButtonLabel ?? "生成",
["items"] = gp.Items.Select(i => new Dictionary<string, object>
{
["name"] = i.Name,
["valueText"] = i.ValueText ?? string.Empty,
["description"] = i.Description ?? string.Empty,
["fieldType"] = i.FieldType.ToString(),
["options"] = i.Options ?? new List<string>()
}).ToList()
};
return ToYaml(dict);
}
private static string ToYaml(Dictionary<string, object> dict)
{
var serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
return serializer.Serialize(dict);
}
}
}

@ -0,0 +1,36 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using AI.ViewModels;
using Control = Avalonia.Controls.Control;
namespace AI
{
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
{
return null;
}
var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
var control = (Control)Activator.CreateInstance(type)!;
control.DataContext = data;
return control;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
}

@ -0,0 +1,79 @@
using AI.Models;
namespace AI.ViewModels
{
/// <summary>
/// 会话列表项的 ViewModel
/// </summary>
public class ChatSessionItemViewModel : ViewModelBase
{
private readonly ChatSession _session;
private bool _isSelected;
public ChatSessionItemViewModel(ChatSession session)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
}
/// <summary>
/// 会话ID
/// </summary>
public string Id => _session.Id;
/// <summary>
/// 会话标题
/// </summary>
public string Title
{
get => _session.Title;
set
{
if (_session.Title != value)
{
_session.Title = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt => _session.CreatedAt;
/// <summary>
/// 最后更新时间
/// </summary>
public DateTime UpdatedAt => _session.UpdatedAt;
/// <summary>
/// 消息数量
/// </summary>
public int MessageCount => _session.MessageCount;
/// <summary>
/// 是否被选中
/// </summary>
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
/// <summary>
/// 获取底层会话对象
/// </summary>
public ChatSession Session => _session;
/// <summary>
/// 更新显示信息(当会话变化时调用)
/// </summary>
public void Refresh()
{
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(UpdatedAt));
OnPropertyChanged(nameof(MessageCount));
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace AI.ViewModels
{
public class ViewModelBase : ObservableObject
{
}
}

@ -0,0 +1,367 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:AI.ViewModels"
xmlns:md="clr-namespace:LiveMarkdown.Avalonia;assembly=LiveMarkdown.Avalonia"
xmlns:models="using:AI.Models"
xmlns:converters="using:AI.Converters"
xmlns:storage="using:Avalonia.Platform.Storage"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="1200" d:DesignHeight="700"
x:Class="AI.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="AI"
Width="1200" Height="700">
<Grid Background="#FFFFFF">
<Grid.ColumnDefinitions>
<!-- 左侧会话列表 -->
<ColumnDefinition Width="280" />
<!-- 分隔线 -->
<ColumnDefinition Width="1" />
<!-- 右侧聊天区域 -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 左侧会话列表区域 -->
<Grid Grid.Column="0" Background="#F5F5F5">
<Border Padding="10,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 新建会话按钮 -->
<Button Grid.Row="0"
Content="创建新会话"
Background="Transparent"
Foreground="#333333"
Height="36"
CornerRadius="10"
HorizontalAlignment="Stretch"
FontWeight="SemiBold"
Command="{Binding CreateNewSessionCommand}" />
<!-- 空白间隔 -->
<Border Grid.Row="1" Height="10" />
<!-- 所有对话标签 -->
<TextBlock Grid.Row="2"
Text="所有对话"
Foreground="#999999"
Height="32"
FontSize="14"
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom" />
<!-- 会话列表 -->
<ListBox x:Name="SessionsListBox"
Grid.Row="3"
ItemsSource="{Binding Sessions}"
SelectedItem="{Binding SelectedSessionItem}"
Background="Transparent"
BorderThickness="0"
Margin="0,0,0,0">
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:ChatSessionItemViewModel">
<Grid Margin="8,0" Height="36">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="{Binding Title}"
FontSize="14"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"
Foreground="#333333" />
<Button Grid.Row="0"
Grid.Column="1"
Content=""
Width="20"
Height="20"
FontSize="16"
Background="Transparent"
Foreground="#999999"
BorderThickness="0"
Padding="0"
Click="OnDeleteSessionClick"
Tag="{Binding Id}"
ToolTip.Tip="删除会话"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center">
<Button.Styles>
<Style Selector="Button">
<Setter Property="CornerRadius" Value="10" />
</Style>
</Button.Styles>
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="0" />
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="#EAEAEA" />
<Setter Property="CornerRadius" Value="10" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#EFEFEF" />
<Setter Property="CornerRadius" Value="10" />
</Style>
</ListBox.Styles>
</ListBox>
</Grid>
</Border>
</Grid>
<!-- 分隔线 -->
<Border Grid.Column="1" Background="#E0E0E0" />
<!-- 右侧聊天区域 -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 聊天消息区域 -->
<ScrollViewer x:Name="ChatMessagesScrollViewer" Grid.Row="0" Margin="20,20,20,0">
<ItemsControl x:Name="ChatMessagesItemsControl"
ItemsSource="{Binding ChatMessages}"
ItemTemplate="{StaticResource MessageTemplateSelector}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
<!-- 聊天输入区域 -->
<Grid Grid.Row="1" Margin="20,0,20,20">
<!-- 输入框容器 -->
<Border Background="#F7F8FC"
CornerRadius="20"
Padding="12,8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 显示待上传文件列表 -->
<WrapPanel Grid.Row="0"
Orientation="Horizontal"
Margin="0,0,0,5">
<ItemsControl ItemsSource="{Binding PendingFiles}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="models:PendingFileModel">
<Grid Margin="2,2">
<Border Background="White"
CornerRadius="8"
BorderBrush="#E1E3EA"
BorderThickness="1"
Padding="8,4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 文件图标 -->
<Viewbox Grid.Row="0" Grid.Column="0"
Grid.RowSpan="2"
Width="16" Height="16"
VerticalAlignment="Center"
Margin="0,0,8,0">
<Path Fill="#666666"
Data="M886.7 247.6L713.4 73.4c-6-6-14.2-9.4-22.7-9.4H192c-35.3 0-64 28.7-64 64v768c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64V270.2c0-8.5-3.3-16.6-9.3-22.6zM832 864c0 17.7-14.3 32-32 32H224c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h384v160c0 35.3 28.7 64 64 64h160v512zM704 288c-17.7 0-32-14.3-32-32V128l160 160H704z M671 672H287c-17.7 0-32 14.3-32 32s14.3 32 32 32h384c17.7 0 32-14.3 32-32s-14.3-32-32-32zM287 480c-17.7 0-32 14.3-32 32s14.3 32 32 32h384c17.7 0 32-14.3 32-32s-14.3-32-32-32H287zM287 352h192c17.7 0 32-14.3 32-32s-14.3-32-32-32H287c-17.7 0-32 14.3-32 32s14.3 32 32 32z" />
</Viewbox>
<TextBlock Grid.Row="0" Grid.Column="1"
Text="{Binding Name}"
VerticalAlignment="Center"
FontSize="12"
Foreground="#333333"
Margin="0,0,16,0"
TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Text="{Binding FormattedSize}"
VerticalAlignment="Center"
FontSize="10"
Foreground="#666666"
Margin="0,0,16,0"/>
</Grid>
</Border>
<!-- 删除按钮 -->
<Button Content=""
Width="14"
Height="14"
FontSize="10"
Background="Transparent"
CornerRadius="7"
Foreground="#999999"
BorderThickness="0"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Margin="0,3,3,0"
Command="{Binding $parent[Window].DataContext.RemovePendingFileCommand}"
CommandParameter="{Binding}">
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</WrapPanel>
<!-- 文本输入框 -->
<TextBox Grid.Row="1"
Name="ChatInputBox"
Background="Transparent"
Foreground="#333333"
BorderThickness="0"
AcceptsReturn="True"
TextWrapping="Wrap"
MaxHeight="100"
MinHeight="30"
Margin="0,0,0,5"
Text="{Binding ChatInput, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Styles>
<Style Selector="TextBox">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="TextBox:focus">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="TextBox /template/ Border#border">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="TextBox:focus /template/ Border#border">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="TextBox:focus /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
</TextBox.Styles>
</TextBox>
<!-- 底部控制栏 -->
<Grid Grid.Row="2" Height="36">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- 文件上传按钮 -->
<Button Grid.Column="0"
Content="+"
Background="Transparent"
Foreground="#666666"
BorderThickness="0"
FontSize="22"
Width="32"
Height="32"
Padding="0"
Margin="0,0,8,0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding AddPendingFileCommand}">
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#E8E8E8" />
</Style>
</Button.Styles>
</Button>
<!-- 模式切换 ComboBox -->
<ComboBox Grid.Column="1"
SelectedIndex="{Binding CurrentWorkflowModeIndex}"
Width="100"
Height="32"
Padding="8,4"
Background="Transparent"
BorderThickness="1"
BorderBrush="#E0E0E0"
CornerRadius="8"
FontSize="13">
<ComboBoxItem Content="Ask" />
<ComboBoxItem Content="Workflow" />
</ComboBox>
<!-- 发送按钮 -->
<Button Grid.Column="3"
Background="#007AFF"
Foreground="White"
CornerRadius="8"
Width="70"
Height="32"
FontSize="14"
FontWeight="SemiBold"
Content="发送"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
BorderThickness="0"
Command="{Binding SendMessageCommand}">
</Button>
<!-- 取消工作流按钮(仅工作流模式且执行中时可用) -->
<Button Grid.Column="4"
Background="#FF3B30"
Foreground="White"
CornerRadius="8"
Width="70"
Height="32"
FontSize="14"
FontWeight="SemiBold"
Content="取消"
Margin="8,0,0,0"
IsVisible="{Binding IsWorkflowRunning}"
IsEnabled="{Binding IsWorkflowRunning}"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
BorderThickness="0"
Command="{Binding CancelWorkflowCommand}">
</Button>
</Grid>
</Grid>
</Border>
</Grid>
</Grid>
</Grid>
</Window>

@ -0,0 +1,93 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using AI.ViewModels;
namespace AI.Views
{
public partial class MainWindow : Window
{
private bool _isUserAtBottom = true; // 跟踪用户是否在底部
private const double ScrollThreshold = 50.0; // 距离底部的阈值(像素)
public MainWindow()
{
InitializeComponent();
DataContextChanged += MainWindow_DataContextChanged;
}
private void MainWindow_DataContextChanged(object? sender, EventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
vm.RequestScrollToBottom += ViewModel_RequestScrollToBottom;
// 订阅文件选择请求事件
vm.RequestFileSelection += async () =>
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "选择文件",
AllowMultiple = false,
FileTypeFilter = new[] { FilePickerFileTypes.All }
});
return files;
};
}
// 初始化滚动视图的滚动事件监听
if (ChatMessagesScrollViewer != null)
{
ChatMessagesScrollViewer.ScrollChanged += ChatMessagesScrollViewer_ScrollChanged;
}
}
private void ChatMessagesScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (sender is ScrollViewer scrollViewer)
{
// 检查用户是否在底部(考虑一个小的阈值,避免浮点数精度问题)
var offset = scrollViewer.Offset.Y;
var extent = scrollViewer.Extent.Height;
var viewport = scrollViewer.Viewport.Height;
// 处理边界情况:如果内容高度小于视口高度,认为在底部
if (extent <= viewport)
{
_isUserAtBottom = true;
return;
}
var distanceFromBottom = extent - (offset + viewport);
// 如果距离底部小于阈值,认为用户在底部
_isUserAtBottom = distanceFromBottom <= ScrollThreshold;
}
}
private void ViewModel_RequestScrollToBottom()
{
Dispatcher.UIThread.InvokeAsync(() =>
{
// 只在用户已经在底部时才自动滚动
// 这样如果用户向上滚动查看历史消息,就不会被强制拉回底部
if (_isUserAtBottom)
{
ChatMessagesScrollViewer.ScrollToEnd();
}
});
}
private void OnDeleteSessionClick(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is string sessionId)
{
if (DataContext is MainWindowViewModel vm)
{
vm.DeleteSessionCommand.Execute(sessionId);
}
}
}
}
}

@ -0,0 +1,572 @@
using AI.Interface;
using AI.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace AI.Workflow
{
/// <summary>
/// 步骤状态更新事件参数
/// </summary>
public class StepStatusUpdatedEventArgs : EventArgs
{
public PlanStep Step { get; set; } = null!;
public int StepIndex { get; set; }
public int TotalSteps { get; set; }
}
/// <summary>
/// 规划完成事件参数
/// </summary>
public class PlanCompletedEventArgs : EventArgs
{
public Plan Plan { get; set; } = null!;
}
/// <summary>
/// 步骤思考更新事件参数
/// </summary>
public class StepThoughtUpdatedEventArgs : EventArgs
{
public PlanStep Step { get; set; } = null!;
public int StepIndex { get; set; }
public string Thought { get; set; } = string.Empty;
}
/// <summary>
/// ReAct 工作流 Agent 配置
/// </summary>
public class AgentConfig
{
/// <summary>
/// 每个步骤最大重试次数默认2次
/// </summary>
public int MaxRetries { get; set; } = 2;
/// <summary>
/// 遇到错误是否继续执行默认false
/// </summary>
public bool ContinueOnError { get; set; } = false;
/// <summary>
/// ReAct 模式最大迭代次数默认3次
/// </summary>
public int MaxReActIterations { get; set; } = 3;
/// <summary>
/// 重试前等待时间毫秒默认1000ms
/// </summary>
public int RetryDelayMs { get; set; } = 1000;
}
/// <summary>
/// ReAct 工作流 Agent - 使用 Planner + ReAct 模式执行任务
/// </summary>
public class Agent
{
#region 事件定义
/// <summary>
/// 步骤状态更新事件
/// </summary>
public event EventHandler<StepStatusUpdatedEventArgs>? StepStatusUpdated;
/// <summary>
/// 规划完成事件
/// </summary>
public event EventHandler<PlanCompletedEventArgs>? PlanCompleted;
/// <summary>
/// 步骤思考更新事件
/// </summary>
public event EventHandler<StepThoughtUpdatedEventArgs>? StepThoughtUpdated;
#endregion
#region 属性
/// <summary>
/// Agent 配置
/// </summary>
public AgentConfig Config { get; }
/// <summary>
/// Agent 唯一标识符
/// </summary>
public string Id { get; }
/// <summary>
/// Agent 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// Agent 描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// Agent 状态
/// </summary>
public WorkflowStatus Status { get; private set; } = WorkflowStatus.NotStarted;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; private set; }
/// <summary>
/// 开始执行时间
/// </summary>
public DateTime? StartedAt { get; private set; }
/// <summary>
/// 完成时间
/// </summary>
public DateTime? CompletedAt { get; private set; }
/// <summary>
/// 错误信息(如果执行失败)
/// </summary>
public string? ErrorMessage { get; private set; }
/// <summary>
/// 执行快照(用于回溯和调试)
/// </summary>
public List<WorkflowState> StateSnapshots { get; } = new List<WorkflowState>();
/// <summary>
/// 规划结果
/// </summary>
public Plan? Plan { get; private set; }
/// <summary>
/// 工作流状态
/// </summary>
public WorkflowState? State { get; private set; }
/// <summary>
/// 当前执行的步骤索引
/// </summary>
public int CurrentStepIndex { get; private set; } = -1;
#endregion
#region 内部组件
/// <summary>
/// Planner - 规划组件
/// </summary>
private Planner? _planner;
/// <summary>
/// Executor - 执行组件
/// </summary>
private Executor? _executor;
/// <summary>
/// 聊天后端
/// </summary>
private readonly IChatBackend _chatBackend;
#endregion
#region 构造函数
/// <summary>
/// 创建 ReAct 工作流 Agent
/// </summary>
/// <param name="chatBackend">聊天后端</param>
/// <param name="config">配置(可选,使用默认配置)</param>
public Agent(IChatBackend chatBackend, AgentConfig? config = null)
{
_chatBackend = chatBackend ?? throw new ArgumentNullException(nameof(chatBackend));
Config = config ?? new AgentConfig();
Id = Guid.NewGuid().ToString();
CreatedAt = DateTime.Now;
UpdatedAt = DateTime.Now;
}
#endregion
#region 公共方法
/// <summary>
/// 重置 Agent 状态
/// </summary>
public void Reset()
{
Status = WorkflowStatus.NotStarted;
StartedAt = null;
CompletedAt = null;
UpdatedAt = DateTime.Now;
ErrorMessage = null;
CurrentStepIndex = -1;
StateSnapshots.Clear();
Plan = null;
State = null;
_planner = null;
_executor = null;
Debug.WriteLine($"ReActWorkflowAgent '{Name}' has been reset");
}
/// <summary>
/// 获取当前执行的步骤
/// </summary>
/// <returns>当前步骤,如果未开始或已完成则返回 null</returns>
public PlanStep? GetCurrentStep()
{
if (Plan == null || CurrentStepIndex < 0 || CurrentStepIndex >= Plan.Steps.Count)
{
return null;
}
return Plan.Steps[CurrentStepIndex];
}
/// <summary>
/// 执行工作流Planner + ReAct 模式,硬流程控制)
/// </summary>
/// <param name="goal">任务目标</param>
/// <param name="cancellationToken">取消令牌</param>
/// <param name="timeout">超时时间,为 null 表示不限制</param>
/// <returns>执行结果</returns>
public async Task<string> ExecuteAsync(string goal, CancellationToken cancellationToken = default, TimeSpan? timeout = null)
{
using var timeoutCts = timeout.HasValue ? new CancellationTokenSource(timeout.Value) : null;
var linkedCts = timeoutCts != null
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
var effectiveToken = linkedCts?.Token ?? cancellationToken;
try
{
Status = WorkflowStatus.Running;
StartedAt = DateTime.Now;
UpdatedAt = DateTime.Now;
ErrorMessage = null;
CurrentStepIndex = -1;
Debug.WriteLine($"开始 ReActWorkflowAgent 执行: {goal}");
// 初始化状态
State = new WorkflowState
{
WorkflowId = Id
};
// 初始化组件
InitializeComponents();
// 1. Planner 规划LLM 负责)
Debug.WriteLine("【阶段1】Planner 规划中...");
Plan = await _planner!.PlanAsync(goal, effectiveToken);
Debug.WriteLine($"规划完成,共 {Plan.Steps.Count} 个步骤");
// 触发规划完成事件
PlanCompleted?.Invoke(this, new PlanCompletedEventArgs { Plan = Plan });
// 2. Runtime 执行循环:代码决定执行流程
string result = await ExecuteRuntimeLoop(effectiveToken);
// 代码控制:所有步骤执行完成
Status = WorkflowStatus.Completed;
CompletedAt = DateTime.Now;
CurrentStepIndex = -1;
Debug.WriteLine($"\nReActWorkflowAgent 执行完成");
return result;
}
catch (OperationCanceledException ex)
{
Status = WorkflowStatus.Cancelled;
CompletedAt = DateTime.Now;
UpdatedAt = DateTime.Now;
ErrorMessage = ex.CancellationToken.IsCancellationRequested ? "已取消" : "已超时";
Debug.WriteLine($"ReActWorkflowAgent 已取消或超时: {ErrorMessage}");
throw;
}
catch (Exception ex)
{
Status = WorkflowStatus.Failed;
CompletedAt = DateTime.Now;
UpdatedAt = DateTime.Now;
ErrorMessage = ex.Message;
Debug.WriteLine($"ReActWorkflowAgent 执行失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 将 PlanStep 转换为 WorkflowStepModel
/// </summary>
public static WorkflowStepModel ConvertToWorkflowStepModel(PlanStep planStep)
{
var model = new WorkflowStepModel
{
Id = planStep.Id,
DisplayName = planStep.Description,
Order = planStep.Order
};
// 转换状态
model.Status = planStep.Status switch
{
PlanStepStatus.Pending => WorkflowStepStatus.Pending,
PlanStepStatus.Running => WorkflowStepStatus.Running,
PlanStepStatus.Completed => WorkflowStepStatus.Completed,
PlanStepStatus.Failed => WorkflowStepStatus.Failed,
_ => WorkflowStepStatus.Pending
};
// 存储结果
if (!string.IsNullOrEmpty(planStep.Result))
{
model.OutputResult = planStep.Result;
}
return model;
}
/// <summary>
/// 将 Plan 转换为 WorkflowStepModel 集合
/// </summary>
public static ObservableCollection<WorkflowStepModel> ConvertPlanToStepModels(Plan plan)
{
var models = new ObservableCollection<WorkflowStepModel>();
foreach (var step in plan.Steps.OrderBy(s => s.Order))
{
models.Add(ConvertToWorkflowStepModel(step));
}
return models;
}
#endregion
#region 私有方法
/// <summary>
/// 初始化组件
/// </summary>
private void InitializeComponents()
{
// 初始化 Planner
_planner = new Planner(_chatBackend);
// 初始化 Executor
_executor = new Executor(_chatBackend, State!, (stepId, thought) =>
{
// 找到对应的步骤并触发 Thought 更新事件
if (Plan != null)
{
var step = Plan.Steps.FirstOrDefault(s => s.Id == stepId);
if (step != null)
{
var stepIndex = Plan.Steps.IndexOf(step);
OnStepThoughtUpdated(step, stepIndex, thought);
}
}
});
}
/// <summary>
/// Runtime 执行循环:控制步骤的执行流程
/// </summary>
private async Task<string> ExecuteRuntimeLoop(CancellationToken cancellationToken)
{
if (Plan == null || _executor == null)
{
throw new InvalidOperationException("Plan 或 Executor 未初始化");
}
var contextBuilder = new StringBuilder();
var executionResults = new List<ExecutionResult>();
// 代码控制:逐步执行每个步骤
for (int i = 0; i < Plan.Steps.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var step = Plan.Steps[i];
CurrentStepIndex = i;
State!.CurrentNodeId = step.Id;
Debug.WriteLine($"\n【代码控制】执行步骤 {i + 1}/{Plan.Steps.Count}: {step.Description}");
// 触发步骤开始事件
OnStepStatusUpdated(step, i, Plan.Steps.Count);
// 构建上下文(之前的步骤结果)
string? context = contextBuilder.Length > 0 ? contextBuilder.ToString() : null;
// 代码控制:重试机制
ExecutionResult? result = null;
int retryCount = 0;
bool stepSuccess = false;
while (retryCount <= Config.MaxRetries && !stepSuccess)
{
try
{
// ReAct 模式执行LLM 负责具体执行)
result = await _executor.ExecuteStepWithReActAsync(step, context, Config.MaxReActIterations, cancellationToken);
if (result.IsSuccess)
{
stepSuccess = true;
executionResults.Add(result);
// 更新上下文
contextBuilder.AppendLine($"步骤 {step.Order}: {step.Description}");
contextBuilder.AppendLine($"结果: {result.Content}");
if (!string.IsNullOrEmpty(result.Observation))
{
contextBuilder.AppendLine($"观察: {result.Observation}");
}
contextBuilder.AppendLine();
Debug.WriteLine($"步骤 {step.Order} 执行成功");
// 触发步骤完成事件
OnStepStatusUpdated(step, i, Plan.Steps.Count);
}
else if (result.ShouldRetry && retryCount < Config.MaxRetries)
{
retryCount++;
Debug.WriteLine($"步骤 {step.Order} 执行失败,重试 {retryCount}/{Config.MaxRetries}");
await Task.Delay(Config.RetryDelayMs, cancellationToken);
}
else
{
stepSuccess = false;
break;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
Debug.WriteLine($"步骤 {step.Order} 执行异常: {ex.Message}");
if (retryCount < Config.MaxRetries)
{
retryCount++;
Debug.WriteLine($"异常重试 {retryCount}/{Config.MaxRetries}");
await Task.Delay(Config.RetryDelayMs, cancellationToken);
}
else
{
result = new ExecutionResult
{
IsSuccess = false,
ErrorMessage = ex.Message,
ShouldRetry = false
};
stepSuccess = false;
break;
}
}
}
// 代码控制:错误处理决策
if (!stepSuccess)
{
if (Config.ContinueOnError)
{
Debug.WriteLine($"步骤 {step.Order} 执行失败,但继续执行后续步骤");
step.Status = PlanStepStatus.Failed;
step.ErrorMessage = result?.ErrorMessage ?? "执行失败";
// 即使失败也更新上下文
contextBuilder.AppendLine($"步骤 {step.Order}: {step.Description} [失败]");
contextBuilder.AppendLine($"错误: {result?.ErrorMessage ?? ""}");
contextBuilder.AppendLine();
// 触发步骤失败事件
OnStepStatusUpdated(step, i, Plan.Steps.Count);
}
else
{
// 代码决定:失败时停止
Debug.WriteLine($"步骤 {step.Order} 执行失败,停止工作流");
Status = WorkflowStatus.Failed;
ErrorMessage = $"步骤 {step.Order} 执行失败: {result?.ErrorMessage ?? ""}";
throw new Exception(ErrorMessage);
}
}
// 保存状态快照
StateSnapshots.Add((WorkflowState)State!.Clone());
UpdatedAt = DateTime.Now;
}
// 构建最终结果
return BuildFinalResult(contextBuilder, executionResults);
}
/// <summary>
/// 构建最终结果
/// </summary>
private string BuildFinalResult(StringBuilder contextBuilder, List<ExecutionResult> executionResults)
{
var result = new StringBuilder();
result.AppendLine("=== 工作流执行结果 ===");
result.AppendLine();
result.AppendLine("执行摘要:");
result.AppendLine($"总步骤数: {Plan?.Steps.Count ?? 0}");
result.AppendLine($"成功步骤: {executionResults.Count(r => r.IsSuccess)}");
result.AppendLine($"失败步骤: {executionResults.Count(r => !r.IsSuccess)}");
result.AppendLine();
result.AppendLine("详细执行过程:");
result.Append(contextBuilder.ToString());
return result.ToString();
}
/// <summary>
/// 触发步骤状态更新事件
/// </summary>
protected virtual void OnStepStatusUpdated(PlanStep step, int stepIndex, int totalSteps)
{
StepStatusUpdated?.Invoke(this, new StepStatusUpdatedEventArgs
{
Step = step,
StepIndex = stepIndex,
TotalSteps = totalSteps
});
}
/// <summary>
/// 触发步骤思考更新事件
/// </summary>
protected virtual void OnStepThoughtUpdated(PlanStep step, int stepIndex, string thought)
{
StepThoughtUpdated?.Invoke(this, new StepThoughtUpdatedEventArgs
{
Step = step,
StepIndex = stepIndex,
Thought = thought
});
}
#endregion
}
}

@ -0,0 +1,365 @@
using AI.Interface;
using AI.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace AI.Workflow
{
/// <summary>
/// 执行结果
/// </summary>
public class ExecutionResult
{
/// <summary>
/// 是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 执行结果内容
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// 错误信息(如果失败)
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// 是否需要重试
/// </summary>
public bool ShouldRetry { get; set; }
/// <summary>
/// 观察信息ReAct 模式)
/// </summary>
public string? Observation { get; set; }
}
/// <summary>
/// 步骤执行器 - 使用 ReAct 模式执行步骤:推理-行动-观察
/// </summary>
public class Executor
{
private readonly IChatBackend _chatBackend;
private readonly ChatSession _executionSession;
private readonly WorkflowState _state;
private readonly List<ReActStep> _reactHistory = new();
private readonly Action<string, string>? _onThoughtUpdated;
public Executor(IChatBackend chatBackend, WorkflowState state, Action<string, string>? onThoughtUpdated = null)
{
_chatBackend = chatBackend;
_state = state;
_executionSession = new ChatSession();
_onThoughtUpdated = onThoughtUpdated;
// 初始化 ReAct 提示词
_executionSession.AddSystemMessage(GetReActPrompt());
}
/// <summary>
/// 使用 ReAct 模式执行单个步骤
/// </summary>
/// <param name="step">要执行的步骤</param>
/// <param name="context">上下文信息(之前的步骤结果)</param>
/// <param name="maxIterations">最大迭代次数默认3次</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>执行结果</returns>
public async Task<ExecutionResult> ExecuteStepWithReActAsync(PlanStep step, string? context = null, int maxIterations = 3, CancellationToken cancellationToken = default)
{
step.Status = PlanStepStatus.Running;
step.StartedAt = DateTime.Now;
step.ErrorMessage = null;
try
{
Debug.WriteLine($"ReAct 模式执行步骤 {step.Order}: {step.Description}");
var reactStep = new ReActStep
{
StepId = step.Id,
StepDescription = step.Description,
Context = context
};
// ReAct 循环:推理 -> 行动 -> 观察
for (int iteration = 0; iteration < maxIterations; iteration++)
{
cancellationToken.ThrowIfCancellationRequested();
Debug.WriteLine($"ReAct 迭代 {iteration + 1}/{maxIterations}");
// 1. 推理Think
string thought = await ThinkAsync(step, context, reactStep, cancellationToken);
reactStep.Thoughts.Add(thought);
Debug.WriteLine($"推理: {thought}");
// 通知 Thought 更新
_onThoughtUpdated?.Invoke(step.Id, thought);
// 2. 行动Act
string action = await ActAsync(step, thought, context, reactStep, cancellationToken);
reactStep.Actions.Add(action);
Debug.WriteLine($"行动: {action}");
// 3. 观察Observe
string observation = await ObserveAsync(action, reactStep, cancellationToken);
reactStep.Observations.Add(observation);
Debug.WriteLine($"观察: {observation}");
// 检查是否完成
if (IsTaskComplete(observation, reactStep))
{
step.Status = PlanStepStatus.Completed;
step.Result = BuildFinalResult(reactStep);
step.CompletedAt = DateTime.Now;
_reactHistory.Add(reactStep);
Debug.WriteLine($"步骤 {step.Order} 执行完成");
return new ExecutionResult
{
IsSuccess = true,
Content = step.Result,
Observation = observation
};
}
}
// 达到最大迭代次数,返回当前结果
step.Status = PlanStepStatus.Completed;
step.Result = BuildFinalResult(reactStep);
step.CompletedAt = DateTime.Now;
_reactHistory.Add(reactStep);
return new ExecutionResult
{
IsSuccess = true,
Content = step.Result,
Observation = "达到最大迭代次数"
};
}
catch (Exception ex)
{
step.Status = PlanStepStatus.Failed;
step.ErrorMessage = ex.Message;
step.CompletedAt = DateTime.Now;
Debug.WriteLine($"步骤 {step.Order} 执行失败: {ex.Message}");
return new ExecutionResult
{
IsSuccess = false,
ErrorMessage = ex.Message,
ShouldRetry = true
};
}
}
/// <summary>
/// 推理阶段:分析当前情况和需要采取的行动
/// </summary>
private async Task<string> ThinkAsync(PlanStep step, string? context, ReActStep reactStep, CancellationToken cancellationToken)
{
var prompt = new StringBuilder();
prompt.AppendLine("【推理阶段】");
prompt.AppendLine($"当前步骤:{step.Description}");
if (!string.IsNullOrEmpty(context))
{
prompt.AppendLine($"\n上下文信息");
prompt.AppendLine(context);
}
if (reactStep.Thoughts.Count > 0)
{
prompt.AppendLine($"\n之前的推理");
for (int i = 0; i < reactStep.Thoughts.Count; i++)
{
prompt.AppendLine($"{i + 1}. {reactStep.Thoughts[i]}");
}
}
if (reactStep.Observations.Count > 0)
{
prompt.AppendLine($"\n之前的观察结果");
for (int i = 0; i < reactStep.Observations.Count; i++)
{
prompt.AppendLine($"{i + 1}. {reactStep.Observations[i]}");
}
}
prompt.AppendLine($"\n请分析当前情况思考需要采取什么行动来完成这个步骤。");
string thought = await _chatBackend.AskAsync(prompt.ToString(), _executionSession, null, cancellationToken);
return thought;
}
/// <summary>
/// 行动阶段:执行具体的操作
/// </summary>
private async Task<string> ActAsync(PlanStep step, string thought, string? context, ReActStep reactStep, CancellationToken cancellationToken)
{
var prompt = new StringBuilder();
prompt.AppendLine("【行动阶段】");
prompt.AppendLine($"步骤描述:{step.Description}");
prompt.AppendLine($"\n推理结果{thought}");
if (!string.IsNullOrEmpty(context))
{
prompt.AppendLine($"\n上下文信息");
prompt.AppendLine(context);
}
if (reactStep.Actions.Count > 0)
{
prompt.AppendLine($"\n之前的行动");
for (int i = 0; i < reactStep.Actions.Count; i++)
{
prompt.AppendLine($"{i + 1}. {reactStep.Actions[i]}");
}
}
prompt.AppendLine($"\n请根据推理结果执行具体的行动来完成这个步骤。");
string action = await _chatBackend.AskAsync(prompt.ToString(), _executionSession, null, cancellationToken);
return action;
}
/// <summary>
/// 观察阶段:观察行动的结果
/// </summary>
private async Task<string> ObserveAsync(string action, ReActStep reactStep, CancellationToken cancellationToken)
{
var prompt = new StringBuilder();
prompt.AppendLine("【观察阶段】");
prompt.AppendLine($"刚才的行动:{action}");
prompt.AppendLine($"\n请观察行动的结果评估是否完成了步骤目标。如果未完成说明还需要做什么。");
string observation = await _chatBackend.AskAsync(prompt.ToString(), _executionSession, null, cancellationToken);
return observation;
}
/// <summary>
/// 判断任务是否完成
/// </summary>
private bool IsTaskComplete(string observation, ReActStep reactStep)
{
// 简单的完成判断:如果观察结果包含完成相关的关键词
var completionKeywords = new[] { "完成", "成功", "已达成", "已完成", "done", "completed", "success" };
var lowerObservation = observation.ToLower();
foreach (var keyword in completionKeywords)
{
if (lowerObservation.Contains(keyword.ToLower()))
{
return true;
}
}
return false;
}
/// <summary>
/// 构建最终结果
/// </summary>
private string BuildFinalResult(ReActStep reactStep)
{
var result = new StringBuilder();
result.AppendLine("执行过程:");
for (int i = 0; i < reactStep.Thoughts.Count; i++)
{
result.AppendLine($"\n迭代 {i + 1}:");
result.AppendLine($"推理: {reactStep.Thoughts[i]}");
if (i < reactStep.Actions.Count)
{
result.AppendLine($"行动: {reactStep.Actions[i]}");
}
if (i < reactStep.Observations.Count)
{
result.AppendLine($"观察: {reactStep.Observations[i]}");
}
}
return result.ToString();
}
/// <summary>
/// 获取 ReAct 提示词
/// </summary>
private string GetReActPrompt()
{
return @"你是一个使用 ReAct推理-行动-观察)模式执行任务的智能助手。
ReAct
ReAct
1. **Think**
-
-
-
-
-
2. **Act**
-
-
-
- 便
3. **Observe**
-
-
-
-
-
- ****
- ****
- ****
- ****
-
-
-
-
-
-
-
-
-
-
-
-
ReAct ";
}
/// <summary>
/// ReAct 步骤记录
/// </summary>
internal class ReActStep
{
public string StepId { get; set; } = string.Empty;
public string StepDescription { get; set; } = string.Empty;
public string? Context { get; set; }
public List<string> Thoughts { get; } = new List<string>();
public List<string> Actions { get; } = new List<string>();
public List<string> Observations { get; } = new List<string>();
}
}
}

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
namespace AI.Workflow
{
/// <summary>
/// 规划结果
/// </summary>
public class Plan
{
/// <summary>
/// 规划唯一标识符
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 规划目标
/// </summary>
public string Goal { get; set; } = string.Empty;
/// <summary>
/// 规划步骤列表
/// </summary>
public List<PlanStep> Steps { get; } = new List<PlanStep>();
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.Now;
}
/// <summary>
/// 规划步骤
/// </summary>
public class PlanStep
{
/// <summary>
/// 步骤唯一标识符
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 步骤序号
/// </summary>
public int Order { get; set; }
/// <summary>
/// 步骤描述
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 步骤状态
/// </summary>
public PlanStepStatus Status { get; set; } = PlanStepStatus.Pending;
/// <summary>
/// 执行结果
/// </summary>
public string? Result { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// 开始执行时间
/// </summary>
public DateTime? StartedAt { get; set; }
/// <summary>
/// 完成时间
/// </summary>
public DateTime? CompletedAt { get; set; }
}
/// <summary>
/// 规划步骤状态
/// </summary>
public enum PlanStepStatus
{
/// <summary>
/// 待执行
/// </summary>
Pending,
/// <summary>
/// 执行中
/// </summary>
Running,
/// <summary>
/// 已完成
/// </summary>
Completed,
/// <summary>
/// 执行失败
/// </summary>
Failed
}
}

@ -0,0 +1,215 @@
using AI.Interface;
using AI.Models;
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace AI.Workflow
{
/// <summary>
/// Planner - 负责规划任务,将任务分解成步骤
/// </summary>
public class Planner
{
private readonly IChatBackend _chatBackend;
private readonly ChatSession _planningSession;
public Planner(IChatBackend chatBackend)
{
_chatBackend = chatBackend;
_planningSession = new ChatSession();
// 初始化规划提示词
_planningSession.AddSystemMessage(GetPlanningPrompt());
}
/// <summary>
/// 规划任务
/// </summary>
/// <param name="goal">任务目标</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>规划结果</returns>
public async Task<Plan> PlanAsync(string goal, CancellationToken cancellationToken = default)
{
var plan = new Plan
{
Goal = goal
};
try
{
// 请求 Planner 生成规划
string planningPrompt = $"请为以下任务制定详细的执行计划:\n\n任务{goal}\n\n请将任务分解为具体的步骤每个步骤应该是可执行的、明确的。";
string response = await _chatBackend.AskAsync(planningPrompt, _planningSession, null, cancellationToken);
// 解析规划结果
ParsePlanFromResponse(response, plan);
Debug.WriteLine($"Planner 生成了 {plan.Steps.Count} 个步骤");
}
catch (Exception ex)
{
Debug.WriteLine($"Planner 规划失败: {ex.Message}");
throw;
}
return plan;
}
/// <summary>
/// 从响应中解析规划步骤
/// </summary>
private void ParsePlanFromResponse(string response, Plan plan)
{
// 尝试解析 JSON 格式
if (TryParseJsonPlan(response, plan))
{
return;
}
// 尝试解析列表格式1. 步骤1 2. 步骤2 ...
if (TryParseListPlan(response, plan))
{
return;
}
// 如果都解析失败,将整个响应作为单个步骤
plan.Steps.Add(new PlanStep
{
Order = 1,
Description = response.Trim()
});
}
/// <summary>
/// 尝试解析 JSON 格式的规划
/// </summary>
private bool TryParseJsonPlan(string response, Plan plan)
{
try
{
// 尝试提取 JSON 部分
var jsonMatch = Regex.Match(response, @"\{[\s\S]*\}");
if (!jsonMatch.Success)
{
return false;
}
var json = jsonMatch.Value;
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.TryGetProperty("steps", out var stepsElement) && stepsElement.ValueKind == JsonValueKind.Array)
{
int order = 1;
foreach (var stepElement in stepsElement.EnumerateArray())
{
var step = new PlanStep
{
Order = order++,
Description = stepElement.GetProperty("description").GetString() ?? string.Empty
};
plan.Steps.Add(step);
}
return true;
}
}
catch
{
// JSON 解析失败,继续尝试其他格式
}
return false;
}
/// <summary>
/// 尝试解析列表格式的规划
/// </summary>
private bool TryParseListPlan(string response, Plan plan)
{
// 匹配各种列表格式1. 2. 3. 或 - 或 * 等
var patterns = new[]
{
@"^\s*(\d+)\.\s+(.+)$", // 1. 步骤
@"^\s*[-*]\s+(.+)$", // - 步骤 或 * 步骤
@"^\s*(\d+)\)\s+(.+)$" // 1) 步骤
};
var lines = response.Split('\n', StringSplitOptions.RemoveEmptyEntries);
int order = 1;
foreach (var line in lines)
{
var trimmedLine = line.Trim();
if (string.IsNullOrWhiteSpace(trimmedLine))
continue;
foreach (var pattern in patterns)
{
var match = Regex.Match(trimmedLine, pattern);
if (match.Success)
{
string description = match.Groups.Count > 2 ? match.Groups[2].Value : match.Groups[1].Value;
plan.Steps.Add(new PlanStep
{
Order = order++,
Description = description.Trim()
});
break;
}
}
}
return plan.Steps.Count > 0;
}
/// <summary>
/// 获取规划提示词
/// </summary>
private string GetPlanningPrompt()
{
return @"你是一个专业的任务规划专家,擅长将复杂任务分解为清晰、可执行的步骤序列。
1. ****
2. ****
3. ****
4. ****
5. ****
- 使""""""""""""
- "" XYZ ""
- 使
- 20-50
使 JSON
{
""steps"": [
{""description"": ""1""},
{""description"": ""2""},
...
]
}
JSON使
1. 1
2. 2
...
-
-
-
- ";
}
}
}

@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace AI.Workflow
{
/// <summary>
/// 工作流状态,在整个流程中唯一,我们使用它来负责节点之间传递数据,避免每个节点添加输入输出接口
/// </summary>
public class WorkflowState : ICloneable
{
private readonly Dictionary<string, object> _slots = new();
/// <summary>
/// 关联的工作流 ID
/// </summary>
public string? WorkflowId { get; set; }
/// <summary>
/// 当前执行的节点 ID
/// </summary>
public string? CurrentNodeId { get; set; }
/// <summary>
/// 状态创建时间
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>
/// 获取指定键的值
/// </summary>
/// <typeparam name="T">值类型</typeparam>
/// <param name="key">键名</param>
/// <returns>值</returns>
/// <exception cref="KeyNotFoundException">当键不存在时抛出</exception>
public T Get<T>(string key) where T : class
{
if (!_slots.TryGetValue(key, out var value))
{
throw new KeyNotFoundException($"Key '{key}' not found in workflow state");
}
return (T)value;
}
/// <summary>
/// 尝试获取指定键的值
/// </summary>
/// <typeparam name="T">值类型</typeparam>
/// <param name="key">键名</param>
/// <param name="value">输出值</param>
/// <returns>如果键存在返回 true否则返回 false</returns>
public bool TryGet<T>(string key, out T? value) where T : class
{
if (_slots.TryGetValue(key, out var obj) && obj is T typedValue)
{
value = typedValue;
return true;
}
value = null;
return false;
}
/// <summary>
/// 设置指定键的值
/// </summary>
/// <typeparam name="T">值类型</typeparam>
/// <param name="key">键名</param>
/// <param name="value">值</param>
/// <exception cref="ArgumentNullException">当键或值为空时抛出</exception>
public void Set<T>(string key, T value) where T : class
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentNullException(nameof(key), "Key cannot be null or whitespace");
}
if (value == null)
{
throw new ArgumentNullException(nameof(value), "Value cannot be null");
}
_slots[key] = value;
}
/// <summary>
/// 检查指定键是否存在
/// </summary>
/// <param name="key">键名</param>
/// <returns>如果键存在返回 true</returns>
public bool ContainsKey(string key)
{
return _slots.ContainsKey(key);
}
/// <summary>
/// 移除指定键
/// </summary>
/// <param name="key">键名</param>
/// <returns>如果成功移除返回 true</returns>
public bool Remove(string key)
{
return _slots.Remove(key);
}
/// <summary>
/// 清空所有数据
/// </summary>
public void Clear()
{
_slots.Clear();
}
/// <summary>
/// 获取所有键
/// </summary>
/// <returns>键集合</returns>
public IEnumerable<string> GetKeys()
{
return _slots.Keys;
}
/// <summary>
/// 克隆工作流状态
/// </summary>
/// <returns>新的工作流状态副本</returns>
public object Clone()
{
var json = JsonSerializer.Serialize(_slots);
var copySlots = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
var copy = new WorkflowState
{
WorkflowId = WorkflowId,
CurrentNodeId = CurrentNodeId,
CreatedAt = CreatedAt
};
if (copySlots != null)
{
foreach (var kvp in copySlots)
{
copy._slots[kvp.Key] = kvp.Value;
}
}
return copy;
}
}
}

@ -0,0 +1,38 @@
namespace AI.Workflow
{
/// <summary>
/// 工作流状态
/// </summary>
public enum WorkflowStatus
{
/// <summary>
/// 未开始
/// </summary>
NotStarted,
/// <summary>
/// 运行中
/// </summary>
Running,
/// <summary>
/// 已完成
/// </summary>
Completed,
/// <summary>
/// 执行失败
/// </summary>
Failed,
/// <summary>
/// 已暂停
/// </summary>
Paused,
/// <summary>
/// 已取消
/// </summary>
Cancelled
}
}

@ -0,0 +1,5 @@
{
"ModelId": "deepseek-v3.2",
"ApiKey": "your-api-key-here",
"Endpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1"
}

@ -1,120 +1,41 @@
<?xml version="1.0" encoding="utf-8"?> <Project Sdk="Microsoft.NET.Sdk">
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <TargetFramework>net8.0-windows</TargetFramework>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{DE7A91F6-260B-4C8F-A2C0-4F080EE09BFE}</ProjectGuid>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GeoSigma</RootNamespace> <RootNamespace>GeoSigma</RootNamespace>
<AssemblyName>ColorPicker</AssemblyName> <UseWindowsForms>true</UseWindowsForms>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
<FileAlignment>512</FileAlignment> <BaseOutputPath>..\bin</BaseOutputPath>
<Deterministic>true</Deterministic> <RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ForceDesignerDpiUnaware>true</ForceDesignerDpiUnaware>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup>
<DebugSymbols>true</DebugSymbols> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet/>
<UseVSHostingProcess>true</UseVSHostingProcess>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet/>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'"> <PropertyGroup>
<OutputPath>bin\x64\Release\</OutputPath> <DefineConstants>WEB_SERVICE</DefineConstants>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="DevExpress.Data.v20.1, Version=20.1.3.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL"/> <ProjectReference Include="..\UCDraw\SigmaDrawerUtil\SigmaDrawerUtil.csproj" />
<Reference Include="DevExpress.Utils.v20.1, Version=20.1.3.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL"/>
<Reference Include="DevExpress.XtraEditors.v20.1, Version=20.1.3.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL"/>
<Reference Include="DevExpress.XtraVerticalGrid.v20.1, Version=20.1.3.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL"/>
<Reference Include="System"/>
<Reference Include="System.Core"/>
<Reference Include="System.Drawing"/>
<Reference Include="System.Drawing.Design"/>
<Reference Include="System.Windows.Forms"/>
<Reference Include="System.Xml.Linq"/>
<Reference Include="System.Data.DataSetExtensions"/>
<Reference Include="Microsoft.CSharp"/>
<Reference Include="System.Data"/>
<Reference Include="System.Net.Http"/>
<Reference Include="System.Xml"/>
</ItemGroup>
<ItemGroup>
<Compile Include="CustomPropertyGridControl.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="FrmPicker.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="FrmPicker.Designer.cs">
<DependentUpon>FrmPicker.cs</DependentUpon>
</Compile>
<Compile Include="FrmSigmaColor.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="FrmSigmaColor.Designer.cs">
<DependentUpon>FrmSigmaColor.cs</DependentUpon>
</Compile>
<Compile Include="PopPropertyEditorColor.cs"/>
<Compile Include="Properties\AssemblyInfo.cs"/>
<Compile Include="PropertyColorIndicatorConvert.cs"/>
<Compile Include="PropertyEditorColor.cs"/>
<Compile Include="UCColorDialog.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UCColorDialog.Designer.cs">
<DependentUpon>UCColorDialog.cs</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="FrmPicker.resx"> <Reference Include="DevExpress.Data.v25.1">
<DependentUpon>FrmPicker.cs</DependentUpon> <HintPath>..\bin\Debug\DevExpress.Data.v25.1.dll</HintPath>
</EmbeddedResource> </Reference>
<EmbeddedResource Include="FrmSigmaColor.resx"> <Reference Include="DevExpress.Utils.v25.1">
<DependentUpon>FrmSigmaColor.cs</DependentUpon> <HintPath>..\bin\Debug\DevExpress.Utils.v25.1.dll</HintPath>
</EmbeddedResource> </Reference>
<EmbeddedResource Include="UCColorDialog.resx"> <Reference Include="DevExpress.XtraEditors.v25.1">
<DependentUpon>UCColorDialog.cs</DependentUpon> <HintPath>..\bin\Debug\DevExpress.XtraEditors.v25.1.dll</HintPath>
</EmbeddedResource> </Reference>
<Reference Include="DevExpress.XtraVerticalGrid.v25.1">
<HintPath>..\bin\Debug\DevExpress.XtraVerticalGrid.v25.1.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\UCDraw\SigmaDrawerUtil\SigmaDrawerUtil.csproj"> <Compile Update="UCColorDialog.cs">
<Project>{9a9ebfa1-819d-4a2f-9dbd-cf01c7994951}</Project> <SubType>UserControl</SubType>
<Name>SigmaDrawerUtil</Name> </Compile>
</ProjectReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets"/>
<Target Name="CustomClean" AfterTargets="Clean">
<RemoveDir Directories="$(BaseIntermediateOutputPath)"/>
<!-- 删除obj -->
</Target>
</Project> </Project>

@ -5,8 +5,9 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Drawing; using System.Drawing;
using System.Drawing.Design; using System.Drawing.Design;
using System.ComponentModel;
using System.Windows.Forms.Design; using System.Windows.Forms.Design;
using System.ComponentModel;
namespace GeoSigma namespace GeoSigma
{ {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save