add JSDoc comments because yes
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Konfiguriert ESLint-Regeln und Browser-Globals für JavaScript-Dateien.
|
||||||
|
*/
|
||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import { defineConfig } from "eslint/config";
|
import { defineConfig } from "eslint/config";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Bündelt JSON-Anfragen und verarbeitet Ereignisse gestreamter Chat-Antworten im Browser.
|
||||||
|
*/
|
||||||
export async function request(url, data, method = "POST") {
|
export async function request(url, data, method = "POST") {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
@@ -60,6 +63,7 @@ export async function stream(url, data, handlers = {}, signal = undefined) {
|
|||||||
if (done) break;
|
if (done) break;
|
||||||
buffer += decoder.decode(value, {stream: true});
|
buffer += decoder.decode(value, {stream: true});
|
||||||
const parts = buffer.split(/\r?\n\r?\n/);
|
const parts = buffer.split(/\r?\n\r?\n/);
|
||||||
|
// Das letzte SSE-Ereignis kann unvollständig sein und wird mit dem nächsten Netzwerkblock ergänzt.
|
||||||
buffer = parts.pop() || "";
|
buffer = parts.pop() || "";
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
const lines = part.split("\n");
|
const lines = part.split("\n");
|
||||||
@@ -81,6 +85,7 @@ export async function stream(url, data, handlers = {}, signal = undefined) {
|
|||||||
}
|
}
|
||||||
if (terminal) break;
|
if (terminal) break;
|
||||||
}
|
}
|
||||||
|
// Ohne explizites Abschluss- oder Fehlerereignis gilt die Verbindung als vorzeitig abgebrochen.
|
||||||
if (!terminal) throw new Error("Response was interrupted. Please try again.");
|
if (!terminal) throw new Error("Response was interrupted. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
await reader.cancel().catch(() => {});
|
await reader.cancel().catch(() => {});
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Verwaltet Bildauswahl, Uploads und Vorschauen sowie die Anzeige gespeicherter Anhänge.
|
||||||
|
*/
|
||||||
export function appendAttachments(container, attachments = []) {
|
export function appendAttachments(container, attachments = []) {
|
||||||
if (!attachments.length) return;
|
if (!attachments.length) return;
|
||||||
const gallery = document.createElement("div");
|
const gallery = document.createElement("div");
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Steuert das Chat-Seitenpanel einschließlich Tastaturbedienung und Fokusverwaltung.
|
||||||
|
*/
|
||||||
import {chatDrawer, chatDrawerButton, chatDrawerCloseBtn} from "./dom.js?v=20260909-account-controls";
|
import {chatDrawer, chatDrawerButton, chatDrawerCloseBtn} from "./dom.js?v=20260909-account-controls";
|
||||||
|
|
||||||
let lastFocusedElement = null;
|
let lastFocusedElement = null;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Stellt gemeinsame Theme-Daten, Einstiegsvorschläge und die angefragte Chat-ID bereit.
|
||||||
|
*/
|
||||||
export const requestedChatId = Number(window.location.pathname.match(/^\/app\/chats\/(\d+)$/)?.[1] || new URLSearchParams(window.location.search).get("chat")) || null;
|
export const requestedChatId = Number(window.location.pathname.match(/^\/app\/chats\/(\d+)$/)?.[1] || new URLSearchParams(window.location.search).get("chat")) || null;
|
||||||
|
|
||||||
export const onboardingPrompts = {
|
export const onboardingPrompts = {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Bündelt Referenzen auf die HTML-Elemente der Chat-Oberfläche.
|
||||||
|
*/
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
const chatDiv = $("chat");
|
const chatDiv = $("chat");
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Steuert die Persona-Marktplatzoberfläche mit Suche, Vorschauen und Persona-Verwaltung.
|
||||||
|
*/
|
||||||
import { get, post, put } from "./api.js?v=20260909-account-controls";
|
import { get, post, put } from "./api.js?v=20260909-account-controls";
|
||||||
import { initializeAppearance } from "./themeController.js?v=20260909-account-controls";
|
import { initializeAppearance } from "./themeController.js?v=20260909-account-controls";
|
||||||
import { clearStatus, setStatus } from "./statusNotice.js?v=20260909-account-controls";
|
import { clearStatus, setStatus } from "./statusNotice.js?v=20260909-account-controls";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Ermittelt die Modellfamilie und prüft die Unterstützung des Reasoning-Modus.
|
||||||
|
*/
|
||||||
// Conservative allowlist for models supporting the boolean think API.
|
// Conservative allowlist for models supporting the boolean think API.
|
||||||
// Unknown models remain usable for normal chat.
|
// Unknown models remain usable for normal chat.
|
||||||
const reasoningFamilies = new Set(["qwen3", "deepseek-r1", "deepseek-v3.1"]);
|
const reasoningFamilies = new Set(["qwen3", "deepseek-r1", "deepseek-v3.1"]);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Ordnet Modellen Profilinformationen für ihre Darstellung und Auswahl in der Oberfläche zu.
|
||||||
|
*/
|
||||||
export const defaultModelProfile = {
|
export const defaultModelProfile = {
|
||||||
badge: "General",
|
badge: "General",
|
||||||
summary: "Best for general questions, drafting, and everyday conversation. Use a specialized model when you need stronger coding, reasoning, or roleplay behavior."
|
summary: "Best for general questions, drafting, and everyday conversation. Use a specialized model when you need stronger coding, reasoning, or roleplay behavior."
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Steuert öffentliche Seiten sowie Anmelde- und Registrierungsformulare mit Rückkehr in den Arbeitsbereich.
|
||||||
|
*/
|
||||||
import { get, post } from "/js/api.js?v=20260909-account-controls";
|
import { get, post } from "/js/api.js?v=20260909-account-controls";
|
||||||
|
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Überträgt Persona-Daten zwischen Formularfeldern und den verwendeten Datenobjekten.
|
||||||
|
*/
|
||||||
const personaFieldNames = ["name", "pronouns", "appearance", "background", "details", "exampleDialogues"];
|
const personaFieldNames = ["name", "pronouns", "appearance", "background", "details", "exampleDialogues"];
|
||||||
|
|
||||||
export function populatePersonaForm(fields, persona = {}) {
|
export function populatePersonaForm(fields, persona = {}) {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Steuert die Reasoning-Auswahl und die aufklappbare Anzeige gestreamter Gedankengänge.
|
||||||
|
*/
|
||||||
import {supportsReasoning} from "./modelCapabilities.js?v=20260921-reasoning-capability";
|
import {supportsReasoning} from "./modelCapabilities.js?v=20260921-reasoning-capability";
|
||||||
|
|
||||||
export function createReasoningToggle(button) {
|
export function createReasoningToggle(button) {
|
||||||
@@ -43,6 +46,7 @@ export function updateReasoning(messageElement, thinking, streaming = false) {
|
|||||||
}
|
}
|
||||||
details.querySelector("summary").textContent = streaming ? "Reasoning · Thinking…" : "Reasoning";
|
details.querySelector("summary").textContent = streaming ? "Reasoning · Thinking…" : "Reasoning";
|
||||||
const content = details.querySelector(".msg-reasoning-content");
|
const content = details.querySelector(".msg-reasoning-content");
|
||||||
|
// Nur automatisch nach unten scrollen, wenn die Ansicht bereits nahe am Ende steht.
|
||||||
const follow = content.scrollHeight - content.scrollTop - content.clientHeight < 40;
|
const follow = content.scrollHeight - content.scrollTop - content.clientHeight < 40;
|
||||||
content.textContent = thinking;
|
content.textContent = thinking;
|
||||||
if (streaming && follow) content.scrollTop = content.scrollHeight;
|
if (streaming && follow) content.scrollTop = content.scrollHeight;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Ersetzt eine Antwort während der Neugenerierung in ihrer Nachrichtenkarte und stellt sie bei Fehlern wieder her.
|
||||||
|
*/
|
||||||
import {stream} from "./api.js";
|
import {stream} from "./api.js";
|
||||||
import {updateReasoning, finishReasoning} from "./reasoning.js";
|
import {updateReasoning, finishReasoning} from "./reasoning.js";
|
||||||
import {appendSources} from "./webSearch.js";
|
import {appendSources} from "./webSearch.js";
|
||||||
@@ -46,6 +49,7 @@ export async function regenerateInline({card, scroller, endpoint, payload, rende
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
// Bei Fehler oder Abbruch die ursprünglichen Knoten samt Ereignishandlern wieder einsetzen.
|
||||||
if (!committed) update(() => card.replaceChildren(...original));
|
if (!committed) update(() => card.replaceChildren(...original));
|
||||||
card.removeAttribute("aria-busy");
|
card.removeAttribute("aria-busy");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Steuert die Chat-Oberfläche mit Gesprächsauswahl, Nachrichteneingabe, Streaming und Persona-Verwaltung.
|
||||||
|
*/
|
||||||
import { del, get, post, put, stream } from "./api.js?v=20260909-account-controls";
|
import { del, get, post, put, stream } from "./api.js?v=20260909-account-controls";
|
||||||
import { regenerateInline } from "./regeneration.js";
|
import { regenerateInline } from "./regeneration.js";
|
||||||
import { appendSources, createWebSearchToggle } from "./webSearch.js";
|
import { appendSources, createWebSearchToggle } from "./webSearch.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Steuert die Einstellungsoberfläche für Darstellung, Konto, Personas und Diagnosen.
|
||||||
|
*/
|
||||||
import { del, get, post, put } from "./api.js?v=20260909-account-controls";
|
import { del, get, post, put } from "./api.js?v=20260909-account-controls";
|
||||||
import {
|
import {
|
||||||
applyTheme as applySharedTheme,
|
applyTheme as applySharedTheme,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Zeigt Statusmeldungen an und setzt deren Inhalt und Sichtbarkeit zurück.
|
||||||
|
*/
|
||||||
export function setStatus(element, message = "", state = "", baseClass = "status") {
|
export function setStatus(element, message = "", state = "", baseClass = "status") {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
element.textContent = message;
|
element.textContent = message;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Übernimmt das gespeicherte Theme frühzeitig in das Dokument, bevor die übrige Oberfläche initialisiert wird.
|
||||||
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
const themes = new Set(["fakegpt", "fraud", "germini", "slopilot", "beta-ai", "confusity"]);
|
const themes = new Set(["fakegpt", "fraud", "germini", "slopilot", "beta-ai", "confusity"]);
|
||||||
const storedTheme = localStorage.getItem("krishd-theme");
|
const storedTheme = localStorage.getItem("krishd-theme");
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Normalisiert, speichert und synchronisiert Theme und Hell-Dunkel-Modus der Oberfläche.
|
||||||
|
*/
|
||||||
import {themes} from "./constants.js?v=20260909-account-controls";
|
import {themes} from "./constants.js?v=20260909-account-controls";
|
||||||
|
|
||||||
export const THEME_STORAGE_KEY = "krishd-theme";
|
export const THEME_STORAGE_KEY = "krishd-theme";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Steuert die Websuche-Auswahl und rendert Quellenlinks zu Chat-Antworten.
|
||||||
|
*/
|
||||||
/** @typedef {{title: string, url: string, snippet: string}} WebSource */
|
/** @typedef {{title: string, url: string, snippet: string}} WebSource */
|
||||||
/** @typedef {{message: string, model: string, chatId: number, webSearch?: boolean, reasoning?: boolean}} ChatRequest */
|
/** @typedef {{message: string, model: string, chatId: number, webSearch?: boolean, reasoning?: boolean}} ChatRequest */
|
||||||
/** @typedef {{role: string, content: string, thinking?: string, sources?: WebSource[]}} ChatMessage */
|
/** @typedef {{role: string, content: string, thinking?: string, sources?: WebSource[]}} ChatMessage */
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Verbindet die Abmeldeknöpfe des gemeinsamen Seitenkopfs mit der Sitzungsverwaltung.
|
||||||
|
*/
|
||||||
import {post} from './api.js?v=20260909-account-controls';
|
import {post} from './api.js?v=20260909-account-controls';
|
||||||
|
|
||||||
// Every page gets this control from the same server-rendered header.
|
// Every page gets this control from the same server-rendered header.
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Startet den HTTP-Server der Chat-Anwendung.
|
||||||
|
*/
|
||||||
import {startServer} from "./src/app.js";
|
import {startServer} from "./src/app.js";
|
||||||
|
|
||||||
startServer();
|
startServer();
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Verbindet Express, Sitzungen, Datenbank und Routen und verwaltet den Server-Lebenszyklus.
|
||||||
|
*/
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import {randomUUID} from "crypto";
|
import {randomUUID} from "crypto";
|
||||||
import {performance} from "perf_hooks";
|
import {performance} from "perf_hooks";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Erstellt die Anwendungskonfiguration aus Umgebungsvariablen, Standardwerten und expliziten Vorgaben.
|
||||||
|
*/
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const bool = (value, fallback = false) => {
|
const bool = (value, fallback = false) => {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Definiert Tabellen für Bildanhänge und Analyse-Caches und bereitet Anhangsmetadaten auf.
|
||||||
|
*/
|
||||||
export function initializeAttachments(db) {
|
export function initializeAttachments(db) {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS attachments (
|
CREATE TABLE IF NOT EXISTS attachments (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Öffnet die SQLite-Datenbank und initialisiert Tabellen sowie Migrationen.
|
||||||
|
*/
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import {initializeSchema, runMigrations} from "./schema.js";
|
import {initializeSchema, runMigrations} from "./schema.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Bündelt Datenbankzugriffe auf Konten, Chats, Nachrichten, Personas und den Marktplatz.
|
||||||
|
*/
|
||||||
import {attachmentMetadata} from "./attachments.js";
|
import {attachmentMetadata} from "./attachments.js";
|
||||||
|
|
||||||
const parseJsonArray = (value) => {
|
const parseJsonArray = (value) => {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Definiert das Datenbankschema und migriert bestehende Daten auf die benötigte Struktur.
|
||||||
|
*/
|
||||||
export function initializeSchema(db) {
|
export function initializeSchema(db) {
|
||||||
db.pragma("journal_mode = WAL");
|
db.pragma("journal_mode = WAL");
|
||||||
db.pragma("foreign_keys = ON");
|
db.pragma("foreign_keys = ON");
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Schützt Routen vor Zugriffen ohne angemeldete Sitzung.
|
||||||
|
*/
|
||||||
export function requireLogin(req, res, next) {
|
export function requireLogin(req, res, next) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
return res.status(403).json({error: "Not logged in"});
|
return res.status(403).json({error: "Not logged in"});
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Begrenzt Anfragen je Schlüssel in einem gleitenden Zeitfenster und entfernt abgelaufene Einträge.
|
||||||
|
*/
|
||||||
function pruneBucket(bucket, windowMs, now) {
|
function pruneBucket(bucket, windowMs, now) {
|
||||||
const cutoff = now - windowMs;
|
const cutoff = now - windowMs;
|
||||||
while (bucket.length && bucket[0] <= cutoff) {
|
while (bucket.length && bucket[0] <= cutoff) {
|
||||||
@@ -21,6 +24,7 @@ export function createRateLimiter({windowMs, max, keyGenerator, message, cleanup
|
|||||||
|
|
||||||
if (cleanupIntervalMs > 0) {
|
if (cleanupIntervalMs > 0) {
|
||||||
cleanupTimer = setInterval(cleanup, cleanupIntervalMs);
|
cleanupTimer = setInterval(cleanup, cleanupIntervalMs);
|
||||||
|
// Der Aufräumtimer allein soll den Node-Prozess beim Herunterfahren nicht offen halten.
|
||||||
cleanupTimer.unref();
|
cleanupTimer.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Erstellt die konfigurierten Anfragebegrenzungen für Anmeldung und Chat-Nutzung.
|
||||||
|
*/
|
||||||
import {createRateLimiter} from "./rateLimit.js";
|
import {createRateLimiter} from "./rateLimit.js";
|
||||||
|
|
||||||
export function createAuthRateLimiters(config) {
|
export function createAuthRateLimiters(config) {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Anfrageinhalte mit einem übergebenen Validator und stellt die validierten Daten für Routen bereit.
|
||||||
|
*/
|
||||||
export function validateBody(validator) {
|
export function validateBody(validator) {
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
const result = validator(req.body || {}, req);
|
const result = validator(req.body || {}, req);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Erzeugt den gemeinsamen HTML-Seitenkopf mit Navigation und Kontosteuerung.
|
||||||
|
*/
|
||||||
const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (character) => ({
|
const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (character) => ({
|
||||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
||||||
})[character]);
|
})[character]);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Verarbeitet Registrierung, Anmeldung, Abmeldung und die Abfrage der aktuellen Sitzung.
|
||||||
|
*/
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import {validateBody} from "../middleware/validate.js";
|
import {validateBody} from "../middleware/validate.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Stellt Chat-Routen für Nachrichten, Streaming, Antwortvarianten, Verzweigungen und Einstellungen bereit.
|
||||||
|
*/
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import {requireLogin} from "../middleware/auth.js";
|
import {requireLogin} from "../middleware/auth.js";
|
||||||
import {validateBody} from "../middleware/validate.js";
|
import {validateBody} from "../middleware/validate.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Liefert öffentliche Seiten und geschützte Ansichten des Arbeitsbereichs aus.
|
||||||
|
*/
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import {readFile} from "node:fs/promises";
|
import {readFile} from "node:fs/promises";
|
||||||
import {renderSiteHeader} from "../pageLayout.js";
|
import {renderSiteHeader} from "../pageLayout.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Verwaltet eigene Personas sowie Veröffentlichung, Suche und Interaktionen auf dem Persona-Marktplatz.
|
||||||
|
*/
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import {requireLogin} from "../middleware/auth.js";
|
import {requireLogin} from "../middleware/auth.js";
|
||||||
import {validateBody} from "../middleware/validate.js";
|
import {validateBody} from "../middleware/validate.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Verarbeitet Benutzer- und Kontoeinstellungen sowie Export und Import des Arbeitsbereichs.
|
||||||
|
*/
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import {requireLogin} from "../middleware/auth.js";
|
import {requireLogin} from "../middleware/auth.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Stellt Modellinformationen und administrative Diagnosedaten bereit.
|
||||||
|
*/
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import {requireLogin} from "../middleware/auth.js";
|
import {requireLogin} from "../middleware/auth.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Verarbeitet das Hochladen, Abrufen und Entfernen von Bildanhängen mit Zugriffskontrolle.
|
||||||
|
*/
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import {requireLogin} from "../middleware/auth.js";
|
import {requireLogin} from "../middleware/auth.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Liefert angemeldeten Benutzern die für sie zugänglichen Diagnosen der Bildverarbeitung.
|
||||||
|
*/
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import {requireLogin} from "../middleware/auth.js";
|
import {requireLogin} from "../middleware/auth.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Validiert und speichert Bildanhänge und bereitet zwischengespeicherte Bildanalysen für den Chat vor.
|
||||||
|
*/
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import {createHash, randomUUID} from "node:crypto";
|
import {createHash, randomUUID} from "node:crypto";
|
||||||
import {attachmentMetadata} from "../db/attachments.js";
|
import {attachmentMetadata} from "../db/attachments.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Normalisiert Nachrichten und erstellt Kontexttexte für Erinnerungen, Zusammenfassungen und Personas.
|
||||||
|
*/
|
||||||
import {normalizeSources} from "./webSearchService.js";
|
import {normalizeSources} from "./webSearchService.js";
|
||||||
|
|
||||||
export function parseSources(value) {
|
export function parseSources(value) {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Koordiniert Chat-Antworten, Modellkontext, Websuche, Anhänge, Antwortvarianten und Gesprächsverzweigungen.
|
||||||
|
*/
|
||||||
import {
|
import {
|
||||||
buildPersonaPrompt,
|
buildPersonaPrompt,
|
||||||
buildRoleplayDirectionPrompt,
|
buildRoleplayDirectionPrompt,
|
||||||
@@ -95,15 +98,18 @@ export function createChatService(repositories, modelService, config, webSearchP
|
|||||||
|
|
||||||
const withChatGenerationLock = (chatId, operation) => {
|
const withChatGenerationLock = (chatId, operation) => {
|
||||||
const previous = chatGenerationQueues.get(chatId) || Promise.resolve();
|
const previous = chatGenerationQueues.get(chatId) || Promise.resolve();
|
||||||
|
// Ein fehlgeschlagener Vorgänger darf die Warteschlange dieses Chats nicht blockieren.
|
||||||
const current = previous.catch(() => undefined).then(operation);
|
const current = previous.catch(() => undefined).then(operation);
|
||||||
chatGenerationQueues.set(chatId, current);
|
chatGenerationQueues.set(chatId, current);
|
||||||
return current.finally(() => {
|
return current.finally(() => {
|
||||||
|
// Nur den eigenen Eintrag entfernen: Inzwischen könnte bereits ein Folgeauftrag warten.
|
||||||
if (chatGenerationQueues.get(chatId) === current) chatGenerationQueues.delete(chatId);
|
if (chatGenerationQueues.get(chatId) === current) chatGenerationQueues.delete(chatId);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const withActiveGeneration = async (chatId, externalSignal, operation) => {
|
const withActiveGeneration = async (chatId, externalSignal, operation) => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
// Verbindungsabbruch und manuelles Stoppen wirken auf denselben Generierungsauftrag.
|
||||||
const abortFromCaller = () => controller.abort(externalSignal?.reason);
|
const abortFromCaller = () => controller.abort(externalSignal?.reason);
|
||||||
if (externalSignal?.aborted) abortFromCaller();
|
if (externalSignal?.aborted) abortFromCaller();
|
||||||
else externalSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
else externalSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
||||||
@@ -157,6 +163,7 @@ export function createChatService(repositories, modelService, config, webSearchP
|
|||||||
const allMessages = repositories.listActiveBranchMessages(chatId)
|
const allMessages = repositories.listActiveBranchMessages(chatId)
|
||||||
.filter((message) => !message.delivery_status || message.delivery_status === "complete");
|
.filter((message) => !message.delivery_status || message.delivery_status === "complete");
|
||||||
const keepRecent = Math.max(0, config.chatHistoryLimit);
|
const keepRecent = Math.max(0, config.chatHistoryLimit);
|
||||||
|
// Die neuesten Nachrichten bleiben vollständig im Kontext; nur ältere werden zusammengefasst.
|
||||||
const cutoffIndex = allMessages.length - keepRecent - 1;
|
const cutoffIndex = allMessages.length - keepRecent - 1;
|
||||||
if (cutoffIndex < 0) return;
|
if (cutoffIndex < 0) return;
|
||||||
const cutoffMessage = allMessages[cutoffIndex];
|
const cutoffMessage = allMessages[cutoffIndex];
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Speichert, liest und löscht hochgeladene Dateien in einem lokalen Speicherverzeichnis.
|
||||||
|
*/
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {randomUUID} from "node:crypto";
|
import {randomUUID} from "node:crypto";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Kommuniziert mit Ollama und verwaltet Antwortstreams, Zeitlimits, Modellstatus und Modell-Caches.
|
||||||
|
*/
|
||||||
import { supportsReasoning } from "../../public/js/modelCapabilities.js";
|
import { supportsReasoning } from "../../public/js/modelCapabilities.js";
|
||||||
import { AttachmentError } from "./attachmentService.js";
|
import { AttachmentError } from "./attachmentService.js";
|
||||||
|
|
||||||
@@ -147,6 +150,7 @@ export function createModelService(config, overrides = {}) {
|
|||||||
touch();
|
touch();
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
const lines = buffer.split("\n");
|
const lines = buffer.split("\n");
|
||||||
|
// Netzwerkblöcke können mitten in einer JSON-Zeile enden; den Rest für den nächsten Block behalten.
|
||||||
buffer = lines.pop() || "";
|
buffer = lines.pop() || "";
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
@@ -160,6 +164,7 @@ export function createModelService(config, overrides = {}) {
|
|||||||
if (!completed && buffer.trim()) {
|
if (!completed && buffer.trim()) {
|
||||||
consume(buffer.trim());
|
consume(buffer.trim());
|
||||||
}
|
}
|
||||||
|
// Erst das Abschlussereignis bestätigt eine vollständige Antwort; ein Stream-Ende allein genügt nicht.
|
||||||
if (!completed) throw new ModelServiceError("MODEL_INTERRUPTED", "Model response was interrupted.", 502);
|
if (!completed) throw new ModelServiceError("MODEL_INTERRUPTED", "Model response was interrupted.", 502);
|
||||||
onRawReply?.(fullReply);
|
onRawReply?.(fullReply);
|
||||||
return fullReply.trim();
|
return fullReply.trim();
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Validiert Persona-Daten und erstellt Rollenspielanweisungen sowie Prüfungen für die Rollenverteilung.
|
||||||
|
*/
|
||||||
const PERSONA_FIELD_LIMITS = {
|
const PERSONA_FIELD_LIMITS = {
|
||||||
name: 200,
|
name: 200,
|
||||||
pronouns: 40,
|
pronouns: 40,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Definiert Anweisungen, Antwortschema und Normalisierung für Bildanalysen und deren Nutzung im Chat.
|
||||||
|
*/
|
||||||
// Bump whenever the perception prompt, schema, or interpretation contract changes.
|
// Bump whenever the perception prompt, schema, or interpretation contract changes.
|
||||||
export const VISION_PROMPT_VERSION = 4;
|
export const VISION_PROMPT_VERSION = 4;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Sammelt begrenzte Diagnosedaten zur Bildverarbeitung und kontrolliert deren benutzerbezogenen Zugriff.
|
||||||
|
*/
|
||||||
const RAW_VISION_LIMIT = 12000;
|
const RAW_VISION_LIMIT = 12000;
|
||||||
const EVIDENCE_LIMIT = 100000;
|
const EVIDENCE_LIMIT = 100000;
|
||||||
const ANSWER_LIMIT = 30000;
|
const ANSWER_LIMIT = 30000;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Entscheidet über Websuchen, fragt SearXNG ab und bereitet relevante Quellen als Modellkontext auf.
|
||||||
|
*/
|
||||||
/** @typedef {{title: string, url: string, snippet: string, score?: number, engine?: string, category?: string, publishedDate?: string}} RawSearchResult */
|
/** @typedef {{title: string, url: string, snippet: string, score?: number, engine?: string, category?: string, publishedDate?: string}} RawSearchResult */
|
||||||
/** @typedef {{title: string, url: string, snippet: string}} SearchResult */
|
/** @typedef {{title: string, url: string, snippet: string}} SearchResult */
|
||||||
/** @typedef {{signal?: AbortSignal, limit?: number, category?: string}} SearchOptions */
|
/** @typedef {{signal?: AbortSignal, limit?: number, category?: string}} SearchOptions */
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft und normalisiert importierte Arbeitsbereiche mit Personas, Chats und Nachrichtenbeziehungen.
|
||||||
|
*/
|
||||||
import {parseSources} from "./chatMessageHelpers.js";
|
import {parseSources} from "./chatMessageHelpers.js";
|
||||||
|
|
||||||
const VALID_PERSONA_TYPES = new Set(["assistant", "user"]);
|
const VALID_PERSONA_TYPES = new Set(["assistant", "user"]);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Anmeldung, Kontoverwaltung, Chats, Personas und den Server-Lebenszyklus der Anwendung.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Anhangsvorschauen, Upload-Zustände, Wiederholungen und das Verwerfen von Entwürfen.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { appendAttachments, createAttachmentComposer } from "../public/js/attachments.js";
|
import { appendAttachments, createAttachmentComposer } from "../public/js/attachments.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Uploads, Zugriffsrechte und den Lebenszyklus von Bildanhängen sowie ihre Nutzung im Modellkontext.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Konfigurationsvorgaben und Sicherheitsanforderungen für Test- und Produktionsbetrieb.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {createConfig} from "../src/config.js";
|
import {createConfig} from "../src/config.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Antwortstreaming, Zeitlimits, Protokollfehler und Abbrüche bei getrennten Verbindungen.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Reasoning-Auswahl, getrennte Gedankenstreams und deren Speicherung sowie Darstellung.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Antwortvarianten und den Erhalt bestehender Antworten bei fehlgeschlagener Neugenerierung.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {createServer} from "node:http";
|
import {createServer} from "node:http";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft die Neugenerierung innerhalb einer Nachrichtenkarte mit Scrollverhalten und Wiederherstellung.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {readFile} from "node:fs/promises";
|
import {readFile} from "node:fs/promises";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Schema, Anweisungen, Normalisierung und Fehlerbehandlung der Bildanalyse.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {parseVisionAnalysis} from "../src/services/attachmentService.js";
|
import {parseVisionAnalysis} from "../src/services/attachmentService.js";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Zugriffsschutz und Diagnoseaufzeichnungen für Bildanalysen, Caches und Modellanfragen.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Prüft Suchentscheidungen, SearXNG, Quellenauswahl und die Einbindung der Websuche in Chat und Oberfläche.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @file Sichert ab, dass explizite und datumsbezogene Websuchen vor der Modellgenerierung passende Quellen liefern.
|
||||||
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {createServer} from "node:http";
|
import {createServer} from "node:http";
|
||||||
|
|||||||
Reference in New Issue
Block a user