From f9b05b4178cf7ab1bb2c67f560bb82867466210e Mon Sep 17 00:00:00 2001 From: cneicy Date: Wed, 26 Aug 2026 02:49:49 +0800 Subject: [PATCH] chore: initialize standalone UPM package --- .gitea/workflows/publish.yml | 49 ++++++ .gitea/workflows/unity-verify.yml | 39 +++++ .gitignore | 10 ++ .npmignore | 8 + CHANGELOG.md | 16 ++ CHANGELOG.md.meta | 7 + Development~/UnityProject/.gitignore | 6 + Development~/UnityProject/Assets/.gitkeep | 0 .../UnityProject/Packages/manifest.json | 15 ++ .../ProjectSettings/ProjectVersion.txt | 2 + README.md | 24 +++ README.md.meta | 7 + ShrinkCommand.Integration.Network.asmdef | 19 +++ ShrinkCommand.Integration.Network.asmdef.meta | 7 + ShrinkCommandNetworkIntegrationComponent.cs | 49 ++++++ ...CommandNetworkIntegrationComponent.cs.meta | 11 ++ ShrinkNetworkCommandBridge.cs | 86 ++++++++++ ShrinkNetworkCommandBridge.cs.meta | 11 ++ ShrinkNetworkCommandBridgeExtensions.cs | 52 ++++++ ShrinkNetworkCommandBridgeExtensions.cs.meta | 11 ++ ShrinkNetworkCommandBridgeOptions.cs | 15 ++ ShrinkNetworkCommandBridgeOptions.cs.meta | 11 ++ ShrinkNetworkCommandContracts.cs | 20 +++ ShrinkNetworkCommandContracts.cs.meta | 11 ++ ShrinkNetworkCommandSource.cs | 79 +++++++++ ShrinkNetworkCommandSource.cs.meta | 11 ++ Tests.meta | 8 + Tests/IntegrationComponentsTests.cs | 151 ++++++++++++++++++ Tests/IntegrationComponentsTests.cs.meta | 11 ++ ...nkCommand.Integration.Network.Tests.asmdef | 28 ++++ ...mand.Integration.Network.Tests.asmdef.meta | 7 + package.json | 24 +++ package.json.meta | 7 + 33 files changed, 812 insertions(+) create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitea/workflows/unity-verify.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.md.meta create mode 100644 Development~/UnityProject/.gitignore create mode 100644 Development~/UnityProject/Assets/.gitkeep create mode 100644 Development~/UnityProject/Packages/manifest.json create mode 100644 Development~/UnityProject/ProjectSettings/ProjectVersion.txt create mode 100644 README.md create mode 100644 README.md.meta create mode 100644 ShrinkCommand.Integration.Network.asmdef create mode 100644 ShrinkCommand.Integration.Network.asmdef.meta create mode 100644 ShrinkCommandNetworkIntegrationComponent.cs create mode 100644 ShrinkCommandNetworkIntegrationComponent.cs.meta create mode 100644 ShrinkNetworkCommandBridge.cs create mode 100644 ShrinkNetworkCommandBridge.cs.meta create mode 100644 ShrinkNetworkCommandBridgeExtensions.cs create mode 100644 ShrinkNetworkCommandBridgeExtensions.cs.meta create mode 100644 ShrinkNetworkCommandBridgeOptions.cs create mode 100644 ShrinkNetworkCommandBridgeOptions.cs.meta create mode 100644 ShrinkNetworkCommandContracts.cs create mode 100644 ShrinkNetworkCommandContracts.cs.meta create mode 100644 ShrinkNetworkCommandSource.cs create mode 100644 ShrinkNetworkCommandSource.cs.meta create mode 100644 Tests.meta create mode 100644 Tests/IntegrationComponentsTests.cs create mode 100644 Tests/IntegrationComponentsTests.cs.meta create mode 100644 Tests/ShrinkCommand.Integration.Network.Tests.asmdef create mode 100644 Tests/ShrinkCommand.Integration.Network.Tests.asmdef.meta create mode 100644 package.json create mode 100644 package.json.meta diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..f2add20 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish UPM package + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }} + steps: + - name: Fetch tagged revision + shell: bash + run: | + set -eu + ref="${{ gitea.sha }}" + test -n "$ref" + git init . + git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network.git" + git fetch --depth=1 origin "$ref" + git checkout --detach FETCH_HEAD + + - name: Validate immutable release version + shell: bash + run: | + set -eu + tag="$(git describe --exact-match --tags HEAD)" + version="$(node -p "require('./package.json').version")" + test "$tag" = "v$version" + npm pack --dry-run + + - name: Publish to ShrinkSDK registry + shell: bash + run: | + set -eu + : "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}" + npmrc="$HOME/.npmrc" + cleanup() { rm -f "$npmrc"; } + trap cleanup EXIT + printf '%s\n' \ + 'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \ + '//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc" + npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/ \ No newline at end of file diff --git a/.gitea/workflows/unity-verify.yml b/.gitea/workflows/unity-verify.yml new file mode 100644 index 0000000..0de59f0 --- /dev/null +++ b/.gitea/workflows/unity-verify.yml @@ -0,0 +1,39 @@ +name: Verify standalone Unity package + +on: + workflow_dispatch: + +jobs: + editmode: + runs-on: unity-2022.3.62f3 + container: + image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3 + volumes: + - /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity + - /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses + steps: + - name: Fetch selected revision + shell: bash + run: | + set -eu + ref="${{ gitea.sha }}" + git init . + git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network.git" + git fetch --depth=1 origin "$ref" + git checkout --detach FETCH_HEAD + + - name: Run package EditMode tests + shell: bash + run: | + set -eu + unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)" + test -n "$unity_bin" + "$unity_bin" \ + -batchmode \ + -nographics \ + -quit \ + -projectPath "$PWD/Development~/UnityProject" \ + -runTests \ + -testPlatform EditMode \ + -testResults "$PWD/TestResults/editmode.xml" \ + -logFile "$PWD/TestResults/unity.log" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..246935d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/Development~/UnityProject/[Ll]ibrary/ +/Development~/UnityProject/[Tt]emp/ +/Development~/UnityProject/[Oo]bj/ +/Development~/UnityProject/[Ll]ogs/ +/Development~/UnityProject/[Uu]ser[Ss]ettings/ +/Development~/UnityProject/TestResults/ +/Tools~/**/[Bb]in/ +/Tools~/**/[Oo]bj/ +*.user +*.DotSettings.user diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..700f03d --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +.git/ +.gitea/ +Development~/ +Tools~/ +*.csproj +*.sln +*.user +*.DotSettings.user diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3885224 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +本文件记录 `ShrinkCommand.Integration.Network` 在当前工作区中的包内变更。 + +## [0.1.0] - 2026-04-07 + +### Added + +- 增加 `command/execute` RPC 桥接。 +- 远程会话可自动映射为 `IShrinkCommandSource`,统一走命令运行时。 +- 命令执行结果可直接通过 `ShrinkNetwork` 返回给远端调用方。 + +### Changed + +- 桥接层公共类型补齐 `#nullable enable`,对齐当前运行时与宿主侧的可空语义。 +- 保持桥接层职责边界,命令核心仍不直接依赖网络实现。 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..b599f33 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cc0d137c46b092f49a54bc3afcd7c161 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Development~/UnityProject/.gitignore b/Development~/UnityProject/.gitignore new file mode 100644 index 0000000..aa2eb00 --- /dev/null +++ b/Development~/UnityProject/.gitignore @@ -0,0 +1,6 @@ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +TestResults/ \ No newline at end of file diff --git a/Development~/UnityProject/Assets/.gitkeep b/Development~/UnityProject/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Development~/UnityProject/Packages/manifest.json b/Development~/UnityProject/Packages/manifest.json new file mode 100644 index 0000000..09f0738 --- /dev/null +++ b/Development~/UnityProject/Packages/manifest.json @@ -0,0 +1,15 @@ +{ + "scopedRegistries": [ + { + "name": "ShrinkSDK", + "url": "https://git.crash.work/api/packages/ShrinkSDK/npm/", + "scopes": [ + "com.cneicy" + ] + } + ], + "dependencies": { + "com.unity.test-framework": "1.1.33", + "com.cneicy.shrink-command-integration-network": "file:../../.." + } +} diff --git a/Development~/UnityProject/ProjectSettings/ProjectVersion.txt b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..587f809 --- /dev/null +++ b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2022.3.62f3 +m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7) diff --git a/README.md b/README.md new file mode 100644 index 0000000..08fe4e3 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# ShrinkCommand.Integration.Network + +`ShrinkCommand` 与 `ShrinkNetwork` 的桥接层。 + +## 能力 + +- 通过网络 RPC 执行命令 +- 远程命令来源自动映射为 `IShrinkCommandSource` +- 继续复用命令系统自己的权限与来源限制 + +## 接入 + +```csharp +commandService = new ShrinkCommandService(); +commandService.AutoRegisterAll(); + +networkService.UseCommandBridge(commandService); +``` + +客户端调用: + +```csharp +var response = await session.ExecuteCommandAsync("help"); +``` diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..4995074 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c85d4974b8207cf45848e1379ad7baa3 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkCommand.Integration.Network.asmdef b/ShrinkCommand.Integration.Network.asmdef new file mode 100644 index 0000000..da2194a --- /dev/null +++ b/ShrinkCommand.Integration.Network.asmdef @@ -0,0 +1,19 @@ +{ + "name": "ShrinkCommand.Integration.Network", + "rootNamespace": "ShrinkCommand.Integration", + "references": [ + "ShrinkCommand.Runtime", + "ShrinkNetwork.Runtime", + "ShrinkContext.Core.Runtime", + "UniTask" + ], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/ShrinkCommand.Integration.Network.asmdef.meta b/ShrinkCommand.Integration.Network.asmdef.meta new file mode 100644 index 0000000..f36a41e --- /dev/null +++ b/ShrinkCommand.Integration.Network.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d9c305f415a0515409b2795a809e7240 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkCommandNetworkIntegrationComponent.cs b/ShrinkCommandNetworkIntegrationComponent.cs new file mode 100644 index 0000000..e880650 --- /dev/null +++ b/ShrinkCommandNetworkIntegrationComponent.cs @@ -0,0 +1,49 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using ShrinkCommand; +using ShrinkContext; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + /// + /// 命令-网络集成组件(论文 6.5 集成组件模式): + /// 核心(command / network)互不依赖,本组件注入双方服务键,全部就绪才激活, + /// 任一提供者退役即自动停用——依赖接线由响应式余效应结构性完成,替代手工桥接编排。 + /// + /// 已知边界:ShrinkNetworkService 当前无 handler 注销 API,停用不撤销已注册的 + /// command/execute 处理器(传输解绑后不可达;网络服务实例重建时自然消失)。 + /// + public sealed class ShrinkCommandNetworkIntegrationComponent : IShrinkComponent + { + /// 与 ShrinkNetworkAppComponent.ServiceKey 保持一致(避免跨包硬引用)。 + public const string NetworkServiceKey = "shrink.service.network"; + + public const string CommandServiceKey = "shrink.service.command"; + public const string ProvideKey = "shrink.integration.command-network"; + + private static readonly string[] InjectKeys = { CommandServiceKey, NetworkServiceKey }; + private static readonly string[] ProvideKeys = { ProvideKey }; + + public string Name => "shrink.integration.command-network"; + + public IReadOnlyList Inject => InjectKeys; + + public IReadOnlyList Provide => ProvideKeys; + + public UniTask ApplyAsync(ShrinkCtx ctx, object? config) + { + var commandService = ctx.Get(CommandServiceKey); + var networkService = ctx.Get(NetworkServiceKey); + var options = config as ShrinkNetworkCommandBridgeOptions; + + ShrinkNetworkCommandBridge.RegisterService(networkService, commandService, options); + ctx.Set(ProvideKey, ShrinkNetworkCommandBridge.ExecuteRoute); + + // 桥注册按网络服务实例幂等去重;可逆面为集成键的发布/撤回 + return UniTask.CompletedTask; + } + } +} diff --git a/ShrinkCommandNetworkIntegrationComponent.cs.meta b/ShrinkCommandNetworkIntegrationComponent.cs.meta new file mode 100644 index 0000000..487675e --- /dev/null +++ b/ShrinkCommandNetworkIntegrationComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c5710d88a762cde489282366255f755c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandBridge.cs b/ShrinkNetworkCommandBridge.cs new file mode 100644 index 0000000..b2f6d31 --- /dev/null +++ b/ShrinkNetworkCommandBridge.cs @@ -0,0 +1,86 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + public static class ShrinkNetworkCommandBridge + { + public const string ExecuteRoute = "command/execute"; + + private static readonly object SyncRoot = new(); + private static readonly HashSet RegisteredServices = new(); + + public static void RegisterService( + ShrinkNetworkService networkService, + ShrinkCommandService commandService, + ShrinkNetworkCommandBridgeOptions? options = null) + { + if (networkService == null) + throw new ArgumentNullException(nameof(networkService)); + if (commandService == null) + throw new ArgumentNullException(nameof(commandService)); + + lock (SyncRoot) + { + if (RegisteredServices.Contains(networkService)) + return; + RegisteredServices.Add(networkService); + } + + options ??= new ShrinkNetworkCommandBridgeOptions(); + + EnsureMessagesRegistered(networkService); + var requirement = new ShrinkNetworkPermissionRequirement(options.Authority, options.Permission); + networkService.RegisterRequestHandler( + (context, request) => HandleExecuteAsync(commandService, options, context, request), + requirement); + } + + public static void EnsureMessagesRegistered(ShrinkNetworkService service) + { + if (!service.MessageRegistry.TryGetMeta(typeof(ShrinkNetworkCommandRequest), out _)) + { + var requestAttribute = (ShrinkNetworkMessageAttribute)Attribute.GetCustomAttribute( + typeof(ShrinkNetworkCommandRequest), + typeof(ShrinkNetworkMessageAttribute), + false)!; + service.RegisterMessage(typeof(ShrinkNetworkCommandRequest), requestAttribute.Opcode, requestAttribute.Route); + } + + if (!service.MessageRegistry.TryGetMeta(typeof(ShrinkNetworkCommandResponse), out _)) + { + var responseAttribute = (ShrinkNetworkMessageAttribute)Attribute.GetCustomAttribute( + typeof(ShrinkNetworkCommandResponse), + typeof(ShrinkNetworkMessageAttribute), + false)!; + service.RegisterMessage(typeof(ShrinkNetworkCommandResponse), responseAttribute.Opcode, responseAttribute.Route); + } + } + + private static async UniTask HandleExecuteAsync( + ShrinkCommandService commandService, + ShrinkNetworkCommandBridgeOptions options, + ShrinkNetworkContext context, + ShrinkNetworkCommandRequest request) + { + var source = options.SourceFactory?.Invoke(context, request) ?? new ShrinkNetworkCommandSource(context, request); + var result = await commandService.ExecuteAsync(source, request.CommandLine); + var output = source is ShrinkNetworkCommandSource networkSource + ? networkSource.BuildOutputMessage(result.Message) + : result.Message; + + return new ShrinkNetworkCommandResponse + { + ServiceName = string.IsNullOrWhiteSpace(request.ServiceName) ? options.ServiceName : request.ServiceName.Trim(), + CommandLine = request.CommandLine ?? string.Empty, + OutputMessage = output, + ErrorCode = result.IsSuccess ? 0 : ShrinkRpcErrorCode.HandlerException, + ErrorMessage = result.IsSuccess ? string.Empty : output + }; + } + } +} diff --git a/ShrinkNetworkCommandBridge.cs.meta b/ShrinkNetworkCommandBridge.cs.meta new file mode 100644 index 0000000..4a9c4f0 --- /dev/null +++ b/ShrinkNetworkCommandBridge.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f22899cbeb3779d4eb1338217fa4d38f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandBridgeExtensions.cs b/ShrinkNetworkCommandBridgeExtensions.cs new file mode 100644 index 0000000..72e5cad --- /dev/null +++ b/ShrinkNetworkCommandBridgeExtensions.cs @@ -0,0 +1,52 @@ +#nullable enable + +using System; +using Cysharp.Threading.Tasks; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + public static class ShrinkNetworkCommandBridgeExtensions + { + public static ShrinkNetworkService UseCommandBridge( + this ShrinkNetworkService networkService, + ShrinkCommandService commandService, + ShrinkNetworkCommandBridgeOptions? options = null) + { + if (networkService == null) + throw new ArgumentNullException(nameof(networkService)); + if (commandService == null) + throw new ArgumentNullException(nameof(commandService)); + + ShrinkNetworkCommandBridge.RegisterService(networkService, commandService, options); + return networkService; + } + + public static async UniTask ExecuteCommandAsync( + this ShrinkNetworkSession session, + string commandLine, + string? serviceName = null, + string? clientLabel = null, + ShrinkRpcCallOptions? options = null) + { + if (session == null) + throw new ArgumentNullException(nameof(session)); + + ShrinkNetworkCommandBridge.EnsureMessagesRegistered(session.Service); + return await session.RpcAsync( + new ShrinkNetworkCommandRequest + { + ServiceName = string.IsNullOrWhiteSpace(serviceName) + ? ShrinkCommandConstants.DefaultServiceName + : serviceName.Trim(), + CommandLine = commandLine ?? string.Empty, + ClientLabel = clientLabel ?? string.Empty + }, + options ?? new ShrinkRpcCallOptions + { + TimeoutMs = 5000, + DebugLabel = "RemoteCommand" + }); + } + } +} diff --git a/ShrinkNetworkCommandBridgeExtensions.cs.meta b/ShrinkNetworkCommandBridgeExtensions.cs.meta new file mode 100644 index 0000000..b8592cf --- /dev/null +++ b/ShrinkNetworkCommandBridgeExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11bf32dbf7be0e14cbddf1f03ded5cbb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandBridgeOptions.cs b/ShrinkNetworkCommandBridgeOptions.cs new file mode 100644 index 0000000..1235a08 --- /dev/null +++ b/ShrinkNetworkCommandBridgeOptions.cs @@ -0,0 +1,15 @@ +#nullable enable + +using System; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + public sealed class ShrinkNetworkCommandBridgeOptions + { + public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; + public ShrinkNetworkAuthority Authority { get; set; } = ShrinkNetworkAuthority.ClientOnly; + public string Permission { get; set; } = string.Empty; + public Func? SourceFactory { get; set; } + } +} diff --git a/ShrinkNetworkCommandBridgeOptions.cs.meta b/ShrinkNetworkCommandBridgeOptions.cs.meta new file mode 100644 index 0000000..8fba3e9 --- /dev/null +++ b/ShrinkNetworkCommandBridgeOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 174745a3cab12534c8d4b09788063fe9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandContracts.cs b/ShrinkNetworkCommandContracts.cs new file mode 100644 index 0000000..c398275 --- /dev/null +++ b/ShrinkNetworkCommandContracts.cs @@ -0,0 +1,20 @@ +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + [ShrinkNetworkMessage(1501, ShrinkNetworkCommandBridge.ExecuteRoute)] + public sealed class ShrinkNetworkCommandRequest : IShrinkNetworkRequest + { + public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; + public string CommandLine { get; set; } = string.Empty; + public string ClientLabel { get; set; } = string.Empty; + } + + [ShrinkNetworkMessage(1502, "command/execute_response")] + public sealed class ShrinkNetworkCommandResponse : ShrinkRpcResponseBase + { + public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; + public string CommandLine { get; set; } = string.Empty; + public string OutputMessage { get; set; } = string.Empty; + } +} diff --git a/ShrinkNetworkCommandContracts.cs.meta b/ShrinkNetworkCommandContracts.cs.meta new file mode 100644 index 0000000..7100270 --- /dev/null +++ b/ShrinkNetworkCommandContracts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8b44977ad838cce438d99ea565848485 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandSource.cs b/ShrinkNetworkCommandSource.cs new file mode 100644 index 0000000..5ba1a2d --- /dev/null +++ b/ShrinkNetworkCommandSource.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Cysharp.Threading.Tasks; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + public sealed class ShrinkNetworkCommandSource : IShrinkCommandSource + { + private readonly List _lines = new(); + private readonly ShrinkNetworkContext _context; + private readonly ShrinkNetworkCommandRequest _request; + + public ShrinkNetworkCommandSource(ShrinkNetworkContext context, ShrinkNetworkCommandRequest request) + { + _context = context; + _request = request; + SourceId = $"network:{context.Session.SessionId}"; + DisplayName = ResolveDisplayName(context, request); + } + + public string SourceId { get; } + public string DisplayName { get; } + public bool IsConsole => false; + public ShrinkNetworkSession Session => _context.Session; + + public bool HasPermission(string permission) + { + return string.IsNullOrWhiteSpace(permission) || Session.HasPermission(permission); + } + + public UniTask WriteLineAsync(string message, CancellationToken cancellationToken = default) + { + if (!string.IsNullOrWhiteSpace(message)) + _lines.Add(message); + return UniTask.CompletedTask; + } + + public string BuildOutputMessage(string resultMessage) + { + if (_lines.Count == 0) + return resultMessage ?? string.Empty; + + var builder = new StringBuilder(); + for (var index = 0; index < _lines.Count; index++) + { + if (index > 0) + builder.AppendLine(); + builder.Append(_lines[index]); + } + + if (!string.IsNullOrWhiteSpace(resultMessage)) + { + if (builder.Length > 0 && !string.Equals(_lines[_lines.Count - 1], resultMessage, StringComparison.Ordinal)) + builder.AppendLine(); + if (!string.Equals(_lines[_lines.Count - 1], resultMessage, StringComparison.Ordinal)) + builder.Append(resultMessage); + } + + return builder.ToString(); + } + + private static string ResolveDisplayName(ShrinkNetworkContext context, ShrinkNetworkCommandRequest request) + { + if (context.Session.Items.TryGetValue("auth.name", out var authName) && authName is string authNameText && + !string.IsNullOrWhiteSpace(authNameText)) + { + return authNameText.Trim(); + } + + if (!string.IsNullOrWhiteSpace(request.ClientLabel)) + return request.ClientLabel.Trim(); + + return context.Session.RemoteAddress; + } + } +} diff --git a/ShrinkNetworkCommandSource.cs.meta b/ShrinkNetworkCommandSource.cs.meta new file mode 100644 index 0000000..91b91b9 --- /dev/null +++ b/ShrinkNetworkCommandSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf31a6f53f1bb0c4ca0af60b92dce81f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..5ea746b --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9777f2e6be37f64e8ec2df9b9afef87 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/IntegrationComponentsTests.cs b/Tests/IntegrationComponentsTests.cs new file mode 100644 index 0000000..df1664e --- /dev/null +++ b/Tests/IntegrationComponentsTests.cs @@ -0,0 +1,151 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using NUnit.Framework; +using ShrinkCommand.Integration; +using ShrinkCommand.Integration.App; +using ShrinkContext; +using ShrinkDataSaver.Integration.App; +using ShrinkNetwork; +using ShrinkNetwork.Integration.App; + +namespace ShrinkCommand.Integration.Network.Tests +{ + /// + /// 阶段 3 集成组件模式(论文 6.5): + /// 核心(command/network/datasaver)互不依赖,集成组件注入双方服务键; + /// 全部提供者就绪才激活,任一退役即停用——依赖接线由响应式余效应完成。 + /// + public class IntegrationComponentsTests + { + private ShrinkContextRuntime _runtime = null!; + + [SetUp] + public void SetUp() + { + _runtime = new ShrinkContextRuntime(); + } + + [TearDown] + public void TearDown() + { + _runtime.ShutdownAsync().GetAwaiter().GetResult(); + } + + private static void Run(UniTask task) => task.GetAwaiter().GetResult(); + + [Test] + public void Integration_ActivatesOnlyWhenBothProvidersActive() + { + var integration = _runtime.Use(new ShrinkCommandNetworkIntegrationComponent()); + Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "两个服务键都缺失时等待"); + + var command = _runtime.Use(new ShrinkCommandAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, command.State); + Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "仅命令服务就绪仍等待网络服务"); + + var network = _runtime.Use(new ShrinkNetworkAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, network.State); + Assert.AreEqual(ShrinkFiberState.Active, integration.State, + "双键齐备后集成组件激活并完成桥注册"); + } + + [Test] + public void EitherProviderRetire_DeactivatesIntegration_AndReactivatesOnReturn() + { + var command = _runtime.Use(new ShrinkCommandAppComponent()); + var network = _runtime.Use(new ShrinkNetworkAppComponent()); + var integration = _runtime.Use(new ShrinkCommandNetworkIntegrationComponent()); + Assert.AreEqual(ShrinkFiberState.Active, integration.State); + + Run(_runtime.RetireAsync(network)); + Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, + "网络提供者退役,集成组件自动停用(服务键撤回触发)"); + Assert.AreEqual(ShrinkFiberState.Active, command.State, "命令核心不受影响"); + + var networkAgain = _runtime.Use(new ShrinkNetworkAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, integration.State, + "提供者回归后集成组件自动重新激活"); + } + + [Test] + public void CommandComponent_ProvidesDefaultServiceKey() + { + var fiber = _runtime.Use(new ShrinkCommandAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, fiber.State); + Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkCommandAppComponent.ServiceKey, out var service)); + Assert.AreSame(ShrinkCommandRuntime.Default, service); + + Run(_runtime.RetireAsync(fiber)); + Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkCommandAppComponent.ServiceKey, out _), "退役撤回服务键"); + } + + [Test] + public void DataSaverComponent_Activates_FlushesOnDeactivate_WithdrawsKey() + { + var fiber = _runtime.Use(new ShrinkDataSaverAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, fiber.State); + Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkDataSaverAppComponent.ServiceKey, out _)); + + Assert.DoesNotThrow(() => Run(_runtime.RetireAsync(fiber)), + "停用执行补偿式收尾(设置最终落盘;未初始化环境安全跳过)"); + Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkDataSaverAppComponent.ServiceKey, out _)); + } + + [Test] + public void DataSaverIntercept_ReadOnlyViewAllowsReadsAndDeniesWriterCapability() + { + var provider = _runtime.Use(new ShrinkDataSaverAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, provider.State); + + var intercept = new Dictionary> + { + [ShrinkDataSaverAppComponent.ServiceKey] = new Dictionary + { + [ShrinkDataSaverAccess.MetadataKey] = ShrinkDataSaverAccess.ReadOnly + } + }; + var consumerComponent = new DataSaverAccessConsumer(); + var consumer = _runtime.Use(consumerComponent, intercept: intercept); + + Assert.AreEqual(ShrinkFiberState.Active, consumer.State); + var reader = consumerComponent.GetReader(); + Assert.IsNotNull(reader); + Assert.IsFalse(reader is IShrinkDataSaverWriter, + "只读上下文取得的对象不应再暴露写接口"); + Assert.Throws(() => consumerComponent.GetWriter()); + + Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkDataSaverAppComponent.ServiceKey, out _), + "编排层 raw read 保留原始服务;intercept 只介导组件访问"); + } + + private sealed class DataSaverAccessConsumer : IShrinkComponent + { + private ShrinkCtx? _ctx; + + public string Name => "test.datasaver.read-only-consumer"; + public IReadOnlyList Inject { get; } = new[] { ShrinkDataSaverAppComponent.ServiceKey }; + public IReadOnlyList Provide => Array.Empty(); + + public UniTask ApplyAsync(ShrinkCtx ctx, object? config) + { + _ctx = ctx; + return UniTask.CompletedTask; + } + + public IShrinkDataSaverReader GetReader() => Context.Get( + ShrinkDataSaverAppComponent.ServiceKey); + + public IShrinkDataSaverWriter GetWriter() => Context.Get( + ShrinkDataSaverAppComponent.ServiceKey); + + private ShrinkCtx Context => _ctx ?? throw new InvalidOperationException("Consumer is not active."); + } + } +} diff --git a/Tests/IntegrationComponentsTests.cs.meta b/Tests/IntegrationComponentsTests.cs.meta new file mode 100644 index 0000000..a08526a --- /dev/null +++ b/Tests/IntegrationComponentsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c9445def3a931642955715272969a72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/ShrinkCommand.Integration.Network.Tests.asmdef b/Tests/ShrinkCommand.Integration.Network.Tests.asmdef new file mode 100644 index 0000000..d4ca15b --- /dev/null +++ b/Tests/ShrinkCommand.Integration.Network.Tests.asmdef @@ -0,0 +1,28 @@ +{ + "name": "ShrinkCommand.Integration.Network.Tests", + "rootNamespace": "ShrinkCommand.Integration.Network.Tests", + "references": [ + "ShrinkCommand.Integration.Network", + "ShrinkCommand.Integration.App", + "ShrinkDataSaver.Integration.App", + "ShrinkNetwork.Integration.App", + "ShrinkCommand.Runtime", + "ShrinkNetwork.Runtime", + "ShrinkContext.Core.Runtime", + "UniTask", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/ShrinkCommand.Integration.Network.Tests.asmdef.meta b/Tests/ShrinkCommand.Integration.Network.Tests.asmdef.meta new file mode 100644 index 0000000..3e0e6bd --- /dev/null +++ b/Tests/ShrinkCommand.Integration.Network.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dd3e105f80569534b9ba2d6fa913a2cc +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..7d709a8 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "com.cneicy.shrink-command-integration-network", + "version": "0.1.0", + "displayName": "ShrinkCommand - Network Integration", + "description": "ShrinkCommand 与 ShrinkNetwork 的桥接层,允许远程会话通过 RPC 执行命令。", + "unity": "2022.3", + "dependencies": { + "com.cneicy.shrink-command": "0.2.0", + "com.cneicy.shrink-network": "0.2.0", + "com.cneicy.shrink-context-core": "0.1.0" + }, + "keywords": [ + "command", + "network", + "integration", + "rpc" + ], + "author": { + "name": "cneicy", + "url": "https://git.crash.work/ShrinkSDK" + }, + "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network", + "changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network/src/branch/main/CHANGELOG.md" +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..22d2898 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 73eb22917e75fcf4b8f1c7df5490014e +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: