Files
Migu2.0/MiGu.Server/Signal/SignalDataStore.cs
T
黄兆尉andCursor 4180140ae4 Signal 数据中心改为运行时加载 schema,解除 StandardScene 工程编译耦合。
加固 Config/Signal 路径解析,前端 DataCenter 表单类型与 API 对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 17:47:15 +08:00

140 lines
4.5 KiB
C#

using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using MiGu.Server.Launcher;
namespace MiGu.Server.Signal;
public sealed record SignalColumn(string Key, string Label, string Type, IReadOnlyList<string>? Options = null, string? Group = null);
public sealed record SignalTableDef(
string Id,
string Title,
string Category,
string FileName,
IReadOnlyList<SignalColumn> Columns);
/// <summary>
/// 读写 Simple3 工作目录 <c>Config/Signal/*.json</c>,供迷毂「数据中心」表格编辑。
/// 表结构只从 Simple3 <c>plugins/StandardScene.Signal.dll</c> 反射;无插件时才读 signal-tables.json。
/// </summary>
public sealed class SignalDataStore
{
private static readonly JsonSerializerOptions FileJson = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly object FileLock = new();
private readonly Simple3Launcher _launcher;
private IReadOnlyList<SignalTableDef>? _cachedTables;
private long _cachedSignature;
public SignalDataStore(Simple3Launcher launcher) => _launcher = launcher;
public IReadOnlyList<SignalTableDef> GetTables()
{
var sig = SignalTableManifestLoader.ComputeManifestSignature(_launcher);
if (_cachedTables == null || sig != _cachedSignature)
{
_cachedTables = SignalTableManifestLoader.Load(_launcher);
_cachedSignature = sig;
}
return _cachedTables;
}
public SignalTableDef? Find(string id) =>
GetTables().FirstOrDefault(t => string.Equals(t.Id, id, StringComparison.OrdinalIgnoreCase));
public string? ResolveWorkingDirectory() => _launcher.ResolveWorkingDirectory();
public (string? Path, string? Error) ResolveFile(SignalTableDef table, bool createDir)
{
var wd = _launcher.ResolveWorkingDirectory();
if (string.IsNullOrWhiteSpace(wd))
return (null, "未找到 Simple3 工作目录,无法定位 Config/Signal");
if (!TryResolveUnderSignalDir(wd, table.FileName, out var dest, out var pathError))
return (null, pathError);
if (File.Exists(dest))
return (dest, null);
if (createDir)
{
try
{
var dir = Path.GetDirectoryName(dest);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
}
catch (Exception ex)
{
return (null, ex.Message);
}
}
return (dest, null);
}
public JsonArray LoadRows(SignalTableDef table)
{
var (path, _) = ResolveFile(table, createDir: false);
if (path == null || !File.Exists(path))
return new JsonArray();
lock (FileLock)
{
var json = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(json))
return new JsonArray();
var node = JsonNode.Parse(json);
if (node is JsonArray arr)
return arr;
return new JsonArray();
}
}
public void SaveRows(SignalTableDef table, JsonArray rows)
{
var (path, error) = ResolveFile(table, createDir: true);
if (path == null)
throw new InvalidOperationException(error ?? "无法解析信号配置文件路径");
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
lock (FileLock)
File.WriteAllText(path, rows.ToJsonString(FileJson));
}
private static bool TryResolveUnderSignalDir(string workingDirectory, string fileName, out string dest, out string error)
{
dest = "";
error = "";
if (string.IsNullOrWhiteSpace(fileName) ||
Path.IsPathRooted(fileName) ||
fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
{
error = "信号表文件名非法";
return false;
}
var root = Path.GetFullPath(Path.Combine(workingDirectory, "Config", "Signal"));
dest = Path.GetFullPath(Path.Combine(root, fileName));
var prefix = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (!dest.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
error = "信号表路径超出 Config/Signal";
dest = "";
return false;
}
return true;
}
}