Generalize MCP servers, add model refresh button, add sonnet-5

- MCP: replace fixed single Unity field with a generic list of servers
  ({name, url, token?}); dynamic mcp-config + allowlist glob mcp__*<name>*
  per entry; new mcp-servers-ui.js editor (no data migration)
- Models: frontend refresh button wired to existing POST /api/models/refresh,
  reloads optgroups and restores selection
- Model: add sonnet-5 alias + dropdown option (bare "sonnet" stays 4.6)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-07-02 08:51:20 +02:00
1 parent 1032cd5c63
commit 5f7dec6448
14 files changed
+257 -50

No files matched your search

+1 -1
View File
@@ -292,7 +292,7 @@ public static class ChatEndpoint
if (anthropic.IsCliAvailable) if (anthropic.IsCliAvailable)
{ {
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, ultracode: body.Ultracode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt); await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, ultracode: body.Ultracode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, mcpServers: ctx.Session?.McpServers, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt);
} }
else else
{ {
+1
View File
@@ -83,6 +83,7 @@ public static class SessionEndpoint
s.PreparerAgentId, s.PreparerAgentId,
s.UnityMcpUrl, s.UnityMcpUrl,
s.UnityMcpToken, s.UnityMcpToken,
s.McpServers,
s.IsDeleted, s.IsDeleted,
s.CreatedAt, s.CreatedAt,
s.UpdatedAt, s.UpdatedAt,
+1
View File
@@ -28,6 +28,7 @@ public class SessionRecord
public int MoaLayers { get; set; } = 2; // MoA: Anzahl Proposer-Runden vor der Synthese (Fusion ignoriert) public int MoaLayers { get; set; } = 2; // MoA: Anzahl Proposer-Runden vor der Synthese (Fusion ignoriert)
public string UnityMcpUrl { get; set; } public string UnityMcpUrl { get; set; }
public string UnityMcpToken { get; set; } public string UnityMcpToken { get; set; }
public string McpServers { get; set; } // JSON-Array benutzerdefinierter MCP-Server [{name,url,token}]
public string CreatedAt { get; set; } public string CreatedAt { get; set; }
public string UpdatedAt { get; set; } public string UpdatedAt { get; set; }
} }
+24 -19
View File
@@ -35,8 +35,7 @@ public partial class AnthropicService
string sessionId = null, string sessionId = null,
string agentId = null, string agentId = null,
RemoteConnectionManager remoteMgr = null, RemoteConnectionManager remoteMgr = null,
string unityMcpUrl = null, string mcpServers = null,
string unityMcpToken = null,
bool useAutoContext = true, bool useAutoContext = true,
string effort = null, string effort = null,
TokenUsage seedUsage = null, TokenUsage seedUsage = null,
@@ -74,7 +73,8 @@ public partial class AnthropicService
var remoteConn = remoteMgr != null && !string.IsNullOrEmpty(sessionId) ? remoteMgr.GetBySessionId(sessionId) : null; var remoteConn = remoteMgr != null && !string.IsNullOrEmpty(sessionId) ? remoteMgr.GetBySessionId(sessionId) : null;
var hasRemote = remoteConn != null; var hasRemote = remoteConn != null;
var remoteOs = remoteConn?.OsVersion ?? ""; var remoteOs = remoteConn?.OsVersion ?? "";
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken, remoteOs, modelId, webSearch); var mcpServerList = ClaudeCliService.ParseMcpServers(mcpServers);
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, mcpServerList, remoteOs, modelId, webSearch);
// Python-Verfügbarkeit prüfen und ggf. warnen // Python-Verfügbarkeit prüfen und ggf. warnen
if (FindPythonCommand() == null) if (FindPythonCommand() == null)
@@ -305,7 +305,7 @@ public partial class AnthropicService
ultracode: ultracode, ultracode: ultracode,
webSearch: webSearch, webSearch: webSearch,
mcpConfigPath: mcpConfigPath, mcpConfigPath: mcpConfigPath,
hasUnityMcp: !string.IsNullOrEmpty(unityMcpUrl), mcpServerNames: mcpServerList.Select(s => s.Name).ToList(),
effort: effort, effort: effort,
sessionId: sessionId, sessionId: sessionId,
chainId: chainId, chainId: chainId,
@@ -907,7 +907,7 @@ public partial class AnthropicService
/// Generiert eine temporäre MCP-Config-Datei für die Claude CLI. /// Generiert eine temporäre MCP-Config-Datei für die Claude CLI.
/// Enthält den MCP-Server für remote_execute. /// Enthält den MCP-Server für remote_execute.
/// </summary> /// </summary>
private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, string unityMcpUrl = null, string unityMcpToken = null, string remoteOs = null, string modelId = null, bool webSearch = false) private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, List<McpServerDef> userMcpServers = null, string remoteOs = null, string modelId = null, bool webSearch = false)
{ {
// Port aus Konfiguration oder Default // Port aus Konfiguration oder Default
var port = 19000; var port = 19000;
@@ -946,25 +946,30 @@ public partial class AnthropicService
if (pythonCmd == null) if (pythonCmd == null)
Console.WriteLine("[MCP] ⚠ Python nicht gefunden (python3/python/py) – MCP-Tools werden nicht funktionieren!"); Console.WriteLine("[MCP] ⚠ Python nicht gefunden (python3/python/py) – MCP-Tools werden nicht funktionieren!");
// Unity MCP Server (pro Session konfigurierbar) // Benutzerdefinierte MCP-Server (pro Session, URL-basiert: http/sse + optionaler Token).
// Claude CLI erfordert expliziten "type" – "http" für streamableHttp, "sse" für SSE // Claude CLI erfordert expliziten "type" – "http" für streamableHttp, "sse" für SSE.
if (!string.IsNullOrEmpty(unityMcpUrl)) // Namen sind in ParseMcpServers bereits gesäubert + von reservierten Namen befreit.
if (userMcpServers != null)
{ {
// URL-basierte Typ-Erkennung: /sse → SSE-Transport, sonst HTTP (streamableHttp) foreach (var srv in userMcpServers)
var mcpType = unityMcpUrl.TrimEnd('/').EndsWith("/sse", StringComparison.OrdinalIgnoreCase) ? "sse" : "http";
var unityConfig = new Dictionary<string, object>
{ {
["type"] = mcpType, if (srv == null || string.IsNullOrWhiteSpace(srv.Url)) continue;
["url"] = unityMcpUrl // URL-basierte Typ-Erkennung: /sse → SSE-Transport, sonst HTTP (streamableHttp)
}; var mcpType = srv.Url.TrimEnd('/').EndsWith("/sse", StringComparison.OrdinalIgnoreCase) ? "sse" : "http";
if (!string.IsNullOrEmpty(unityMcpToken)) var srvConfig = new Dictionary<string, object>
{
unityConfig["headers"] = new Dictionary<string, string>
{ {
["Authorization"] = $"Bearer {unityMcpToken}" ["type"] = mcpType,
["url"] = srv.Url
}; };
if (!string.IsNullOrEmpty(srv.Token))
{
srvConfig["headers"] = new Dictionary<string, string>
{
["Authorization"] = $"Bearer {srv.Token}"
};
}
mcpServers[srv.Name] = srvConfig;
} }
mcpServers["unity"] = unityConfig;
} }
// z.ai Web-Search-Prime-MCP: GLM-Modelle können Anthropics native WebSearch nicht nutzen. // z.ai Web-Search-Prime-MCP: GLM-Modelle können Anthropics native WebSearch nicht nutzen.
+1
View File
@@ -68,6 +68,7 @@ Nur deine finale Text-Antwort wird weiterverwendet. Formuliere sie vollständig
"sonnet" => "claude-sonnet-4-6", "sonnet" => "claude-sonnet-4-6",
"sonnet-4-5" => "claude-sonnet-4-5", "sonnet-4-5" => "claude-sonnet-4-5",
"sonnet-4-6" => "claude-sonnet-4-6", "sonnet-4-6" => "claude-sonnet-4-6",
"sonnet-5" => "claude-sonnet-5",
"opus" => "claude-opus-4-8", "opus" => "claude-opus-4-8",
"opus-4-5" => "claude-opus-4-5", "opus-4-5" => "claude-opus-4-5",
"opus-4-6" => "claude-opus-4-6", "opus-4-6" => "claude-opus-4-6",
+75 -5
View File
@@ -11,6 +11,16 @@ using Microsoft.Extensions.Logging;
namespace Gott.Services; namespace Gott.Services;
/// <summary>
/// Benutzerdefinierter MCP-Server (pro Session). URL-basiert (http/sse), optionaler Bearer-Token.
/// </summary>
public class McpServerDef
{
public string Name { get; set; }
public string Url { get; set; }
public string Token { get; set; }
}
/// <summary> /// <summary>
/// Service für die Kommunikation mit der Claude Code CLI. /// Service für die Kommunikation mit der Claude Code CLI.
/// Verwendet die CLI als Backend für OAuth-basierte Chats (kein API-Key nötig). /// Verwendet die CLI als Backend für OAuth-basierte Chats (kein API-Key nötig).
@@ -238,6 +248,61 @@ public class ClaudeCliService
return null; return null;
} }
/// <summary>
/// Reservierte MCP-Server-Namen (gott-intern) – dürfen von benutzerdefinierten Servern
/// nicht überschrieben werden, sonst würde die interne Config/Allowlist kollidieren.
/// </summary>
private static readonly HashSet<string> _reservedMcpNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"gott-remote", "web-search-prime"
};
/// <summary>
/// Parst die in der Session hinterlegte MCP-Server-Liste (JSON-Array von {name,url,token}).
/// Säubert die Namen (glob-/config-tauglich), filtert reservierte Namen, Duplikate und
/// Einträge ohne URL. Wirft nie – ungültige Eingaben ergeben eine leere Liste.
/// </summary>
public static List<McpServerDef> ParseMcpServers(string json)
{
var result = new List<McpServerDef>();
if (string.IsNullOrWhiteSpace(json)) return result;
try
{
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.ValueKind != JsonValueKind.Array) return result;
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var el in doc.RootElement.EnumerateArray())
{
if (el.ValueKind != JsonValueKind.Object) continue;
var rawName = el.TryGetProperty("name", out var n) && n.ValueKind == JsonValueKind.String ? n.GetString() : null;
var url = el.TryGetProperty("url", out var u) && u.ValueKind == JsonValueKind.String ? u.GetString() : null;
var token = el.TryGetProperty("token", out var t) && t.ValueKind == JsonValueKind.String ? t.GetString() : null;
var name = SanitizeMcpName(rawName);
if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(url)) continue;
if (_reservedMcpNames.Contains(name)) continue;
if (!seen.Add(name)) continue; // doppelter Name → ignorieren
result.Add(new McpServerDef { Name = name, Url = url.Trim(), Token = string.IsNullOrWhiteSpace(token) ? null : token.Trim() });
}
}
catch { /* ungültiges JSON → leere Liste */ }
return result;
}
/// <summary>
/// Reduziert einen MCP-Server-Namen auf glob-/config-taugliche Zeichen (Buchstaben, Ziffern,
/// '_' und '-'). Andere Zeichen (inkl. Leerzeichen) werden entfernt.
/// </summary>
private static string SanitizeMcpName(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return null;
var sb = new StringBuilder();
foreach (var c in raw.Trim())
{
if (char.IsLetterOrDigit(c) || c == '_' || c == '-') sb.Append(c);
}
return sb.Length > 0 ? sb.ToString() : null;
}
/// <summary> /// <summary>
/// Streaming-Chat über die Claude CLI mit Event-basiertem Callback. /// Streaming-Chat über die Claude CLI mit Event-basiertem Callback.
/// Unterstützt Text-Streaming und Tool-Use Events. /// Unterstützt Text-Streaming und Tool-Use Events.
@@ -253,7 +318,7 @@ public class ClaudeCliService
bool ultracode = false, bool ultracode = false,
bool webSearch = false, bool webSearch = false,
string mcpConfigPath = null, string mcpConfigPath = null,
bool hasUnityMcp = false, List<string> mcpServerNames = null,
string effort = null, string effort = null,
string sessionId = null, string sessionId = null,
string chainId = null, string chainId = null,
@@ -344,11 +409,16 @@ public class ClaudeCliService
tools.Add("mcp__gott-remote__memory_write"); tools.Add("mcp__gott-remote__memory_write");
tools.Add("mcp__gott-remote__memory_list"); tools.Add("mcp__gott-remote__memory_list");
} }
// Unity MCP Tools erlauben (alle Tools vom unity MCP-Server) // Benutzerdefinierte MCP-Server: alle Tools je Server erlauben.
if (hasUnityMcp) // Glob mcp__*<name>* ist bewusst locker (fängt Casing-/Präfix-Varianten ab,
// z.B. Servername "unity" vs. tatsächlicher Tool-Präfix "UnityMCP").
if (mcpServerNames != null)
{ {
tools.Add("mcp__unity__*"); foreach (var mcpName in mcpServerNames)
tools.Add("mcp__UnityMCP__*"); {
if (!string.IsNullOrWhiteSpace(mcpName))
tools.Add($"mcp__*{mcpName}*");
}
} }
args.Add("--allowed-tools"); args.Add("--allowed-tools");
args.Add(tools.Count > 0 ? string.Join(" ", tools) : "\"\""); args.Add(tools.Count > 0 ? string.Join(" ", tools) : "\"\"");
+1
View File
@@ -163,6 +163,7 @@ public partial class DatabaseService
if (body.TryGetProperty("moaLayers", out var mol) && mol.ValueKind == System.Text.Json.JsonValueKind.Number) { sets.Add("moa_layers = @moaLayers"); p.Add("moaLayers", mol.GetInt32()); } if (body.TryGetProperty("moaLayers", out var mol) && mol.ValueKind == System.Text.Json.JsonValueKind.Number) { sets.Add("moa_layers = @moaLayers"); p.Add("moaLayers", mol.GetInt32()); }
if (body.TryGetProperty("unityMcpUrl", out var umu)) { sets.Add("unity_mcp_url = @unityMcpUrl"); p.Add("unityMcpUrl", umu.ValueKind == System.Text.Json.JsonValueKind.Null ? null : umu.GetString()); } if (body.TryGetProperty("unityMcpUrl", out var umu)) { sets.Add("unity_mcp_url = @unityMcpUrl"); p.Add("unityMcpUrl", umu.ValueKind == System.Text.Json.JsonValueKind.Null ? null : umu.GetString()); }
if (body.TryGetProperty("unityMcpToken", out var umt)) { sets.Add("unity_mcp_token = @unityMcpToken"); p.Add("unityMcpToken", umt.ValueKind == System.Text.Json.JsonValueKind.Null ? null : umt.GetString()); } if (body.TryGetProperty("unityMcpToken", out var umt)) { sets.Add("unity_mcp_token = @unityMcpToken"); p.Add("unityMcpToken", umt.ValueKind == System.Text.Json.JsonValueKind.Null ? null : umt.GetString()); }
if (body.TryGetProperty("mcpServers", out var mcps)) { sets.Add("mcp_servers = @mcpServers"); p.Add("mcpServers", mcps.ValueKind == System.Text.Json.JsonValueKind.Null ? null : mcps.GetRawText()); }
await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p); await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
} }
+1
View File
@@ -196,6 +196,7 @@ public partial class DatabaseService
TryAddColumn(conn, "sessions", "preparer_agent_id", "TEXT"); TryAddColumn(conn, "sessions", "preparer_agent_id", "TEXT");
TryAddColumn(conn, "sessions", "unity_mcp_url", "TEXT"); TryAddColumn(conn, "sessions", "unity_mcp_url", "TEXT");
TryAddColumn(conn, "sessions", "unity_mcp_token", "TEXT"); TryAddColumn(conn, "sessions", "unity_mcp_token", "TEXT");
TryAddColumn(conn, "sessions", "mcp_servers", "TEXT");
TryAddColumn(conn, "messages", "tag", "TEXT"); TryAddColumn(conn, "messages", "tag", "TEXT");
TryAddColumn(conn, "agents", "effort", "TEXT NOT NULL DEFAULT ''"); TryAddColumn(conn, "agents", "effort", "TEXT NOT NULL DEFAULT ''");
TryAddColumn(conn, "chains", "effort", "TEXT NOT NULL DEFAULT ''"); TryAddColumn(conn, "chains", "effort", "TEXT NOT NULL DEFAULT ''");
+51
View File
@@ -10,6 +10,17 @@
margin-top: var(--box-spacing); margin-top: var(--box-spacing);
} }
.form-label-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: var(--box-spacing);
}
.form-label-row label {
margin-top: 0;
}
.more { .more {
margin: var(--box-spacing) auto; margin: var(--box-spacing) auto;
cursor: pointer; cursor: pointer;
@@ -214,6 +225,46 @@
align-self: flex-start; align-self: flex-start;
} }
.mcp-servers {
display: flex;
flex-direction: column;
gap: var(--badge-spacing);
}
.mcp-server-item {
display: flex;
}
.mcp-server-fields {
flex: 1;
display: flex;
gap: var(--badge-spacing);
overflow: hidden;
}
.mcp-server-fields .mcp-name {
flex: 2;
}
.mcp-server-fields .mcp-url {
flex: 4;
}
.mcp-server-fields .mcp-token {
flex: 3;
}
.mcp-server-item .remove-mcp-btn {
margin-left: var(--badge-spacing);
color: var(--dangerous);
align-self: center;
}
.mcp-server-item .remove-mcp-btn:hover {
background: var(--dangerous);
color: var(--f1-bg);
}
/* ── Checkbox ── */ /* ── Checkbox ── */
label.checkbox-label { label.checkbox-label {
+10 -5
View File
@@ -172,10 +172,10 @@
<label>Benutzerdefinierte Befehle</label> <label>Benutzerdefinierte Befehle</label>
<div class="custom-commands" id="customCommandsList"></div> <div class="custom-commands" id="customCommandsList"></div>
<button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button> <button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
<label>Unity MCP URL <small>(optional)</small></label> <label>MCP-Server <small>(optional, URL-basiert)</small></label>
<input type="text" id="sessionUnityMcpUrl" placeholder="z.B. http://localhost:8080" autocomplete="off"> <div class="mcp-servers" id="mcpServersList"></div>
<label>Unity MCP Token <small>(optional)</small></label> <button type="button" id="addMcpServerBtn" class="add-cmd-btn">+ MCP-Server hinzufügen</button>
<input type="password" id="sessionUnityMcpToken" placeholder="MCP-Server Token" autocomplete="off"> <small class="form-hint">Pro Eintrag: Name, URL (http/sse) und optionaler Token. Tools werden headless als <code>mcp__*&lt;name&gt;*</code> freigegeben.</small>
<label>Telegram Bot</label> <label>Telegram Bot</label>
<div id="telegramBotSelectCard" class="agent-select-card"> <div id="telegramBotSelectCard" class="agent-select-card">
<div class="agent-select-empty"><i class="fa fa-paper-plane"></i> Kein Bot zugewiesen</div> <div class="agent-select-empty"><i class="fa fa-paper-plane"></i> Kein Bot zugewiesen</div>
@@ -218,10 +218,14 @@
<div class="avatar-preview" id="avatarPreview">?</div> <div class="avatar-preview" id="avatarPreview">?</div>
<label>Name</label> <label>Name</label>
<input type="text" id="agentName" placeholder="Name des Agents"> <input type="text" id="agentName" placeholder="Name des Agents">
<label>Model</label> <div class="form-label-row">
<label>Model</label>
<button type="button" id="refreshModelsBtn" class="icon-btn" title="Modell-Listen der Provider neu laden"><i class="fa fa-refresh"></i></button>
</div>
<select id="agentModel"> <select id="agentModel">
<option value="sonnet-4-5">sonnet-4-5</option> <option value="sonnet-4-5">sonnet-4-5</option>
<option value="sonnet-4-6">sonnet-4-6</option> <option value="sonnet-4-6">sonnet-4-6</option>
<option value="sonnet-5">sonnet-5</option>
<option value="opus-4-5">opus-4-5</option> <option value="opus-4-5">opus-4-5</option>
<option value="opus-4-6">opus-4-6</option> <option value="opus-4-6">opus-4-6</option>
<option value="opus-4-7">opus-4-7</option> <option value="opus-4-7">opus-4-7</option>
@@ -818,6 +822,7 @@
<script src="js/archive.js"></script> <script src="js/archive.js"></script>
<script src="js/router.js"></script> <script src="js/router.js"></script>
<script src="js/commands-ui.js"></script> <script src="js/commands-ui.js"></script>
<script src="js/mcp-servers-ui.js"></script>
<script src="js/avatar-editor.js"></script> <script src="js/avatar-editor.js"></script>
<script src="js/pages/dashboard.js"></script> <script src="js/pages/dashboard.js"></script>
<script src="js/pages/cronjob-page.js"></script> <script src="js/pages/cronjob-page.js"></script>
+3
View File
@@ -252,6 +252,9 @@ function apiDeleteApiKey(id) { return _del('/api/admin/api-keys/' + id, 'Fehler
function apiCheckApiKey(id) { return _post('/api/admin/api-keys/' + id + '/check', undefined, 'Fehler beim Prüfen'); } function apiCheckApiKey(id) { return _post('/api/admin/api-keys/' + id + '/check', undefined, 'Fehler beim Prüfen'); }
function apiCheckAllApiKeys() { return _post('/api/admin/api-keys/check-all', undefined, 'Fehler beim Prüfen'); } function apiCheckAllApiKeys() { return _post('/api/admin/api-keys/check-all', undefined, 'Fehler beim Prüfen'); }
// ── Models API ──
function apiRefreshModels() { return _post('/api/models/refresh', undefined, 'Fehler beim Neu-Laden der Modelle'); }
// ── Key Scanner API (Admin) ── // ── Key Scanner API (Admin) ──
function apiTriggerScan(mode) { return _post('/api/admin/api-keys/scan?mode=' + (mode || 'full'), undefined, 'Fehler beim Starten des Scans'); } function apiTriggerScan(mode) { return _post('/api/admin/api-keys/scan?mode=' + (mode || 'full'), undefined, 'Fehler beim Starten des Scans'); }
+48
View File
@@ -0,0 +1,48 @@
// ── MCP-Server UI (Liste pro Session) ──
// Spiegelt das Muster von commands-ui.js: dynamische Add/Remove-Zeilen, JSON-Persistenz.
// Pro Eintrag: Name, URL (http/sse), optionaler Token.
function createMcpServerRow(container, name, url, token) {
name = name || '';
url = url || '';
token = token || '';
const item = document.createElement('div');
item.className = 'mcp-server-item';
item.innerHTML = `
<div class="mcp-server-fields">
<input type="text" class="mcp-name" placeholder="Name (z.B. UnityMCP)" value="${esc(name)}" autocomplete="off">
<input type="text" class="mcp-url" placeholder="URL (z.B. http://localhost:8080)" value="${esc(url)}" autocomplete="off">
<input type="password" class="mcp-token" placeholder="Token (optional)" value="${esc(token)}" autocomplete="off">
</div>
<button type="button" class="remove-mcp-btn icon-btn"><i class="fa fa-times"></i></button>
`;
item.querySelector('.remove-mcp-btn').addEventListener('click', function() { item.remove(); });
container.appendChild(item);
}
function getMcpServersFromUI(container) {
var items = container.querySelectorAll('.mcp-server-item');
var servers = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
var name = item.querySelector('.mcp-name').value.trim();
var url = item.querySelector('.mcp-url').value.trim();
var token = item.querySelector('.mcp-token').value.trim();
// Nur vollständige Einträge (Name + URL) übernehmen.
if (name && url) servers.push({ name: name, url: url, token: token || null });
}
return servers.length > 0 ? servers : null;
}
function loadMcpServersIntoUI(container, mcpServers) {
if (!mcpServers) return;
try {
var list = typeof mcpServers === 'string' ? JSON.parse(mcpServers) : mcpServers;
if (!Array.isArray(list)) return;
for (var i = 0; i < list.length; i++) {
var s = list[i];
createMcpServerRow(container, s.name || '', s.url || '', s.token || '');
}
} catch (e) { /* ignore parse errors */ }
}
+32 -13
View File
@@ -23,12 +23,6 @@ registerPage('agent', {
const nameEl = document.getElementById('agentName'); const nameEl = document.getElementById('agentName');
const descEl = document.getElementById('agentDescription'); const descEl = document.getElementById('agentDescription');
const modelEl = document.getElementById('agentModel'); const modelEl = document.getElementById('agentModel');
const nvidiaOptgroup = document.getElementById('nvidiaOptgroup');
const pollinationsOptgroup = document.getElementById('pollinationsOptgroup');
const geminiOptgroup = document.getElementById('geminiOptgroup');
const openaiOptgroup = document.getElementById('openaiOptgroup');
const minimaxOptgroup = document.getElementById('minimaxOptgroup');
const glmOptgroup = document.getElementById('glmOptgroup');
// Modelle dynamisch laden // Modelle dynamisch laden
const loadProviderModels = async (url, prefix, optgroup) => { const loadProviderModels = async (url, prefix, optgroup) => {
@@ -51,14 +45,39 @@ registerPage('agent', {
} }
}; };
await Promise.all([ // Optgroups bei jedem Aufruf frisch abfragen (nach dem ersten Laden können leere entfernt worden sein)
loadProviderModels('/api/models/openai', 'openai', openaiOptgroup), const loadAllModels = () => Promise.all([
loadProviderModels('/api/models/nvidia', 'nvidia', nvidiaOptgroup), loadProviderModels('/api/models/openai', 'openai', document.getElementById('openaiOptgroup')),
loadProviderModels('/api/models/pollinations', 'pollinations', pollinationsOptgroup), loadProviderModels('/api/models/nvidia', 'nvidia', document.getElementById('nvidiaOptgroup')),
loadProviderModels('/api/models/gemini', 'gemini', geminiOptgroup), loadProviderModels('/api/models/pollinations', 'pollinations', document.getElementById('pollinationsOptgroup')),
loadProviderModels('/api/models/minimax', 'minimax', minimaxOptgroup), loadProviderModels('/api/models/gemini', 'gemini', document.getElementById('geminiOptgroup')),
loadProviderModels('/api/models/glm', 'glm', glmOptgroup) loadProviderModels('/api/models/minimax', 'minimax', document.getElementById('minimaxOptgroup')),
loadProviderModels('/api/models/glm', 'glm', document.getElementById('glmOptgroup'))
]); ]);
await loadAllModels();
// Refresh-Button: Modelle live von den Providern neu laden (Backend erneuert seinen Cache)
const refreshModelsBtn = document.getElementById('refreshModelsBtn');
if (refreshModelsBtn) {
refreshModelsBtn.addEventListener('click', async () => {
const origHtml = refreshModelsBtn.innerHTML;
refreshModelsBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
refreshModelsBtn.disabled = true;
const prevValue = modelEl.value;
try {
await apiRefreshModels();
await loadAllModels();
modelEl.value = prevValue; // vorherige Auswahl wiederherstellen (falls noch vorhanden)
showToast('Modell-Listen neu geladen');
} catch (err) {
showToast(err.message || 'Fehler beim Neu-Laden der Modelle', 'error');
} finally {
refreshModelsBtn.innerHTML = origHtml;
refreshModelsBtn.disabled = false;
}
});
}
const allowUserChoiceEl = document.getElementById('agentAllowUserChoice'); const allowUserChoiceEl = document.getElementById('agentAllowUserChoice');
const autoCommitEl = document.getElementById('agentAutoCommit'); const autoCommitEl = document.getElementById('agentAutoCommit');
const effortEl = document.getElementById('agentEffort'); const effortEl = document.getElementById('agentEffort');
+8 -7
View File
@@ -38,8 +38,8 @@ registerPage('session', {
const deleteBtn = document.getElementById('sessionDeleteBtn'); const deleteBtn = document.getElementById('sessionDeleteBtn');
const commandsList = document.getElementById('customCommandsList'); const commandsList = document.getElementById('customCommandsList');
const addCommandBtn = document.getElementById('addCommandBtn'); const addCommandBtn = document.getElementById('addCommandBtn');
const unityMcpUrlEl = document.getElementById('sessionUnityMcpUrl'); const mcpServersList = document.getElementById('mcpServersList');
const unityMcpTokenEl = document.getElementById('sessionUnityMcpToken'); const addMcpServerBtn = document.getElementById('addMcpServerBtn');
const ownerRow = document.getElementById('sessionOwnerRow'); const ownerRow = document.getElementById('sessionOwnerRow');
const userSelectCard = document.getElementById('sessionUserSelectCard'); const userSelectCard = document.getElementById('sessionUserSelectCard');
const userSelect = document.getElementById('sessionUserSelect'); const userSelect = document.getElementById('sessionUserSelect');
@@ -64,9 +64,8 @@ registerPage('session', {
// Custom Commands laden // Custom Commands laden
loadCommandsIntoUI(commandsList, session.customCommands); loadCommandsIntoUI(commandsList, session.customCommands);
// Unity MCP // MCP-Server
unityMcpUrlEl.value = session.unityMcpUrl || ''; loadMcpServersIntoUI(mcpServersList, session.mcpServers);
unityMcpTokenEl.value = session.unityMcpToken || '';
} else { } else {
titleEl.textContent = 'Neue Session'; titleEl.textContent = 'Neue Session';
deleteBtn.hidden = true; deleteBtn.hidden = true;
@@ -81,6 +80,9 @@ registerPage('session', {
// Befehl hinzufügen // Befehl hinzufügen
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList)); addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
// MCP-Server hinzufügen
addMcpServerBtn.addEventListener('click', () => createMcpServerRow(mcpServersList));
// Telegram-Section nur für existierende Sessions // Telegram-Section nur für existierende Sessions
if (!isNew) { if (!isNew) {
initSessionTelegram(sessionId, session); initSessionTelegram(sessionId, session);
@@ -217,8 +219,7 @@ registerPage('session', {
avatarScale: _sessionAvatarEditor.getScale(), avatarScale: _sessionAvatarEditor.getScale(),
avatarOffsetX: _sessionAvatarEditor.getOffsetX(), avatarOffsetX: _sessionAvatarEditor.getOffsetX(),
avatarOffsetY: _sessionAvatarEditor.getOffsetY(), avatarOffsetY: _sessionAvatarEditor.getOffsetY(),
unityMcpUrl: unityMcpUrlEl.value.trim() || null, mcpServers: getMcpServersFromUI(mcpServersList)
unityMcpToken: unityMcpTokenEl.value.trim() || null
}; };
// Session einem anderen Benutzer zuweisen. Wechselt der Besitzer tatsächlich, // Session einem anderen Benutzer zuweisen. Wechselt der Besitzer tatsächlich,