ich bin (k) ein Agent

Eigentlich beschäftige ich mich mit der Vollautomatisierung von Belegen. Ein Detail Aspekt ist die Zusammenführung aus verschieden Quellen auf einen FileShare. Ganz ohne AI oder LLM.

In unserem Fall liegen Eingangsrechungen von Lieferanten (meist Trainern) auf unserem externen Server. Samt Überweisungsdaten im XML Format.
Diese Daten sollen nun in unser internes CRM überführt werden. Dazu habe ich mir zwei Fake Services auf Basis von einfachen ASPX Seiten gebaut.

Nur damit man sich das vorstellen kann der VB. NET Code

   1:  <%@ Page Language="VB" %>
   2:   
   3:  <%@ Import Namespace="System.IO" %>
   4:  <%@ Import Namespace="Microsoft.AspNet.FriendlyUrls" %>
   5:  <script runat="server">
   6:   
   7:      Private Const BASE_PATH As String = "c:\inetpub\pdf\cash\"
   8:      Private Const API_KEY As String = "CHANGEME"
   9:   
  10:      Protected Sub Page_Load(sender As Object, e As EventArgs)
  11:          Dim mode As String = Request.QueryString("mode")
  12:          If Not String.IsNullOrWhiteSpace(mode) AndAlso mode.Equals("ping", StringComparison.OrdinalIgnoreCase) Then
  13:              Response.StatusCode = 200
  14:              Response.ContentType = "text/plain"
  15:              Response.Write("ok")
  16:              Response.End()
  17:              Return
  18:          End If
  19:   
  20:          Dim id = Request.QueryString("id")
  21:   
  22:          Dim pdfPath = Path.Combine(BASE_PATH, id & ".pdf")
  23:          If Not File.Exists(pdfPath) Then
  24:              Response.StatusCode = 404
  25:              Response.Write("pdf not found " + pdfPath)
  26:              Response.End()
  27:              Return
  28:          End If
  29:   
  30:          Dim bytes = File.ReadAllBytes(pdfPath)
  31:          Response.Clear()
  32:          Response.ContentType = "application/pdf"
  33:   
  34:          ' Show im Browser (inline). Wenn du immer Download willst: attachment statt inline
  35:          Response.AddHeader("Content-Disposition", "inline; filename=" & id & ".pdf")
  36:          Response.OutputStream.Write(bytes, 0, bytes.Length)
  37:          Response.End()
  38:      End Sub
  39:   
  40:      Private Function GetId() As String
  41:          Dim seg = TryGetFriendlySegment()
  42:          If Not String.IsNullOrWhiteSpace(seg) Then Return seg
  43:          Return Request.QueryString("id")
  44:      End Function
  45:   
  46:      Private Function TryGetFriendlySegment() As String
  47:          Try
  48:              Dim segs = Request.GetFriendlyUrlSegments()
  49:              If segs IsNot Nothing AndAlso segs.Count > 0 Then Return segs(0)
  50:          Catch
  51:          End Try
  52:          Return Nothing
  53:      End Function
  54:  </script>

In meiner Server side Blazor App werden diese Services aufgerufen und so das PDF in Memory des Webservers geholt. Auch hier der Code nur für die Vollständigkeit

   1:   private async Task<byte[]> GetBytesAsync(string url)
   2:   {
   3:       using var req = new HttpRequestMessage(HttpMethod.Get, url);
   4:       req.Headers.Add("X-Api-Key", ApiKey);
   5:       using var resp = await Http.SendAsync(req);
   6:       if (!resp.IsSuccessStatusCode)
   7:       {
   8:           var t = await resp.Content.ReadAsStringAsync();
   9:           throw new Exception($"Download fehlgeschlagen: {(int)resp.StatusCode} {resp.ReasonPhrase} - {t}");
  10:       }
  11:       return await resp.Content.ReadAsByteArrayAsync();
  12:   }

Die Daten sollen aber final auf einem File Share eines Windows Servers landen. Dieser steht innerhalb der Domain. Der Blazor Server außerhalb.

Der Benutzer auf der Windows Maschine hat aber das Recht auf den File Share zu schreiben. Das geht nur per JavaScript und einem Call auf eine REST API. Und genau diese API baue ich nun und installiere diese als Service auf dem Windows Server. Etwas was ich noch nie gemacht habe, aber mit ChatGPT in Minuten erledigt wurde.

