Backend:
- SharedMessageRecord Model + shared_messages DB-Tabelle
- SharedEndpoint: POST /api/messages/{id}/share, GET /api/shared/{token},
GET /api/shared (Liste), DELETE /api/shared/{token}
- Auth-Ausnahme für GET /api/shared/{token} (öffentlich ohne Login)
- DB-Methoden: CreateSharedMessage, GetSharedMessage, GetChainMessagesUpTo
Frontend:
- Share-Button (fa-share-alt) an jeder Nachricht
- Kontextmenü: Einzelnachricht oder mit Thread-Kontext teilen
- Link wird automatisch in Zwischenablage kopiert
- Öffentliche Shared-Page (#shared/{token}) ohne Login
- Markdown-Rendering in der geteilten Ansicht
- CSS für Share-Button, Share-Menü und Shared-Page
424 lines
12 KiB
JavaScript
424 lines
12 KiB
JavaScript
// ── Global 401 Handler ──
|
|
|
|
(function() {
|
|
var _origFetch = window.fetch;
|
|
window.fetch = function(url, opts) {
|
|
return _origFetch.apply(this, arguments).then(function(resp) {
|
|
if (resp.status === 401 && typeof url === 'string' && !url.includes('/api/auth/')) {
|
|
if (typeof currentPage !== 'undefined' && currentPage !== 'login') {
|
|
navigateTo('login');
|
|
}
|
|
}
|
|
return resp;
|
|
});
|
|
};
|
|
})();
|
|
|
|
// ── Auth API ──
|
|
|
|
async function apiLogin(username, password) {
|
|
const resp = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await resp.json();
|
|
throw new Error(err.error || 'Login failed');
|
|
}
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiLogout() {
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
}
|
|
|
|
async function apiMe() {
|
|
const resp = await fetch('/api/auth/me');
|
|
if (!resp.ok) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
// ── API Calls ──
|
|
|
|
async function apiChat(content, sessionId, signal) {
|
|
return fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content, sessionId }),
|
|
signal
|
|
});
|
|
}
|
|
|
|
// ── Agent API ──
|
|
|
|
async function apiGetAgents() {
|
|
const resp = await fetch('/api/agents');
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGetAgent(agentId) {
|
|
const resp = await fetch(`/api/agents/${agentId}`);
|
|
if (!resp.ok) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiCreateAgent(data) {
|
|
const resp = await fetch('/api/agents', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiUpdateAgent(agentId, data) {
|
|
const resp = await fetch(`/api/agents/${agentId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiDeleteAgent(agentId) {
|
|
return fetch(`/api/agents/${agentId}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async function apiHistory(sessionId, beforeId, includeDeleted = false) {
|
|
let url = `/api/history?sessionId=${sessionId}`;
|
|
if (beforeId) url += `&beforeId=${beforeId}`;
|
|
if (includeDeleted) url += `&includeDeleted=true`;
|
|
const resp = await fetch(url);
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiHistoryPoll(sessionId, afterId, includeDeleted = false) {
|
|
let url = `/api/history/poll?sessionId=${sessionId}&afterId=${afterId}`;
|
|
if (includeDeleted) url += `&includeDeleted=true`;
|
|
const resp = await fetch(url);
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiArchive(sessionId) {
|
|
const resp = await fetch(`/api/archive?sessionId=${sessionId}`, { method: 'POST' });
|
|
if (resp.status === 204) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiListArchives(sessionId) {
|
|
const resp = await fetch(`/api/archives?sessionId=${sessionId}`);
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGetArchive(archiveId) {
|
|
const resp = await fetch(`/api/archive/${archiveId}`);
|
|
return resp.json();
|
|
}
|
|
|
|
// ── Session API ──
|
|
|
|
async function apiGetSessions() {
|
|
const resp = await fetch('/api/sessions');
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiCreateSession(data = {}) {
|
|
const resp = await fetch('/api/sessions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGetSession(sessionId) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}`);
|
|
if (!resp.ok) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiDeleteSession(sessionId) {
|
|
return fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async function apiUpdateSession(sessionId, data) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiExecCommand(sessionId, command, mode = 'background') {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/exec`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ command, mode })
|
|
});
|
|
if (mode === 'foreground') return resp.json();
|
|
return resp;
|
|
}
|
|
|
|
async function apiGetSystemPrompt(sessionId) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/system-prompt`);
|
|
if (!resp.ok) throw new Error('System-Prompt konnte nicht geladen werden');
|
|
return resp.text();
|
|
}
|
|
|
|
async function apiGetSystemPromptBlocks(sessionId) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/system-prompt-blocks`);
|
|
if (!resp.ok) throw new Error('System-Prompt konnte nicht geladen werden');
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiIsStreaming(sessionId) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/streaming`);
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGetChain(chainId) {
|
|
const resp = await fetch(`/api/chains/${chainId}`);
|
|
if (!resp.ok) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiToggleMessageMark(messageId) {
|
|
const resp = await fetch(`/api/messages/${messageId}/mark`, { method: 'PUT' });
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiToggleMessageDelete(messageId) {
|
|
const resp = await fetch(`/api/messages/${messageId}/delete`, { method: 'PUT' });
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGitStatus(sessionId) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/git-status`);
|
|
if (!resp.ok) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
// ── Skills API ──
|
|
|
|
async function apiGetSkills() {
|
|
const resp = await fetch('/api/skills');
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGetAgentSkills(agentId) {
|
|
const resp = await fetch(`/api/agents/${agentId}/skills`);
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiSetAgentSkills(agentId, skills) {
|
|
const resp = await fetch(`/api/agents/${agentId}/skills`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ skills })
|
|
});
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiAddAgentSkill(agentId, slug) {
|
|
return fetch(`/api/agents/${agentId}/skills/${slug}`, { method: 'POST' });
|
|
}
|
|
|
|
async function apiRemoveAgentSkill(agentId, slug) {
|
|
return fetch(`/api/agents/${agentId}/skills/${slug}`, { method: 'DELETE' });
|
|
}
|
|
|
|
// ── Profile API ──
|
|
|
|
async function apiGetProfile() {
|
|
const resp = await fetch('/api/profile');
|
|
if (!resp.ok) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiUpdateProfile(data) {
|
|
const resp = await fetch('/api/profile', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await resp.json();
|
|
throw new Error(err.error || 'Fehler beim Speichern');
|
|
}
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiChangePassword(currentPassword, newPassword) {
|
|
const resp = await fetch('/api/profile/password', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ currentPassword, newPassword })
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await resp.json();
|
|
throw new Error(err.error || 'Fehler beim Ändern');
|
|
}
|
|
return resp.json();
|
|
}
|
|
|
|
// ── Telegram API (pro Session) ──
|
|
|
|
async function apiSessionTelegramGet(sessionId) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/telegram`);
|
|
if (!resp.ok) return { configured: false };
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiSessionTelegramSave(sessionId, botToken, webhookSecret) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/telegram`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ botToken, webhookSecret })
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Telegram-Config speichern fehlgeschlagen');
|
|
}
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiSessionTelegramRegisterWebhook(sessionId, baseUrl) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/telegram/register-webhook`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ baseUrl })
|
|
});
|
|
if (!resp.ok) throw new Error('Webhook-Registrierung fehlgeschlagen');
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiSessionTelegramWebhookInfo(sessionId) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/telegram/webhook-info`);
|
|
if (!resp.ok) throw new Error('Webhook-Info fehlgeschlagen');
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiSessionTelegramDeleteWebhook(sessionId) {
|
|
const resp = await fetch(`/api/sessions/${sessionId}/telegram/webhook`, { method: 'DELETE' });
|
|
if (!resp.ok) throw new Error('Webhook-Löschung fehlgeschlagen');
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGetSettings() {
|
|
const resp = await fetch('/api/settings');
|
|
if (!resp.ok) return {};
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiSaveSettings(settings) {
|
|
const resp = await fetch('/api/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(settings)
|
|
});
|
|
return resp.json();
|
|
}
|
|
|
|
// ── Cronjob API ──
|
|
|
|
async function apiGetCronjobs() {
|
|
const resp = await fetch('/api/cronjobs');
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGetCronjob(id) {
|
|
const resp = await fetch(`/api/cronjobs/${id}`);
|
|
if (!resp.ok) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiCreateCronjob(data) {
|
|
const resp = await fetch('/api/cronjobs', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Cronjob erstellen fehlgeschlagen');
|
|
}
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiUpdateCronjob(id, data) {
|
|
const resp = await fetch(`/api/cronjobs/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Cronjob speichern fehlgeschlagen');
|
|
}
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiDeleteCronjob(id) {
|
|
return fetch(`/api/cronjobs/${id}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async function apiToggleCronjob(id) {
|
|
const resp = await fetch(`/api/cronjobs/${id}/toggle`, { method: 'POST' });
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiRunCronjob(id) {
|
|
const resp = await fetch(`/api/cronjobs/${id}/run`, { method: 'POST' });
|
|
return resp.json();
|
|
}
|
|
|
|
// ── Shared Message API ──
|
|
|
|
async function apiShareMessage(messageId, includeContext = false) {
|
|
const resp = await fetch(`/api/messages/${messageId}/share`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ includeContext })
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Teilen fehlgeschlagen');
|
|
}
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiGetShared(token) {
|
|
const resp = await fetch(`/api/shared/${token}`);
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Geteilte Nachricht nicht gefunden');
|
|
}
|
|
return resp.json();
|
|
}
|
|
|
|
async function apiDeleteShare(token) {
|
|
return fetch(`/api/shared/${token}`, { method: 'DELETE' });
|
|
}
|
|
|
|
// ── Backup API ──
|
|
|
|
async function apiBackupDb() {
|
|
const resp = await fetch('/api/backup', { method: 'POST', credentials: 'include' });
|
|
if (!resp.ok) throw new Error('Backup fehlgeschlagen');
|
|
const blob = await resp.blob();
|
|
const cd = resp.headers.get('content-disposition') || '';
|
|
const filename = cd.match(/filename\*=UTF-8''(.+?)(?:;|$)/i)?.[1]
|
|
|| cd.match(/filename="?([^";]+)"?/)?.[1]
|
|
|| 'gott-backup.tar.gz';
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|