Yess
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled

This commit is contained in:
2026-03-28 00:26:55 +02:00
parent ea2d84f5cc
commit ae0994409a
15 changed files with 660 additions and 94 deletions

View File

@@ -2,56 +2,110 @@ using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using VisionAsist.SDK;
using System.Linq;
using System.Text.Json;
using VisionAsist.SDK;
namespace VisionAsist.Models;
public class Core
{
public class Modules
public class LoadedModule
{
public string Name { get; set; }
public IModule Module { get; set; }
public string[] commands { get; set; }
public string Name { get; set; } = string.Empty;
public IModule Module { get; set; } = null!;
public List<ToolDefinition> Tools { get; set; } = new();
}
public static List<Modules> modulelist = new();
static string Plugin = Path.Combine(AppContext.BaseDirectory, "Modules");
public static List<LoadedModule> ModuleList = new();
static readonly string PluginPath = Path.Combine(AppContext.BaseDirectory, "Modules");
static Core()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.InputEncoding = System.Text.Encoding.UTF8;
if (!Directory.Exists(Plugin))
{
Directory.CreateDirectory(Plugin);
}
if (!Directory.Exists(PluginPath)) Directory.CreateDirectory(PluginPath);
string[] folderNames = new DirectoryInfo(Plugin)
.GetDirectories()
.Select(d => d.Name)
.ToArray();
foreach (string folderName in folderNames)
AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
LoadModules();
}
private static Assembly? ResolveAssembly(object? sender, ResolveEventArgs args)
{
string assemblyName = new AssemblyName(args.Name).Name + ".dll";
foreach (var dir in Directory.GetDirectories(PluginPath))
{
string mpn = Path.Combine(Plugin, folderName, "Module.dll");
if (File.Exists(mpn))
string assemblyPath = Path.Combine(dir, assemblyName);
if (File.Exists(assemblyPath)) return Assembly.LoadFrom(assemblyPath);
}
return null;
}
private static void LoadModules()
{
if (!Directory.Exists(PluginPath)) return;
foreach (var dir in Directory.GetDirectories(PluginPath))
{
string folderName = Path.GetFileName(dir);
// DLL называется так же, как и папка
string dllPath = Path.Combine(dir, folderName + ".dll");
if (File.Exists(dllPath))
{
Assembly assembly = Assembly.LoadFrom(mpn);
var type = assembly.GetTypes().FirstOrDefault(t =>
typeof(IModule).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
if (type != null)
try
{
var module = (IModule)Activator.CreateInstance(type)!;
modulelist.Add(new Modules { Name = module.Name, Module = module, commands = module.GetCommands() });
foreach (var cmd in module.GetCommands())
Assembly assembly = Assembly.LoadFrom(dllPath);
var type = assembly.GetTypes().FirstOrDefault(t =>
typeof(IModule).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
if (type != null)
{
Console.WriteLine($"- {cmd}");
var module = (IModule)Activator.CreateInstance(type)!;
if (!ModuleList.Any(m => m.Name == module.Name))
{
ModuleList.Add(new LoadedModule
{
Name = module.Name,
Module = module,
Tools = module.GetTools()
});
Console.WriteLine($"[CORE]: Загружен модуль '{module.Name}' из {dllPath}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[CORE ERROR]: Не удалось загрузить {dllPath}: {ex.Message}");
}
}
}
}
public static string GetToolsManifestJson()
{
var allTools = ModuleList.SelectMany(m => m.Tools.Select(t => new
{
type = "function",
function = new
{
name = t.Name,
description = t.Description,
parameters = JsonDocument.Parse(t.ParametersSchema).RootElement
}
})).ToList();
return JsonSerializer.Serialize(allTools, new JsonSerializerOptions { WriteIndented = true });
}
public static string CallTool(string toolName, string argsJson)
{
foreach (var module in ModuleList)
{
var tool = module.Tools.FirstOrDefault(t => t.Name == toolName);
if (tool != null) return module.Module.Execute(toolName, argsJson);
}
return "Ошибка: Инструмент не найден";
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.IO;
namespace VisionAsist.Models;
public class OllamaMessage
{
public string role { get; set; } = string.Empty;
public string content { get; set; } = string.Empty;
}
public static class OllamaService
{
private static readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromMinutes(5) };
private const string BaseUrl = "http://localhost:11434/api";
public static async IAsyncEnumerable<string> SendChatStreamAsync(string model, List<OllamaMessage> messages, string? toolsJson = null)
{
var requestBody = new Dictionary<string, object>
{
{ "model", model },
{ "messages", messages },
{ "stream", true } // ВКЛЮЧАЕМ СТРИМИНГ
};
if (!string.IsNullOrEmpty(toolsJson))
{
requestBody.Add("tools", JsonDocument.Parse(toolsJson).RootElement);
}
var json = JsonSerializer.Serialize(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/chat") { Content = content };
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (!string.IsNullOrWhiteSpace(line))
{
yield return line;
}
}
}
}

View File

@@ -1,38 +1,116 @@
using System;
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using System.Linq;
namespace VisionAsist.Models;
public class Selector
{
public static void selector(string text)
private static List<OllamaMessage> _chatHistory = new();
public static string CurrentModel = "qwen3.5:4b";
public static event Action<string>? OnLogUpdate;
private static void UpdateUI(string text) => OnLogUpdate?.Invoke(text);
public static async Task<string> selector(string text)
{
if (text.Contains("вижен"))
try
{
string novision = text.Replace("вижен", "").Trim();
foreach (var module in Core.modulelist)
UpdateUI($"\n[Вы]: {text}\n");
_chatHistory.Add(new OllamaMessage { role = "user", content = text });
while (true)
{
foreach (var command in module.commands)
string toolsJson = Core.GetToolsManifestJson();
string fullContent = "";
string toolCallsRaw = "";
bool isThinking = false;
bool hasStartedContent = false;
UpdateUI("\n[ИИ]: ");
await foreach (var line in OllamaService.SendChatStreamAsync(CurrentModel, _chatHistory, toolsJson))
{
if (command.Contains("*"))
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
if (!root.TryGetProperty("message", out var message)) continue;
// 1. ОБРАБОТКА 'thinking' (если есть в этом чанке)
if (message.TryGetProperty("thinking", out var thinkingEl))
{
string wopo = command.Replace("*", "").Trim();
if (novision.Contains(wopo))
string thinkPart = thinkingEl.GetString() ?? "";
if (!string.IsNullOrEmpty(thinkPart))
{
Console.WriteLine(module.Module.Execute(command));
break;
if (!isThinking)
{
UpdateUI("\n[Мысли]: ");
isThinking = true;
}
UpdateUI(thinkPart);
}
}
else
// 2. ОБРАБОТКА 'content' (если есть в этом чанке)
if (message.TryGetProperty("content", out var contentEl))
{
if (command == novision)
string part = contentEl.GetString() ?? "";
if (!string.IsNullOrEmpty(part))
{
Console.WriteLine(module.Module.Execute(novision));
break;
// Если мы только что "думали", переключаемся на ответ
if (isThinking)
{
isThinking = false;
hasStartedContent = true;
UpdateUI("\n[Ответ]: ");
}
else if (!hasStartedContent && !string.IsNullOrWhiteSpace(part))
{
UpdateUI("\n[Ответ]: ");
hasStartedContent = true;
}
fullContent += part;
UpdateUI(part);
}
}
// 3. ОБРАБОТКА 'tool_calls' (если есть в этом чанке)
if (message.TryGetProperty("tool_calls", out var toolsEl))
{
toolCallsRaw = toolsEl.GetRawText();
}
}
_chatHistory.Add(new OllamaMessage { role = "assistant", content = fullContent });
// 4. ВЫПОЛНЕНИЕ ИНСТРУМЕНТОВ (после завершения стрима)
if (!string.IsNullOrEmpty(toolCallsRaw))
{
using var toolDoc = JsonDocument.Parse(toolCallsRaw);
foreach (var call in toolDoc.RootElement.EnumerateArray())
{
string toolName = call.GetProperty("function").GetProperty("name").GetString() ?? "";
string argsJson = call.GetProperty("function").GetProperty("arguments").GetRawText();
UpdateUI($"\n\n[ДЕЙСТВИЕ]: {toolName}\n[АРГУМЕНТЫ]: {argsJson}");
string result = Core.CallTool(toolName, argsJson);
UpdateUI($"\n[РЕЗУЛЬТАТ]: {result}\n");
_chatHistory.Add(new OllamaMessage { role = "tool", content = result });
}
continue; // Снова к ИИ с результатами инструментов
}
return fullContent;
}
}
catch (Exception ex)
{
UpdateUI($"\n[ОШИБКА]: {ex.Message}");
return ex.Message;
}
}
public static void ClearHistory() => _chatHistory.Clear();
}

View File

@@ -1,20 +1,30 @@
using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using VisionAsist.Models;
using VisionAsist.Views;
using Avalonia.Threading;
namespace VisionAsist.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
Selector.OnLogUpdate += text =>
{
Dispatcher.UIThread.Post(() =>
{
CommandLog += text;
});
};
}
[RelayCommand]
private void Settingse()
{
new Settings
{
DataContext = new SettingsViewModel()
}.Show();
new Settings { DataContext = new SettingsViewModel() }.Show();
}
[ObservableProperty]
@@ -23,19 +33,23 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty]
private string commandText = string.Empty;
[ObservableProperty]
private bool isGenerating = false;
[RelayCommand]
private void Send()
private async Task Send()
{
if (!string.IsNullOrWhiteSpace(CommandText))
if (!string.IsNullOrWhiteSpace(CommandText) && !IsGenerating)
{
// Отображаем введенную команду в логе
CommandLog = CommandText;
// Отправляем в селектор
Selector.selector(CommandText);
// Очищаем поле ввода
string input = CommandText;
CommandText = string.Empty;
IsGenerating = true;
// Запускаем логику в фоновом потоке, чтобы UI не зависал
await Task.Run(async () => {
await Selector.selector(input);
IsGenerating = false;
});
}
}
}

View File

@@ -23,7 +23,7 @@ public class SettingsViewModel : ViewModelBase
public SettingsViewModel()
{
foreach (var module in Core.modulelist)
foreach (var module in Core.ModuleList)
{
AddModule(module.Name);
@@ -43,7 +43,7 @@ public class SettingsViewModel : ViewModelBase
private void OpenSettings(string moduleName)
{
var module = Core.modulelist.FirstOrDefault(m => m.Name == moduleName);
var module = Core.ModuleList.FirstOrDefault(m => m.Name == moduleName);
if (module != null)
{
module.Module.Settings(new object[] { this });

View File

@@ -16,12 +16,14 @@
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="10">
<Button Content="Настройки" Command="{Binding SettingseCommand}"/>
<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsGenerating}" Width="100" Margin="10,0"/>
</StackPanel>
<Grid ColumnDefinitions="*, Auto" Margin="10,0,10,10">
<TextBox Grid.Column="0"
Text="{Binding CommandText}"
Watermark="Введите команду (например: вижен погода)..."
IsEnabled="{Binding !IsGenerating}"
Watermark="Введите команду..."
VerticalAlignment="Center">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SendCommand}"/>
@@ -30,14 +32,16 @@
<Button Grid.Column="1"
Content="Отправить"
Command="{Binding SendCommand}"
IsEnabled="{Binding !IsGenerating}"
Margin="5,0,0,0"/>
</Grid>
<ScrollViewer Height="300" CornerRadius="5" Background="Black">
<TextBlock Text="{Binding CommandLog}"
<ScrollViewer Height="300" CornerRadius="5" Background="#1E1E1E" Name="ChatScroller">
<SelectableTextBlock Text="{Binding CommandLog}"
TextWrapping="Wrap"
Padding="10"
Foreground="White"/>
FontFamily="Cascadia Code, Consolas, Monospace"
Foreground="#DCDCDC"/>
</ScrollViewer>
</StackPanel>
</Window>

View File

@@ -23,6 +23,7 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
</ItemGroup>
<ItemGroup>