Compare commits

..

18 Commits

Author SHA1 Message Date
687b4d50c5 Whisper.cs
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled
2026-04-02 23:45:52 +02:00
00d4461a92 settings
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled
2026-03-28 08:57:29 +02:00
14ebb04c1c CrossPlatform
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled
2026-03-28 00:29:00 +02:00
ae0994409a Yess
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled
2026-03-28 00:26:55 +02:00
ea2d84f5cc no voice
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled
2026-03-27 16:37:05 +02:00
635dacb2ad Merge remote-tracking branch 'main/main'
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled
2026-03-26 23:48:08 +02:00
27817754ce disable voice 2026-03-26 23:47:58 +02:00
Egor Basalyga
9088810aaa Update mirror.yml
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled
2026-03-26 23:40:37 +02:00
Egor Basalyga
03c323a1a4 Update mirror.yml 2026-03-26 23:39:28 +02:00
Egor Basalyga
a300b7033a Rename mirror.yml to .github/workflows/mirror.yml 2026-03-26 23:36:43 +02:00
Egor Basalyga
a0833865c8 Create mirror.yml 2026-03-26 23:30:47 +02:00
78e2483e7f yay 2026-03-20 21:10:28 +02:00
ac183f8eb6 Well Done 2026-03-20 19:26:33 +02:00
f123690cb4 Module Archeticture 2026-03-19 14:24:32 +02:00
0dd13c5a8b Work Modules 2026-03-19 13:58:29 +02:00
7fde404b5a Modules 2026-03-19 13:14:53 +02:00
9ce8e190a2 Add settings func 2026-03-18 23:16:07 +02:00
2ee4515dfd Add vosk, Naudio 2026-03-18 21:55:11 +02:00
27 changed files with 1144 additions and 21 deletions

20
.github/workflows/mirror.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Mirror to Gitea
on:
push:
delete:
jobs:
git-sync:
runs-on: ubuntu-latest
steps:
- name: Sync to Gitea
uses: wei/git-sync@v3
with:
# Теперь мы используем автоматический токен GitHub для доступа к исходному коду
source_repo: "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/Egorbasalyga/VisionAsist.git"
source_branch: "main"
# Ссылка на Gitea остается без изменений
destination_repo: "https://${{ secrets.GITEA_USER }}:${{ secrets.GITEA_TOKEN }}@git.vision-software.ru/Egor/Vision.git"
destination_branch: "main"

View File

@@ -4,6 +4,7 @@
<option name="projectPerEditor">
<map>
<entry key="VisionAsist/Views/MainWindow.axaml" value="VisionAsist/VisionAsist.csproj" />
<entry key="VisionAsist/Views/Settings.axaml" value="VisionAsist/VisionAsist.csproj" />
</map>
</option>
</component>

View File

@@ -0,0 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ModuleWeather.WeatherView">
<StackPanel Margin="20" Spacing="10" Background="#2b2b2b">
<Button Content="Обновить" Click="Update"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
<TextBlock Name="StatusText" Text="Выберите порт" Foreground="White"/>
<ComboBox Name="PortComboBox" SelectionChanged="OnPortChanged"/>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,21 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using System.Reflection;
namespace ModuleWeather;
public partial class WeatherView : UserControl
{
public WeatherView() => InitializeComponent();
private void Update(object? sender, RoutedEventArgs e)
{
}
private void OnPortChanged(object sender, SelectionChangedEventArgs e)
{
}
}

View File

