Signal 数据中心改为运行时加载 schema,解除 StandardScene 工程编译耦合。

加固 Config/Signal 路径解析,前端 DataCenter 表单类型与 API 对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
黄兆尉
2026-08-26 17:47:15 +08:00
co-authored by Cursor
parent 3686abdc78
commit 4180140ae4
5 changed files with 103 additions and 62 deletions
+33 -6
View File
@@ -15,8 +15,8 @@ public sealed record SignalTableDef(
IReadOnlyList<SignalColumn> Columns); IReadOnlyList<SignalColumn> Columns);
/// <summary> /// <summary>
/// 读写 SimpleLite 工作目录 <c>Config/Signal/*.json</c>,供迷毂「数据中心」表格编辑。 /// 读写 Simple3 工作目录 <c>Config/Signal/*.json</c>,供迷毂「数据中心」表格编辑。
/// 表结构优先从 StandardScene.Signal.dll 反射;无插件时才读 signal-tables.json。 /// 表结构Simple3 <c>plugins/StandardScene.Signal.dll</c> 反射;无插件时才读 signal-tables.json。
/// </summary> /// </summary>
public sealed class SignalDataStore public sealed class SignalDataStore
{ {
@@ -28,11 +28,11 @@ public sealed class SignalDataStore
private static readonly object FileLock = new(); private static readonly object FileLock = new();
private readonly SimpleLiteLauncher _launcher; private readonly Simple3Launcher _launcher;
private IReadOnlyList<SignalTableDef>? _cachedTables; private IReadOnlyList<SignalTableDef>? _cachedTables;
private long _cachedSignature; private long _cachedSignature;
public SignalDataStore(SimpleLiteLauncher launcher) => _launcher = launcher; public SignalDataStore(Simple3Launcher launcher) => _launcher = launcher;
public IReadOnlyList<SignalTableDef> GetTables() public IReadOnlyList<SignalTableDef> GetTables()
{ {
@@ -55,9 +55,11 @@ public sealed class SignalDataStore
{ {
var wd = _launcher.ResolveWorkingDirectory(); var wd = _launcher.ResolveWorkingDirectory();
if (string.IsNullOrWhiteSpace(wd)) if (string.IsNullOrWhiteSpace(wd))
return (null, "未找到 SimpleLite 工作目录,无法定位 Config/Signal"); return (null, "未找到 Simple3 工作目录,无法定位 Config/Signal");
if (!TryResolveUnderSignalDir(wd, table.FileName, out var dest, out var pathError))
return (null, pathError);
var dest = Path.GetFullPath(Path.Combine(wd, "Config", "Signal", table.FileName));
if (File.Exists(dest)) if (File.Exists(dest))
return (dest, null); return (dest, null);
@@ -109,4 +111,29 @@ public sealed class SignalDataStore
lock (FileLock) lock (FileLock)
File.WriteAllText(path, rows.ToJsonString(FileJson)); 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;
}
} }
+63 -49
View File
@@ -1,13 +1,12 @@
using System.ComponentModel; using System.ComponentModel;
using System.Reflection; using System.Reflection;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using StandardScene.Signal.Model;
namespace MiGu.Server.Signal; namespace MiGu.Server.Signal;
/// <summary> /// <summary>
/// 从 StandardScene.Signal 程序集反射数据中心表和列。 /// 从 Simple3 <c>plugins/StandardScene.Signal.dll</c> 反射数据中心表和列。
/// 优先使用 MiGu.Server 编译引用的程序集;否则再从 SimpleLite plugins 加载 /// 平台不编译引用该插件工程,避免仓外路径和 Windows TFM 耦合
/// </summary> /// </summary>
public static class SignalModelSchemaResolver public static class SignalModelSchemaResolver
{ {
@@ -24,18 +23,19 @@ public static class SignalModelSchemaResolver
public static IReadOnlyList<SignalTableDef> ResolveTables(string? pluginsDir, string? workingDirectory = null) public static IReadOnlyList<SignalTableDef> ResolveTables(string? pluginsDir, string? workingDirectory = null)
{ {
var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory); return WithSignalAssembly(pluginsDir, workingDirectory, assembly =>
if (assembly == null)
return Array.Empty<SignalTableDef>();
try
{ {
return BuildTablesFromAssembly(assembly); if (assembly == null)
} return Array.Empty<SignalTableDef>();
catch try
{ {
return Array.Empty<SignalTableDef>(); return BuildTablesFromAssembly(assembly);
} }
catch
{
return Array.Empty<SignalTableDef>();
}
});
} }
public static IReadOnlyList<SignalColumn> ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null) public static IReadOnlyList<SignalColumn> ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null)
@@ -43,61 +43,76 @@ public static class SignalModelSchemaResolver
if (string.IsNullOrWhiteSpace(modelName)) if (string.IsNullOrWhiteSpace(modelName))
return Array.Empty<SignalColumn>(); return Array.Empty<SignalColumn>();
var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory); return WithSignalAssembly(pluginsDir, workingDirectory, assembly =>
if (assembly == null)
return Array.Empty<SignalColumn>();
try
{ {
var type = assembly.GetType($"{ModelNamespace}.{modelName.Trim()}", throwOnError: false, ignoreCase: true); if (assembly == null)
if (type == null)
return Array.Empty<SignalColumn>(); return Array.Empty<SignalColumn>();
try
return DiscoverColumns(type); {
} var type = assembly.GetType($"{ModelNamespace}.{modelName.Trim()}", throwOnError: false, ignoreCase: true);
catch if (type == null)
{ return Array.Empty<SignalColumn>();
return Array.Empty<SignalColumn>(); return DiscoverColumns(type);
} }
catch
{
return Array.Empty<SignalColumn>();
}
});
} }
private static Assembly? TryGetSignalAssembly(string? pluginsDir, string? workingDirectory) private static T WithSignalAssembly<T>(string? pluginsDir, string? workingDirectory, Func<Assembly?, T> use)
{ {
try
{
var referenced = typeof(PlcStationModel).Assembly;
if (HasModelTypes(referenced))
return referenced;
}
catch
{
/* 未引用插件工程时继续走 LoadFrom */
}
var assemblyPath = FindAssemblyPath(pluginsDir); var assemblyPath = FindAssemblyPath(pluginsDir);
if (assemblyPath == null) if (assemblyPath == null)
return null; return use(null);
var expected = Path.GetFullPath(assemblyPath);
var cached = FindLoadedFrom(expected);
if (cached != null)
return use(cached);
var probeDirs = BuildProbeDirs(pluginsDir, workingDirectory); var probeDirs = BuildProbeDirs(pluginsDir, workingDirectory);
ResolveEventHandler? handler = null; ResolveEventHandler handler = (_, args) => ResolveAssembly(args.Name, probeDirs);
handler = (_, args) => ResolveAssembly(args.Name, probeDirs);
AppDomain.CurrentDomain.AssemblyResolve += handler; AppDomain.CurrentDomain.AssemblyResolve += handler;
try try
{ {
var loaded = Assembly.LoadFrom(assemblyPath); var loaded = Assembly.LoadFrom(expected);
return HasModelTypes(loaded) ? loaded : null; return use(HasModelTypes(loaded) ? loaded : null);
} }
catch catch
{ {
return null; return use(null);
} }
finally finally
{ {
if (handler != null) AppDomain.CurrentDomain.AssemblyResolve -= handler;
AppDomain.CurrentDomain.AssemblyResolve -= handler;
} }
} }
private static Assembly? FindLoadedFrom(string expectedFullPath)
{
try
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (!string.Equals(assembly.GetName().Name, "StandardScene.Signal", StringComparison.OrdinalIgnoreCase))
continue;
if (string.IsNullOrWhiteSpace(assembly.Location))
continue;
if (!string.Equals(Path.GetFullPath(assembly.Location), expectedFullPath, StringComparison.OrdinalIgnoreCase))
continue;
return HasModelTypes(assembly) ? assembly : null;
}
}
catch
{
/* 继续 LoadFrom */
}
return null;
}
private static bool HasModelTypes(Assembly assembly) private static bool HasModelTypes(Assembly assembly)
{ {
return SafeGetTypes(assembly).Any(t => return SafeGetTypes(assembly).Any(t =>
@@ -119,7 +134,6 @@ public static class SignalModelSchemaResolver
Add(workingDirectory); Add(workingDirectory);
if (!string.IsNullOrWhiteSpace(pluginsDir)) if (!string.IsNullOrWhiteSpace(pluginsDir))
Add(Path.GetDirectoryName(pluginsDir)); Add(Path.GetDirectoryName(pluginsDir));
Add(AppContext.BaseDirectory);
return dirs; return dirs;
} }
@@ -16,7 +16,7 @@ public static class SignalTableManifestLoader
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true
}; };
public static IReadOnlyList<SignalTableDef> Load(SimpleLiteLauncher launcher) public static IReadOnlyList<SignalTableDef> Load(Simple3Launcher launcher)
{ {
var workingDirectory = launcher.ResolveWorkingDirectory(); var workingDirectory = launcher.ResolveWorkingDirectory();
var pluginsDir = launcher.ResolvePluginsDir(); var pluginsDir = launcher.ResolvePluginsDir();
@@ -31,7 +31,7 @@ public static class SignalTableManifestLoader
.ToList(); .ToList();
} }
public static long ComputeManifestSignature(SimpleLiteLauncher launcher) public static long ComputeManifestSignature(Simple3Launcher launcher)
{ {
long sig = 0; long sig = 0;
foreach (var path in ResolveManifestPaths(launcher)) foreach (var path in ResolveManifestPaths(launcher))
@@ -61,7 +61,7 @@ public static class SignalTableManifestLoader
return sig; return sig;
} }
private static SignalTableManifest TryLoadManifest(SimpleLiteLauncher launcher) private static SignalTableManifest TryLoadManifest(Simple3Launcher launcher)
{ {
foreach (var path in ResolveManifestPaths(launcher)) foreach (var path in ResolveManifestPaths(launcher))
{ {
@@ -80,7 +80,7 @@ public static class SignalTableManifestLoader
?? new SignalTableManifest(); ?? new SignalTableManifest();
} }
private static IEnumerable<string> ResolveManifestPaths(SimpleLiteLauncher launcher) private static IEnumerable<string> ResolveManifestPaths(Simple3Launcher launcher)
{ {
var wd = launcher.ResolveWorkingDirectory(); var wd = launcher.ResolveWorkingDirectory();
if (!string.IsNullOrWhiteSpace(wd)) if (!string.IsNullOrWhiteSpace(wd))
@@ -34,7 +34,7 @@ export async function listSignalTables(): Promise<SignalDataListDto> {
if (MOCK) { if (MOCK) {
return { return {
signalEnabled: true, signalEnabled: true,
workingDirectory: 'D:\\工作\\stand\\SimpleLite', workingDirectory: 'D:\\工作\\stand\\Simple3',
tables: [ tables: [
{ id: 'stations', title: 'PLC机构', category: 'PLC数据管理', fileName: 'stations.json' }, { id: 'stations', title: 'PLC机构', category: 'PLC数据管理', fileName: 'stations.json' },
{ id: 'docks', title: '机构工位', category: 'PLC数据管理', fileName: 'station-docks.json' }, { id: 'docks', title: '机构工位', category: 'PLC数据管理', fileName: 'station-docks.json' },
@@ -6,7 +6,7 @@
<div> <div>
<h2>{{ table?.title ?? title }}</h2> <h2>{{ table?.title ?? title }}</h2>
<p> <p>
读写 SimpleLite 工作目录 <code>Config/Signal</code>改完后到信号交互进程点重新加载配置 读写 Simple3 工作目录 <code>Config/Signal</code>改完后到信号交互进程点重新加载配置
点位为 BOOL 时按字节 + 直接读写 点位为 BOOL 时按字节 + 直接读写
<span v-if="table"> <span v-if="table">
{{ table.fileName }}{{ table.exists ? '' : ' · 文件尚未创建保存后写入' }} {{ table.fileName }}{{ table.exists ? '' : ' · 文件尚未创建保存后写入' }}
@@ -117,7 +117,7 @@ const formGroups = computed(() => {
const dialogVisible = ref(false) const dialogVisible = ref(false)
const editingIndex = ref(-1) const editingIndex = ref(-1)
const form = reactive<Record<string, unknown>>({}) const form = reactive<Record<string, any>>({})
function truthy(v: unknown): boolean { function truthy(v: unknown): boolean {
return v === true || v === 'true' || v === 1 || v === '1' return v === true || v === 'true' || v === 1 || v === '1'