diff --git a/Convention/[Runtime]/Interaction.cs b/Convention/[Runtime]/Interaction.cs
new file mode 100644
index 0000000..e448380
--- /dev/null
+++ b/Convention/[Runtime]/Interaction.cs
@@ -0,0 +1,679 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace Convention
+{
+ ///
+ /// 统一的文件交互类,支持本地文件、网络文件和localhost路径的自适应处理
+ /// 使用UnityWebRequest实现跨平台兼容,支持WebGL和IL2CPP
+ ///
+ /// 是依赖项
+ ///
+ [Serializable]
+ public sealed class Interaction
+ {
+ #region Fields and Properties
+
+ private string originalPath;
+ private string processedPath;
+ private PathType pathType;
+ private object cachedData;
+
+ public string OriginalPath => originalPath;
+ public string ProcessedPath => processedPath;
+ public PathType Type => pathType;
+
+ public enum PathType
+ {
+ LocalFile, // 本地文件 (file://)
+ NetworkHTTP, // 网络HTTP (http://)
+ NetworkHTTPS, // 网络HTTPS (https://)
+ LocalServer, // 本地服务器 (localhost)
+ StreamingAssets // StreamingAssets目录
+ }
+
+ #endregion
+
+ public Interaction(string path)
+ {
+ SetPath(path);
+ }
+
+ #region Setup
+
+ ///
+ /// 设置并处理路径
+ ///
+ public Interaction SetPath(string path)
+ {
+ originalPath = path;
+ processedPath = ProcessPath(path);
+ pathType = DeterminePathType(path);
+ return this;
+ }
+
+ ///
+ /// 处理路径,转换为UnityWebRequest可识别的格式
+ ///
+ private string ProcessPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ return path;
+
+ // 网络路径直接返回
+ if (path.StartsWith("http://") || path.StartsWith("https://"))
+ {
+ return path;
+ }
+
+ // localhost处理
+ if (path.StartsWith("localhost"))
+ {
+ return path.StartsWith("localhost/") ? "http://" + path : "http://localhost/" + path;
+ }
+
+ // StreamingAssets路径处理
+ if (path.StartsWith("StreamingAssets/") || path.StartsWith("StreamingAssets\\"))
+ {
+ return Path.Combine(Application.streamingAssetsPath, path.Substring(16)).Replace("\\", "/");
+ }
+
+ // 本地文件路径处理
+ string fullPath = Path.IsPathRooted(path) ? path : Path.GetFullPath(path);
+
+ // WebGL平台特殊处理
+ if (Application.platform == RuntimePlatform.WebGLPlayer)
+ {
+ // WebGL只能访问StreamingAssets,尝试构建StreamingAssets路径
+ return Application.streamingAssetsPath + "/" + Path.GetFileName(fullPath);
+ }
+
+ // 其他平台使用file://协议
+ return "file:///" + fullPath.Replace("\\", "/");
+ }
+
+ ///
+ /// 确定路径类型
+ ///
+ private PathType DeterminePathType(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ return PathType.LocalFile;
+
+ if (path.StartsWith("https://"))
+ return PathType.NetworkHTTPS;
+ if (path.StartsWith("http://"))
+ return PathType.NetworkHTTP;
+ if (path.StartsWith("localhost"))
+ return PathType.LocalServer;
+ if (path.StartsWith("StreamingAssets"))
+ return PathType.StreamingAssets;
+
+ return PathType.LocalFile;
+ }
+
+ #endregion
+
+ #region Load
+
+ #region LoadAsync
+
+ public IEnumerator LoadAsTextAsync(Action onSuccess, Action onError = null)
+ {
+ using UnityWebRequest request = UnityWebRequest.Get(processedPath);
+ yield return request.SendWebRequest();
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ cachedData = request.downloadHandler.text;
+ onSuccess?.Invoke((string)cachedData);
+ }
+ else
+ {
+ string errorMsg = $"Failed to load text from {originalPath}: {request.error}";
+ onError?.Invoke(errorMsg);
+ }
+ }
+
+ public IEnumerator LoadAsBinaryAsync(Action onSuccess, Action onError = null)
+ {
+ using UnityWebRequest request = UnityWebRequest.Get(processedPath);
+ yield return request.SendWebRequest();
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ cachedData = request.downloadHandler.data;
+ onSuccess?.Invoke((byte[])cachedData);
+ }
+ else
+ {
+ string errorMsg = $"Failed to load binary from {originalPath}: {request.error}";
+ onError?.Invoke(errorMsg);
+ }
+ }
+ public IEnumerator LoadAsBinaryAsync(Action progress, Action onSuccess, Action onError = null)
+ {
+ using UnityWebRequest request = UnityWebRequest.Get(processedPath);
+ var result = request.SendWebRequest();
+ while (result.isDone == false)
+ {
+ progress(result.progress);
+ yield return null;
+ }
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ cachedData = request.downloadHandler.data;
+ onSuccess?.Invoke((byte[])cachedData);
+ }
+ else
+ {
+ string errorMsg = $"Failed to load binary from {originalPath}: {request.error}";
+ onError?.Invoke(errorMsg);
+ }
+ }
+
+ public IEnumerator LoadAsRawJsonAsync(Action onSuccess, Action onError = null)
+ {
+ yield return LoadAsTextAsync(
+ text =>
+ {
+ try
+ {
+ T result = JsonUtility.FromJson(text);
+ cachedData = result;
+ onSuccess?.Invoke(result);
+ }
+ catch (Exception e)
+ {
+ onError?.Invoke($"Failed to parse JSON: {e.Message}");
+ }
+ },
+ onError
+ );
+ }
+
+ public IEnumerator LoadAsJsonAsync(Action onSuccess, Action onError = null)
+ {
+ yield return LoadAsTextAsync(
+ text =>
+ {
+ try
+ {
+ using StreamReader reader = new(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(text)));
+ var jsonReader = new ES3Internal.ES3JSONReader(reader.BaseStream, new(originalPath));
+ onSuccess?.Invoke(jsonReader.Read());
+ }
+ catch (Exception e)
+ {
+ onError?.Invoke($"Failed to parse JSON: {e.Message}");
+ }
+ },
+ onError
+ );
+ }
+
+ public IEnumerator LoadAsImageAsync(Action onSuccess, Action onError = null)
+ {
+ using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(processedPath))
+ {
+ yield return request.SendWebRequest();
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ Texture2D texture = DownloadHandlerTexture.GetContent(request);
+ cachedData = texture;
+ onSuccess?.Invoke(texture);
+ }
+ else
+ {
+ string errorMsg = $"Failed to load image from {originalPath}: {request.error}";
+ onError?.Invoke(errorMsg);
+ }
+ }
+ }
+
+ public IEnumerator LoadAsAudioAsync(Action onSuccess, Action onError = null, AudioType audioType = AudioType.UNKNOWN)
+ {
+ if (audioType == AudioType.UNKNOWN)
+ audioType = GetAudioType(originalPath);
+
+ using (UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(processedPath, audioType))
+ {
+ yield return request.SendWebRequest();
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ AudioClip audioClip = DownloadHandlerAudioClip.GetContent(request);
+ cachedData = audioClip;
+ onSuccess?.Invoke(audioClip);
+ }
+ else
+ {
+ string errorMsg = $"Failed to load audio from {originalPath}: {request.error}";
+ onError?.Invoke(errorMsg);
+ }
+ }
+ }
+
+ public IEnumerator LoadAsAssetBundle(Action progress, Action onSuccess, Action onError = null)
+ {
+ yield return LoadAsBinaryAsync(
+ progress,
+ data =>
+ {
+ try
+ {
+ AssetBundle bundle = AssetBundle.LoadFromMemory(data);
+ if (bundle != null)
+ {
+ cachedData = bundle;
+ onSuccess?.Invoke(bundle);
+ }
+ else
+ {
+ onError?.Invoke($"Failed to load AssetBundle from data.");
+ }
+ }
+ catch (Exception e)
+ {
+ onError?.Invoke($"Failed to load AssetBundle: {e.Message}");
+ }
+ },
+ onError
+ );
+ }
+
+ #endregion
+
+ #region Load Sync
+
+ public string LoadAsText()
+ {
+ string buffer = null;
+ bool isEnd = false;
+ bool IsError = false;
+ var it = LoadAsTextAsync(x =>
+ {
+ buffer = x;
+ isEnd = true;
+ }, e =>
+ {
+ IsError = true;
+ throw new Exception(e);
+ });
+ try
+ {
+ while (!isEnd)
+ {
+ it.MoveNext();
+ }
+ }
+ catch
+ {
+ if(IsError)
+ {
+ throw;
+ }
+ }
+ return buffer;
+ }
+
+ public byte[] LoadAsBinary()
+ {
+ byte[] buffer = null;
+ bool isEnd = false;
+ bool IsError = false;
+ var it = LoadAsBinaryAsync(x =>
+ {
+ buffer = x;
+ isEnd = true;
+ }, e =>
+ {
+ IsError = true;
+ throw new Exception(e);
+ });
+ try
+ {
+ while (!isEnd)
+ {
+ it.MoveNext();
+ }
+ }
+ catch
+ {
+ if (IsError)
+ {
+ throw;
+ }
+ }
+ return buffer;
+ }
+
+ public T LoadAsRawJson()
+ {
+ T buffer = default;
+ bool isEnd = false;
+ bool IsError = false;
+ var it = LoadAsRawJsonAsync(x =>
+ {
+ buffer = x;
+ isEnd = true;
+ }, e =>
+ {
+ IsError = true;
+ throw new Exception(e);
+ });
+ try
+ {
+ while (!isEnd)
+ {
+ it.MoveNext();
+ }
+ }
+ catch
+ {
+ if (IsError)
+ {
+ throw;
+ }
+ }
+ return buffer;
+ }
+
+ public T LoadAsJson()
+ {
+ T buffer = default;
+ bool isEnd = false;
+ bool IsError = false;
+ var it = LoadAsJsonAsync(x =>
+ {
+ buffer = x;
+ isEnd = true;
+ }, e =>
+ {
+ IsError = true;
+ throw new Exception(e);
+ });
+ try
+ {
+ while (!isEnd)
+ {
+ it.MoveNext();
+ }
+ }
+ catch
+ {
+ if (IsError)
+ {
+ throw;
+ }
+ }
+ return buffer;
+ }
+
+ public Texture2D LoadAsImage()
+ {
+ Texture2D buffer = null;
+ bool isEnd = false;
+ bool IsError = false;
+ var it = LoadAsImageAsync(x =>
+ {
+ buffer = x;
+ isEnd = true;
+ }, e =>
+ {
+ IsError = true;
+ throw new Exception(e);
+ });
+ try
+ {
+ while (!isEnd)
+ {
+ it.MoveNext();
+ }
+ }
+ catch
+ {
+ if (IsError)
+ {
+ throw;
+ }
+ }
+ return buffer;
+ }
+
+ public AudioClip LoadAsAudio(AudioType audioType = AudioType.UNKNOWN)
+ {
+ AudioClip buffer = null;
+ bool isEnd = false;
+ bool IsError = false;
+ var it = LoadAsAudioAsync(x =>
+ {
+ buffer = x;
+ isEnd = true;
+ }, e =>
+ {
+ IsError = true;
+ throw new Exception(e);
+ }, audioType);
+ try
+ {
+ while (!isEnd)
+ {
+ it.MoveNext();
+ }
+ }
+ catch
+ {
+ if (IsError)
+ {
+ throw;
+ }
+ }
+ return buffer;
+ }
+
+ #endregion
+
+ #endregion
+
+ #region Save
+
+ public Interaction SaveAsText(string content)
+ {
+ if (Application.platform == RuntimePlatform.WebGLPlayer)
+ {
+ throw new NotSupportedException("WebGL平台不支持文件保存。");
+ }
+
+ if (pathType != PathType.LocalFile)
+ {
+ throw new NotSupportedException("仅支持保存到本地文件路径。");
+ }
+
+ new ToolFile(originalPath).SaveAsText(content);
+ return this;
+
+ }
+
+ public Interaction SaveAsBinary(byte[] data)
+ {
+ if (Application.platform == RuntimePlatform.WebGLPlayer)
+ {
+ throw new NotSupportedException("WebGL平台不支持文件保存。");
+ }
+
+ if (pathType != PathType.LocalFile)
+ {
+ throw new NotSupportedException("仅支持保存到本地文件路径。");
+ }
+
+ new ToolFile(originalPath).SaveAsBinary(data);
+ return this;
+ }
+
+ public Interaction SaveAsRawJson(T obj)
+ {
+ new ToolFile(originalPath).SaveAsRawJson(obj);
+ return this;
+ }
+
+ public Interaction SaveAsJson(T obj)
+ {
+ new ToolFile(originalPath).SaveAsJson(obj);
+ return this;
+ }
+
+ #endregion
+
+ #region Tools
+
+ ///
+ /// 获取本地文件路径
+ ///
+ private string GetLocalPath()
+ {
+ if (processedPath.StartsWith("file:///"))
+ {
+ return processedPath.Substring(8);
+ }
+ return originalPath;
+ }
+
+ ///
+ /// 根据文件扩展名获取音频类型
+ ///
+ private AudioType GetAudioType(string path)
+ {
+ return BasicAudioSystem.GetAudioType(path);
+ }
+
+ ///
+ /// 检查文件是否存在
+ ///
+ public bool Exists()
+ {
+ if (pathType == PathType.LocalFile && Application.platform != RuntimePlatform.WebGLPlayer)
+ {
+ return File.Exists(GetLocalPath());
+ }
+ else
+ {
+ // TODO : 网络文件和WebGL平台需要通过请求检查
+ // 当前默认存在
+ return true;
+ }
+ }
+
+ ///
+ /// 异步检查文件是否存在
+ ///
+ public IEnumerator ExistsAsync(Action callback)
+ {
+ using UnityWebRequest request = UnityWebRequest.Head(processedPath);
+ yield return request.SendWebRequest();
+ callback?.Invoke(request.result == UnityWebRequest.Result.Success);
+ }
+
+ #endregion
+
+ #region Operator
+
+ public static implicit operator string(Interaction interaction) => interaction?.originalPath;
+
+ public override string ToString() => originalPath;
+
+ ///
+ /// 路径连接操作符
+ ///
+ public static Interaction operator |(Interaction left, string rightPath)
+ {
+ if (left.pathType == PathType.NetworkHTTP || left.pathType == PathType.NetworkHTTPS || left.pathType == PathType.LocalServer)
+ {
+ string baseUrl = left.originalPath;
+ string newUrl = baseUrl.EndsWith("/") ? baseUrl + rightPath : baseUrl + "/" + rightPath;
+ return new Interaction(newUrl);
+ }
+ else
+ {
+ string newPath = Path.Combine(left.originalPath, rightPath);
+ return new Interaction(newPath);
+ }
+ }
+
+ #endregion
+
+ #region Tools
+
+ ///
+ /// 获取文件名
+ ///
+ public string GetFilename()
+ {
+ if (pathType == PathType.NetworkHTTP || pathType == PathType.NetworkHTTPS || pathType == PathType.LocalServer)
+ {
+ var uri = new Uri(processedPath);
+ return Path.GetFileName(uri.AbsolutePath);
+ }
+ return Path.GetFileName(originalPath);
+ }
+
+ ///
+ /// 获取文件扩展名
+ ///
+ public string GetExtension()
+ {
+ return Path.GetExtension(GetFilename());
+ }
+
+ ///
+ /// 检查扩展名
+ ///
+ public bool ExtensionIs(params string[] extensions)
+ {
+ string ext = GetExtension().ToLower();
+ string extWithoutDot = ext.Length > 1 ? ext[1..] : "";
+
+ foreach (string extension in extensions)
+ {
+ string checkExt = extension.ToLower();
+ if (ext == checkExt || extWithoutDot == checkExt || ext == "." + checkExt)
+ return true;
+ }
+ return false;
+ }
+
+ #endregion
+
+ #region Tools
+
+ ///
+ /// 创建一个新的Interaction实例
+ ///
+ public static Interaction Create(string path) => new(path);
+
+ ///
+ /// 从StreamingAssets创建
+ ///
+ public static Interaction FromStreamingAssets(string relativePath)
+ {
+ return new Interaction("StreamingAssets/" + relativePath);
+ }
+
+ ///
+ /// 从本地服务器创建
+ ///
+ public static Interaction FromLocalhost(string path, int port = 80)
+ {
+ string url = port == 80 ? $"localhost/{path}" : $"localhost:{port}/{path}";
+ return new Interaction(url);
+ }
+
+ #endregion
+ }
+}