@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using VisionAsist.SDK;
namespace ModuleArduino;
public class ArduinoModule : IModule
{
public string Name => "ArduinoModule";
public string Description => "Модуль для работы с COM-портами и прямой прошивки кода в Arduino";
public void Settings(object[] args) { }
public List<ToolDefinition> GetTools()
{
return new List<ToolDefinition>
{
new ToolDefinition
{
Name = "list_com_ports",
Description = "Получает список всех доступных COM-портов в системе",
ParametersSchema = "{\"type\": \"object\", \"properties\": {}}"
},
new ToolDefinition
{
Name = "send_serial_data",
Description = "Отправляет строку в указанный COM-порт",
ParametersSchema = JsonSerializer.Serialize(new
{
type = "object",
properties = new
{
port = new { type = "string", description = "Имя порта (например, COM3)" },
baudRate = new { type = "integer", description = "Скорость передачи (по умолчанию 9600)", @default = 9600 },
data = new { type = "string", description = "Данные для отправки" }
},
required = new[] { "port", "data" }
})
},
new ToolDefinition
{
Name = "read_serial_data",
Description = "Читает любые доступные данные из указанного COM-порта",
ParametersSchema = JsonSerializer.Serialize(new
{
type = "object",
properties = new
{
port = new { type = "string", description = "Имя порта (например, COM3)" },
baudRate = new { type = "integer", description = "Скорость передачи (по умолчанию 9600)", @default = 9600 },
timeoutMs = new { type = "integer", description = "Максимальное время ожидания данных в миллисекундах (по умолчанию 3000)", @default = 3000 }
},
required = new[] { "port" }
})
},
new ToolDefinition
{
Name = "compile_upload_code",
Description = "Принимает исходный код C++ (Arduino), компилирует его и загружает на плату",
ParametersSchema = JsonSerializer.Serialize(new
{
type = "object",
properties = new
{
code = new { type = "string", description = "Полный текст скетча .ino (включая setup и loop)" },
port = new { type = "string", description = "Имя порта (например, COM3)" },
boardType = new { type = "string", description = "Тип платы (например, arduino:avr:uno)" }
},
required = new[] { "code", "port", "boardType" }
})
}
};
}
public string Execute(string toolName, string argumentsJson)
{
try
{
var doc = JsonDocument.Parse(argumentsJson);
var root = doc.RootElement;
switch (toolName)
{
case "list_com_ports":
var ports = SerialPort.GetPortNames();
return ports.Length > 0 ? string.Join(", ", ports) : "Порты не найдены";
case "send_serial_data":
string portName = root.GetProperty("port").GetString()!;
string data = root.GetProperty("data").GetString()!;
int baud = root.TryGetProperty("baudRate", out var b) ? b.GetInt32() : 9600;
using (var serial = new SerialPort(portName, baud))
{
serial.DtrEnable = true; // Важно для многих плат Arduino
serial.RtsEnable = true;
serial.Open();
serial.WriteLine(data);
Thread.Sleep(100); // Даем время на отправку
serial.Close();
}
return $"Отправлено в {portName}: {data}";
case "read_serial_data":
string rPortName = root.GetProperty("port").GetString()!;
int rBaud = root.TryGetProperty("baudRate", out var rb) ? rb.GetInt32() : 9600;
int timeout = root.TryGetProperty("timeoutMs", out var rt) ? rt.GetInt32() : 3000;
using (var serial = new SerialPort(rPortName, rBaud))
{
serial.DtrEnable = true; // Без этого Arduino может не слать данные
serial.RtsEnable = true;
serial.Open();
// После открытия порта Arduino может перезагрузиться,
// дадим ей немного времени прийти в себя
Thread.Sleep(500);
int waited = 500;
while (serial.BytesToRead == 0 && waited < timeout)
{
Thread.Sleep(100);
waited += 100;
}
if (serial.BytesToRead > 0)
{
string received = serial.ReadExisting();
serial.Close();
return $"Получено из {rPortName}: {received}";
}
serial.Close();
return $"Данные из {rPortName} не поступили за {timeout}мс.";
}
case "compile_upload_code":
string code = root.GetProperty("code").GetString()!;
string p = root.GetProperty("port").GetString()!;
string board = root.GetProperty("boardType").GetString()!;
return HandleCompileAndUpload(code, p, board);
default:
return "Инструмент не найден";
}
}
catch (Exception ex) { return $"Ошибка: {ex.Message}"; }
}
private string HandleCompileAndUpload(string code, string port, string board)
{
string tempDir = Path.Combine(Path.GetTempPath(), "VisionAsist_Arduino_" + Guid.NewGuid().ToString("N"));
string sketchName = "Sketch";
string sketchDir = Path.Combine(tempDir, sketchName);
string sketchPath = Path.Combine(sketchDir, sketchName + ".ino");
string modulePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
// Определяем имя файла в зависимости от ОС
bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
string exeName = isWindows ? "arduino-cli.exe" : "arduino-cli";
string arduinoCliPath = Path.Combine(modulePath, exeName);
if (!File.Exists(arduinoCliPath)) arduinoCliPath = exeName;
try
{
Directory.CreateDirectory(sketchDir);
File.WriteAllText(sketchPath, code);
var compileResult = RunCommand(arduinoCliPath, $"compile --fqbn {board} \"{sketchDir}\"");
if (compileResult.ExitCode != 0) return $"Ошибка компиляции:\n{compileResult.Error}";
var uploadResult = RunCommand(arduinoCliPath, $"upload -p {port} --fqbn {board} \"{sketchDir}\"");
if (uploadResult.ExitCode != 0) return $"Ошибка загрузки:\n{uploadResult.Error}";
return "Код успешно скомпилирован и загружен в Arduino!";
}
catch (Exception ex) { return $"Критическая ошибка: {ex.Message}"; }
finally
{
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
}
}
private (int ExitCode, string Output, string Error) RunCommand(string cmd, string args)
{
try
{
var process = Process.Start(new ProcessStartInfo
{
FileName = cmd,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
});
string output = process?.StandardOutput.ReadToEnd() ?? "";
string error = process?.StandardError.ReadToEnd() ?? "";
process?.WaitForExit();
return (process?.ExitCode ?? -1, output, error);
}
catch (System.ComponentModel.Win32Exception)
{
return (-1, "", $"Ошибка: Исполняемый файл '{cmd}' не найден в папке модуля или в системе.");
}
}
}

