fix: custom command execution via systemd-run für prozessunabhängigkeit
This commit is contained in:
1 parent
2b8749ee3a
commit
73c6d38254
11 files changed
+242
-12
No files matched your search
@@ -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}");
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
});
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
Reference in new issue
Block a user