diff --git a/Drawer/.editorconfig b/Drawer/.editorconfig
new file mode 100644
index 00000000..5b5c7082
--- /dev/null
+++ b/Drawer/.editorconfig
@@ -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
diff --git a/Drawer/.gitignore b/Drawer/.gitignore
index f2c42941..d56c7c76 100644
--- a/Drawer/.gitignore
+++ b/Drawer/.gitignore
@@ -1,3 +1,4 @@
+bin
packages
# 忽略中间文件
@@ -25,4 +26,5 @@ MLMicroStructure.dll
MLMicroStructure.pdb
MicroStructurePP.dll
MicroStructurePP.pdb
-*.FileListAbsolute.txt
\ No newline at end of file
+*.FileListAbsolute.txt
+obj/*
\ No newline at end of file
diff --git a/Drawer/AI/AI.csproj b/Drawer/AI/AI.csproj
new file mode 100644
index 00000000..1dabb7a3
--- /dev/null
+++ b/Drawer/AI/AI.csproj
@@ -0,0 +1,36 @@
+
+
+
+ Library
+ net8.0-windows
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Drawer/AI/AI.sln b/Drawer/AI/AI.sln
new file mode 100644
index 00000000..6e942cc8
--- /dev/null
+++ b/Drawer/AI/AI.sln
@@ -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
diff --git a/Drawer/AI/AISettings.cs b/Drawer/AI/AISettings.cs
new file mode 100644
index 00000000..d87dad5c
--- /dev/null
+++ b/Drawer/AI/AISettings.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using System.Text.Json;
+
+namespace AI
+{
+ ///
+ /// AI 服务配置
+ ///
+ public class AISettings
+ {
+ ///
+ /// 模型 ID
+ ///
+ public string ModelId { get; set; } = string.Empty;
+
+ ///
+ /// API Key
+ ///
+ public string ApiKey { get; set; } = string.Empty;
+
+ ///
+ /// API Endpoint(OpenAI 兼容接口地址)
+ ///
+ public string Endpoint { get; set; } = string.Empty;
+
+ ///
+ /// 从 DLL 所在目录旁的 ai-settings.json 加载配置。
+ /// 若文件不存在,抛出 FileNotFoundException。
+ ///
+ 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(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;
+ }
+ }
+}
diff --git a/Drawer/AI/AgentIntegration/ActionHandlerAttribute.cs b/Drawer/AI/AgentIntegration/ActionHandlerAttribute.cs
new file mode 100644
index 00000000..a8b8bed2
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/ActionHandlerAttribute.cs
@@ -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;
+ }
+ }
+}
+
diff --git a/Drawer/AI/AgentIntegration/AgentMode.cs b/Drawer/AI/AgentIntegration/AgentMode.cs
new file mode 100644
index 00000000..f05fa905
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/AgentMode.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace AI.AgentIntegration
+{
+ ///
+ /// 定义代理的操作模式
+ ///
+ public enum AgentMode
+ {
+ ///
+ /// 观察模式:仅观察用户操作,不主动干预
+ ///
+ Observe,
+
+ ///
+ /// 协助模式:在用户请求时提供帮助
+ ///
+ Assist,
+
+ ///
+ /// 控制模式:主动控制系统行为
+ ///
+ Control,
+ }
+}
+
diff --git a/Drawer/AI/AgentIntegration/AppAction.cs b/Drawer/AI/AgentIntegration/AppAction.cs
new file mode 100644
index 00000000..d8b6f092
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/AppAction.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+
+namespace AI.AgentIntegration
+{
+ ///
+ /// 表示一个应用程序操作
+ ///
+ public class AppAction
+ {
+ ///
+ /// 获取或设置操作类型
+ ///
+ public AppActionType Action { get; set; }
+
+ ///
+ /// 获取或设置操作参数字典
+ ///
+ public Dictionary Parameters { get; set; } = new Dictionary();
+
+ ///
+ /// 创建 AppAction,使用它可以让调用代码变得简短
+ ///
+ /// 类型
+ /// 参数
+ /// AppAction 对象
+ public static AppAction CreateAction(AppActionType type, Dictionary? parameters = null)
+ {
+ return new AppAction()
+ {
+ Action = type,
+ Parameters = parameters ?? new Dictionary(),
+ };
+ }
+
+ ///
+ /// 创建一个支持单个参数的 Action
+ ///
+ /// 类型
+ /// 参数名
+ /// 参数值
+ /// AppAction 对象
+ public static AppAction CreateAction(AppActionType type, string name, object value)
+ {
+ return new AppAction()
+ {
+ Action = type,
+ Parameters = new Dictionary() { [name] = value },
+ };
+ }
+
+ ///
+ /// 创建加载散点文件的 action
+ ///
+ /// 文件路径
+ /// AppAction 对象
+ public static AppAction CreateLoadXyz(string path)
+ {
+ return CreateAction(AppActionType.GriddingModuleLoadXyz, nameof(path), path);
+ }
+
+ ///
+ /// 创建从文件导入井点数据的 action
+ ///
+ /// 井点数据文件路径
+ /// AppAction 对象
+ public static AppAction CreateImportWellPoints(string path)
+ {
+ return CreateAction(AppActionType.WellModuleImportWellPoints, nameof(path), path);
+ }
+
+ ///
+ /// 创建从文件导入井曲线数据的 action
+ ///
+ /// 井曲线数据文件路径
+ /// AppAction 对象
+ public static AppAction CreateImportWellCurves(string path)
+ {
+ return CreateAction(AppActionType.WellModuleImportCurves, nameof(path), path);
+ }
+
+ ///
+ /// 创建打开文件的 action
+ ///
+ /// 文件路径
+ /// AppAction 对象
+ public static AppAction CreateOpenFile(string path)
+ {
+ return CreateAction(AppActionType.OpenFile, nameof(path), path);
+ }
+
+ ///
+ /// 创建参数设置
+ ///
+ /// 参数
+ /// AppAction 对象
+ public static AppAction CreateSetParameters(string parameters)
+ {
+ return CreateAction(AppActionType.GriddingModuleSetParameters, nameof(parameters), parameters);
+ }
+ }
+}
+
diff --git a/Drawer/AI/AgentIntegration/AppActionResult.cs b/Drawer/AI/AgentIntegration/AppActionResult.cs
new file mode 100644
index 00000000..117c400a
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/AppActionResult.cs
@@ -0,0 +1,43 @@
+namespace AI.AgentIntegration
+{
+ ///
+ /// 表示应用程序操作的结果
+ ///
+ public class AppActionResult
+ {
+ ///
+ /// 获取或设置操作是否成功
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// 获取或设置操作结果消息
+ ///
+ public string? Message { get; set; }
+
+ ///
+ /// 获取或设置操作返回的数据
+ ///
+ 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,
+ };
+ }
+ }
+}
+
diff --git a/Drawer/AI/AgentIntegration/AppActionType.cs b/Drawer/AI/AgentIntegration/AppActionType.cs
new file mode 100644
index 00000000..b6c05e16
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/AppActionType.cs
@@ -0,0 +1,164 @@
+namespace AI.AgentIntegration
+{
+ ///
+ /// 定义应用程序可执行的操作类型
+ ///
+ public enum AppActionType
+ {
+ ///
+ /// 获取打开的标签页列表
+ ///
+ GetOpenTabs,
+
+ ///
+ /// 打开文件
+ ///
+ OpenFile,
+
+ ///
+ /// 关闭当前文件
+ ///
+ CloseFile,
+
+ ///
+ /// 关闭所有文件
+ ///
+ CloseAllFiles,
+
+ ///
+ /// 保存当前文件
+ ///
+ SaveFile,
+
+ ///
+ /// 保存所有文件
+ ///
+ SaveAll,
+
+ ///
+ /// 重命名文件
+ ///
+ RenameFile,
+
+ ///
+ /// 重新加载文件
+ ///
+ ReloadFile,
+
+ ///
+ /// 切换标签页
+ ///
+ SwitchTab,
+
+ ///
+ /// 导航到指定视图
+ ///
+ Navigate,
+
+ ///
+ /// 后退导航
+ ///
+ NavigateBack,
+
+ ///
+ /// 前进导航
+ ///
+ NavigateForward,
+
+ ///
+ /// 设置系统忙状态
+ ///
+ SetBusy,
+
+ ///
+ /// 刷新界面
+ ///
+ Refresh,
+
+ ///
+ /// 退出应用程序
+ ///
+ Exit,
+
+ ///
+ /// 撤销操作
+ ///
+ Undo,
+
+ ///
+ /// 重做操作
+ ///
+ Redo,
+
+ ///
+ /// 添加比例尺
+ ///
+ AddScaleBar,
+
+ ///
+ /// 添加边框
+ ///
+ AddBorder,
+
+ ///
+ /// 添加图例
+ ///
+ AddLegend,
+
+ ///
+ /// 描述当前状态
+ ///
+ DescribeState,
+
+ ///
+ /// 加载 xyz 文件
+ ///
+ GriddingModuleLoadXyz,
+
+ ///
+ /// 获取列信息
+ ///
+ GriddingModuleGetColumns,
+
+ ///
+ /// 列头匹配
+ ///
+ GriddingModuleMatchColumns,
+
+ ///
+ /// 获取网络化参数
+ ///
+ GriddingModuleGetParameters,
+
+ ///
+ /// 设置网络化参数
+ ///
+ GriddingModuleSetParameters,
+
+ ///
+ /// 导入数据
+ ///
+ GriddingModuleImport,
+
+ ///
+ /// 数据预览
+ ///
+ GriddingModulePreviewData,
+
+ ///
+ /// 成图
+ ///
+ GriddingModuleRun,
+
+ ///
+ /// 从文件导入井点数据
+ ///
+ WellModuleImportWellPoints,
+
+ ///
+ /// 从文件导入井曲线数据
+ ///
+ WellModuleImportCurves,
+ }
+}
+
diff --git a/Drawer/AI/AgentIntegration/AppControllerHolder.cs b/Drawer/AI/AgentIntegration/AppControllerHolder.cs
new file mode 100644
index 00000000..19d2799c
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/AppControllerHolder.cs
@@ -0,0 +1,14 @@
+namespace AI.AgentIntegration
+{
+ ///
+ /// 供宿主(如 Drawer)在构建 DI 前注入真实 IAppController 的占位。
+ /// 在 Avalonia 应用启动前设置 ,则 会注册该实例。
+ ///
+ public static class AppControllerHolder
+ {
+ ///
+ /// 宿主设置的应用程序控制器,若未设置则使用 。
+ ///
+ public static IAppController? Instance { get; set; }
+ }
+}
diff --git a/Drawer/AI/AgentIntegration/AppPrompt.cs b/Drawer/AI/AgentIntegration/AppPrompt.cs
new file mode 100644
index 00000000..2b4563fa
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/AppPrompt.cs
@@ -0,0 +1,33 @@
+namespace AI.AgentIntegration
+{
+ public static class AppPrompt
+ {
+ ///
+ /// 成图助手的交互式引导提示词(用于新会话的 System Message)
+ ///
+ public static string InteractiveGuidePrompt =>
+@"你是一名智能的成图助手,负责引导用户完成以下两步成图流程。
+
+【成图流程 - 严格按顺序执行】
+
+第一步:散点文件加载
+调用 ShowForm(""gridding-load-xyz"") 弹出散点文件加载卡片。
+卡片会自动引导用户完成文件选择、数据预览和列头匹配,无需你介入。
+用户确认列头匹配后,系统会向你发送一条包含文件信息与匹配结果的摘要消息,收到后再进行第二步。
+
+第二步:网格化参数设置与成图
+调用 ShowForm(""gridding-parameters"") 弹出网格化参数设置卡片。
+卡片会自动加载当前参数,用户可在卡片中查看和修改参数,确认后点击「生成」按钮,卡片内部自动完成成图。
+调用后只需告知用户参数卡片已弹出,请在卡片中确认参数并点击生成,无需你再做任何操作。
+
+【禁止的行为】
+- 禁止在第一步完成之前进行第二步
+- 禁止向用户暴露函数名称
+- 禁止用纯文字询问文件路径等信息,需要用户输入时必须通过卡片交互
+
+【对话要求】
+- 每次调用函数前,先用一句自然语言告知用户即将执行的操作
+- 语言自然、简洁、友好,使用日常语言而非技术术语
+- 每步完成后明确告知用户结果并引导下一步";
+ }
+}
diff --git a/Drawer/AI/AgentIntegration/AppState.cs b/Drawer/AI/AgentIntegration/AppState.cs
new file mode 100644
index 00000000..50cea87e
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/AppState.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace AI.AgentIntegration
+{
+ ///
+ /// 表示应用程序的整体状态
+ ///
+ public class AppState
+ {
+ ///
+ /// 获取或设置用户界面状态
+ ///
+ public UIState UI { get; set; } = new UIState();
+
+ ///
+ /// 获取或设置文件状态
+ ///
+ public FileState File { get; set; } = new FileState();
+
+ ///
+ /// 获取或设置导航状态
+ ///
+ public NavigationState Navigation { get; set; } = new NavigationState();
+
+ ///
+ /// 获取或设置系统状态
+ ///
+ public SystemState System { get; set; } = new SystemState();
+
+ ///
+ /// 获取或设置代理状态
+ ///
+ public AgentState Agent { get; set; } = new AgentState();
+
+ ///
+ /// 将当前状态序列化为 JSON 字符串
+ ///
+ /// 是否使用缩进格式
+ /// 表示当前状态的 JSON 字符串
+ public string ToJson(bool indented = true)
+ {
+ return JsonConvert.SerializeObject(this, indented ? Formatting.Indented : Formatting.None);
+ }
+ }
+
+ ///
+ /// 表示用户界面状态
+ ///
+ public class UIState
+ {
+ ///
+ /// 获取或设置活动标签页的标识符
+ ///
+ public string ActiveTab { get; set; } = string.Empty;
+
+ ///
+ /// 获取或设置打开的标签页列表
+ ///
+ public List OpenTabs { get; set; } = new List();
+
+ ///
+ /// 获取或设置当前获得焦点的控件
+ ///
+ public string FocusedControl { get; set; } = string.Empty;
+ }
+
+ ///
+ /// 表示文件状态
+ ///
+ public class FileState
+ {
+ ///
+ /// 获取或设置活动文件的路径
+ ///
+ public string ActiveFilePath { get; set; } = string.Empty;
+
+ ///
+ /// 获取或设置是否有未保存的更改
+ ///
+ public bool HasUnsavedChanges { get; set; }
+
+ ///
+ /// 获取或设置文件类型
+ ///
+ public string FileType { get; set; } = string.Empty;
+ }
+
+ ///
+ /// 表示导航状态
+ ///
+ public class NavigationState
+ {
+ ///
+ /// 获取或设置当前视图名称
+ ///
+ public string CurrentView { get; set; } = "Home";
+
+ ///
+ /// 获取或设置导航堆栈
+ ///
+ public List NavigationStack { get; set; } = new List();
+ }
+
+ ///
+ /// 表示系统状态
+ ///
+ public class SystemState
+ {
+ ///
+ /// 获取或设置系统是否处于忙碌状态
+ ///
+ public bool IsBusy { get; set; }
+
+ ///
+ /// 获取或设置是否有模态对话框打开
+ ///
+ public bool ModalOpen { get; set; }
+
+ ///
+ /// 获取或设置最后一次操作的时间(UTC)
+ ///
+ public DateTime LastActionTimeUtc { get; set; } = DateTime.UtcNow;
+ }
+
+ ///
+ /// 表示代理状态
+ ///
+ public class AgentState
+ {
+ ///
+ /// 获取或设置代理的操作模式
+ ///
+ public AgentMode Mode { get; set; } = AgentMode.Observe;
+
+ ///
+ /// 获取或设置最后执行的命令
+ ///
+ public string LastCommand { get; set; } = string.Empty;
+
+ ///
+ /// 获取或设置最后命令的执行结果
+ ///
+ public string LastResult { get; set; } = string.Empty;
+ }
+}
+
diff --git a/Drawer/AI/AgentIntegration/IAppController.cs b/Drawer/AI/AgentIntegration/IAppController.cs
new file mode 100644
index 00000000..b564773d
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/IAppController.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Threading.Tasks;
+
+namespace AI.AgentIntegration
+{
+ ///
+ /// 定义应用程序控制器的接口
+ ///
+ public interface IAppController
+ {
+ ///
+ /// 当应用程序状态发生变化时触发的事件
+ ///
+ event EventHandler StateChanged;
+
+ ///
+ /// 获取应用程序的当前状态
+ ///
+ /// 当前应用程序状态
+ AppState GetCurrentState();
+
+ ///
+ /// 执行指定的应用程序操作
+ ///
+ /// 要执行的操作
+ /// 操作执行结果
+ Task ExecuteAsync(AppAction action);
+ }
+}
\ No newline at end of file
diff --git a/Drawer/AI/AgentIntegration/NoOpAppController.cs b/Drawer/AI/AgentIntegration/NoOpAppController.cs
new file mode 100644
index 00000000..e8e31c41
--- /dev/null
+++ b/Drawer/AI/AgentIntegration/NoOpAppController.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Threading.Tasks;
+
+namespace AI.AgentIntegration
+{
+ ///
+ /// 无操作 AppController,在未注入真实控制器时使用(如独立运行 AI 模块时)。
+ ///
+ public sealed class NoOpAppController : IAppController
+ {
+ public event EventHandler? StateChanged;
+
+ public AppState GetCurrentState() => new AppState();
+
+ public Task ExecuteAsync(AppAction action)
+ {
+ return Task.FromResult(AppActionResult.Fail("未连接应用控制器"));
+ }
+ }
+}
diff --git a/Drawer/AI/App.axaml b/Drawer/AI/App.axaml
new file mode 100644
index 00000000..aa6fc580
--- /dev/null
+++ b/Drawer/AI/App.axaml
@@ -0,0 +1,1012 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Drawer/AI/App.axaml.cs b/Drawer/AI/App.axaml.cs
new file mode 100644
index 00000000..1ee36461
--- /dev/null
+++ b/Drawer/AI/App.axaml.cs
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Drawer/AI/Assets/avalonia-logo.ico b/Drawer/AI/Assets/avalonia-logo.ico
new file mode 100644
index 00000000..da8d49ff
Binary files /dev/null and b/Drawer/AI/Assets/avalonia-logo.ico differ
diff --git a/Drawer/AI/Assets/file.svg b/Drawer/AI/Assets/file.svg
new file mode 100644
index 00000000..4b25723d
--- /dev/null
+++ b/Drawer/AI/Assets/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Drawer/AI/Converters/AuthorToColorConverter.cs b/Drawer/AI/Converters/AuthorToColorConverter.cs
new file mode 100644
index 00000000..ada5471b
--- /dev/null
+++ b/Drawer/AI/Converters/AuthorToColorConverter.cs
@@ -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();
+ }
+ }
+}
diff --git a/Drawer/AI/Converters/FileExtensionConverter.cs b/Drawer/AI/Converters/FileExtensionConverter.cs
new file mode 100644
index 00000000..72a5c0b1
--- /dev/null
+++ b/Drawer/AI/Converters/FileExtensionConverter.cs
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Drawer/AI/Converters/FileSizeConverter.cs b/Drawer/AI/Converters/FileSizeConverter.cs
new file mode 100644
index 00000000..b28ffc7c
--- /dev/null
+++ b/Drawer/AI/Converters/FileSizeConverter.cs
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Drawer/AI/Converters/FormBoolConverter.cs b/Drawer/AI/Converters/FormBoolConverter.cs
new file mode 100644
index 00000000..d3541750
--- /dev/null
+++ b/Drawer/AI/Converters/FormBoolConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace AI.Converters
+{
+ ///
+ /// 表单布尔字段与字符串双向转换(CheckBox 与 CurrentValue 绑定)
+ ///
+ 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";
+ }
+ }
+}
diff --git a/Drawer/AI/Converters/FormFieldTemplateSelector.cs b/Drawer/AI/Converters/FormFieldTemplateSelector.cs
new file mode 100644
index 00000000..a582559c
--- /dev/null
+++ b/Drawer/AI/Converters/FormFieldTemplateSelector.cs
@@ -0,0 +1,34 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using Avalonia.Markup.Xaml.Templates;
+using AI.Models.Form;
+
+namespace AI.Converters
+{
+ ///
+ /// 按表单字段类型选择模板,避免 Boolean 的 CheckBox 与 Choice 的 ComboBox 同屏时互相覆盖 CurrentValue(如列名 "0" 被写成 "False")。
+ ///
+ 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;
+ }
+}
diff --git a/Drawer/AI/Converters/FormFieldTypeVisibilityConverter.cs b/Drawer/AI/Converters/FormFieldTypeVisibilityConverter.cs
new file mode 100644
index 00000000..bd420754
--- /dev/null
+++ b/Drawer/AI/Converters/FormFieldTypeVisibilityConverter.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using AI.Models.Form;
+
+namespace AI.Converters
+{
+ ///
+ /// 表单字段类型与 ConverterParameter 相同时可见(用于按类型显示不同控件)
+ ///
+ 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();
+ }
+}
diff --git a/Drawer/AI/Converters/FormNullableDoubleConverter.cs b/Drawer/AI/Converters/FormNullableDoubleConverter.cs
new file mode 100644
index 00000000..71afcea2
--- /dev/null
+++ b/Drawer/AI/Converters/FormNullableDoubleConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace AI.Converters
+{
+ ///
+ /// 将 double? 转为 double,用于 NumericUpDown 的 Minimum/Maximum(null 时用默认边界)
+ /// ConverterParameter: "min" -> null 转为 double.MinValue, "max" -> null 转为 double.MaxValue
+ ///
+ 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();
+ }
+}
diff --git a/Drawer/AI/Converters/FormNumberConverter.cs b/Drawer/AI/Converters/FormNumberConverter.cs
new file mode 100644
index 00000000..05fb8b53
--- /dev/null
+++ b/Drawer/AI/Converters/FormNumberConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace AI.Converters
+{
+ ///
+ /// 表单数字字段与字符串双向转换(NumericUpDown Value 与 CurrentValue 绑定)
+ ///
+ 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";
+ }
+ }
+}
diff --git a/Drawer/AI/Converters/MessageTemplateSelector.cs b/Drawer/AI/Converters/MessageTemplateSelector.cs
new file mode 100644
index 00000000..0d69090a
--- /dev/null
+++ b/Drawer/AI/Converters/MessageTemplateSelector.cs
@@ -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;
+ }
+ }
+}
diff --git a/Drawer/AI/Converters/ParameterSetFieldTypeVisibilityConverter.cs b/Drawer/AI/Converters/ParameterSetFieldTypeVisibilityConverter.cs
new file mode 100644
index 00000000..e9b821ac
--- /dev/null
+++ b/Drawer/AI/Converters/ParameterSetFieldTypeVisibilityConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using AI.Models.SpecialMessages;
+
+namespace AI.Converters
+{
+ ///
+ /// 参数集字段类型与 ConverterParameter 相同时可见(用于按类型显示不同控件)
+ ///
+ 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();
+ }
+}
diff --git a/Drawer/AI/Converters/StatusToCompletedConverter.cs b/Drawer/AI/Converters/StatusToCompletedConverter.cs
new file mode 100644
index 00000000..15f398a2
--- /dev/null
+++ b/Drawer/AI/Converters/StatusToCompletedConverter.cs
@@ -0,0 +1,28 @@
+using Avalonia.Data.Converters;
+using AI.Models;
+using System;
+using System.Globalization;
+
+namespace AI.Converters
+{
+ ///
+ /// 判断工作流步骤状态是否为已完成(用于显示勾)
+ ///
+ 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();
+ }
+ }
+}
diff --git a/Drawer/AI/Converters/StringToBoolConverter.cs b/Drawer/AI/Converters/StringToBoolConverter.cs
new file mode 100644
index 00000000..f3fc135f
--- /dev/null
+++ b/Drawer/AI/Converters/StringToBoolConverter.cs
@@ -0,0 +1,27 @@
+using Avalonia.Data.Converters;
+using System;
+using System.Globalization;
+
+namespace AI.Converters
+{
+ ///
+ /// 将字符串转换为布尔值(非空字符串为 true,空或 null 为 false)
+ ///
+ 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();
+ }
+ }
+}
diff --git a/Drawer/AI/Converters/WorkflowModeToColorConverter.cs b/Drawer/AI/Converters/WorkflowModeToColorConverter.cs
new file mode 100644
index 00000000..d1acd24d
--- /dev/null
+++ b/Drawer/AI/Converters/WorkflowModeToColorConverter.cs
@@ -0,0 +1,35 @@
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using AI.Models;
+using System;
+using System.Globalization;
+
+namespace AI.Converters
+{
+ ///
+ /// 将工作模式转换为按钮背景颜色
+ ///
+ 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();
+ }
+ }
+}
+
diff --git a/Drawer/AI/Converters/WorkflowModeToTextColorConverter.cs b/Drawer/AI/Converters/WorkflowModeToTextColorConverter.cs
new file mode 100644
index 00000000..811d0fc8
--- /dev/null
+++ b/Drawer/AI/Converters/WorkflowModeToTextColorConverter.cs
@@ -0,0 +1,35 @@
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using AI.Models;
+using System;
+using System.Globalization;
+
+namespace AI.Converters
+{
+ ///
+ /// 将工作模式转换为按钮文字颜色
+ ///
+ 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();
+ }
+ }
+}
+
diff --git a/Drawer/AI/Converters/WorkflowStatusToColorConverter.cs b/Drawer/AI/Converters/WorkflowStatusToColorConverter.cs
new file mode 100644
index 00000000..2b1dfe24
--- /dev/null
+++ b/Drawer/AI/Converters/WorkflowStatusToColorConverter.cs
@@ -0,0 +1,38 @@
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using AI.Models;
+using System;
+using System.Globalization;
+
+namespace AI.Converters
+{
+ ///
+ /// 将工作流步骤状态转换为颜色
+ ///
+ 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();
+ }
+ }
+}
+
diff --git a/Drawer/AI/Converters/WorkflowStatusToIconConverter.cs b/Drawer/AI/Converters/WorkflowStatusToIconConverter.cs
new file mode 100644
index 00000000..535feb9f
--- /dev/null
+++ b/Drawer/AI/Converters/WorkflowStatusToIconConverter.cs
@@ -0,0 +1,39 @@
+using Avalonia.Data.Converters;
+using AI.Models;
+using System;
+using System.Globalization;
+
+namespace AI.Converters
+{
+ ///
+ /// 将工作流步骤状态转换为图标
+ ///
+ 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();
+ }
+ }
+}
+
diff --git a/Drawer/AI/Filter/KnowledgeBaseStoreFilter.cs b/Drawer/AI/Filter/KnowledgeBaseStoreFilter.cs
new file mode 100644
index 00000000..b8861bfc
--- /dev/null
+++ b/Drawer/AI/Filter/KnowledgeBaseStoreFilter.cs
@@ -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
+{
+ ///
+ /// 在知识库查询执行完成后,将结果写入当前会话的 Store,保证插件只负责查询、不依赖会话上下文。
+ ///
+ 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 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());
+ }
+ }
+}
diff --git a/Drawer/AI/Filter/LogFunctionFilter.cs b/Drawer/AI/Filter/LogFunctionFilter.cs
new file mode 100644
index 00000000..238ca69c
--- /dev/null
+++ b/Drawer/AI/Filter/LogFunctionFilter.cs
@@ -0,0 +1,31 @@
+using System.Diagnostics;
+using Microsoft.SemanticKernel;
+
+namespace AI.Filter
+{
+ ///
+ /// 函数调用过滤器,用于记录日志
+ ///
+ public sealed class LogFunctionFilter : IFunctionInvocationFilter
+ {
+ public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func 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");
+ }
+ }
+ }
+}
diff --git a/Drawer/AI/Filter/LogPromptFilter.cs b/Drawer/AI/Filter/LogPromptFilter.cs
new file mode 100644
index 00000000..ec6b2d6e
--- /dev/null
+++ b/Drawer/AI/Filter/LogPromptFilter.cs
@@ -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 next)
+ {
+ Debug.WriteLine($"Rendering prompt for {context.Function.Name}");
+
+ await next(context);
+
+ Debug.WriteLine($"Rendered prompt: {context.RenderedPrompt}");
+ }
+ }
+}
diff --git a/Drawer/AI/Interface/IChatBackend.cs b/Drawer/AI/Interface/IChatBackend.cs
new file mode 100644
index 00000000..6e8fec0f
--- /dev/null
+++ b/Drawer/AI/Interface/IChatBackend.cs
@@ -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
+ {
+ ///
+ /// 向模型发送自然语言请求。发给 LLM 的历史由 session.GetHistoryForLlm(guidePrompt) 从会话存储生成。
+ ///
+ /// 用户消息
+ /// 聊天会话。如果为 null,将创建新的会话。
+ /// 可选 System 引导词;若提供则在生成的 History 开头插入
+ /// 取消令牌,用于取消请求或配合超时
+ /// 模型的响应文本
+ Task AskAsync(string userMessage, ChatSession? session = null, string? guidePrompt = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// 流式向模型发送自然语言请求。发给 LLM 的历史由 session.GetHistoryForLlm(guidePrompt) 从会话存储生成。
+ ///
+ /// 用户消息
+ /// 聊天会话。如果为 null,将创建新的会话。
+ /// 可选 System 引导词
+ /// 取消令牌,用于取消请求或配合超时
+ /// 流式返回模型的响应文本片段
+ IAsyncEnumerable AskStreamAsync(string userMessage, ChatSession? session = null, string? guidePrompt = null, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/Drawer/AI/Interface/IFormRegistry.cs b/Drawer/AI/Interface/IFormRegistry.cs
new file mode 100644
index 00000000..8bda7dce
--- /dev/null
+++ b/Drawer/AI/Interface/IFormRegistry.cs
@@ -0,0 +1,20 @@
+using AI.Models.Form;
+
+namespace AI.Interface
+{
+ ///
+ /// 表单注册表:根据 formId 获取表单定义。
+ ///
+ public interface IFormRegistry
+ {
+ ///
+ /// 根据表单 ID 获取表单定义,未注册则返回 null。
+ ///
+ FormDefinition? GetForm(string formId);
+
+ ///
+ /// 获取所有已注册的表单定义(用于生成 Schema YAML、供 LLM 上下文等)。
+ ///
+ IEnumerable GetAll();
+ }
+}
diff --git a/Drawer/AI/Interface/IFormRequestNotifier.cs b/Drawer/AI/Interface/IFormRequestNotifier.cs
new file mode 100644
index 00000000..4bd8de51
--- /dev/null
+++ b/Drawer/AI/Interface/IFormRequestNotifier.cs
@@ -0,0 +1,14 @@
+namespace AI.Interface
+{
+ ///
+ /// 表单请求通知:当 AI 调用 ShowForm 时,由插件触发,通知 UI 插入对应表单消息。
+ ///
+ public interface IFormRequestNotifier
+ {
+ ///
+ /// 请求在触发该请求的会话中显示指定 ID 的表单(绑定到当前 LLM 请求所在会话,避免串会话)。
+ ///
+ /// 表单 ID,如 gridding-load-xyz
+ void RequestForm(string formId);
+ }
+}
diff --git a/Drawer/AI/Interface/IMessageNotifier.cs b/Drawer/AI/Interface/IMessageNotifier.cs
new file mode 100644
index 00000000..73d9268b
--- /dev/null
+++ b/Drawer/AI/Interface/IMessageNotifier.cs
@@ -0,0 +1,14 @@
+namespace AI.Interface
+{
+ ///
+ /// 用于通知 UI 层添加消息的接口
+ ///
+ public interface IMessageNotifier
+ {
+ ///
+ /// 通知添加一条工具调用消息到 UI(仅显示,不进入 AI 上下文)
+ ///
+ /// 消息内容
+ void NotifyToolCall(string message);
+ }
+}
diff --git a/Drawer/AI/KnowledgeBase/IKnowledgeBase.cs b/Drawer/AI/KnowledgeBase/IKnowledgeBase.cs
new file mode 100644
index 00000000..45a45473
--- /dev/null
+++ b/Drawer/AI/KnowledgeBase/IKnowledgeBase.cs
@@ -0,0 +1,17 @@
+using System.Threading.Tasks;
+
+namespace AI.KnowledgeBase
+{
+ ///
+ /// 知识库接口
+ ///
+ public interface IKnowledgeBase
+ {
+ ///
+ /// 查询知识库中的信息
+ ///
+ /// 查询语句
+ /// 返回匹配的知识条目
+ Task SearchKnowledgeAsync(string query);
+ }
+}
\ No newline at end of file
diff --git a/Drawer/AI/KnowledgeBase/SimpleKnowledgeBase.cs b/Drawer/AI/KnowledgeBase/SimpleKnowledgeBase.cs
new file mode 100644
index 00000000..47fd5282
--- /dev/null
+++ b/Drawer/AI/KnowledgeBase/SimpleKnowledgeBase.cs
@@ -0,0 +1,130 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading.Tasks;
+
+namespace AI.KnowledgeBase
+{
+ ///
+ /// 简单知识库实现
+ ///
+ public class SimpleKnowledgeBase : IKnowledgeBase
+ {
+ private static readonly Dictionary QueryAliasToTopic = new(StringComparer.OrdinalIgnoreCase)
+ {
+ // 常用中文/英文说法归一到知识条目 key
+ { "网格化", "成图" },
+ { "网格化流程", "成图" },
+ { "成图流程", "成图" },
+ { "gridding", "成图" },
+ { "grid", "成图" },
+ { "gridding workflow", "成图" },
+ };
+
+ ///
+ /// 每个主题对应的知识条目 Id 与版本号,便于在会话存储或日志中引用和比对。
+ ///
+ private static readonly Dictionary 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 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""),界面会弹出「网格化参数设置」综合卡片。
+ 卡片会自动加载当前参数,用户可在卡片中查看、编辑参数,点击「生成」按钮后由卡片内部完成参数设置与成图,无需你再调用获取/设置参数或成图函数。
+ 调用后只需一句话告知用户参数卡片已展示、可在卡片中修改并点击生成即可。"
+ },
+ };
+
+ ///
+ public Task 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));
+ }
+
+ ///
+ /// 按主题 key 为知识正文加上 [KB-ID; Version] 头部,便于回放与引用。
+ ///
+ 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}";
+ }
+ }
+}
diff --git a/Drawer/AI/Models/ChatInputModel.cs b/Drawer/AI/Models/ChatInputModel.cs
new file mode 100644
index 00000000..98a7e8b1
--- /dev/null
+++ b/Drawer/AI/Models/ChatInputModel.cs
@@ -0,0 +1,7 @@
+namespace AI.Models
+{
+ public class ChatInputModel
+ {
+ public string Message { get; set; } = string.Empty;
+ }
+}
diff --git a/Drawer/AI/Models/ChatMessageModel.cs b/Drawer/AI/Models/ChatMessageModel.cs
new file mode 100644
index 00000000..6da420e8
--- /dev/null
+++ b/Drawer/AI/Models/ChatMessageModel.cs
@@ -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; }
+
+ ///
+ /// 特殊消息内容(用于扩展消息类型)
+ ///
+ 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;
+ }
+ }
+}
diff --git a/Drawer/AI/Models/ChatSession.cs b/Drawer/AI/Models/ChatSession.cs
new file mode 100644
index 00000000..eb6fb899
--- /dev/null
+++ b/Drawer/AI/Models/ChatSession.cs
@@ -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
+{
+ ///
+ /// 表示一个聊天会话,包含会话信息和历史记录。
+ /// 会话存储(Store)为唯一事实来源;History 仅作兼容,发给 LLM 的历史由 GetHistoryForLlm 从 Store 生成。
+ ///
+ public class ChatSession
+ {
+ ///
+ /// 会话唯一标识符
+ ///
+ public string Id { get; }
+
+ ///
+ /// 会话标题(通常从第一条用户消息生成)
+ ///
+ public string Title { get; set; }
+
+ ///
+ /// 会话创建时间
+ ///
+ public DateTime CreatedAt { get; }
+
+ ///
+ /// 最后更新时间
+ ///
+ public DateTime UpdatedAt { get; private set; }
+
+ ///
+ /// 对话历史记录(兼容保留;发给 LLM 的请求应使用 GetHistoryForLlm 从 Store 生成)
+ ///
+ public ChatHistory History { get; }
+
+ ///
+ /// 会话级结构化存储,唯一事实来源。UI 与 Prompt 视图均由此派生。
+ ///
+ public ConversationStore Store { get; }
+
+ ///
+ /// 消息数量
+ ///
+ public int MessageCount => History.Count;
+
+ ///
+ /// 用户消息数量(用于判断是否是第一条用户消息)
+ ///
+ private int _userMessageCount = 0;
+
+ ///
+ /// 当前会话的工作模式
+ ///
+ public WorkflowMode WorkflowMode { get; set; } = WorkflowMode.Ask;
+
+ ///
+ /// 初始化新的聊天会话
+ ///
+ /// 会话ID,如果为null则自动生成GUID
+ /// 会话标题
+ 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();
+ }
+
+ ///
+ /// 从持久化数据还原的会话(用于加载已保存的会话文件)
+ ///
+ 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().Count(t => t.Role == AuthorRole.User);
+ }
+
+ ///
+ /// 更新会话的最后更新时间
+ ///
+ public void UpdateTimestamp()
+ {
+ UpdatedAt = DateTime.Now;
+ }
+
+ ///
+ /// 添加用户消息(同时写入 Store,作为真相源)
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// 添加助手消息(同时写入 Store)
+ ///
+ public void AddAssistantMessage(string message)
+ {
+ History.AddAssistantMessage(message);
+ Store.AppendText(AuthorRole.Assistant, message);
+ UpdateTimestamp();
+ }
+
+ ///
+ /// 向会话存储追加一条特殊消息(不写入 History)。供 ViewModel 在展示表单/参数集/表格/列匹配时调用。
+ ///
+ public void AppendSpecialEntry(string type, string yamlPayload)
+ {
+ Store.AppendSpecial(type, yamlPayload ?? string.Empty);
+ UpdateTimestamp();
+ }
+
+ ///
+ /// 从会话存储生成发给 LLM 的 ChatHistory:仅包含 User/Assistant 文本及可选的 System(guidePrompt)。
+ /// 特殊消息不追加原始 YAML,可替换为一句短占位(如「[已展示表单:xxx]」)。
+ ///
+ /// 可选;若提供则在开头插入一条 System 消息
+ /// 供 KernelService 使用的 ChatHistory,不含原始 YAML
+ 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;
+ }
+
+ ///
+ /// 将 Store 中的一条文本条目按角色加入 ChatHistory,供 GetHistoryForLlm 使用。
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 添加系统消息
+ ///
+ public void AddSystemMessage(string message)
+ {
+ History.AddSystemMessage(message);
+ Store.AppendText(AuthorRole.System, message);
+ UpdateTimestamp();
+ }
+
+ ///
+ /// 仅在需要时确保 History 中存在一条 System(guidePrompt)。不再写入 formsYaml 或摘要。
+ /// 发给 LLM 的 System 由 GetHistoryForLlm(guidePrompt) 在生成时插入,调用方传入 guidePrompt 即可。
+ ///
+ /// 引导提示词
+ /// 已忽略,保留参数以兼容现有调用
+ 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();
+ }
+ }
+}
+
diff --git a/Drawer/AI/Models/Form/FormDefinition.cs b/Drawer/AI/Models/Form/FormDefinition.cs
new file mode 100644
index 00000000..7663471f
--- /dev/null
+++ b/Drawer/AI/Models/Form/FormDefinition.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace AI.Models.Form
+{
+ ///
+ /// 表单定义:Id、标题、提交目标、扁平字段列表
+ ///
+ public class FormDefinition
+ {
+ /// 表单唯一标识
+ public string Id { get; set; } = string.Empty;
+
+ /// 表单标题
+ public string Title { get; set; } = string.Empty;
+
+ /// 提交后执行目标,如 AppActionType 名或 plugin:function
+ public string SubmitTarget { get; set; } = string.Empty;
+
+ /// 提交按钮文案
+ public string SubmitLabel { get; set; } = "提交";
+
+ /// 字段列表
+ public List Fields { get; set; } = new List();
+ }
+}
diff --git a/Drawer/AI/Models/Form/FormField.cs b/Drawer/AI/Models/Form/FormField.cs
new file mode 100644
index 00000000..7902dd1d
--- /dev/null
+++ b/Drawer/AI/Models/Form/FormField.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+
+namespace AI.Models.Form
+{
+ ///
+ /// 表单字段定义(通用 schema,仅定义不含当前值)
+ /// 类型与常用属性对应:number(min,max,step,default), string(maxLength,placeholder,default),
+ /// boolean(default), select(options[],default), multi-select(options[],default[])。
+ ///
+ public class FormField
+ {
+ /// 字段唯一标识,对应参数名
+ public string Id { get; set; } = string.Empty;
+
+ /// 显示名称
+ public string Label { get; set; } = string.Empty;
+
+ /// 可选描述/提示
+ public string? Description { get; set; }
+
+ /// 控件/值类型
+ public FormFieldType Type { get; set; }
+
+ /// 是否必填
+ public bool Required { get; set; }
+
+ /// 默认值(可选)
+ public object? DefaultValue { get; set; }
+
+ // ----- number -----
+ /// 数值最小值(Number)
+ public double? Min { get; set; }
+ /// 数值最大值(Number)
+ public double? Max { get; set; }
+ /// 步长(Number)
+ public double? Step { get; set; }
+
+ // ----- string / text -----
+ /// 最大长度(Text/MultiLine)
+ public int? MaxLength { get; set; }
+ /// 占位提示(Text/MultiLine)
+ public string? Placeholder { get; set; }
+
+ // ----- select / multi-select -----
+ /// 下拉/单选选项(Choice);多选选项(MultiSelect)
+ public List? Options { get; set; }
+ /// 多选默认值(MultiSelect)
+ public List? DefaultValues { get; set; }
+ }
+}
diff --git a/Drawer/AI/Models/Form/FormFieldEntry.cs b/Drawer/AI/Models/Form/FormFieldEntry.cs
new file mode 100644
index 00000000..3cb60026
--- /dev/null
+++ b/Drawer/AI/Models/Form/FormFieldEntry.cs
@@ -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
+{
+ ///
+ /// 表单字段条目:schema + 当前值,用于绑定与提交
+ ///
+ public class FormFieldEntry : INotifyPropertyChanged
+ {
+ private string _currentValue = string.Empty;
+
+ public string Id { get; set; } = string.Empty;
+ public string Label { get; set; } = string.Empty;
+ /// 可选描述/提示
+ public string? Description { get; set; }
+ public FormFieldType Type { get; set; }
+ public bool Required { get; set; }
+ public object? DefaultValue { get; set; }
+ public List? 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? DefaultValues { get; set; }
+ /// 多选当前选中项(MultiSelect 绑定用)
+ public ObservableCollection SelectedValues { get; } = new ObservableCollection();
+ /// 多选选项列表(用于绑定 CheckBox 列表,与 SelectedValues 同步)
+ public ObservableCollection MultiSelectOptions { get; } = new ObservableCollection();
+
+ /// 当前输入值(绑定用,提交时按 Type 转换)
+ 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(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value))
+ return false;
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+ }
+}
diff --git a/Drawer/AI/Models/Form/FormFieldType.cs b/Drawer/AI/Models/Form/FormFieldType.cs
new file mode 100644
index 00000000..80395274
--- /dev/null
+++ b/Drawer/AI/Models/Form/FormFieldType.cs
@@ -0,0 +1,22 @@
+namespace AI.Models.Form
+{
+ ///
+ /// 表单字段控件/值类型(通用 schema 类型)
+ ///
+ public enum FormFieldType
+ {
+ /// 单行文本,常用属性: maxLength, placeholder, default
+ Text,
+ MultiLine,
+ /// 数值,常用属性: min, max, step, default
+ Number,
+ /// 勾选/开关,常用属性: default
+ Boolean,
+ FilePath,
+ /// 单选下拉,常用属性: options[], default
+ Choice,
+ /// 多选,常用属性: options[], default[]
+ MultiSelect,
+ Json,
+ }
+}
diff --git a/Drawer/AI/Models/Form/MultiSelectOptionItem.cs b/Drawer/AI/Models/Form/MultiSelectOptionItem.cs
new file mode 100644
index 00000000..abf51127
--- /dev/null
+++ b/Drawer/AI/Models/Form/MultiSelectOptionItem.cs
@@ -0,0 +1,47 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace AI.Models.Form
+{
+ ///
+ /// 多选项条目,用于绑定 CheckBox 与 FormFieldEntry.SelectedValues 同步
+ ///
+ 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));
+ }
+}
diff --git a/Drawer/AI/Models/PendingFileModel.cs b/Drawer/AI/Models/PendingFileModel.cs
new file mode 100644
index 00000000..9b22f633
--- /dev/null
+++ b/Drawer/AI/Models/PendingFileModel.cs
@@ -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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Drawer/AI/Models/SpecialMessages/ColumnMatchMessage.cs b/Drawer/AI/Models/SpecialMessages/ColumnMatchMessage.cs
new file mode 100644
index 00000000..2faedefa
--- /dev/null
+++ b/Drawer/AI/Models/SpecialMessages/ColumnMatchMessage.cs
@@ -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
+{
+ ///
+ /// 表示一条「必需列 -> 预览列」的匹配项,用于绑定显示。
+ ///
+ public class ColumnMappingItem : INotifyPropertyChanged
+ {
+ private string _requiredColumn = string.Empty;
+ private string _matchedColumn = string.Empty;
+
+ /// 必需列名称
+ public string RequiredColumn
+ {
+ get => _requiredColumn;
+ set => SetProperty(ref _requiredColumn, value ?? string.Empty);
+ }
+
+ /// 匹配到的预览列名称(未匹配时可为空)
+ 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(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value)) return false;
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+ }
+
+ ///
+ /// 列头匹配特殊消息 - 用于在聊天流中展示「必需列」与「预览列」及其匹配关系。
+ ///
+ public class ColumnMatchMessage : ISpecialMessage, INotifyPropertyChanged
+ {
+ private string _title = "列头匹配";
+
+ /// 消息唯一标识符
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+
+ /// 类型名称
+ public string TypeName => "ColumnMatch";
+
+ /// 不需要实时更新
+ public bool IsLive => false;
+
+ /// 标题(如「列头匹配」)
+ public string Title
+ {
+ get => _title;
+ set => SetProperty(ref _title, value ?? string.Empty);
+ }
+
+ /// 必需列名称列表
+ public ObservableCollection RequiredColumns { get; } = new ObservableCollection();
+
+ /// 预览列(可用列)名称列表
+ public ObservableCollection PreviewColumns { get; } = new ObservableCollection();
+
+ /// 匹配关系:必需列 -> 匹配的预览列
+ public ObservableCollection Mappings { get; } = new ObservableCollection();
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value)) return false;
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ /// 设置必需列与预览列,并清空匹配关系
+ public void SetColumns(IEnumerable requiredColumns, IEnumerable previewColumns)
+ {
+ RequiredColumns.Clear();
+ foreach (var c in requiredColumns ?? Array.Empty())
+ RequiredColumns.Add(c ?? string.Empty);
+ PreviewColumns.Clear();
+ foreach (var c in previewColumns ?? Array.Empty())
+ PreviewColumns.Add(c ?? string.Empty);
+ Mappings.Clear();
+ }
+
+ /// 设置匹配关系(必需列 -> 预览列),会清空并替换当前 Mappings
+ public void SetMappings(IEnumerable> 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,
+ });
+ }
+ }
+ }
+}
diff --git a/Drawer/AI/Models/SpecialMessages/FormRequestMessage.cs b/Drawer/AI/Models/SpecialMessages/FormRequestMessage.cs
new file mode 100644
index 00000000..35d3f529
--- /dev/null
+++ b/Drawer/AI/Models/SpecialMessages/FormRequestMessage.cs
@@ -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
+{
+ ///
+ /// 表单请求特殊消息 - 在聊天流中显示可填写的表单
+ ///
+ public class FormRequestMessage : ISpecialMessage, INotifyPropertyChanged
+ {
+ private string _submitLabel = "提交";
+
+ /// 消息唯一标识符
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+
+ /// 类型名称
+ public string TypeName => "Form";
+
+ /// 不需要实时更新
+ public bool IsLive => false;
+
+ /// 表单定义(只读)
+ public FormDefinition Definition { get; }
+
+ /// 带当前值的字段列表,用于绑定与提交
+ public ObservableCollection FieldsWithValues { get; } = new ObservableCollection();
+
+ /// 提交按钮文案(可从 Definition 覆盖)
+ 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(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);
+ }
+ }
+
+ /// 收集当前值,用于提交。Key 为字段 Id,Value 为对象(按 Type 转换)
+ public Dictionary GetValues()
+ {
+ var dict = new Dictionary(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(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(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value))
+ return false;
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+ }
+}
diff --git a/Drawer/AI/Models/SpecialMessages/GriddingParamCardMessage.cs b/Drawer/AI/Models/SpecialMessages/GriddingParamCardMessage.cs
new file mode 100644
index 00000000..f70bef54
--- /dev/null
+++ b/Drawer/AI/Models/SpecialMessages/GriddingParamCardMessage.cs
@@ -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
+{
+ ///
+ /// 网格化参数卡片的执行阶段
+ ///
+ public enum GriddingParamCardPhase
+ {
+ /// 正在从业务层加载参数
+ Loading = 0,
+ /// 参数已加载,等待用户编辑并点击生成
+ Ready = 1,
+ /// 正在执行成图
+ Generating = 2,
+ /// 成图完成
+ Done = 3,
+ /// 出错(加载失败或成图失败)
+ Error = 4,
+ }
+
+ ///
+ /// 网格化参数设置综合卡片:将「获取参数」「编辑参数」「执行成图」合并到一张卡片。
+ /// 整个业务流程由卡片本身驱动,AI 只需调用一次展示此卡片;
+ /// 用户确认参数后点击"生成"按钮,卡片自动完成参数设置和成图,最终摘要发回 AI。
+ ///
+ 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));
+ }
+ }
+ }
+
+ // ── 状态派生属性 ─────────────────────────────────────────────────────
+
+ /// 是否正在加载参数(显示 loading 占位)
+ public bool IsLoading => _phase == GriddingParamCardPhase.Loading;
+
+ /// 是否显示参数列表(非 Loading 阶段)
+ public bool ShowItems => _phase != GriddingParamCardPhase.Loading;
+
+ /// 是否显示"生成"按钮(Ready 阶段)
+ public bool ShowGenerateButton => _phase == GriddingParamCardPhase.Ready;
+
+ /// 是否正在执行成图
+ public bool IsGenerating => _phase == GriddingParamCardPhase.Generating;
+
+ /// 成图是否已完成
+ public bool IsDone => _phase == GriddingParamCardPhase.Done;
+
+ /// 是否处于错误状态
+ public bool IsError => _phase == GriddingParamCardPhase.Error;
+
+ /// 是否显示"成图完成"标记
+ public bool ShowDoneBadge => _phase == GriddingParamCardPhase.Done;
+
+ /// 是否显示错误信息
+ public bool ShowErrorMessage => _phase == GriddingParamCardPhase.Error && !string.IsNullOrEmpty(_statusMessage);
+
+ // ── 状态文案 ──────────────────────────────────────────────────────────
+
+ /// 状态提示文案(加载中、错误信息、成功提示等)
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set
+ {
+ SetProperty(ref _statusMessage, value ?? string.Empty);
+ OnPropertyChanged(nameof(ShowErrorMessage));
+ OnPropertyChanged(nameof(HasStatusMessage));
+ }
+ }
+
+ /// 是否有状态信息
+ public bool HasStatusMessage => !string.IsNullOrEmpty(_statusMessage);
+
+ /// "生成"按钮文案(执行中可切换为"生成中...")
+ public string GenerateButtonLabel
+ {
+ get => _generateButtonLabel;
+ set => SetProperty(ref _generateButtonLabel, value ?? "生成");
+ }
+
+ // ── 参数项列表 ────────────────────────────────────────────────────────
+
+ /// 参数项列表(从业务层加载后填充,用户可编辑)
+ public ObservableCollection Items { get; } = new ObservableCollection();
+
+ // ── INotifyPropertyChanged ───────────────────────────────────────────
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value)) return false;
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+ }
+}
diff --git a/Drawer/AI/Models/SpecialMessages/ISpecialMessage.cs b/Drawer/AI/Models/SpecialMessages/ISpecialMessage.cs
new file mode 100644
index 00000000..ce8fba2c
--- /dev/null
+++ b/Drawer/AI/Models/SpecialMessages/ISpecialMessage.cs
@@ -0,0 +1,24 @@
+namespace AI.Models.SpecialMessages
+{
+ ///
+ /// 特殊消息接口 - 所有特殊消息类型的基接口
+ ///
+ public interface ISpecialMessage
+ {
+ ///
+ /// 特殊消息的唯一标识符
+ ///
+ string Id { get; }
+
+ ///
+ /// 特殊消息类型名称
+ ///
+ string TypeName { get; }
+
+ ///
+ /// 是否需要实时更新
+ ///
+ bool IsLive { get; }
+ }
+}
+
diff --git a/Drawer/AI/Models/SpecialMessages/KnowledgeBaseMessage.cs b/Drawer/AI/Models/SpecialMessages/KnowledgeBaseMessage.cs
new file mode 100644
index 00000000..b5a9cfd0
--- /dev/null
+++ b/Drawer/AI/Models/SpecialMessages/KnowledgeBaseMessage.cs
@@ -0,0 +1,32 @@
+namespace AI.Models.SpecialMessages
+{
+ ///
+ /// 知识库查询结果消息,用于存入会话 Store 并在 Prompt 视图中带给 LLM。
+ ///
+ public class KnowledgeBaseMessage : ISpecialMessage
+ {
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ public string TypeName => "KnowledgeBase";
+
+ ///
+ public bool IsLive => false;
+
+ /// 知识条目 ID(如 gridding-flow)
+ public string KnowledgeId { get; set; } = string.Empty;
+
+ /// 版本号
+ public string Version { get; set; } = "1";
+
+ /// 命中的主题 key(如 成图)
+ public string TopicKey { get; set; } = string.Empty;
+
+ /// 用户原始查询
+ public string Query { get; set; } = string.Empty;
+
+ /// 知识库返回的完整内容,供 LLM 与 UI 使用
+ public string RawContent { get; set; } = string.Empty;
+ }
+}
diff --git a/Drawer/AI/Models/SpecialMessages/ParameterSetMessage.cs b/Drawer/AI/Models/SpecialMessages/ParameterSetMessage.cs
new file mode 100644
index 00000000..707a811e
--- /dev/null
+++ b/Drawer/AI/Models/SpecialMessages/ParameterSetMessage.cs
@@ -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
+{
+ ///
+ /// 参数集单条字段的控件类型(用于可编辑参数卡片)
+ ///
+ public enum ParameterSetFieldType
+ {
+ /// 单行文本
+ Text,
+ /// 数值
+ Number,
+ /// 下拉选择(如成图算法)
+ Choice,
+ /// 文件路径,带“选择文件”按钮
+ FilePath,
+ }
+
+ ///
+ /// 单条参数项:名称、当前值(可编辑)、描述、控件类型及选项。
+ ///
+ 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);
+ }
+
+ /// 当前值的展示/编辑文本
+ public string ValueText
+ {
+ get => _valueText;
+ set
+ {
+ if (SetProperty(ref _valueText, value ?? string.Empty))
+ {
+ OnPropertyChanged(nameof(NumericValue));
+ }
+ }
+ }
+
+ ///
+ /// 数字值(仅 Number 类型控件绑定使用)。
+ /// 写回时仅在 FieldType == Number 时才更新 ValueText,
+ /// 防止不可见 NumericUpDown 的双向绑定覆盖 FilePath / Text 等字段的值。
+ ///
+ 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);
+ }
+
+ /// 控件类型(Text / Number / Choice / FilePath)
+ public ParameterSetFieldType FieldType
+ {
+ get => _fieldType;
+ set => SetProperty(ref _fieldType, value);
+ }
+
+ /// 下拉选项(Choice 类型时使用)
+ public List Options { get; set; } = new List();
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value)) return false;
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+ }
+
+ ///
+ /// 参数集特殊消息 - 用于在聊天流中展示一组「名称 / 值 / 描述」参数(如网格化参数等),由基本消息样式组合而成。
+ ///
+ 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);
+ }
+
+ /// 参数项列表(名称、值、描述)
+ public ObservableCollection Items { get; } = new ObservableCollection();
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value)) return false;
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ ///
+ /// 从 YAML 文本填充参数集。格式示例:
+ /// Input:
+ /// Value: ...
+ /// Description: 散点数据文件路径
+ /// Type: FilePath
+ /// 支持 PascalCase / camelCase 键(Value/value, Type/type 等)。
+ ///
+ public void LoadFromJson(string text)
+ {
+ Items.Clear();
+ if (string.IsNullOrWhiteSpace(text)) return;
+ LoadFromYamlInternal(text.Trim());
+ }
+
+ ///
+ /// 从 YAML 解析参数集。根级为参数名(如 Input, Faultage),每项含 Value/Description/Type/Options。
+ /// 支持根级单键包装(如 parameters: 下再挂 Input/Faultage/...),会自动解包一层。
+ ///
+ private void LoadFromYamlInternal(string yaml)
+ {
+ try
+ {
+ var deserializer = new DeserializerBuilder()
+ .IgnoreUnmatchedProperties()
+ .Build();
+
+ var root = deserializer.Deserialize>(yaml);
+ if (root == null || root.Count == 0) return;
+
+ // 若根级只有一键且其值为映射,认为是 parameters: { input: ..., faultage: ... },用内层作为参数表
+ IEnumerable> paramsIter = root;
+ if (root.Count == 1)
+ {
+ var singleValue = root.Values.First();
+ if (singleValue is Dictionary