fix: custom command execution via systemd-run für prozessunabhängigkeit

This commit is contained in:
Meleki committed 2026-02-27 13:34:14 +01:00
1 parent 2b8749ee3a
commit 73c6d38254
11 files changed
+242 -12

No files matched your search

+1 -1
View File
@@ -84,7 +84,7 @@ public static class ChatEndpoint
promptParts.Add($"## Instructions\n{agent.Instructions}");
if (!string.IsNullOrWhiteSpace(session.Workspace))
promptParts.Add($"## Workspace\n{session.Workspace}");
promptParts.Add("## Tools\nDu hast Tools zur Verfügung (read_file, write_file, edit_file, list_files, delete_file, execute). Benutze sie um Dateien zu lesen, schreiben und bearbeiten. Beschreibe NICHT was du tun würdest - tu es einfach über die Tools.");
if (!string.IsNullOrWhiteSpace(session.Context))
promptParts.Add($"## Kontext\n{session.Context}");
+68 -2
View File
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
@@ -19,6 +21,7 @@ public static class SessionEndpoint
app.MapPut("/api/sessions/{sessionId}", HandleUpdate);
app.MapDelete("/api/sessions/{sessionId}", HandleDelete);
app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt);
app.MapPost("/api/sessions/{sessionId}/exec", HandleExec);
}
private static async Task HandleList(HttpContext ctx, DatabaseService db)
@@ -54,8 +57,9 @@ public static class SessionEndpoint
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(name, description, image, agentId, workspace, context);
var session = await db.CreateSession(name, description, image, agentId, workspace, context, customCommands);
await ctx.Response.WriteAsJsonAsync(session);
}
@@ -122,7 +126,7 @@ public static class SessionEndpoint
if (!string.IsNullOrWhiteSpace(agent.Instructions))
promptParts.Add($"## Instructions\n{agent.Instructions}");
if (!string.IsNullOrWhiteSpace(session.Workspace))
promptParts.Add($"## Workspace\n{session.Workspace}");
promptParts.Add("## Tools\nDu hast Tools zur Verfügung (read_file, write_file, edit_file, list_files, delete_file, execute). Benutze sie um Dateien zu lesen, schreiben und bearbeiten. Beschreibe NICHT was du tun würdest - tu es einfach über die Tools.");
if (!string.IsNullOrWhiteSpace(session.Context))
promptParts.Add($"## Kontext\n{session.Context}");
@@ -131,4 +135,66 @@ public static class SessionEndpoint
ctx.Response.ContentType = "text/plain; charset=utf-8";
await ctx.Response.WriteAsync(systemPrompt);
}
private static async Task HandleExec(HttpContext ctx, DatabaseService db)
{
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)
{
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;
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";
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(5000);
}
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 });
}
}
}
+1
View File
@@ -10,6 +10,7 @@ public class SessionRecord
public string AgentId { get; set; }
public string Workspace { get; set; }
public string Context { get; set; }
public string CustomCommands { get; set; }
public bool IsDeleted { get; set; }
public string CreatedAt { get; set; }
public string UpdatedAt { get; set; }
+6 -3
View File
@@ -120,6 +120,7 @@ public class DatabaseService
TryAddColumn(conn, "sessions", "workspace", "TEXT");
TryAddColumn(conn, "sessions", "context", "TEXT");
TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "custom_commands", "TEXT");
TryAddColumn(conn, "chains", "agent_id", "TEXT");
TryAddColumn(conn, "messages", "attachment_count", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "messages", "attachment_names", "TEXT");
@@ -182,7 +183,7 @@ public class DatabaseService
// ── Sessions ──
public async Task<SessionRecord> CreateSession(string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null)
public async Task<SessionRecord> CreateSession(string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null)
{
using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o");
@@ -195,12 +196,13 @@ public class DatabaseService
AgentId = agentId,
Workspace = workspace,
Context = context,
CustomCommands = customCommands,
CreatedAt = now,
UpdatedAt = now
};
await conn.ExecuteAsync(@"
INSERT INTO sessions (session_id, name, description, image, agent_id, workspace, context, created_at, updated_at)
VALUES (@SessionId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CreatedAt, @UpdatedAt)
INSERT INTO sessions (session_id, name, description, image, agent_id, workspace, context, custom_commands, created_at, updated_at)
VALUES (@SessionId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @CreatedAt, @UpdatedAt)
", session);
return session;
}
@@ -241,6 +243,7 @@ public class DatabaseService
if (body.TryGetProperty("agentId", out var a)) { sets.Add("agent_id = @agentId2"); p.Add("agentId2", a.GetString()); }
if (body.TryGetProperty("workspace", out var w)) { sets.Add("workspace = @workspace"); p.Add("workspace", w.GetString()); }
if (body.TryGetProperty("context", out var ctx)) { sets.Add("context = @context"); p.Add("context", ctx.GetString()); }
if (body.TryGetProperty("customCommands", out var cc)) { sets.Add("custom_commands = @customCommands"); p.Add("customCommands", cc.ValueKind == System.Text.Json.JsonValueKind.Null ? null : cc.GetRawText()); }
await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
}
+2 -2
View File
@@ -7,7 +7,7 @@ _hostname=$(hostname)
if [ "$_hostname" = "stein2" ]; then
cd /home/node/.openclaw/workspace/projects/gott
systemctl stop gott
elif [ "$_hostname" = "meleki" ]; then
elif [ "$_hostname" = "meleki-container" ]; then
cd ~/.openclaw/workspace/projects/gott
else
echo "$_hostname: deployment not allowed"
@@ -21,7 +21,7 @@ cp wwwroot/index.html publish/wwwroot/index.html
if [ "$_hostname" = "stein2" ]; then
systemctl start gott
elif [ "$_hostname" = "meleki" ]; then
elif [ "$_hostname" = "meleki-container" ]; then
ssh -o ConnectTimeout=5 meleki@192.168.100.200 "nohup sudo systemctl restart gott > /dev/null 2>&1 &"
fi
+33
View File
@@ -1,3 +1,36 @@
/* Custom Command Buttons */
.custom-cmd-bar {
display: flex;
gap: var(--s2);
margin-left: var(--s3);
flex-wrap: wrap;
align-items: center;
}
.custom-cmd-bar:empty {
display: none;
}
.custom-cmd-btn {
padding: var(--s1) var(--s2);
background: var(--f2-bg);
border: var(--border-width) solid var(--f2-border);
white-space: nowrap;
cursor: pointer;
font-size: 0.85em;
}
.custom-cmd-btn:hover {
background: var(--f3-bg);
border-color: var(--f3-border);
}
.custom-cmd-btn.running {
opacity: 0.5;
cursor: wait;
}
/* Header Menu */
.header-menu {
+38
View File
@@ -52,6 +52,44 @@
margin-top: var(--s4);
}
/* ── Custom Commands ── */
.custom-commands {
display: flex;
flex-direction: column;
gap: var(--s3);
}
.custom-command-item {
display: flex;
gap: var(--s2);
align-items: start;
}
.custom-command-fields {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--s2);
}
.custom-command-item .remove-cmd-btn {
flex-shrink: 0;
color: #ff6b6b;
border-color: #ff6b6b;
padding: var(--s2) var(--s3);
align-self: center;
}
.custom-command-item .remove-cmd-btn:hover {
background: #ff6b6b;
color: var(--f1-bg);
}
.add-cmd-btn {
align-self: flex-start;
}
.danger-btn {
color: #ff6b6b;
border-color: #ff6b6b;
+7 -3
View File
@@ -66,13 +66,16 @@
<textarea id="sessionDescription" rows="3" placeholder="Beschreibung (optional)"></textarea>
<label>Agent</label>
<select id="agentSelect"></select>
<label>Kontext</label>
<textarea id="sessionContext" rows="4" placeholder="Zusätzlicher Kontext für den System-Prompt (optional)"></textarea>
<label>Workspace</label>
<div class="input-with-btn">
<input type="text" id="sessionWorkspace" placeholder="Absoluter Pfad zum Arbeitsverzeichnis (optional)">
<button type="button" id="sessionWorkspaceBrowse" class="input-addon-btn">📂</button>
</div>
<label>Kontext</label>
<textarea id="sessionContext" rows="4" placeholder="Zusätzlicher Kontext für den System-Prompt (optional)"></textarea>
<label>Custom-Befehle</label>
<div class="custom-commands" id="customCommandsList"></div>
<button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
<div class="form-actions">
<button id="sessionSaveBtn">Speichern</button>
<button id="sessionCancelBtn">Abbrechen</button>
@@ -106,7 +109,7 @@
</select>
<label>Persona</label>
<textarea id="agentPersona" rows="6" placeholder="Persönlichkeit / Seele des Agents"></textarea>
<label>Instructions</label>
<label>Anweisungen</label>
<textarea id="agentInstructions" rows="6" placeholder="Technische Anweisungen"></textarea>
<div class="form-actions">
<button id="agentSaveBtn">Speichern</button>
@@ -122,6 +125,7 @@
<header>
<button class="back-btn" id="chatBackBtn">←</button>
<h1 id="sessionTitle">-</h1>
<div class="custom-cmd-bar" id="customCmdBar"></div>
<div class="header-menu">
<button class="header-menu-trigger" id="chatMenuBtn">⋮</button>
<div class="header-menu-dropdown" id="chatMenuDropdown" hidden>
+8
View File
@@ -103,3 +103,11 @@ async function apiUpdateSession(sessionId, data) {
});
return resp.json();
}
async function apiExecCommand(sessionId, command) {
return fetch(`/api/sessions/${sessionId}/exec`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
}
+29
View File
@@ -21,6 +21,35 @@ registerPage('chat', {
titleEl.textContent = session.name || 'Unbenannt';
}
// Custom Command Buttons
const cmdBar = document.getElementById('customCmdBar');
if (cmdBar) {
cmdBar.innerHTML = '';
let cmds = [];
try { cmds = JSON.parse(session.customCommands || '[]') || []; } catch(e) {}
cmds.forEach(cmd => {
if (!cmd.name || !cmd.command) return;
const btn = document.createElement('button');
btn.className = 'custom-cmd-btn';
btn.textContent = cmd.name;
btn.title = cmd.command;
btn.addEventListener('click', async () => {
btn.disabled = true;
btn.classList.add('running');
try {
await apiExecCommand(sessionId, cmd.command);
} catch(e) {
console.error('Exec failed:', e);
}
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('running');
}, 1000);
});
cmdBar.appendChild(btn);
});
}
// Zurück zur Session-Edit-Seite
const backBtn = document.getElementById('chatBackBtn');
if (backBtn) {
+49 -1
View File
@@ -2,6 +2,35 @@
let _sessionImageData = null;
function _createCommandRow(container, name = '', command = '') {
const item = document.createElement('div');
item.className = 'custom-command-item';
item.innerHTML = `
<div class="custom-command-fields">
<input type="text" class="cmd-name" placeholder="Name (z.B. Deploy)" value="${_escAttr(name)}">
<input type="text" class="cmd-command" placeholder="Befehl (z.B. ./deploy.sh)" value="${_escAttr(command)}">
</div>
<button type="button" class="remove-cmd-btn">✕</button>
`;
item.querySelector('.remove-cmd-btn').addEventListener('click', () => item.remove());
container.appendChild(item);
}
function _escAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function _getCommandsFromUI(container) {
const items = container.querySelectorAll('.custom-command-item');
const commands = [];
for (const item of items) {
const name = item.querySelector('.cmd-name').value.trim();
const command = item.querySelector('.cmd-command').value.trim();
if (name || command) commands.push({ name, command });
}
return commands.length > 0 ? commands : null;
}
registerPage('session', {
async init(params) {
const sessionId = params.id;
@@ -27,6 +56,8 @@ registerPage('session', {
const cancelBtn = document.getElementById('sessionCancelBtn');
const deleteBtn = document.getElementById('sessionDeleteBtn');
const openChatBtn = document.getElementById('openChatBtn');
const commandsList = document.getElementById('customCommandsList');
const addCommandBtn = document.getElementById('addCommandBtn');
_sessionImageData = null;
@@ -43,12 +74,27 @@ registerPage('session', {
if (session.image) {
imagePreview.innerHTML = `<img src="${session.image}" alt="">`;
}
// Custom Commands laden
if (session.customCommands) {
try {
const cmds = typeof session.customCommands === 'string'
? JSON.parse(session.customCommands)
: session.customCommands;
for (const cmd of cmds) {
_createCommandRow(commandsList, cmd.name || '', cmd.command || '');
}
} catch (e) { /* ignore parse errors */ }
}
} else {
titleEl.textContent = 'Neue Session';
deleteBtn.hidden = true;
openChatBtn.disabled = true;
}
// Befehl hinzufügen
addCommandBtn.addEventListener('click', () => _createCommandRow(commandsList));
// Agents laden
const agents = await apiGetAgents();
agentSelect.innerHTML = '<option value="">-- Kein Agent --</option>';
@@ -94,13 +140,15 @@ registerPage('session', {
// Speichern
saveBtn.addEventListener('click', async () => {
const commands = _getCommandsFromUI(commandsList);
const data = {
name: nameEl.value.trim() || 'Unbenannt',
description: descEl.value.trim(),
image: _sessionImageData,
agentId: agentSelect.value || null,
context: contextEl.value || null,
workspace: workspaceEl.value.trim() || null
workspace: workspaceEl.value.trim() || null,
customCommands: commands
};
if (isNew) {