更新客户端渲染,更新了壳

This commit is contained in:
QWQLwToo
2026-07-06 23:05:40 +08:00
parent e7dd87bf7e
commit 31d778710b
1311 changed files with 172662 additions and 1582 deletions
+127
View File
@@ -0,0 +1,127 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Data.Sqlite;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Plugins;
using YMhut.Box.Core.Plugins.Runtime;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class PluginRuntimeTests
{
[TestMethod]
public void RuntimeProtocolRoundTripsShellOutput()
{
var output = new ShellOutputEvent("session-1", ShellOutputStream.Stdout, "hello", DateTimeOffset.Now, 7);
var message = new PluginRuntimeMessage(PluginRuntimeProtocol.ShellOutput, "req-1", ShellOutput: output);
var parsed = PluginRuntimeProtocol.Deserialize(PluginRuntimeProtocol.Serialize(message));
Assert.IsNotNull(parsed);
Assert.AreEqual(PluginRuntimeProtocol.ShellOutput, parsed.Type);
Assert.AreEqual("session-1", parsed.ShellOutput?.SessionId);
Assert.AreEqual(ShellOutputStream.Stdout, parsed.ShellOutput?.Stream);
Assert.AreEqual("hello", parsed.ShellOutput?.Line);
}
[TestMethod]
public async Task ManifestV2AcceptsShellCommandInsidePluginRoot()
{
using var workspace = new TempDirectory(Path.Combine(Path.GetTempPath(), "ymhut-runtime-tests", Guid.NewGuid().ToString("N")));
var pluginRoot = CreatePlugin(workspace.Path, "shell-demo", """
{
"id": "shell-demo",
"name": "Shell Demo",
"version": "1.0.0",
"author": "tester",
"description": "Shell plugin",
"entry": "index.html",
"runtime": "Shell",
"permissions": ["ShellExecute", "Log"],
"surfaces": [
{ "kind": "ToolboxTool", "id": "run", "name": "Run", "description": "Run script", "entry": "index.html" }
],
"resources": ["index.html", "run.ps1"],
"commands": [
{ "id": "hello", "name": "Hello", "language": "powershell", "entry": "run.ps1", "timeoutMs": 30000 }
],
"security": {
"requiredPermissions": ["ShellExecute", "Log"],
"readPaths": ["."],
"writePaths": ["."]
}
}
""");
File.WriteAllText(Path.Combine(pluginRoot, "index.html"), "<h1>Shell</h1>");
File.WriteAllText(Path.Combine(pluginRoot, "run.ps1"), "Write-Output 'hello'");
var paths = AppPaths.ForCurrentUser(workspace.Path);
var registry = new PluginRegistryService(paths, new PluginStateStore(paths));
var plugin = (await registry.LoadPluginsAsync()).Single();
Assert.IsTrue(plugin.IsValid, string.Join("; ", plugin.Errors));
Assert.AreEqual(PluginRuntimeKind.Shell, plugin.Manifest.Runtime);
Assert.HasCount(1, plugin.Manifest.Commands!);
}
[TestMethod]
public async Task ManifestV2RejectsCommandPathEscapingPluginRoot()
{
using var workspace = new TempDirectory(Path.Combine(Path.GetTempPath(), "ymhut-runtime-tests", Guid.NewGuid().ToString("N")));
_ = CreatePlugin(workspace.Path, "bad-shell", """
{
"id": "bad-shell",
"name": "Bad Shell",
"version": "1.0.0",
"author": "tester",
"description": "Bad shell plugin",
"entry": "index.html",
"runtime": "Shell",
"permissions": ["ShellExecute"],
"surfaces": [
{ "kind": "ToolboxTool", "id": "run", "name": "Run", "description": "Run script", "entry": "index.html" }
],
"resources": ["index.html"],
"commands": [
{ "id": "escape", "name": "Escape", "language": "powershell", "entry": "..\\escape.ps1" }
]
}
""");
var paths = AppPaths.ForCurrentUser(workspace.Path);
var registry = new PluginRegistryService(paths, new PluginStateStore(paths));
var plugin = (await registry.LoadPluginsAsync()).Single();
Assert.IsFalse(plugin.IsValid);
Assert.IsTrue(plugin.Errors.Any(error => error.Contains("Command entry", StringComparison.OrdinalIgnoreCase)));
}
private static string CreatePlugin(string root, string folder, string manifest)
{
var pluginRoot = Path.Combine(root, "Plugins", folder);
Directory.CreateDirectory(pluginRoot);
File.WriteAllText(Path.Combine(pluginRoot, PluginManifest.FileName), manifest);
File.WriteAllText(Path.Combine(pluginRoot, "README.md"), "# Test plugin");
return pluginRoot;
}
private sealed class TempDirectory : IDisposable
{
public TempDirectory(string path)
{
Path = path;
Directory.CreateDirectory(path);
}
public string Path { get; }
public void Dispose()
{
SqliteConnection.ClearAllPools();
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
}
}
+16 -15
View File
@@ -708,26 +708,27 @@ public sealed class ToolExecutorTests
public void UpdateNoticeJsonKeepsPlainTextAndAddsMarkdown()
{
var repoRoot = Directory.GetParent(FindAssetsRoot())!.FullName;
var noticePath = Path.Combine(repoRoot, "update-notice", "2.0.7.5.json");
var totalPath = Path.Combine(repoRoot, "update-notice", "total.json");
using var notice = JsonDocument.Parse(File.ReadAllText(noticePath));
var noticeRoot = notice.RootElement;
Assert.AreEqual("2.0.7.5", noticeRoot.GetProperty("app_version").GetString());
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("message").GetString()));
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("release_notes").GetString()));
StringAssert.Contains(noticeRoot.GetProperty("message_md").GetString(), "YMhut Box 2.0.7.5");
StringAssert.Contains(noticeRoot.GetProperty("release_notes_md").GetString(), "随机放映室");
StringAssert.Contains(noticeRoot.GetProperty("release_notes_md").GetString(), "音量控制");
using var total = JsonDocument.Parse(File.ReadAllText(totalPath));
var totalRoot = total.RootElement;
Assert.AreEqual("2.0.7.5", totalRoot.GetProperty("latest_version").GetString());
var latestVersion = totalRoot.GetProperty("latest_version").GetString();
var latestNoticeFile = totalRoot.GetProperty("latest_notice_file").GetString();
var noticePath = Path.Combine(repoRoot, "update-notice", latestNoticeFile!);
using var notice = JsonDocument.Parse(File.ReadAllText(noticePath));
var noticeRoot = notice.RootElement;
Assert.AreEqual(latestVersion, noticeRoot.GetProperty("app_version").GetString());
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("message").GetString()));
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("release_notes").GetString()));
StringAssert.Contains(noticeRoot.GetProperty("message_md").GetString(), $"YMhut Box {latestVersion}");
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("release_notes_md").GetString()));
var latest = totalRoot.GetProperty("latest");
Assert.AreEqual("2.0.7.5", latest.GetProperty("version").GetString());
StringAssert.Contains(latest.GetProperty("release_notes_md").GetString(), "随机放映室");
Assert.AreEqual("2.0.7.5", totalRoot.GetProperty("versions")[0].GetProperty("version").GetString());
StringAssert.Contains(totalRoot.GetProperty("versions")[0].GetProperty("summary").GetString(), "随机放映室");
Assert.AreEqual(latestVersion, latest.GetProperty("version").GetString());
Assert.IsFalse(string.IsNullOrWhiteSpace(latest.GetProperty("release_notes").GetString()));
Assert.AreEqual(latestVersion, totalRoot.GetProperty("versions")[0].GetProperty("version").GetString());
Assert.IsFalse(string.IsNullOrWhiteSpace(totalRoot.GetProperty("versions")[0].GetProperty("summary").GetString()));
}
[TestMethod]
@@ -49,16 +49,42 @@ public sealed class ToolResultExperienceCatalogTests
}
[TestMethod]
public void PrivacySanitizerOnlyRedactsYmHutAndApiRequestUrls()
public void RepresentativeToolsUseSpecializedProfiles()
{
var catalog = new ToolCatalog(ToolCatalog.DefaultModules());
var experiences = new ToolResultExperienceCatalog(catalog);
Assert.AreEqual("sanguosha_skin", experiences.GetRequired("sanguosha_skin").Profile);
Assert.AreEqual("network_dns", experiences.GetRequired("dns_record_lookup").Profile);
Assert.AreEqual("ranked_hotboard", experiences.GetRequired("hotboard").Profile);
Assert.AreEqual("code_formatter", experiences.GetRequired("json_formatter").Profile);
Assert.AreEqual("security_decode", experiences.GetRequired("base64_codec").Profile);
}
[TestMethod]
public void PrivacySanitizerKeepsNormalUrlsAndRedactsOnlySensitiveFragments()
{
const string input =
"public https://example.com/path?q=1; ymhut https://update.ymhut.cn/update-info.json; api_url=https://api.example.net/v1/items";
"public https://example.com/path?q=1; ymhut https://update.ymhut.cn/update-info.json; api_url=https://api.example.net/v1/items?token=abc123; api_key=secret123";
var sanitized = ToolResultPrivacySanitizer.Redact(input, "en-US");
StringAssert.Contains(sanitized, "https://example.com/path?q=1");
StringAssert.Contains(sanitized, "https://update.ymhut.cn/update-info.json");
StringAssert.Contains(sanitized, "https://api.example.net/v1/items?token=[redacted]");
Assert.IsFalse(sanitized.Contains("abc123", StringComparison.OrdinalIgnoreCase), sanitized);
Assert.IsFalse(sanitized.Contains("secret123", StringComparison.OrdinalIgnoreCase), sanitized);
StringAssert.Contains(sanitized, "[redacted]");
}
[TestMethod]
public void PrivacySanitizerCanHideExplicitPrivateEndpointContext()
{
const string input = "api_url=https://update.ymhut.cn/private/config.json";
var sanitized = ToolResultPrivacySanitizer.Redact(input, "zh-CN", ["update.ymhut.cn"]);
Assert.IsFalse(sanitized.Contains("update.ymhut.cn", StringComparison.OrdinalIgnoreCase), sanitized);
Assert.IsFalse(sanitized.Contains("api.example.net", StringComparison.OrdinalIgnoreCase), sanitized);
StringAssert.Contains(sanitized, "[YMhut endpoint hidden]");
StringAssert.Contains(sanitized, "[已隐藏私有接口]");
}
}
@@ -91,6 +91,9 @@ public sealed class ToolWebPayloadSerializationTests
Assert.IsNotNull(roundTrip);
Assert.HasCount(blocks.Length, roundTrip.ResultDocument.Blocks);
Assert.AreEqual("ranked_hotboard", roundTrip.Experience.Profile);
Assert.IsFalse(roundTrip.PrivacyPolicy.RedactVisibleContent);
Assert.AreEqual("show-original", roundTrip.PrivacyPolicy.VisibleMode);
CollectionAssert.Contains(roundTrip.ResultDocument.Blocks.Select(block => block.Kind).ToList(), ToolResultBlockKind.RankedList);
CollectionAssert.Contains(roundTrip.ResultDocument.Blocks.Select(block => block.Kind).ToList(), ToolResultBlockKind.Media);
StringAssert.Contains(json, nameof(ToolResultBlockKind.RankedList));