Recherche abbrechen + Chats löschen (mit Aufräumen) + Truncation-Fix
- Manueller Abbruch: POST /api/sessions/:id/abort (nur Owner), AbortController
für laufende Jobs (auch mitten im API-Stream), cancelQueued für wartende,
⛔-Button in der Activity-Bar, 'abgebrochen'-Event + History-Eintrag
- Chats löschen: 🗑-Button in der Sidebar mit Bestätigung, Ownership-Check
(vorher konnte jeder fremde Chats löschen), löscht jetzt auch Messages
und Dashboard-Dateien (HTML/GPX/tour.json) mit
- Truncation-Fix: finish_reason 'length' wird erkannt (sichtbarer Fehler
statt leerer Abschluss), max_tokens auf 131072 (GLM-Max), 10-Min-Timeout
gegen hängende Streams
This commit is contained in:
1 parent
0b51bb6142
commit
8a2bf46b09
8 files changed
+237
-39
No files matched your search
+71
-9
@@ -228,6 +228,12 @@ function summarizeTool(toolName, output) {
|
||||
|
||||
const MAX_ITERATIONS = 40;
|
||||
|
||||
// Abbruch, wenn der API-Stream länger hängt (z.B. OpenRouter ohne Keep-Alive)
|
||||
const REQUEST_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
/** Wird geworfen, wenn der Nutzer die Recherche manuell abbricht. */
|
||||
class JobAbortedError extends Error {}
|
||||
|
||||
/** Baut den System-Prompt inkl. aktuellem Dashboard-Stand und Vision-Hinweisen. */
|
||||
function buildSystemPrompt(session, model) {
|
||||
let prompt = SYSTEM_PROMPT;
|
||||
@@ -258,7 +264,7 @@ function isChangeRequest(text) {
|
||||
);
|
||||
}
|
||||
|
||||
async function chatCompletion(config, messages, systemPrompt, model, tools) {
|
||||
async function chatCompletion(config, messages, systemPrompt, model, tools, signal) {
|
||||
const body = {
|
||||
model: model.id,
|
||||
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
||||
@@ -293,11 +299,23 @@ async function chatCompletion(config, messages, systemPrompt, model, tools) {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
};
|
||||
|
||||
const resp = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
const fetchSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: fetchSignal,
|
||||
});
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
if (signal?.aborted) throw new JobAbortedError();
|
||||
throw new Error(`Modell-API nicht erreichbar (${model.id}): ${e.message.slice(0, 200)}`);
|
||||
}
|
||||
clearTimeout(timer);
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.text().catch(() => '');
|
||||
@@ -344,6 +362,7 @@ async function chatCompletion(config, messages, systemPrompt, model, tools) {
|
||||
}
|
||||
}
|
||||
if (choice.finish_reason) delta.finishReason = choice.finish_reason;
|
||||
else if (d.finish_reason) delta.finishReason = d.finish_reason;
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
@@ -358,6 +377,31 @@ async function chatCompletion(config, messages, systemPrompt, model, tools) {
|
||||
}
|
||||
if (buffer.trim()) feed(buffer);
|
||||
|
||||
// Antwort abgeschnitten (max_tokens erreicht): Wenn weder Inhalt noch
|
||||
// vollständige Tool-Calls vorhanden sind, endet die Recherche sonst
|
||||
// stillschweigend ohne Ergebnis. Dann lieber sichtbar abbrechen.
|
||||
if (delta.finishReason === 'length') {
|
||||
const hasCalls = Array.isArray(delta.toolCalls) && delta.toolCalls.length > 0;
|
||||
const callsComplete = !hasCalls || delta.toolCalls.every((tc) => {
|
||||
try {
|
||||
JSON.parse(tc.function.arguments || '{}');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!hasCalls && !delta.content) {
|
||||
throw new Error(
|
||||
`Modell-Antwort abgeschnitten (Token-Limit bei ${model.id} erreicht) – Ergebnis unvollständig. Bitte erneut versuchen oder ein anderes Modell wählen.`
|
||||
);
|
||||
}
|
||||
if (hasCalls && !callsComplete) {
|
||||
throw new Error(
|
||||
`Tool-Aufruf abgeschnitten (Token-Limit bei ${model.id} erreicht) – Recherche wird abgebrochen. Bitte erneut versuchen.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: delta.content,
|
||||
reasoning: delta.reasoning,
|
||||
@@ -375,13 +419,14 @@ async function chatCompletion(config, messages, systemPrompt, model, tools) {
|
||||
* @param {object} emit Event-Helfer: { token(t), thinking(t), progress(icon, text), step(msg), done(obj), error(msg) }
|
||||
* @param {object} db Datenbankmodul
|
||||
*/
|
||||
async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
async function runResearchJob(config, session, userMessageText, emit, db, signal) {
|
||||
let dashboardUrl = session.dashboard || null;
|
||||
let dashboardSaved = false;
|
||||
|
||||
const model = getModel(session.model || config.model);
|
||||
const toolDefs = model.vision ? [...TOOL_DEFINITIONS, SCREENSHOT_TOOL] : TOOL_DEFINITIONS;
|
||||
const prices = model.prices;
|
||||
const isAborted = () => signal?.aborted === true;
|
||||
|
||||
const handlers = {
|
||||
async search({ query }) {
|
||||
@@ -450,9 +495,25 @@ async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
return { text: finalText, dashboardUrl, usage, costEstimate, model: model.id };
|
||||
};
|
||||
|
||||
const finishAborted = () => {
|
||||
db.createMessage(session.id, { role: 'assistant', content: '⛔ Recherche abgebrochen.' });
|
||||
db.setSessionUsage(session.id, usage, prices);
|
||||
db.setSessionStatus(session.id, 'done');
|
||||
const costEstimate = db.computeCost(usage, prices);
|
||||
emit.aborted?.();
|
||||
return { text: '⛔ Recherche abgebrochen.', dashboardUrl, usage, costEstimate, model: model.id, aborted: true };
|
||||
};
|
||||
|
||||
for (let iter = 0; iter < MAX_ITERATIONS; iter++) {
|
||||
if (isAborted()) return finishAborted();
|
||||
emit.progress('🤖', iter === 0 ? 'Analysiere Anfrage und plane Suche…' : 'Weitere Analyse…');
|
||||
const result = await chatCompletion(config, apiMessages, buildSystemPrompt(session, model), model, toolDefs);
|
||||
let result;
|
||||
try {
|
||||
result = await chatCompletion(config, apiMessages, buildSystemPrompt(session, model), model, toolDefs, signal);
|
||||
} catch (e) {
|
||||
if (isAborted() || e instanceof JobAbortedError) return finishAborted();
|
||||
throw e;
|
||||
}
|
||||
trackUsage(result.usage);
|
||||
|
||||
if (result.reasoning) emit.thinking(result.reasoning);
|
||||
@@ -511,10 +572,11 @@ async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
apiMessages.push({ role: 'tool', tool_call_id: tc.id, content: outStr.slice(0, 8000) });
|
||||
}
|
||||
}
|
||||
if (isAborted()) return finishAborted();
|
||||
continue;
|
||||
}
|
||||
|
||||
const finalText = result.content || (dashboardUrl ? 'Recherche abgeschlossen – siehe Dashboard.' : '');
|
||||
const finalText = result.content || (dashboardUrl ? 'Recherche abgeschlossen – siehe Dashboard.' : 'Recherche abgeschlossen.');
|
||||
if (!dashboardSaved && dashboardUrl && isChangeRequest(userMessageText)) {
|
||||
emit.progress(
|
||||
'⚠️',
|
||||
|
||||
Reference in new issue
Block a user