chore: initial skeleton + KOMPAS connection spike (S1/S2)

Зафиксирован фундамент проекта MCP-сервера КОМПАС-3D:

- docs/: ARCHITECTURE.md, IMPLEMENTATION_PLAN.md, presentation.html
- tools/kdoc.py: навигатор по справке SDK
- libs/kompas-interop/: вендорские interop-сборки КОМПАС из SDK
- src/Kompas.Mcp.ConnectSpike: net8.0-windows консоль, доказывает
  работу COM-интеропа и attach/launch подключения к КОМПАС
  (IApplication, версия 24.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 23:28:29 +03:00
commit 3a98afc67f
17 changed files with 1297 additions and 0 deletions
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<AssemblyName>kompas-connect-spike</AssemblyName>
<RootNamespace>Kompas.Mcp.ConnectSpike</RootNamespace>
</PropertyGroup>
<!-- Вендорские interop-сборки КОМПАС (из SDK Samples/Common). -->
<ItemGroup>
<Reference Include="KompasAPI7">
<HintPath>$(KompasInteropDir)\KompasAPI7.dll</HintPath>
<Private>true</Private>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
<Reference Include="Kompas6API5">
<HintPath>$(KompasInteropDir)\Kompas6API5.dll</HintPath>
<Private>true</Private>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
<Reference Include="Kompas6Constants">
<HintPath>$(KompasInteropDir)\Kompas6Constants.dll</HintPath>
<Private>true</Private>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
<Reference Include="Kompas6Constants3D">
<HintPath>$(KompasInteropDir)\Kompas6Constants3D.dll</HintPath>
<Private>true</Private>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
</ItemGroup>
</Project>
+137
View File
@@ -0,0 +1,137 @@
// Спайк S1+S2: проверка interop-сборок и подключения к КОМПАС-3D.
//
// Доказывает, что:
// * вендорские interop-сборки (KompasAPI7 / Kompas6API5) собираются и загружаются под net8.0-windows;
// * клиент умеет attach к запущенному КОМПАС и launch нового экземпляра;
// * получает IApplication (API7), делает окно видимым и читает версию.
//
// Внимание: Marshal.GetActiveObject отсутствует в .NET 5+, поэтому attach делается
// через нативные CLSIDFromProgID + GetActiveObject (oleaut32).
//
// Логи -> stderr, итог -> stdout. Код возврата 0 = успех.
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas6API5;
using KompasAPI7;
namespace Kompas.Mcp.ConnectSpike;
[SupportedOSPlatform("windows")]
internal static class Program
{
// Порядок попыток подключения. На v24 Home проверено: есть .7 и .5, нет LT.
private static readonly string[] ProgIds =
{
"KOMPAS.Application.7", // прямой API7
"KOMPAS.Application.5", // API5 -> ksGetApplication7()
"KOMPASLT.Application.5", // запасной (LT-инсталляции)
};
private static int Main()
{
Log("=== KOMPAS connect spike ===");
bool launchedByUs = false;
IApplication? app = null;
// 1) attach к уже запущенному экземпляру
foreach (var id in ProgIds)
{
var root = AttachByProgId(id);
if (root is null) continue;
app = AsApplication(root);
if (app != null) { Log($"attached: {id}"); break; }
}
// 2) запустить новый экземпляр
if (app is null)
{
foreach (var id in ProgIds)
{
var root = CreateByProgId(id);
if (root is null) continue;
app = AsApplication(root);
if (app != null) { launchedByUs = true; Log($"launched: {id}"); break; }
}
}
if (app is null)
{
Log("РЕЗУЛЬТАТ: подключиться к КОМПАС не удалось.");
return 1;
}
app.Visible = true;
Console.WriteLine("CONNECTED");
ReportVersion(app);
Log(launchedByUs
? "Экземпляр запущен этим процессом (в реальном сервере здесь решаем про Quit при выходе)."
: "Подключились к уже запущенному экземпляру (Quit не вызываем).");
Log("Готово. Окно КОМПАС должно быть видимым.");
return 0;
}
/// <summary>Привести COM-корень к IApplication: API7 — прямой каст, API5 — через ksGetApplication7().</summary>
private static IApplication? AsApplication(object root)
{
if (root is IApplication direct)
return direct;
if (root is KompasObject ko)
{
try { return ko.ksGetApplication7() as IApplication; }
catch (Exception ex) { Log($"ksGetApplication7 failed: {ex.Message}"); }
}
try { return (IApplication)root; }
catch { return null; }
}
private static void ReportVersion(IApplication app)
{
try { Console.WriteLine($"BuildNumber: {app.BuildNumber}"); }
catch (Exception ex) { Log($"BuildNumber failed: {ex.Message}"); }
try
{
int major = 0, minor = 0, release = 0, build = 0;
app.GetSystemVersion(out major, out minor, out release, out build);
Console.WriteLine($"Version: {major}.{minor}.{release} (build {build})");
}
catch (Exception ex) { Log($"GetSystemVersion failed: {ex.Message}"); }
}
private static object? AttachByProgId(string progId)
{
try
{
CLSIDFromProgID(progId, out Guid clsid);
GetActiveObject(ref clsid, IntPtr.Zero, out object obj);
return obj;
}
catch
{
return null; // не запущен / не зарегистрирован
}
}
private static object? CreateByProgId(string progId)
{
var type = Type.GetTypeFromProgID(progId, throwOnError: false);
if (type is null) return null;
try { return Activator.CreateInstance(type); }
catch (Exception ex) { Log($"CreateInstance({progId}) failed: {ex.Message}"); return null; }
}
[DllImport("ole32.dll", PreserveSig = false)]
private static extern void CLSIDFromProgID(
[MarshalAs(UnmanagedType.LPWStr)] string progId, out Guid clsid);
[DllImport("oleaut32.dll", PreserveSig = false)]
private static extern void GetActiveObject(
ref Guid rclsid, IntPtr pvReserved,
[MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
private static void Log(string msg) => Console.Error.WriteLine(msg);
}