Starten wir per CLI Kommando
dotnet new web -n BelegKorbAgent
Die .csproj Datei wird dann mit Visual Studio geöffnet und program.cs zum Miniwebserver.
Zunächst braucht man einen Listener für HTTP Kommandos

   1:  var listener = new HttpListener();
   2:  listener.Prefixes.Add(prefix);
   3:  listener.Start();
   4:  Console.WriteLine("BelegKorbAgent läuft auf " + prefix);

Dann lauscht man für die Ewigkeit in einer Schleife. Der Header erlaubt eine sogenannte Cross Origin Zugriff, wie in JavaScript benötigt, wenn JS und API nicht in der selben Domain liegen.

   1:  while (true)
   2:  {
   3:      var ctx = await listener.GetContextAsync();
   4:      ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
   5:      ctx.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
   6:      ctx.Response.AddHeader("Access-Control-Allow-Headers", "*");

Wenn HTTP Kommandos einlangen, braucht man unterschiedliche Logik. Im Code auskommentiert, eine Begrenzung auf bestimmte IP Adressen. Dort könnte man auch eine APIKey abfangen. Der Bereich Options dient dem Preflight für CORS.
POST samt URL und Querystring dient dem eigentlichen Datei Transfer der PDF über alle Grenzen hinweg.

   1:      _ = Task.Run(async () =>
   2:      {
   3:          try
   4:          {
   5:              var ip = ctx.Request.RemoteEndPoint?.Address.ToString() ?? "";
   6:              //if (!allowedIps.Contains(ip))
   7:              //{
   8:              //    ctx.Response.StatusCode = 403;
   9:              //    await Write(ctx, "forbidden");
  10:              //    return;
  11:              //}
  12:              if (ctx.Request.HttpMethod == "OPTIONS")
  13:              {
  14:                  ctx.Response.StatusCode = 200;
  15:                  ctx.Response.Close();
  16:                  return;
  17:              }
  18:   
  19:   
  20:              if (ctx.Request.HttpMethod != "POST" ||
  21:                  ctx.Request.Url?.AbsolutePath != "/belegkorb/store")
  22:              {
  23:                  ctx.Response.StatusCode = 404;
  24:                  await Write(ctx, "not found");
  25:                  return;
  26:              }
  27:   
  28:              var id = ctx.Request.QueryString["id"];
  29:              if (string.IsNullOrWhiteSpace(id))
  30:              {
  31:                  ctx.Response.StatusCode = 400;
  32:                  await Write(ctx, "missing id");
  33:                  return;
  34:              }
  35:   
  36:              var target = Path.Combine(sharePath, id );
  37:              if (File.Exists(target))
  38:              {
  39:                  ctx.Response.StatusCode = 409;
  40:                  await Write(ctx, "exists");
  41:                  return;
  42:              }
  43:   
  44:              await using var fs = File.Create(target);
  45:              await ctx.Request.InputStream.CopyToAsync(fs);
  46:   
  47:              await Write(ctx, "ok");
  48:          }
  49:          catch (Exception ex)
  50:          {
  51:              ctx.Response.StatusCode = 500;
  52:              await Write(ctx, ex.Message);
  53:          }
  54:          finally
  55:          {
  56:              ctx.Response.Close();
  57:          }
  58:      });
  59:  }


Allerdings unterbindet Windows den Listener. Wäre zu einfach. Was ist zu tun? Powershell console im Admin Mode und

netsh http add urlacl url=http://+:5010/belegkorb/ user=Jeder

Ich weis, everyone ist aus Sicherheitsgründen nicht perfekt. Jetzt kann man bereits im Debugging Mode den Service testen.

Um daraus einen Windows Service zu machen, müssen auf der Command Shell noch zwei Befehle ausgeführt werden
sc.exe create BelegKorbAgent binPath= "C:\BelegKorbAgent\BelegKorbAgent.exe"
sc.exe start BelegKorbAgent

Hier ist jetzt keine KI oder MCP Protokoll im Einsatz. Aber die Konzepte und Probleme sind die gleichen.

Praxisnahe Ergänzungen

Wer das Thema strukturiert weiterdenken, praxisnah einordnen oder ähnliche Themen vertiefen möchte, findet in folgenden Trainings passende Anknüpfungspunkte:

Kommentare sind geschlossen