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:
1 parent
1032cd5c63
commit
5f7dec6448
14 files changed
+257
-50
No files matched your search
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) : "\"\"");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ''");
|
||||||
|
|||||||
@@ -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
@@ -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__*<name>*</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>
|
||||||
|
|||||||
@@ -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'); }
|
||||||
|
|||||||
@@ -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 */ }
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user