View File

@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>ArduinoModule</AssemblyName>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\VisionAsist.SDK\VisionAsist.SDK.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
</ItemGroup>
<Target Name="CopyModuleToCore" AfterTargets="PostBuildEvent">
<ItemGroup>
<!-- Копируем ВСЕ DLL и PDB рекурсивно, чтобы не потерять зависимости NuGet -->
<ModuleFiles Include="$(TargetDir)**\*.dll" />
<ModuleFiles Include="$(TargetDir)**\*.pdb" />
</ItemGroup>
<Message Text="Копирую файлы модуля и все зависимости..." Importance="high" />
<Copy SourceFiles="@(ModuleFiles)"
DestinationFolder="$(SolutionDir)VisionAsist\bin\$(Configuration)\$(TargetFramework)\Modules\ArduinoModule"
OverwriteReadOnlyFiles="true" />
</Target>
<ItemGroup>
<Compile Update="MainWindow.axaml.cs">
<DependentUpon>MainWindow.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ModuleWeather.WeatherView">
<StackPanel Margin="20" Spacing="10" Background="#2b2b2b">
<Button Content="Обновить" Click="Update"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
<TextBlock Name="StatusText" Text="Выберите порт" Foreground="White"/>
<ComboBox Name="PortComboBox" SelectionChanged="OnPortChanged"/>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,21 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using System.Reflection;
namespace ModuleWeather;
public partial class WeatherView : UserControl
{
public WeatherView() => InitializeComponent();
private void Update(object? sender, RoutedEventArgs e)
{
}
private void OnPortChanged(object sender, SelectionChangedEventArgs e)
{
}
}

102
ModuleWeather/Module.cs Normal file
View File

