Files
gott/Endpoints/SessionEndpoint.cs
T
Meleki 7614141760 feat: User-Zuordnung für Sessions und Agents
- UserId-Feld zu SessionRecord und AgentRecord hinzugefügt
- DB-Migration: user_id Spalte + bestehende Daten dem Admin zuweisen
- Alle Endpoints filtern/prüfen nach UserId des eingeloggten Users
- HttpContextExtensions.GetUserId() Helper für konsistenten Zugriff
- Betrifft: Agent, Session, Chat, History, Workspace, Skill Endpoints
- Sessions/Agents anderer User sind nicht sichtbar/zugreifbar
2026-03-02 11:39:55 +01:00

460 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Extensions;
using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Gott.Endpoints;
public static class SessionEndpoint
{
public static void MapSessionEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/api/sessions", HandleList);
app.MapGet("/api/sessions/{sessionId}", HandleGet);
app.MapPost("/api/sessions", HandleCreate);
app.MapPut("/api/sessions/{sessionId}", HandleUpdate);
app.MapDelete("/api/sessions/{sessionId}", HandleDelete);
app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt);
app.MapGet("/api/sessions/{sessionId}/system-prompt-blocks", HandleSystemPromptBlocks);
app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming);
app.MapPost("/api/sessions/{sessionId}/exec", HandleExec);
app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus);
}
private static async Task HandleList(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var sessions = await db.GetSessionsWithStats(userId);
await ctx.Response.WriteAsJsonAsync(sessions);
}
private static async Task HandleGet(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
ctx.Response.StatusCode = 404;
return;
}
await ctx.Response.WriteAsJsonAsync(session);
}
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService)
{
var userId = ctx.GetUserId();
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var name = body.TryGetProperty("name", out var n) ? n.GetString() : "Neue Session";
var description = body.TryGetProperty("description", out var d) ? d.GetString() : null;
var imageRaw = body.TryGetProperty("image", out var img) ? img.GetString() : null;
var image = imageService.ResizeDataUrl(imageRaw);
var agentId = body.TryGetProperty("agentId", out var a) ? a.GetString() : null;
var workspace = body.TryGetProperty("workspace", out var w) ? w.GetString() : null;
var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null;
var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null;
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands);
await ctx.Response.WriteAsJsonAsync(session);
}
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var existing = await db.GetSession(sessionId);
if (existing == null || existing.UserId != userId)
{
ctx.Response.StatusCode = 404;
return;
}
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
await db.UpdateSession(sessionId, body, imageService);
var session = await db.GetSession(sessionId);
await ctx.Response.WriteAsJsonAsync(session);
}
private static async Task HandleDelete(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
await db.DeleteSession(sessionId, userId);
ctx.Response.StatusCode = 204;
}
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
ctx.Response.StatusCode = 404;
return;
}
if (string.IsNullOrEmpty(session.AgentId))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
return;
}
var agent = await db.GetAgent(session.AgentId);
if (agent == null)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
return;
}
var markedMessages = await db.GetMarkedMessages(sessionId);
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
var dataDir = memoryService.DataDir;
var agentSkills = await db.GetAgentSkills(agent.AgentId);
var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, agentSkills, skillService);
ctx.Response.ContentType = "text/plain; charset=utf-8";
await ctx.Response.WriteAsync(systemPrompt);
}
private static async Task HandleSystemPromptBlocks(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId) { ctx.Response.StatusCode = 404; return; }
if (string.IsNullOrEmpty(session.AgentId))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
return;
}
var agent = await db.GetAgent(session.AgentId);
if (agent == null)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
return;
}
var markedMessages = await db.GetMarkedMessages(sessionId);
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
var dataDir = memoryService.DataDir;
var agentSkills = await db.GetAgentSkills(agent.AgentId);
var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, agentSkills, skillService);
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
}
private static async Task HandleStreaming(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
ctx.Response.StatusCode = 404;
return;
}
var isStreaming = streamingTracker.IsStreaming(sessionId);
var chainId = streamingTracker.GetChainId(sessionId);
await ctx.Response.WriteAsJsonAsync(new { streaming = isStreaming, chainId });
}
private static async Task HandleExec(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
ctx.Response.StatusCode = 404;
return;
}
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var command = body.TryGetProperty("command", out var c) ? c.GetString() : null;
var mode = body.TryGetProperty("mode", out var m) ? m.GetString() ?? "background" : "background";
if (string.IsNullOrWhiteSpace(command))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Befehl angegeben" });
return;
}
var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : "/tmp";
if (mode == "foreground")
{
// Synchron ausführen, auf Ergebnis warten
try
{
var psi = new ProcessStartInfo
{
FileName = "/bin/bash",
WorkingDirectory = workingDir,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var proc = System.Diagnostics.Process.Start(psi);
if (proc == null)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = "Prozess konnte nicht gestartet werden" });
return;
}
await proc.StandardInput.WriteAsync(command);
proc.StandardInput.Close();
using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
await proc.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException)
{
proc.Kill(entireProcessTree: true);
await ctx.Response.WriteAsJsonAsync(new { status = "timeout", output = "Timeout nach 30 Sekunden", exitCode = -1 });
return;
}
var stdout = await proc.StandardOutput.ReadToEndAsync();
var stderr = await proc.StandardError.ReadToEndAsync();
var output = new System.Text.StringBuilder();
if (!string.IsNullOrEmpty(stdout)) output.Append(stdout);
if (!string.IsNullOrEmpty(stderr))
{
if (output.Length > 0) output.Append("\n--- stderr ---\n");
output.Append(stderr);
}
await ctx.Response.WriteAsJsonAsync(new { status = "completed", output = output.ToString(), exitCode = proc.ExitCode });
}
catch (Exception ex)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
}
}
else
{
// Fire-and-forget via systemd-run
try
{
var scriptPath = $"/tmp/gott-exec-{Guid.NewGuid():N}.sh";
await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n");
var psi = new ProcessStartInfo
{
FileName = "systemd-run",
Arguments = $"--scope --quiet bash {scriptPath}",
WorkingDirectory = workingDir,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
var process = Process.Start(psi);
if (process != null)
{
process.WaitForExit(500);
}
ctx.Response.StatusCode = 202;
await ctx.Response.WriteAsJsonAsync(new { status = "started", command = command });
}
catch (Exception ex)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
}
}
}
private static async Task HandleGitStatus(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var session = await db.GetSession(sessionId);
if (session == null || session.UserId != userId)
{
ctx.Response.StatusCode = 404;
return;
}
var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : null;
if (string.IsNullOrEmpty(workingDir))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
return;
}
try
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = "status --porcelain",
WorkingDirectory = workingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var proc = Process.Start(psi);
if (proc == null)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = "Git-Prozess konnte nicht gestartet werden" });
return;
}
var stdout = await proc.StandardOutput.ReadToEndAsync();
var stderr = await proc.StandardError.ReadToEndAsync();
await proc.WaitForExitAsync();
if (proc.ExitCode != 0)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = stderr.Trim() });
return;
}
var add = new List<string>();
var edit = new List<string>();
var delete = new List<string>();
var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Length < 4) continue;
var x = line[0]; // index status
var y = line[1]; // working tree status
var file = line.Substring(3);
// Bei Renames: "R old -> new" => nur den neuen Namen nehmen
var arrowIdx = file.IndexOf(" -> ", StringComparison.Ordinal);
if (arrowIdx >= 0)
file = file.Substring(arrowIdx + 4);
if (x == '?' && y == '?')
{
add.Add(file);
}
else if (x == 'D' || y == 'D')
{
delete.Add(file);
}
else if (x == 'A')
{
add.Add(file);
}
else
{
edit.Add(file);
}
}
// Outgoing commits zählen (nur wenn Remote existiert)
int outgoing = 0;
try
{
var outPsi = new ProcessStartInfo
{
FileName = "git",
Arguments = "rev-list --count @{u}..HEAD",
WorkingDirectory = workingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var outProc = Process.Start(outPsi);
if (outProc != null)
{
var outResult = (await outProc.StandardOutput.ReadToEndAsync()).Trim();
await outProc.WaitForExitAsync();
if (outProc.ExitCode == 0 && int.TryParse(outResult, out var count))
outgoing = count;
}
}
catch
{
// Kein Remote oder kein Upstream – outgoing bleibt 0
}
await ctx.Response.WriteAsJsonAsync(new { add, edit, delete, outgoing });
}
catch (Exception ex)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
}
}
}