feat: Payload-Preview im Chat-Menü + Reminder von hinten gezählt
- Neuer Endpoint GET /api/sessions/{id}/payload-preview
Gibt den exakten Anthropic-API-Payload als JSON zurück (read-only)
- Chat-Menü: 'Payload anzeigen' öffnet Modal mit formatiertem JSON
- Reminder-Logik umgebaut: halbes Intervall (15) vor Ende,
dann volle Intervalle (30) rückwärts statt von vorne gezählt
- CSS: Payload-Modal Styling, kleine Variable-Fixes
This commit is contained in:
1 parent
229e6d9e53
commit
49ef6bedc6
7 files changed
+154
-21
No files matched your search
@@ -26,6 +26,7 @@ public static class SessionEndpoint
|
|||||||
app.MapDelete("/api/sessions/{sessionId}", HandleDelete);
|
app.MapDelete("/api/sessions/{sessionId}", HandleDelete);
|
||||||
app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt);
|
app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt);
|
||||||
app.MapGet("/api/sessions/{sessionId}/system-prompt-blocks", HandleSystemPromptBlocks);
|
app.MapGet("/api/sessions/{sessionId}/system-prompt-blocks", HandleSystemPromptBlocks);
|
||||||
|
app.MapGet("/api/sessions/{sessionId}/payload-preview", HandlePayloadPreview);
|
||||||
app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming);
|
app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming);
|
||||||
app.MapPost("/api/sessions/{sessionId}/exec", HandleExec);
|
app.MapPost("/api/sessions/{sessionId}/exec", HandleExec);
|
||||||
app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus);
|
app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus);
|
||||||
@@ -266,6 +267,75 @@ public static class SessionEndpoint
|
|||||||
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
|
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task HandlePayloadPreview(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService, ChatService chatService, ToolService toolService, ServerSettingsService settings)
|
||||||
|
{
|
||||||
|
var result = await ResolvePromptContext(ctx, db, memoryService);
|
||||||
|
if (result == null) return;
|
||||||
|
var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value;
|
||||||
|
|
||||||
|
// System-Prompt
|
||||||
|
var promptBlocks = SystemPromptBuilder.BuildBlocks(agent, session, marked, memoryService, memDir,
|
||||||
|
skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode);
|
||||||
|
var systemPromptText = string.Join("\n\n", promptBlocks.Select(b => b.Content));
|
||||||
|
|
||||||
|
var systemPrompt = new List<object>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(systemPromptText))
|
||||||
|
systemPrompt.Add(new { type = "text", text = systemPromptText });
|
||||||
|
|
||||||
|
// Model
|
||||||
|
var modelId = ChatService.ResolveModelId(agent.Model);
|
||||||
|
|
||||||
|
// Messages aus der DB (exakt wie beim Chat)
|
||||||
|
var messages = await chatService.BuildHistoryFromDb(session.SessionId, promptBlocks: promptBlocks);
|
||||||
|
|
||||||
|
// Tools (exakt wie AddToolsToPayload es machen würde)
|
||||||
|
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
||||||
|
var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId);
|
||||||
|
var includeGitHubSearch = agent.UseGitHubSearch && toolService.HasGitHubToken;
|
||||||
|
var includeSerperSearch = agent.UseSerperSearch && toolService.HasSerperKey;
|
||||||
|
var hasTools = hasWorkspace || includeMemory || includeGitHubSearch || includeSerperSearch;
|
||||||
|
|
||||||
|
var tools = new List<object>();
|
||||||
|
if (hasTools)
|
||||||
|
{
|
||||||
|
var allTools = toolService.GetToolDefinitions(
|
||||||
|
includeFileTools: hasWorkspace,
|
||||||
|
includeMemory: includeMemory,
|
||||||
|
includeUserChoice: agent.AllowUserChoice,
|
||||||
|
includeGitHubSearch: includeGitHubSearch,
|
||||||
|
includeSerperSearch: includeSerperSearch);
|
||||||
|
if (planMode)
|
||||||
|
allTools = toolService.FilterReadOnlyTools(allTools);
|
||||||
|
tools.AddRange(allTools);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(agent.WebSearchVersion))
|
||||||
|
tools.Add(new { type = agent.WebSearchVersion, name = "web_search", max_uses = 5 });
|
||||||
|
|
||||||
|
var toolChoice = hasTools || !string.IsNullOrEmpty(agent.WebSearchVersion) ? "any" : null;
|
||||||
|
var effectiveChoice = toolChoice == "any" ? "any" : "auto";
|
||||||
|
|
||||||
|
// Payload zusammenbauen (Anthropic-Format)
|
||||||
|
var payload = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["model"] = modelId,
|
||||||
|
["max_tokens"] = 16384,
|
||||||
|
["messages"] = messages,
|
||||||
|
["stream"] = true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (systemPrompt.Count > 0)
|
||||||
|
payload["system"] = systemPrompt;
|
||||||
|
|
||||||
|
if (tools.Count > 0)
|
||||||
|
{
|
||||||
|
payload["tools"] = tools;
|
||||||
|
payload["tool_choice"] = new { type = effectiveChoice };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task HandleStreaming(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker)
|
private static async Task HandleStreaming(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker)
|
||||||
{
|
{
|
||||||
var userId = ctx.GetUserId();
|
var userId = ctx.GetUserId();
|
||||||
|
|||||||
+31
-18
@@ -549,9 +549,10 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
|||||||
history.RemoveRange(0, removeCount);
|
history.RemoveRange(0, removeCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reminder einfügen: alle ~30 user/assistant-Paare ein Reminder-Paar einfügen
|
// Reminder einfügen: von hinten gezählt – halbes Intervall vor Ende, dann volle Intervalle
|
||||||
var reminder = SystemPromptBuilder.BuildReminder(promptBlocks);
|
var reminder = SystemPromptBuilder.BuildReminder(promptBlocks);
|
||||||
if (!string.IsNullOrWhiteSpace(reminder) && ReminderInterval > 0 && history.Count > ReminderInterval)
|
int halfInterval = ReminderInterval / 2;
|
||||||
|
if (!string.IsNullOrWhiteSpace(reminder) && ReminderInterval > 0 && history.Count > halfInterval)
|
||||||
{
|
{
|
||||||
var reminderUser = new Dictionary<string, object>
|
var reminderUser = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
@@ -564,17 +565,29 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
|||||||
["content"] = new List<object> { new { type = "text", text = "Verstanden." } }
|
["content"] = new List<object> { new { type = "text", text = "Verstanden." } }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Alle 30 Messages ein Reminder-Paar einfügen (von hinten nach vorne, damit Indizes stimmen)
|
// Zielpositionen von hinten berechnen:
|
||||||
var insertPositions = new List<int>();
|
// Erste Position: history.Count - halfInterval (z.B. 15 vom Ende)
|
||||||
int messageCount = 0;
|
// Weitere Positionen: jeweils - ReminderInterval (z.B. 30er-Schritte rückwärts)
|
||||||
for (int i = 0; i < history.Count; i++)
|
var targetPositions = new List<int>();
|
||||||
|
int pos = history.Count - halfInterval;
|
||||||
|
while (pos > 0)
|
||||||
{
|
{
|
||||||
messageCount++;
|
targetPositions.Add(pos);
|
||||||
if (messageCount > 0 && messageCount % ReminderInterval == 0)
|
pos -= ReminderInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für jede Zielposition die nächste valide Einfügestelle finden
|
||||||
|
var insertPositions = new List<int>();
|
||||||
|
foreach (int target in targetPositions)
|
||||||
|
{
|
||||||
|
// Suche ab target rückwärts nach einer assistant-Message,
|
||||||
|
// deren Folge-Message kein tool_result ist
|
||||||
|
for (int i = Math.Min(target, history.Count - 1); i >= 1; i--)
|
||||||
{
|
{
|
||||||
// Position nach dem aktuellen Element – aber nur einfügen wenn es eine
|
if (!(history[i] is Dictionary<string, object> d &&
|
||||||
// assistant-Message ist (damit user/assistant-Alternierung nicht bricht)
|
d.TryGetValue("role", out var r) && r.ToString() == "assistant"))
|
||||||
// UND die nächste Message kein tool_result ist (sonst bricht tool_use/tool_result-Paar)
|
continue;
|
||||||
|
|
||||||
bool nextIsToolResult = false;
|
bool nextIsToolResult = false;
|
||||||
if (i + 1 < history.Count && history[i + 1] is Dictionary<string, object> nextDict &&
|
if (i + 1 < history.Count && history[i + 1] is Dictionary<string, object> nextDict &&
|
||||||
nextDict.TryGetValue("role", out var nextRole) && nextRole.ToString() == "user" &&
|
nextDict.TryGetValue("role", out var nextRole) && nextRole.ToString() == "user" &&
|
||||||
@@ -587,20 +600,20 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nextIsToolResult && history[i] is Dictionary<string, object> d &&
|
if (!nextIsToolResult)
|
||||||
d.TryGetValue("role", out var r) && r.ToString() == "assistant")
|
|
||||||
{
|
{
|
||||||
insertPositions.Add(i + 1);
|
insertPositions.Add(i + 1);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Von hinten nach vorne einfügen, damit Indizes nicht verschoben werden
|
// Deduplizieren und von hinten nach vorne einfügen, damit Indizes nicht verschoben werden
|
||||||
for (int j = insertPositions.Count - 1; j >= 0; j--)
|
insertPositions = insertPositions.Distinct().OrderByDescending(x => x).ToList();
|
||||||
|
foreach (int insertPos in insertPositions)
|
||||||
{
|
{
|
||||||
var pos = insertPositions[j];
|
history.Insert(insertPos, reminderAssistant);
|
||||||
history.Insert(pos, reminderAssistant);
|
history.Insert(insertPos, reminderUser);
|
||||||
history.Insert(pos, reminderUser);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-3
@@ -874,6 +874,23 @@
|
|||||||
padding: 2em;
|
padding: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Payload Preview Modal */
|
||||||
|
|
||||||
|
.payload-preview-modal {
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payload-json {
|
||||||
|
font-family: var(--mono);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--box-padding);
|
||||||
|
background: var(--f3-bg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: var(--border-width) solid var(--f2-border);
|
||||||
|
}
|
||||||
|
|
||||||
/* Command Output Modal */
|
/* Command Output Modal */
|
||||||
|
|
||||||
.cmd-output-modal {
|
.cmd-output-modal {
|
||||||
@@ -1276,7 +1293,7 @@
|
|||||||
/* ── OpenClaw Import Modal ── */
|
/* ── OpenClaw Import Modal ── */
|
||||||
|
|
||||||
.openclaw-import-modal {
|
.openclaw-import-modal {
|
||||||
max-width: 700px;
|
max-width: var(--modal-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.openclaw-hint {
|
.openclaw-hint {
|
||||||
@@ -1314,8 +1331,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openclaw-session-id {
|
.openclaw-session-id {
|
||||||
font-family: var(--mono-font);
|
font-family: var(--mono);
|
||||||
font-size: var(--small-font-size);
|
font-size: var(--mono-size);
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,6 +386,7 @@
|
|||||||
<div class="header-menu-dropdown" id="chatMenuDropdown" hidden>
|
<div class="header-menu-dropdown" id="chatMenuDropdown" hidden>
|
||||||
<button class="header-menu-item" id="remoteBtn"><i class="fa fa-desktop"></i> Remote-Verbindungen</button>
|
<button class="header-menu-item" id="remoteBtn"><i class="fa fa-desktop"></i> Remote-Verbindungen</button>
|
||||||
<button class="header-menu-item" id="systemPromptBtn">System-Prompt anzeigen</button>
|
<button class="header-menu-item" id="systemPromptBtn">System-Prompt anzeigen</button>
|
||||||
|
<button class="header-menu-item" id="payloadPreviewBtn">Payload anzeigen</button>
|
||||||
<button class="header-menu-item" id="archiveBtn">Archivieren</button>
|
<button class="header-menu-item" id="archiveBtn">Archivieren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ function apiGetSystemPromptBlocks(sessionId, planMode) {
|
|||||||
return _get(url);
|
return _get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function apiGetPayloadPreview(sessionId, planMode) {
|
||||||
|
var url = '/api/sessions/' + sessionId + '/payload-preview' + (planMode ? '?planMode=true' : '');
|
||||||
|
return _get(url);
|
||||||
|
}
|
||||||
|
|
||||||
function apiIsStreaming(sessionId) { return _get('/api/sessions/' + sessionId + '/streaming'); }
|
function apiIsStreaming(sessionId) { return _get('/api/sessions/' + sessionId + '/streaming'); }
|
||||||
function apiGitStatus(sessionId) { return _get('/api/sessions/' + sessionId + '/git-status', null); }
|
function apiGitStatus(sessionId) { return _get('/api/sessions/' + sessionId + '/git-status', null); }
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,26 @@ async function showSystemPromptModal(sessionId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Payload Preview Modal ──
|
||||||
|
|
||||||
|
async function showPayloadPreviewModal(sessionId) {
|
||||||
|
const { body } = _createModal('Payload Preview', { modal: 'payload-preview-modal' });
|
||||||
|
body.innerHTML = '<div class="sp-loading">Laden...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var planMode = typeof _planModeActive !== 'undefined' && _planModeActive;
|
||||||
|
var payload = await apiGetPayloadPreview(sessionId, planMode);
|
||||||
|
body.innerHTML = '';
|
||||||
|
|
||||||
|
var pre = document.createElement('pre');
|
||||||
|
pre.className = 'payload-json';
|
||||||
|
pre.textContent = JSON.stringify(payload, null, 2);
|
||||||
|
body.appendChild(pre);
|
||||||
|
} catch (e) {
|
||||||
|
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des Payloads</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Command Output Modal ──
|
// ── Command Output Modal ──
|
||||||
|
|
||||||
function showCommandOutputModal(name, command, result) {
|
function showCommandOutputModal(name, command, result) {
|
||||||
|
|||||||
@@ -416,6 +416,13 @@ registerPage('chat', {
|
|||||||
showSystemPromptModal(sessionId);
|
showSystemPromptModal(sessionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Payload anzeigen
|
||||||
|
const payloadPreviewBtn = document.getElementById('payloadPreviewBtn');
|
||||||
|
if (payloadPreviewBtn) payloadPreviewBtn.addEventListener('click', () => {
|
||||||
|
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||||||
|
showPayloadPreviewModal(sessionId);
|
||||||
|
});
|
||||||
|
|
||||||
// Archivieren
|
// Archivieren
|
||||||
const archiveBtn = document.getElementById('archiveBtn');
|
const archiveBtn = document.getElementById('archiveBtn');
|
||||||
if (archiveBtn) archiveBtn.addEventListener('click', () => {
|
if (archiveBtn) archiveBtn.addEventListener('click', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user