@@ -0,0 +1,102 @@
using Avalonia.Controls;
using System.Collections.Generic;
using System.Text.Json;
using VisionAsist.SDK;
namespace ModuleWeather;
public class WeatherModule : IModule
{
public string Name => "WeatherModule";
public string Description => "Модуль для получения данных о погоде";
public void Settings(object[] args)
{
// args[0] — это родительское окно из Ядра
var parentWindow = args != null && args.Length > 0 ? args[0] as Window : null;
var win = new Window
{
Title = "Настройки",
Content = new WeatherView(), // Вставляем наш контрол
Width = 350,
Height = 250,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
if (parentWindow != null) win.Show(parentWindow);
else win.Show();
}
public List<ToolDefinition> GetTools()
{
return new List<ToolDefinition>
{
new ToolDefinition
{
Name = "get_weather",
Description = "Получает текущую погоду для указанного города",
ParametersSchema = JsonSerializer.Serialize(new
{
type = "object",
properties = new
{
city = new { type = "string", description = "Название города на русском языке" }
},
required = new[] { "city" }
})
},
new ToolDefinition
{
Name = "get_qnh",
Description = "Получает текущее давление над уровнем моря для указанного города",
ParametersSchema = JsonSerializer.Serialize(new
{
type = "object",
properties = new
{
city = new { type = "string", description = "Название города на русском языке" }
},
required = new[] { "city" }
})
}
};
}
public string Execute(string toolName, string argumentsJson)
{
if (toolName == "get_weather")
{
try
{
var args = JsonDocument.Parse(argumentsJson);
if (args.RootElement.TryGetProperty("city", out var cityElement))
{
string city = cityElement.GetString() ?? "неизвестном городе";
return $"Погода в {city} сейчас отличная, солнечно, +25 градусов!";
}
}
catch
{
return "Ошибка при обработке аргументов погоды";
}
}
else if (toolName == "get_qnh")
{
try
{
var args = JsonDocument.Parse(argumentsJson);
if (args.RootElement.TryGetProperty("city", out var cityElement))
{
string city = cityElement.GetString() ?? "неизвестном городе";
return $"Давление над уровнем моря в {city} сейчас 1020 мм рт. ст!";
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
return "Инструмент не найден";
}
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- ВОТ ЭТА СТРОЧКА: она заставит компилятор создать Module.dll -->
<AssemblyName>ModuleWeather</AssemblyName>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\VisionAsist.SDK\VisionAsist.SDK.csproj" />
</ItemGroup>
<Target Name="CopyModuleToCore" AfterTargets="PostBuildEvent">
<ItemGroup>
<!-- Копируем все DLL и PDB из корня -->
<ModuleFiles Include="$(TargetDir)*.dll" />
<ModuleFiles Include="$(TargetDir)*.pdb" />
<!-- Ищем нативную либу .so ВЕЗДЕ в выходной папке (включая подпапки runtimes) -->
<NativeLibs Include="$(TargetDir)**\*.so" />
</ItemGroup>
<Message Text="Копирую файлы модуля и нативные библиотеки..." Importance="high" />
<Copy SourceFiles="@(ModuleFiles)"
DestinationFolder="E:\Project\Visual\VisionAsist\VisionAsist\bin\Debug\net10.0\Modules\ModuleWeather"
OverwriteReadOnlyFiles="true" />
<!-- Копируем .so файлы ПРЯМО в корень папки модуля -->
<Copy SourceFiles="@(NativeLibs)"
DestinationFolder="E:\Project\Visual\VisionAsist\VisionAsist\bin\Debug\net10.0\Modules\ModuleWeather"
OverwriteReadOnlyFiles="true" />
</Target>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace VisionAsist.SDK;
public interface IModule
{
string Name { get; }
string Description { get; }
void Settings(object[] args);
// Возвращает список инструментов.
// Каждый инструмент описывает себя через JSON Schema.
List<ToolDefinition> GetTools();
// Выполняет инструмент.
// argumentsJson — это JSON объект с параметрами, который сгенерирeовал ИИ.
string Execute(string toolName, string argumentsJson);
}
public class ToolDefinition
{
// Имя функции (например, "get_weather")
public string Name { get; set; } = string.Empty;
// Описание для ИИ (что делает эта функция)
public string Description { get; set; } = string.Empty;
// Схема параметров в формате JSON Schema.
// Именно это поле мы будем скармливать нейросети.
public string ParametersSchema { get; set; } = "{\"type\": \"object\", \"properties\": {}}";
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -2,6 +2,15 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionAsist", "VisionAsist\VisionAsist.csproj", "{C5BD9C58-AE7A-4BBA-8700-1E71F48DBCA0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionAsist.SDK", "VisionAsist.SDK\VisionAsist.SDK.csproj", "{9E547157-DA01-40FB-9DB1-67A1C310E3F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModuleWeather", "ModuleWeather\ModuleWeather.csproj", "{5103146A-34FD-4431-856E-AB78EF523C03}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModuleArduinoCompile", "ModuleArduinoCompile\ModuleArduinoCompile.csproj", "{167D1FD2-01E8-4AC5-96FA-548BEAE07822}"
ProjectSection(ProjectDependencies) = postProject
{9E547157-DA01-40FB-9DB1-67A1C310E3F1} = {9E547157-DA01-40FB-9DB1-67A1C310E3F1}
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -12,5 +21,17 @@ Global
{C5BD9C58-AE7A-4BBA-8700-1E71F48DBCA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5BD9C58-AE7A-4BBA-8700-1E71F48DBCA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5BD9C58-AE7A-4BBA-8700-1E71F48DBCA0}.Release|Any CPU.Build.0 = Release|Any CPU
{9E547157-DA01-40FB-9DB1-67A1C310E3F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E547157-DA01-40FB-9DB1-67A1C310E3F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E547157-DA01-40FB-9DB1-67A1C310E3F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E547157-DA01-40FB-9DB1-67A1C310E3F1}.Release|Any CPU.Build.0 = Release|Any CPU
{5103146A-34FD-4431-856E-AB78EF523C03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5103146A-34FD-4431-856E-AB78EF523C03}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5103146A-34FD-4431-856E-AB78EF523C03}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5103146A-34FD-4431-856E-AB78EF523C03}.Release|Any CPU.Build.0 = Release|Any CPU
{167D1FD2-01E8-4AC5-96FA-548BEAE07822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{167D1FD2-01E8-4AC5-96FA-548BEAE07822}.Debug|Any CPU.Build.0 = Debug|Any CPU
{167D1FD2-01E8-4AC5-96FA-548BEAE07822}.Release|Any CPU.ActiveCfg = Release|Any CPU
{167D1FD2-01E8-4AC5-96FA-548BEAE07822}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInitHelpers_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3bfb329b50ab4b719e770a577377990fdba600_003F92_003F7acfd65c_003FInitHelpers_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APorcupine_002Ecs_002Fl_003AC_0021_003FUsers_003Fmarin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F395b25d8175b43798becf32fa73896954c00_003Fa6_003F28640b1d_003FPorcupine_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASerialPort_002EUnix_002Ecs_002Fl_003AC_0021_003FUsers_003Fmarin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F955be2f0e9f11ba554df3fb7b4d1c16483286c82bb3b26ac164fc3657e4477_003FSerialPort_002EUnix_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASoundIO_002Ecs_002Fl_003AC_0021_003FUsers_003Fmarin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2ac0091ca0084789a96acdec8e3a9449d000_003F84_003Fd6e02c94_003FSoundIO_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASoundIOInStream_002Ecs_002Fl_003AC_0021_003FUsers_003Fmarin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2ac0091ca0084789a96acdec8e3a9449d000_003F37_003F2b136ae3_003FSoundIOInStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASoundIO_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2ac0091ca0084789a96acdec8e3a9449d000_003F84_003Fd6e02c94_003FSoundIO_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

111
VisionAsist/Models/Core.cs Normal file
View File

@@ -0,0 +1,111 @@
using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using VisionAsist.SDK;
namespace VisionAsist.Models;
public class Core
{
public class LoadedModule
{
public string Name { get; set; } = string.Empty;
public IModule Module { get; set; } = null!;
public List<ToolDefinition> Tools { get; set; } = new();
}
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(PluginPath)) Directory.CreateDirectory(PluginPath);
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 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))
{
try
{
Assembly assembly = Assembly.LoadFrom(dllPath);
var type = assembly.GetTypes().FirstOrDefault(t =>
typeof(IModule).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
if (type != null)
{
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 static string BaseUrl => SettingsManager.Current.OllamaBaseUrl;
public static async IAsyncEnumerable<string> SendChatStreamAsync(string model, List<OllamaMessage> messages, string? toolsJson = null)
{
var requestBody = new Dictionary<string, object>
{
{ "model", string.IsNullOrEmpty(model) ? SettingsManager.Current.OllamaModel : 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

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using System.Linq;
namespace VisionAsist.Models;
public class Selector
{
private static List<OllamaMessage> _chatHistory = new();
public static string CurrentModel => SettingsManager.Current.OllamaModel;
public static event Action<string>? OnLogUpdate;
private static void UpdateUI(string text) => OnLogUpdate?.Invoke(text);
public static async Task<string> selector(string text)
{
try
{
UpdateUI($"\n[Вы]: {text}\n");
_chatHistory.Add(new OllamaMessage { role = "user", content = text });
while (true)
{
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))
{
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 thinkPart = thinkingEl.GetString() ?? "";
if (!string.IsNullOrEmpty(thinkPart))
{
if (!isThinking)
{
UpdateUI("\n[Мысли]: ");
isThinking = true;
}
UpdateUI(thinkPart);
}
}
// 2. ОБРАБОТКА 'content' (если есть в этом чанке)
if (message.TryGetProperty("content", out var contentEl))
{
string part = contentEl.GetString() ?? "";
if (!string.IsNullOrEmpty(part))
{
// Если мы только что "думали", переключаемся на ответ
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

@@ -0,0 +1,57 @@
using System;
using System.IO;
using System.Text.Json;
namespace VisionAsist.Models;
public class AppSettings
{
public string OllamaBaseUrl { get; set; } = "http://localhost:11434/api";
public string OllamaModel { get; set; } = "llama3";
}
public static class SettingsManager
{
private static readonly string SettingsFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json");
private static AppSettings _current = new();
public static AppSettings Current => _current;
static SettingsManager()
{
Load();
}
public static void Load()
{
try
{
if (File.Exists(SettingsFilePath))
{
var json = File.ReadAllText(SettingsFilePath);
_current = JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
}
else
{
Save();
}
}
catch
{
_current = new AppSettings();
}
}
public static void Save()
{
try
{
var json = JsonSerializer.Serialize(_current, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettingsFilePath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving settings: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,6 @@
namespace VisionAsist.Models;
public class Whisper
{
}

View File

@@ -11,11 +11,11 @@ sealed class Program
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
}

View File

@@ -1,6 +1,55 @@
namespace VisionAsist.ViewModels;
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 string Greeting { get; } = "Welcome to Avalonia!";
public MainWindowViewModel()
{
Selector.OnLogUpdate += text =>
{
Dispatcher.UIThread.Post(() =>
{
CommandLog += text;
});
};
}
[RelayCommand]
private void Settingse()
{
new Settings { DataContext = new SettingsViewModel() }.Show();
}
[ObservableProperty]
private string commandLog = string.Empty;
[ObservableProperty]
private string commandText = string.Empty;
[ObservableProperty]
private bool isGenerating = false;
[RelayCommand]
private async Task Send()
{
if (!string.IsNullOrWhiteSpace(CommandText) && !IsGenerating)
{
string input = CommandText;
CommandText = string.Empty;
IsGenerating = true;
// Запускаем логику в фоновом потоке, чтобы UI не зависал
await Task.Run(async () => {
await Selector.selector(input);
IsGenerating = false;
});
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Reflection;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using VisionAsist.Models;
using VisionAsist.SDK;
using System.IO;
using System.Linq;
namespace VisionAsist.ViewModels;
public class ModuleItem
{
public string Name { get; set; }
public IRelayCommand SettingsCommand { get; set; }
}
public class SettingsViewModel : ViewModelBase
{
public string OllamaBaseUrl
{
get => SettingsManager.Current.OllamaBaseUrl;
set
{
SettingsManager.Current.OllamaBaseUrl = value;
OnPropertyChanged();
}
}
public string OllamaModel
{
get => SettingsManager.Current.OllamaModel;
set
{
SettingsManager.Current.OllamaModel = value;
OnPropertyChanged();
}
}
public IRelayCommand SaveCommand { get; }
public ObservableCollection<ModuleItem> Modules { get; } = new();
public SettingsViewModel()
{
SaveCommand = new RelayCommand(SettingsManager.Save);
foreach (var module in Core.ModuleList)
{
AddModule(module.Name);
}
}
public void AddModule(string name)
{
Modules.Add(new ModuleItem
{
Name = name,
SettingsCommand = new RelayCommand(() => OpenSettings(name))
});
}
private void OpenSettings(string moduleName)
{
var module = Core.ModuleList.FirstOrDefault(m => m.Name == moduleName);
if (module != null)
{
module.Module.Settings(new object[] { this });
}
}
}

View File

@@ -1,7 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace VisionAsist.ViewModels;
public abstract class ViewModelBase : ObservableObject
public partial class ViewModelBase : ObservableObject
{
}

View File

@@ -7,14 +7,41 @@
x:Class="VisionAsist.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="VisionAsist">
Title="VisionAsist" Height="400" Width="600">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainWindowViewModel/>
</Design.DataContext>
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="10">
<Button Content="Настройки" Command="{Binding SettingseCommand}"/>
<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsGenerating}" Width="100" Margin="10,0"/>
</StackPanel>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Window>
<Grid ColumnDefinitions="*, Auto" Margin="10,0,10,10">
<TextBox Grid.Column="0"
Text="{Binding CommandText}"
IsEnabled="{Binding !IsGenerating}"
Watermark="Введите команду..."
VerticalAlignment="Center">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SendCommand}"/>
</TextBox.KeyBindings>
</TextBox>
<Button Grid.Column="1"
Content="Отправить"
Command="{Binding SendCommand}"
IsEnabled="{Binding !IsGenerating}"
Margin="5,0,0,0"/>
</Grid>
<ScrollViewer Height="300" CornerRadius="5" Background="#1E1E1E" Name="ChatScroller">
<SelectableTextBlock Text="{Binding CommandLog}"
TextWrapping="Wrap"
Padding="10"
FontFamily="Cascadia Code, Consolas, Monospace"
Foreground="#DCDCDC"/>
</ScrollViewer>
</StackPanel>
</Window>

View File

@@ -0,0 +1,51 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:VisionAsist.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="VisionAsist.Views.Settings"
x:DataType="vm:SettingsViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="VisionAsist" Height="400" Width="600">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:SettingsViewModel/>
</Design.DataContext>
<DockPanel LastChildFill="True" Margin="10">
<StackPanel DockPanel.Dock="Top" Spacing="10" Margin="0,0,0,10">
<TextBlock Text="Ollama Settings" FontSize="20" FontWeight="Bold"/>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
<TextBlock Text="Base URL:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Column="1" Text="{Binding OllamaBaseUrl}" Margin="0,5"/>
<TextBlock Grid.Row="1" Text="Model:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding OllamaModel}" Margin="0,5"/>
</Grid>
<Button Content="Save Settings" Command="{Binding SaveCommand}" HorizontalAlignment="Right"/>
<Separator Margin="0,10"/>
<TextBlock Text="Modules" FontSize="18" FontWeight="SemiBold"/>
</StackPanel>
<ListBox ItemsSource="{Binding Modules}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ModuleItem">
<Border Background="#2a2a2a" CornerRadius="5" Padding="10" Margin="5">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
<Button Content="Settings"
Grid.Column="1"
Command="{Binding SettingsCommand}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace VisionAsist.Views;
public partial class Settings : Window
{
public Settings()
{
InitializeComponent();
}
}

View File

@@ -1,27 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<RunCommand>LD_LIBRARY_PATH=/usr/lib $(RunCommand)</RunCommand>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\"/>
<AvaloniaResource Include="Assets\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.11"/>
<PackageReference Include="Avalonia.Desktop" Version="11.3.11"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.11"/>
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.11">
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="..\VisionAsist.SDK\VisionAsist.SDK.csproj" />
</ItemGroup>
</Project>