- WorkspaceHelper.cs: Zentrale Workspace-Auflösung (relativ → absolut) - Session-Workspace wird relativ zum User-DefaultPath gespeichert - System-Prompt bekommt aufgelösten absoluten Pfad - Path-Traversal-Schutz: '..' in Workspace-Pfaden blockiert - Browse-API gibt '..' nicht mehr als Eintrag zurück - Folder-Browser: basePath-Limit, Back-Button navigiert zum Parent - DefaultPath nur durch Admin bearbeitbar (Profil: readonly) - Legacy: Absolute Pfade in session.Workspace weiterhin kompatibel
342 lines
12 KiB
JavaScript
342 lines
12 KiB
JavaScript
// ── Session Edit Page ──
|
|
|
|
let _sessionImageData = null;
|
|
let _sessionPasteHandler = null;
|
|
let _sessionContextMde = null;
|
|
|
|
registerPage('session', {
|
|
async init(params) {
|
|
const sessionId = params.id;
|
|
const isNew = !sessionId;
|
|
let session = null;
|
|
|
|
if (!isNew) {
|
|
session = await apiGetSession(sessionId);
|
|
if (!session) return navigateTo('dashboard');
|
|
}
|
|
|
|
const titleEl = document.getElementById('sessionPageTitle');
|
|
const nameEl = document.getElementById('sessionName');
|
|
const descEl = document.getElementById('sessionDescription');
|
|
const contextEl = document.getElementById('sessionContext');
|
|
const workspaceEl = document.getElementById('sessionWorkspace');
|
|
const agentSelect = document.getElementById('agentSelect');
|
|
const imagePreview = document.getElementById('sessionImagePreview');
|
|
const imageInput = document.getElementById('sessionImageInput');
|
|
const imageBtn = document.getElementById('sessionImageBtn');
|
|
const imageClearBtn = document.getElementById('sessionImageClearBtn');
|
|
const saveBtn = document.getElementById('sessionSaveBtn');
|
|
const cancelBtn = document.getElementById('sessionCancelBtn');
|
|
const deleteBtn = document.getElementById('sessionDeleteBtn');
|
|
const commandsList = document.getElementById('customCommandsList');
|
|
const addCommandBtn = document.getElementById('addCommandBtn');
|
|
|
|
_sessionImageData = null;
|
|
|
|
if (session) {
|
|
titleEl.textContent = session.name || 'Unbenannt';
|
|
nameEl.value = session.name || '';
|
|
descEl.value = session.description || '';
|
|
workspaceEl.value = session.workspace || '';
|
|
_sessionImageData = session.image || null;
|
|
deleteBtn.hidden = false;
|
|
|
|
if (session.image) {
|
|
imagePreview.innerHTML = `<img src="${session.image}" alt="">`;
|
|
}
|
|
|
|
// Custom Commands laden
|
|
loadCommandsIntoUI(commandsList, session.customCommands);
|
|
} else {
|
|
titleEl.textContent = 'Neue Session';
|
|
deleteBtn.hidden = true;
|
|
}
|
|
|
|
// EasyMDE für Kontext
|
|
_sessionContextMde = new EasyMDE({
|
|
element: contextEl,
|
|
initialValue: (session && session.context) || '',
|
|
toolbar: ['bold', 'italic', 'heading', '|', 'unordered-list', 'ordered-list', '|', 'link', 'code', '|', 'preview'],
|
|
status: false,
|
|
spellChecker: false,
|
|
autoDownloadFontAwesome: false,
|
|
minHeight: '120px',
|
|
placeholder: 'Zusätzlicher Kontext für den System-Prompt (optional)',
|
|
previewClass: ['editor-preview', 'markdown']
|
|
});
|
|
_sessionContextMde.codemirror.setOption('theme', 'default');
|
|
_sessionContextMde.codemirror.getWrapperElement().closest('.EasyMDEContainer').classList.add('fe-mde-wrap');
|
|
|
|
// Befehl hinzufügen
|
|
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
|
|
|
|
// Telegram-Section nur für existierende Sessions
|
|
if (!isNew) {
|
|
initSessionTelegram(sessionId);
|
|
}
|
|
|
|
// Agents laden
|
|
const agents = await apiGetAgents();
|
|
agentSelect.innerHTML = '<option value="">-- Kein Agent --</option>';
|
|
for (const a of agents) {
|
|
const opt = document.createElement('option');
|
|
opt.value = a.agentId;
|
|
opt.textContent = `${a.name} (${a.model})`;
|
|
agentSelect.appendChild(opt);
|
|
}
|
|
if (session && session.agentId) agentSelect.value = session.agentId;
|
|
|
|
// Bild Upload
|
|
imageBtn.addEventListener('click', () => imageInput.click());
|
|
imageInput.addEventListener('change', () => {
|
|
const file = imageInput.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
_sessionImageData = reader.result;
|
|
imagePreview.innerHTML = `<img src="${reader.result}" alt="">`;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
imageClearBtn.addEventListener('click', () => {
|
|
_sessionImageData = null;
|
|
imagePreview.innerHTML = '?';
|
|
imageInput.value = '';
|
|
});
|
|
|
|
// Bild Paste (Ctrl+V)
|
|
document.addEventListener('paste', _sessionPasteHandler = (e) => {
|
|
const items = e.clipboardData?.items;
|
|
if (!items) return;
|
|
for (const item of items) {
|
|
if (item.type.startsWith('image/')) {
|
|
e.preventDefault();
|
|
const file = item.getAsFile();
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
_sessionImageData = reader.result;
|
|
imagePreview.innerHTML = `<img src="${reader.result}" alt="">`;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Workspace Browse
|
|
const workspaceBrowse = document.getElementById('sessionWorkspaceBrowse');
|
|
if (workspaceBrowse) {
|
|
workspaceBrowse.addEventListener('click', () => {
|
|
const defaultPath = (currentUser && currentUser.defaultPath) || '';
|
|
const currentVal = workspaceEl.value.trim();
|
|
// Für den Folder-Browser den absoluten Startpfad zusammenbauen
|
|
let startPath = '/';
|
|
if (defaultPath && currentVal) {
|
|
startPath = defaultPath.replace(/\/+$/, '') + '/' + currentVal;
|
|
} else if (defaultPath) {
|
|
startPath = defaultPath;
|
|
} else if (currentVal) {
|
|
startPath = currentVal;
|
|
}
|
|
openFolderBrowser({
|
|
startPath,
|
|
basePath: defaultPath || null,
|
|
onSelect(absolutePath) {
|
|
// Relativen Pfad zum DefaultPath berechnen
|
|
if (defaultPath && absolutePath.startsWith(defaultPath)) {
|
|
let rel = absolutePath.slice(defaultPath.length).replace(/^\/+/, '');
|
|
workspaceEl.value = rel || '';
|
|
} else {
|
|
// Kein DefaultPath oder Pfad außerhalb → absoluten Pfad beibehalten (Legacy)
|
|
workspaceEl.value = absolutePath;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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: _sessionContextMde.value().trim() || null,
|
|
workspace: workspaceEl.value.trim() || null,
|
|
customCommands: commands
|
|
};
|
|
|
|
if (isNew) {
|
|
await apiCreateSession(data);
|
|
} else {
|
|
await apiUpdateSession(sessionId, data);
|
|
// Telegram-Config mitspeichern (falls Section sichtbar)
|
|
var tgToken = document.getElementById('sessionTelegramToken');
|
|
var tgSecret = document.getElementById('sessionTelegramSecret');
|
|
if (tgToken) {
|
|
try {
|
|
await saveTelegramConfig(sessionId, tgToken, tgSecret);
|
|
} catch(e) {
|
|
showToast('Session gespeichert, aber Telegram-Fehler: ' + e.message, 'error');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
navigateTo('dashboard');
|
|
});
|
|
|
|
// Abbrechen
|
|
cancelBtn.addEventListener('click', () => navigateTo('dashboard'));
|
|
|
|
// Löschen
|
|
deleteBtn.addEventListener('click', async () => {
|
|
if (!confirm('Session wirklich löschen?')) return;
|
|
await apiDeleteSession(sessionId);
|
|
navigateTo('dashboard');
|
|
});
|
|
},
|
|
destroy() {
|
|
if (_sessionContextMde) {
|
|
_sessionContextMde.toTextArea();
|
|
_sessionContextMde = null;
|
|
}
|
|
if (_sessionPasteHandler) {
|
|
document.removeEventListener('paste', _sessionPasteHandler);
|
|
_sessionPasteHandler = null;
|
|
}
|
|
_sessionImageData = null;
|
|
}
|
|
});
|
|
|
|
function initSessionTelegram(sessionId) {
|
|
var tokenInput = document.getElementById('sessionTelegramToken');
|
|
var secretInput = document.getElementById('sessionTelegramSecret');
|
|
var registerBtn = document.getElementById('sessionTelegramRegisterBtn');
|
|
var deleteBtn = document.getElementById('sessionTelegramDeleteBtn');
|
|
var statusBox = document.getElementById('sessionTelegramStatusBox');
|
|
|
|
// Vorhandene Config laden + Status abrufen
|
|
apiSessionTelegramGet(sessionId).then(async function(config) {
|
|
if (config.botToken) tokenInput.value = config.botToken;
|
|
if (config.webhookSecret) {
|
|
secretInput.value = config.webhookSecret;
|
|
} else {
|
|
// Secret einmalig generieren und sofort persistent speichern
|
|
var newSecret = crypto.randomUUID().replace(/-/g, '');
|
|
secretInput.value = newSecret;
|
|
await apiSessionTelegramSave(sessionId, config.botToken || '', newSecret);
|
|
}
|
|
// Status automatisch abrufen wenn Token konfiguriert ist
|
|
if (config.botToken) {
|
|
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
|
|
} else {
|
|
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
|
|
}
|
|
}).catch(function() {
|
|
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
|
|
});
|
|
|
|
// Webhook registrieren
|
|
registerBtn.addEventListener('click', async function() {
|
|
var baseUrl = prompt('Base-URL der App:', window.location.origin);
|
|
if (!baseUrl) return;
|
|
registerBtn.disabled = true;
|
|
registerBtn.textContent = 'Wird registriert...';
|
|
try {
|
|
// Erst Token + Secret speichern (falls geändert)
|
|
await saveTelegramConfig(sessionId, tokenInput, secretInput);
|
|
var res = await apiSessionTelegramRegisterWebhook(sessionId, baseUrl.trim());
|
|
if (res.ok) {
|
|
showToast('Webhook registriert!', 'success');
|
|
} else {
|
|
showToast('Antwort: ' + JSON.stringify(res), 'error');
|
|
}
|
|
} catch(e) {
|
|
showToast('Fehler: ' + e.message, 'error');
|
|
} finally {
|
|
registerBtn.disabled = false;
|
|
registerBtn.textContent = 'Webhook registrieren';
|
|
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
|
|
}
|
|
});
|
|
|
|
// Webhook entfernen
|
|
deleteBtn.addEventListener('click', async function() {
|
|
if (!confirm('Webhook wirklich entfernen?')) return;
|
|
deleteBtn.disabled = true;
|
|
deleteBtn.textContent = 'Wird entfernt...';
|
|
try {
|
|
var res = await apiSessionTelegramDeleteWebhook(sessionId);
|
|
if (res.ok) {
|
|
showToast('Webhook entfernt', 'success');
|
|
} else {
|
|
showToast('Antwort: ' + JSON.stringify(res), 'error');
|
|
}
|
|
} catch(e) {
|
|
showToast('Fehler: ' + e.message, 'error');
|
|
} finally {
|
|
deleteBtn.disabled = false;
|
|
deleteBtn.textContent = 'Webhook entfernen';
|
|
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
|
|
}
|
|
});
|
|
}
|
|
|
|
function showTelegramUnconfigured(statusBox, registerBtn, deleteBtn) {
|
|
statusBox.innerHTML = '<p>⚠️ Kein Bot-Token konfiguriert. Speichere zuerst die Session mit einem Token.</p>';
|
|
statusBox.hidden = false;
|
|
registerBtn.hidden = true;
|
|
deleteBtn.hidden = true;
|
|
}
|
|
|
|
async function refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn) {
|
|
statusBox.hidden = true;
|
|
try {
|
|
var info = await apiSessionTelegramWebhookInfo(sessionId);
|
|
var result = info.result || {};
|
|
var url = result.url || '';
|
|
var pending = result.pending_update_count || 0;
|
|
var lastError = result.last_error_message;
|
|
var active = !!url;
|
|
|
|
var html = '';
|
|
if (active) {
|
|
html += '<p>✅ <strong>Webhook aktiv</strong></p>';
|
|
html += '<p><strong>URL:</strong> ' + escapeHtml(url) + '</p>';
|
|
html += '<p><strong>Ausstehend:</strong> ' + pending + '</p>';
|
|
if (lastError) html += '<p class="tg-error"><strong>Letzter Fehler:</strong> ' + escapeHtml(lastError) + '</p>';
|
|
} else {
|
|
html += '<p>⚠️ <strong>Webhook nicht registriert</strong></p>';
|
|
}
|
|
statusBox.innerHTML = html;
|
|
statusBox.hidden = false;
|
|
|
|
// Buttons ein/ausblenden je nach Status
|
|
registerBtn.hidden = active;
|
|
deleteBtn.hidden = !active;
|
|
} catch(e) {
|
|
statusBox.innerHTML = '<p class="tg-error">Status konnte nicht abgerufen werden: ' + escapeHtml(e.message) + '</p>';
|
|
statusBox.hidden = false;
|
|
registerBtn.hidden = false;
|
|
deleteBtn.hidden = true;
|
|
}
|
|
}
|
|
|
|
async function saveTelegramConfig(sessionId, tokenInput, secretInput) {
|
|
var token = tokenInput.value.trim();
|
|
var secret = secretInput.value.trim();
|
|
await apiSessionTelegramSave(sessionId, token, secret);
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|