diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs
index 5eddd2d..8261edc 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
// https://github.com/gnh1201/welsonjs
-//
+//
using System;
using System.Collections;
using System.Collections.Generic;
@@ -16,6 +16,10 @@ namespace WelsonJS.Launcher
private readonly JsCore _core;
private readonly bool _ownsCore;
+ // In-engine parsed document store management
+ private bool _storeReady;
+ private int _nextId = 1; // 0 is reserved (unused)
+
public JsSerializer() : this(new JsCore(), true) { }
public JsSerializer(JsCore core, bool ownsCore)
@@ -25,6 +29,100 @@ namespace WelsonJS.Launcher
_ownsCore = ownsCore;
}
+ // ---------------- Engine-backed document store ----------------
+
+ ///
+ /// Parses JSON once and stores it in the engine under a numeric id.
+ /// Returns the id which can be used for fast repeated extraction.
+ ///
+ public int Load(string json)
+ {
+ if (json == null) throw new ArgumentNullException("json");
+ EnsureStore();
+
+ int id = _nextId++;
+ // Create slot and parse
+ // Using Object.create(null) for a clean dictionary without prototype.
+ var sb = new StringBuilder();
+ sb.Append("(function(){var S=globalThis.__WJ_STORE;");
+ sb.Append("S[").Append(id.ToString(CultureInfo.InvariantCulture)).Append("]=JSON.parse(").Append(Q(json)).Append(");");
+ sb.Append("return '1';})()");
+ string r = _core.EvaluateToString(sb.ToString());
+ if (r != "1") throw new InvalidOperationException("Failed to load JSON into the engine store.");
+ return id;
+ }
+
+ ///
+ /// Removes a previously loaded document from the engine store.
+ /// After this, the id becomes invalid.
+ ///
+ public void Unload(int id)
+ {
+ EnsureStore();
+ string script = "(function(){var S=globalThis.__WJ_STORE; delete S[" + id.ToString(CultureInfo.InvariantCulture) + "]; return '1';})()";
+ _core.EvaluateToString(script);
+ }
+
+ ///
+ /// Replaces the stored JSON at a given id (parse once, reuse later).
+ ///
+ public void Reload(int id, string json)
+ {
+ if (json == null) throw new ArgumentNullException("json");
+ EnsureStore();
+ string script = "(function(){var S=globalThis.__WJ_STORE; S[" + id.ToString(CultureInfo.InvariantCulture) + "]=JSON.parse(" + Q(json) + "); return '1';})()";
+ _core.EvaluateToString(script);
+ }
+
+ ///
+ /// Stringifies the stored value identified by id (no reparse).
+ ///
+ public string ToJson(int id, int space)
+ {
+ EnsureStore();
+ space = Clamp(space, 0, 10);
+ string script = "(function(){var v=globalThis.__WJ_STORE[" + id.ToString(CultureInfo.InvariantCulture) + "]; return JSON.stringify(v,null," + space.ToString(CultureInfo.InvariantCulture) + ");})()";
+ return _core.EvaluateToString(script);
+ }
+
+ ///
+ /// Extracts from a stored document identified by id (no reparse).
+ /// Returns JSON of the selected value.
+ ///
+ public string ExtractFrom(int id, params object[] path)
+ {
+ EnsureStore();
+ if (path == null) path = new object[0];
+ string jsPath = BuildJsPath(path);
+
+ var sb = new StringBuilder();
+ sb.Append("(function(){var v=globalThis.__WJ_STORE[")
+ .Append(id.ToString(CultureInfo.InvariantCulture))
+ .Append("];var p=").Append(jsPath).Append(";");
+ sb.Append("for(var i=0;i
- /// Extracts a value by a simple path of property names (numeric segment as string = array index).
- /// Returns the selected value as a JSON string.
+ /// Extracts by string-only path (kept for backward compatibility).
+ /// Internally forwards to the mixed-path overload.
///
public string Extract(string json, params string[] path)
{
@@ -67,9 +165,8 @@ namespace WelsonJS.Launcher
}
///
- /// Extracts by a mixed path. Segments can be strings (object keys) or integers (array indices).
- /// Returns the selected value as a JSON string (e.g., a JS string returns with quotes).
- /// Usage: Extract(json, "items", 0, "name")
+ /// Extracts by a mixed path directly from a JSON string (parses every call).
+ /// Prefer Load(...) + ExtractFrom(...) to avoid repeated parsing.
///
public string Extract(string json, params object[] path)
{
@@ -96,6 +193,8 @@ namespace WelsonJS.Launcher
return _core.EvaluateToString(script);
}
+ // ---------------- Helpers ----------------
+
private static int Clamp(int v, int min, int max)
{
if (v < min) return min;
@@ -292,4 +391,4 @@ namespace WelsonJS.Launcher
}
}
}
-}
\ No newline at end of file
+}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs
index be77132..e9830b0 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs
@@ -67,6 +67,7 @@ namespace WelsonJS.Launcher
_tools.Add(new ResourceTools.IpQuery(this, _httpClient, _logger));
_tools.Add(new ResourceTools.TwoFactorAuth(this, _httpClient, _logger));
_tools.Add(new ResourceTools.Whois(this, _httpClient, _logger));
+ _tools.Add(new ResourceTools.ImageColorPicker(this, _httpClient, _logger));
// Register the prefix
_listener.Prefixes.Add(prefix);
@@ -132,14 +133,14 @@ namespace WelsonJS.Launcher
// Serve from a resource name
if (String.IsNullOrEmpty(path))
{
- ServeResource(context, GetResource(_resourceName), "text/html");
+ await ServeResource(context, GetResource(_resourceName), "text/html");
return;
}
// Serve the favicon.ico file
if ("favicon.ico".Equals(path, StringComparison.OrdinalIgnoreCase))
{
- ServeResource(context, GetResource("favicon"), "image/x-icon");
+ await ServeResource(context, GetResource("favicon"), "image/x-icon");
return;
}
@@ -157,7 +158,7 @@ namespace WelsonJS.Launcher
if (await ServeBlob(context, path)) return;
// Fallback to serve from a resource name
- ServeResource(context, GetResource(_resourceName), "text/html");
+ await ServeResource(context, GetResource(_resourceName), "text/html");
}
private async Task ServeBlob(HttpListenerContext context, string path, string prefix = null)
@@ -188,7 +189,7 @@ namespace WelsonJS.Launcher
data = await response.Content.ReadAsByteArrayAsync();
mimeType = response.Content.Headers.ContentType?.MediaType ?? _defaultMimeType;
- ServeResource(context, data, mimeType);
+ await ServeResource(context, data, mimeType);
_ = TrySaveCachedBlob(path, data, mimeType);
return true;
@@ -211,7 +212,7 @@ namespace WelsonJS.Launcher
mimeType = _defaultMimeType;
}
- ServeResource(context, data, mimeType);
+ await ServeResource(context, data, mimeType);
return true;
}
}
@@ -345,12 +346,12 @@ namespace WelsonJS.Launcher
}
}
- public void ServeResource(HttpListenerContext context)
+ public async Task ServeResource(HttpListenerContext context)
{
- ServeResource(context, "Not Found", "application/xml", 404);
+ await ServeResource(context, "Not Found", "application/xml", 404);
}
- public void ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html", int statusCode = 200)
+ public async Task ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html", int statusCode = 200)
{
string xmlHeader = "";
@@ -364,13 +365,10 @@ namespace WelsonJS.Launcher
context.Response.StatusCode = statusCode;
context.Response.ContentType = mimeType;
context.Response.ContentLength64 = data.Length;
- using (Stream outputStream = context.Response.OutputStream)
- {
- outputStream.Write(data, 0, data.Length);
- }
+ await context.Response.OutputStream.WriteAsync(data, 0, data.Length);
}
- public void ServeResource(HttpListenerContext context, string data, string mimeType = "text/html", int statusCode = 200)
+ public async Task ServeResource(HttpListenerContext context, string data, string mimeType = "text/html", int statusCode = 200)
{
string xmlHeader = "";
@@ -385,7 +383,7 @@ namespace WelsonJS.Launcher
data = xmlHeader + "\r\n" + data;
}
- ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode);
+ await ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode);
}
private byte[] GetResource(string resourceName)
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs
index d56c5ec..b06368e 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs
@@ -47,11 +47,11 @@ namespace WelsonJS.Launcher.ResourceTools
string baseUrl = Program.GetAppConfig("ChromiumDevToolsPrefix"); // e.g., http://localhost:9222/
string url = baseUrl.TrimEnd('/') + "/" + endpoint;
string data = await _httpClient.GetStringAsync(url);
- Server.ServeResource(context, data, "application/json");
+ await Server.ServeResource(context, data, "application/json");
}
catch (Exception ex)
{
- Server.ServeResource(context, $"Failed to process DevTools request. {EscapeXml(ex.Message)}", "application/xml", 500);
+ await Server.ServeResource(context, $"Failed to process DevTools request. {EscapeXml(ex.Message)}", "application/xml", 500);
}
return;
}
@@ -62,7 +62,7 @@ namespace WelsonJS.Launcher.ResourceTools
string baseUrl = Program.GetAppConfig("ChromiumDevToolsPrefix");
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out Uri uri))
{
- Server.ServeResource(context, "Invalid ChromiumDevToolsPrefix", "application/xml", 500);
+ await Server.ServeResource(context, "Invalid ChromiumDevToolsPrefix", "application/xml", 500);
return;
}
@@ -97,17 +97,17 @@ namespace WelsonJS.Launcher.ResourceTools
try
{
string response = await _wsManager.SendAndReceiveAsync(hostname, port, wsPath, postBody, timeout);
- Server.ServeResource(context, response, "application/json", 200);
+ await Server.ServeResource(context, response, "application/json", 200);
}
catch (Exception ex)
{
_wsManager.Remove(hostname, port, wsPath);
- Server.ServeResource(context, $"WebSocket communication error: {EscapeXml(ex.Message)}", "application/xml", 500);
+ await Server.ServeResource(context, $"WebSocket communication error: {EscapeXml(ex.Message)}", "application/xml", 500);
}
return;
}
- Server.ServeResource(context, "Invalid DevTools endpoint", "application/xml", 404);
+ await Server.ServeResource(context, "Invalid DevTools endpoint", "application/xml", 404);
}
private string EscapeXml(string text)
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs
index 34abe58..ea11b3b 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs
@@ -72,11 +72,11 @@ namespace WelsonJS.Launcher.ResourceTools
))
);
- Server.ServeResource(context, response.ToString(), "application/xml");
+ await Server.ServeResource(context, response.ToString(), "application/xml");
}
catch (Exception ex)
{
- Server.ServeResource(context, $"Failed to try completion. {ex.Message}", "application/xml", 500);
+ await Server.ServeResource(context, $"Failed to try completion. {ex.Message}", "application/xml", 500);
}
}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs
index 831294b..1522b75 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs
@@ -42,13 +42,11 @@ namespace WelsonJS.Launcher.ResourceTools
public async Task HandleAsync(HttpListenerContext context, string path)
{
- await Task.Delay(0);
-
string query = path.Substring(Prefix.Length);
if (string.IsNullOrWhiteSpace(query) || query.Length > 255)
{
- Server.ServeResource(context, "Invalid query parameter", "application/xml", 400);
+ await Server.ServeResource(context, "Invalid query parameter", "application/xml", 400);
return;
}
@@ -67,11 +65,11 @@ namespace WelsonJS.Launcher.ResourceTools
}
string data = result.ToString();
- Server.ServeResource(context, data, "text/plain", 200);
+ await Server.ServeResource(context, data, "text/plain", 200);
}
catch (Exception ex)
{
- Server.ServeResource(context, $"Failed to process DNS query. {ex.Message}", "application/xml", 500);
+ await Server.ServeResource(context, $"Failed to process DNS query. {ex.Message}", "application/xml", 500);
}
}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ImageColorPicker.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ImageColorPicker.cs
new file mode 100644
index 0000000..bfff778
--- /dev/null
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ImageColorPicker.cs
@@ -0,0 +1,343 @@
+// ImageColorPicker.cs
+// SPDX-License-Identifier: GPL-3.0-or-later
+// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
+// https://github.com/gnh1201/welsonjs
+//
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace WelsonJS.Launcher.ResourceTools
+{
+ ///
+ /// POST image-color-picker/ with a unified JSON body:
+ /// { "image": "", "point": { "x": 10, "y": 20 } }
+ ///
+ /// Response (success):
+ /// { "ok": true, "hex": "#RRGGBB", "name": "white", "rgb": { "r": 255, "g": 255, "b": 255 } }
+ ///
+ /// Response (error):
+ /// { "ok": false, "error": "..." }
+ ///
+ /// JSON handling:
+ /// - Request parse: JsSerializer.Load/ExtractFrom (engine) + tiny local decoders for literals.
+ /// - Response encode: Dictionary → JsSerializer.Serialize(...).
+ ///
+ public class ImageColorPicker : IResourceTool
+ {
+ private readonly ResourceServer Server;
+ private readonly HttpClient _httpClient;
+ private readonly ICompatibleLogger _logger;
+ private const string Prefix = "image-color-picker";
+
+ public ImageColorPicker(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger)
+ {
+ Server = server;
+
+ _httpClient = httpClient;
+ _logger = logger;
+ }
+
+ public bool CanHandle(string path)
+ {
+ return path.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public async Task HandleAsync(HttpListenerContext context, string path)
+ {
+ context.Response.ContentType = "application/json; charset=utf-8";
+
+ try
+ {
+ if (!string.Equals(context.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = "Only POST is supported."
+ });
+ return;
+ }
+
+ // ---- Read request body ---------------------------------------------------------
+ string body;
+ using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding ?? Encoding.UTF8))
+ body = await reader.ReadToEndAsync();
+
+ if (string.IsNullOrWhiteSpace(body))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = "Empty request body."
+ });
+ return;
+ }
+
+ // ---- Extract strictly in unified format ---------------------------------------
+ string imageJson, xJson, yJson;
+ using (var ser = new JsSerializer())
+ {
+ int id = ser.Load(body);
+
+ // Required: "image" as JSON string
+ imageJson = ser.ExtractFrom(id, "image");
+
+ // Required: "point": { "x": , "y": }
+ xJson = ser.ExtractFrom(id, "point", "x");
+ yJson = ser.ExtractFrom(id, "point", "y");
+
+ ser.Unload(id);
+ }
+
+ if (string.IsNullOrEmpty(imageJson))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = "Missing required field: 'image' (base64 string)."
+ });
+ return;
+ }
+ if (string.IsNullOrEmpty(xJson) || string.IsNullOrEmpty(yJson))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = "Missing required field: 'point' with numeric 'x' and 'y'."
+ });
+ return;
+ }
+
+ // ---- Convert JSON literals → CLR ----------------------------------------------
+ string imageB64;
+ if (!TryParseJsonString(imageJson, out imageB64) || string.IsNullOrWhiteSpace(imageB64))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = "'image' must be a non-empty JSON string."
+ });
+ return;
+ }
+
+ if (!TryParseJsonInt(xJson, out var x) || !TryParseJsonInt(yJson, out var y))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = "'point.x' and 'point.y' must be JSON numbers."
+ });
+ return;
+ }
+
+ // Allow data URL prefix (data:*;base64,....)
+ imageB64 = StripDataUrlPrefix(imageB64);
+
+ byte[] bytes;
+ try { bytes = Convert.FromBase64String(imageB64); }
+ catch
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = "Invalid base64 image data."
+ });
+ return;
+ }
+
+ // ---- Decode image and sample pixel --------------------------------------------
+ using (var ms = new MemoryStream(bytes, writable: false))
+ using (var bmp = new Bitmap(ms))
+ {
+ if (x < 0 || y < 0 || x >= bmp.Width || y >= bmp.Height)
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = $"Point out of bounds. Image size: {bmp.Width}x{bmp.Height}, requested: ({x},{y})."
+ });
+ return;
+ }
+
+ var color = bmp.GetPixel(x, y);
+ var hex = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
+ var name = ToCommonColorName(color); // null if not close to a known color
+
+ context.Response.StatusCode = (int)HttpStatusCode.OK;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = true,
+ ["hex"] = hex,
+ ["name"] = name, // may be null → serializer will emit "name": null
+ ["rgb"] = new Dictionary
+ {
+ ["r"] = color.R,
+ ["g"] = color.G,
+ ["b"] = color.B
+ }
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Error($"ImageColorPicker error: {ex}");
+ context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ await WriteJsonAsync(context, new Dictionary
+ {
+ ["ok"] = false,
+ ["error"] = "Internal error while processing image."
+ });
+ }
+ finally
+ {
+ try { context.Response.OutputStream.Flush(); } catch { }
+ try { context.Response.OutputStream.Close(); } catch { }
+ }
+ }
+
+ // ---------------------------- Helpers -----------------------------------------------
+
+ private static bool TryParseJsonInt(string jsonNumberLiteral, out int value)
+ {
+ // Accept JSON numbers like: 12, -3, 10.0, 1e2, -4E+1
+ // Strategy: parse as double using invariant culture, then cast if within int range.
+ value = default;
+
+ if (string.IsNullOrWhiteSpace(jsonNumberLiteral))
+ return false;
+
+ var s = jsonNumberLiteral.Trim();
+
+ // Reject if wrapped in quotes (should be a number literal, not a string)
+ if (s.Length >= 2 && s[0] == '"' && s[s.Length - 1] == '"')
+ return false;
+
+ if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d))
+ {
+ if (d >= int.MinValue && d <= int.MaxValue)
+ {
+ value = (int)d; // truncate toward zero (JSON has no ints; this is reasonable)
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static bool TryParseJsonString(string jsonStringLiteral, out string value)
+ {
+ value = null;
+ if (string.IsNullOrWhiteSpace(jsonStringLiteral))
+ return false;
+
+ var s = jsonStringLiteral.Trim();
+ if (s.Length < 2 || s[0] != '"' || s[s.Length - 1] != '"')
+ return false;
+
+ // Remove surrounding quotes and unescape JSON sequences
+ value = JsonUnescape(s.Substring(1, s.Length - 2));
+ return true;
+ }
+
+ private static string JsonUnescape(string s)
+ {
+ var sb = new StringBuilder(s.Length);
+ for (int i = 0; i < s.Length; i++)
+ {
+ char c = s[i];
+ if (c != '\\') { sb.Append(c); continue; }
+
+ if (i + 1 >= s.Length) { sb.Append('\\'); break; }
+ char n = s[++i];
+ switch (n)
+ {
+ case '\"': sb.Append('\"'); break;
+ case '\\': sb.Append('\\'); break;
+ case '/': sb.Append('/'); break;
+ case 'b': sb.Append('\b'); break;
+ case 'f': sb.Append('\f'); break;
+ case 'n': sb.Append('\n'); break;
+ case 'r': sb.Append('\r'); break;
+ case 't': sb.Append('\t'); break;
+ case 'u':
+ if (i + 4 < s.Length)
+ {
+ string hex = s.Substring(i + 1, 4);
+ if (ushort.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cp))
+ {
+ sb.Append((char)cp);
+ i += 4;
+ }
+ else sb.Append('u');
+ }
+ else sb.Append('u');
+ break;
+ default:
+ sb.Append(n);
+ break;
+ }
+ }
+ return sb.ToString();
+ }
+
+ private static string StripDataUrlPrefix(string b64)
+ {
+ var idx = b64.IndexOf("base64,", StringComparison.OrdinalIgnoreCase);
+ return idx >= 0 ? b64.Substring(idx + "base64,".Length) : b64;
+ }
+
+ private static string ToCommonColorName(Color c)
+ {
+ (string name, Color color)[] palette =
+ {
+ ("white", Color.FromArgb(255,255,255)),
+ ("black", Color.FromArgb(0,0,0)),
+ ("red", Color.FromArgb(255,0,0)),
+ ("green", Color.FromArgb(0,128,0)),
+ ("blue", Color.FromArgb(0,0,255)),
+ ("yellow", Color.FromArgb(255,255,0)),
+ ("cyan", Color.FromArgb(0,255,255)),
+ ("magenta", Color.FromArgb(255,0,255)),
+ ("gray", Color.FromArgb(128,128,128))
+ };
+
+ const int tolerance = 20; // distance threshold
+ string best = null;
+ int bestDist = int.MaxValue;
+
+ foreach (var (name, col) in palette)
+ {
+ int dr = c.R - col.R, dg = c.G - col.G, db = c.B - col.B;
+ int dist = dr * dr + dg * dg + db * db;
+ if (dist < bestDist) { bestDist = dist; best = name; }
+ }
+
+ return bestDist <= tolerance * tolerance ? best : null;
+ }
+
+ private async Task WriteJsonAsync(HttpListenerContext ctx, Dictionary doc, int space = 0)
+ {
+ using (var ser = new JsSerializer())
+ {
+ string json = ser.Serialize(doc, space);
+ byte[] payload = Encoding.UTF8.GetBytes(json);
+
+ await Server.ServeResource(ctx, payload, "application/json", 200);
+ }
+ }
+ }
+}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs
index 68d7400..42475c2 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs
@@ -38,7 +38,7 @@ namespace WelsonJS.Launcher.ResourceTools
string apiKey = Program.GetAppConfig("CriminalIpApiKey");
if (string.IsNullOrEmpty(apiKey))
{
- Server.ServeResource(context, "Missing API key", "application/xml", 500);
+ await Server.ServeResource(context, "Missing API key", "application/xml", 500);
return;
}
@@ -54,11 +54,11 @@ namespace WelsonJS.Launcher.ResourceTools
string content = await response.Content.ReadAsStringAsync();
context.Response.StatusCode = (int)response.StatusCode;
- Server.ServeResource(context, content, "application/json", (int)response.StatusCode);
+ await Server.ServeResource(context, content, "application/json", (int)response.StatusCode);
}
catch (Exception ex)
{
- Server.ServeResource(context, $"{ex.Message}", "application/xml", 500);
+ await Server.ServeResource(context, $"{ex.Message}", "application/xml", 500);
}
}
}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs
index 6d6fd94..9be9759 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs
@@ -79,7 +79,7 @@ namespace WelsonJS.Launcher.ResourceTools
finalConfig.Select(kv => new XElement(kv.Key, kv.Value))
);
- Server.ServeResource(context, xml.ToString(), "application/xml");
+ await Server.ServeResource(context, xml.ToString(), "application/xml");
}
}
}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs
index aa44ba6..ed0832a 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs
@@ -53,7 +53,7 @@ namespace WelsonJS.Launcher.ResourceTools
length = parsed;
}
- Server.ServeResource(context, GetPubKey(length), "text/plain", 200);
+ await Server.ServeResource(context, GetPubKey(length), "text/plain", 200);
return;
}
@@ -71,7 +71,7 @@ namespace WelsonJS.Launcher.ResourceTools
var parsed = ParseFormEncoded(body);
if (!parsed.TryGetValue("secret", out string secret) || string.IsNullOrWhiteSpace(secret))
{
- Server.ServeResource(context, "missing 'secret' parameter", "text/plain", 400);
+ await Server.ServeResource(context, "missing 'secret' parameter", "text/plain", 400);
return;
}
@@ -79,17 +79,17 @@ namespace WelsonJS.Launcher.ResourceTools
{
int otp = GetOtp(secret.Trim());
string otp6 = otp.ToString("D6"); // always 6 digits
- Server.ServeResource(context, otp6, "text/plain", 200);
+ await Server.ServeResource(context, otp6, "text/plain", 200);
}
catch (Exception ex)
{
- Server.ServeResource(context, $"invalid secret: {ex.Message}", "text/plain", 400);
+ await Server.ServeResource(context, $"invalid secret: {ex.Message}", "text/plain", 400);
}
return;
}
// Default: not found
- Server.ServeResource(context, "not found", "text/plain", 404);
+ await Server.ServeResource(context, "not found", "text/plain", 404);
}
///
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs
index fc82e85..463fb04 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs
@@ -37,7 +37,7 @@ namespace WelsonJS.Launcher.ResourceTools
if (string.IsNullOrWhiteSpace(query) || query.Length > 255)
{
- Server.ServeResource(context, "Invalid query parameter", "application/xml", 400);
+ await Server.ServeResource(context, "Invalid query parameter", "application/xml", 400);
return;
}
@@ -57,11 +57,11 @@ namespace WelsonJS.Launcher.ResourceTools
HttpResponseMessage response = await _httpClient.SendAsync(request);
string responseBody = await response.Content.ReadAsStringAsync();
- Server.ServeResource(context, responseBody, "text/plain", (int)response.StatusCode);
+ await Server.ServeResource(context, responseBody, "text/plain", (int)response.StatusCode);
}
catch (Exception ex)
{
- Server.ServeResource(context, $"Failed to process WHOIS request. {ex.Message}", "application/xml", 500);
+ await Server.ServeResource(context, $"Failed to process WHOIS request. {ex.Message}", "application/xml", 500);
}
}
}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
index 0d5a5fc..229b09b 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
@@ -92,6 +92,7 @@
+