com.alicizax.unity.packagem.../Editor/PackageManager/RepositoryDataFetcher.cs

359 lines
12 KiB
C#
Raw Normal View History

2026-04-17 14:21:41 +08:00
using System;
2025-02-20 10:48:57 +08:00
using System.Collections;
using System.Collections.Generic;
2026-04-17 14:21:41 +08:00
using System.Text;
2025-02-20 10:48:57 +08:00
using Newtonsoft.Json;
using UnityEditor;
2026-04-17 14:21:41 +08:00
using UnityEngine;
using UnityEngine.Networking;
2025-02-20 10:48:57 +08:00
namespace AlicizaX.PackageManager.Editor
{
public enum PackageState
{
UnInstall,
InstallLocal,
Install,
Update
}
[Serializable]
public class RepositoryPackageData
{
public string name;
public string description;
public string cloneUrl;
public string htmlUrl;
public string updatedAt;
2026-04-17 14:21:41 +08:00
public string defaultBranch;
public string apiUrl;
public string remoteHead;
public string ownerName;
2025-02-20 10:48:57 +08:00
public string displayName;
public string version;
public string unityVersion;
public string category;
public string packageDescription;
public string authorName;
2026-04-17 14:21:41 +08:00
public string ManifestVersion;
public string InstalledVersion;
public string InstalledHash;
public string InstalledSource;
2025-02-20 10:48:57 +08:00
public PackageState PackageState;
}
[Serializable]
public class RepoApiResponse
{
public bool ok;
public List<RepoItem> data;
}
[Serializable]
public class RepoItem
{
public string name;
public string description;
public string clone_url;
public string html_url;
public string updated_at;
2026-04-17 14:21:41 +08:00
public string default_branch;
public string url;
2025-02-20 10:48:57 +08:00
public Owner owner;
[Serializable]
public class Owner
{
public string login;
}
}
2026-04-17 14:21:41 +08:00
[Serializable]
public class RepoBranchResponse
{
public string name;
public BranchCommit commit;
[Serializable]
public class BranchCommit
{
public string id;
}
}
[Serializable]
public class PackageJsonContentResponse
{
public string content;
public string encoding;
}
2025-02-20 10:48:57 +08:00
[Serializable]
public class PackageJsonResponse
{
public string name;
public string displayName;
public string category;
public string description;
public string version;
public string unity;
public Author author;
[Serializable]
public class Author
{
public string name;
}
}
2026-04-17 14:21:41 +08:00
internal sealed class PackageManagerCoroutines : MonoBehaviour
2025-11-17 20:28:48 +08:00
{
public static PackageManagerCoroutines Coroutines;
public static void CreateCoroutines()
{
2026-04-17 14:21:41 +08:00
if (Coroutines != null)
2025-11-17 20:28:48 +08:00
{
2026-04-17 14:21:41 +08:00
return;
2025-11-17 20:28:48 +08:00
}
2026-04-17 14:21:41 +08:00
var gameObject = new GameObject("PackageManagerCoroutines");
gameObject.hideFlags = HideFlags.HideAndDontSave;
Coroutines = gameObject.AddComponent<PackageManagerCoroutines>();
2025-11-17 20:28:48 +08:00
}
public static void DestroyCoroutines()
{
2026-04-17 14:21:41 +08:00
if (Coroutines == null)
2025-11-17 20:28:48 +08:00
{
2026-04-17 14:21:41 +08:00
return;
2025-11-17 20:28:48 +08:00
}
2026-04-17 14:21:41 +08:00
Coroutines.StopAllCoroutines();
DestroyImmediate(Coroutines.gameObject);
Coroutines = null;
2025-11-17 20:28:48 +08:00
}
}
2025-02-20 10:48:57 +08:00
public static class RepositoryDataFetcher
{
private const string BaseApiUrl = "http://101.34.252.46:3000/api/v1/repos/search?q=unity&topic=false";
2026-04-17 14:21:41 +08:00
private const string PackageJsonPathTemplate = "/contents/package.json?ref={0}";
private const string BranchPathTemplate = "/branches/{0}";
private const string LastUpdateEditorPrefsKey = "PackageUpdateDate";
2025-02-20 10:48:57 +08:00
public static void FetchRepositoryData(Action<List<RepositoryPackageData>> callback)
{
2025-11-17 20:28:48 +08:00
PackageManagerCoroutines.CreateCoroutines();
PackageManagerCoroutines.Coroutines.StartCoroutine(FetchDataRoutine(callback));
2025-02-20 10:48:57 +08:00
}
2026-04-17 14:21:41 +08:00
public static string GetLastRefreshTime()
{
return EditorPrefs.GetString(LastUpdateEditorPrefsKey, string.Empty);
}
2025-02-20 10:48:57 +08:00
private static IEnumerator FetchDataRoutine(Action<List<RepositoryPackageData>> callback)
{
2026-04-17 14:21:41 +08:00
List<RepositoryPackageData> results = null;
Exception fatalError = null;
2025-02-20 10:48:57 +08:00
using (var request = CreateWebRequest(BaseApiUrl))
{
2026-04-17 14:21:41 +08:00
yield return request.SendWebRequest();
if (!TryValidateRequest(request, "Repository request failed"))
{
fatalError = new Exception(request.error);
}
else
{
RepoApiResponse response = null;
try
{
response = JsonConvert.DeserializeObject<RepoApiResponse>(request.downloadHandler.text);
}
catch (Exception e)
{
fatalError = e;
}
2025-02-20 10:48:57 +08:00
2026-04-17 14:21:41 +08:00
if (fatalError == null)
{
if (response?.ok != true || response.data == null)
{
fatalError = new Exception("Repository response is invalid.");
}
else
{
results = new List<RepositoryPackageData>(response.data.Count);
foreach (var repo in response.data)
{
var data = CreateBaseRepositoryData(repo);
yield return FetchBranchHeadRoutine(data);
yield return FetchPackageInfoRoutine(data);
results.Add(data);
}
}
}
}
}
if (fatalError != null)
{
Debug.LogError($"Fetch repository data failed: {fatalError.Message}");
callback?.Invoke(new List<RepositoryPackageData>());
}
else
{
EditorPrefs.SetString(LastUpdateEditorPrefsKey, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
callback?.Invoke(results ?? new List<RepositoryPackageData>());
}
PackageManagerCoroutines.DestroyCoroutines();
}
private static IEnumerator FetchBranchHeadRoutine(RepositoryPackageData packageData)
{
if (string.IsNullOrEmpty(packageData.apiUrl) || string.IsNullOrEmpty(packageData.defaultBranch))
{
yield break;
}
var branchUrl = packageData.apiUrl + string.Format(BranchPathTemplate, UnityWebRequest.EscapeURL(packageData.defaultBranch));
using (var request = CreateWebRequest(branchUrl))
{
2025-11-17 20:28:48 +08:00
yield return request.SendWebRequest();
2026-04-17 14:21:41 +08:00
if (!TryValidateRequest(request, $"Branch request failed for {packageData.name}"))
2025-02-20 10:48:57 +08:00
{
yield break;
}
2026-04-17 14:21:41 +08:00
try
2025-02-20 10:48:57 +08:00
{
2026-04-17 14:21:41 +08:00
var branch = JsonConvert.DeserializeObject<RepoBranchResponse>(request.downloadHandler.text);
packageData.remoteHead = branch?.commit?.id;
}
catch (Exception e)
{
Debug.LogError($"Branch response parse failed for {packageData.name}: {e.Message}");
2025-02-20 10:48:57 +08:00
}
2026-04-17 14:21:41 +08:00
}
}
2025-02-20 10:48:57 +08:00
2026-04-17 14:21:41 +08:00
private static IEnumerator FetchPackageInfoRoutine(RepositoryPackageData packageData)
{
if (string.IsNullOrEmpty(packageData.apiUrl) || string.IsNullOrEmpty(packageData.defaultBranch))
{
yield break;
}
2025-02-20 10:48:57 +08:00
2026-04-17 14:21:41 +08:00
var packageUrl = packageData.apiUrl + string.Format(PackageJsonPathTemplate, UnityWebRequest.EscapeURL(packageData.defaultBranch));
using (var request = CreateWebRequest(packageUrl))
{
yield return request.SendWebRequest();
if (!TryValidateRequest(request, $"Package request failed for {packageData.name}"))
2025-02-20 10:48:57 +08:00
{
2026-04-17 14:21:41 +08:00
yield break;
}
2025-11-17 20:28:48 +08:00
2026-04-17 14:21:41 +08:00
try
{
var contentResponse = JsonConvert.DeserializeObject<PackageJsonContentResponse>(request.downloadHandler.text);
var packageJson = DecodeContent(contentResponse);
if (string.IsNullOrEmpty(packageJson))
2025-02-20 10:48:57 +08:00
{
2026-04-17 14:21:41 +08:00
yield break;
2025-02-20 10:48:57 +08:00
}
2026-04-17 14:21:41 +08:00
var packageInfo = JsonConvert.DeserializeObject<PackageJsonResponse>(packageJson);
UpdateWithPackageInfo(packageData, packageInfo);
}
catch (Exception e)
{
Debug.LogError($"Package json parse failed for {packageData.name}: {e.Message}");
}
2025-02-20 10:48:57 +08:00
}
}
private static UnityWebRequest CreateWebRequest(string url)
{
2026-04-17 14:21:41 +08:00
var request = UnityWebRequest.Get(url);
2025-02-20 10:48:57 +08:00
request.downloadHandler = new DownloadHandlerBuffer();
2026-04-17 14:21:41 +08:00
request.timeout = 20;
2025-02-20 10:48:57 +08:00
request.SetRequestHeader("accept", "application/json");
return request;
}
2026-04-17 14:21:41 +08:00
private static bool TryValidateRequest(UnityWebRequest request, string errorMessage)
2025-02-20 10:48:57 +08:00
{
2026-04-17 14:21:41 +08:00
if (request.result == UnityWebRequest.Result.Success)
2025-02-20 10:48:57 +08:00
{
2026-04-17 14:21:41 +08:00
return true;
2025-02-20 10:48:57 +08:00
}
2026-04-17 14:21:41 +08:00
Debug.LogError($"{errorMessage}: {request.error}");
return false;
2025-02-20 10:48:57 +08:00
}
private static RepositoryPackageData CreateBaseRepositoryData(RepoItem repo)
{
return new RepositoryPackageData
{
name = repo.name,
description = repo.description,
cloneUrl = repo.clone_url,
htmlUrl = repo.html_url,
2026-04-17 14:21:41 +08:00
updatedAt = repo.updated_at,
defaultBranch = string.IsNullOrEmpty(repo.default_branch) ? "main" : repo.default_branch,
apiUrl = repo.url,
ownerName = repo.owner?.login
2025-02-20 10:48:57 +08:00
};
}
2026-04-17 14:21:41 +08:00
private static void UpdateWithPackageInfo(RepositoryPackageData data, PackageJsonResponse package)
2025-02-20 10:48:57 +08:00
{
2026-04-17 14:21:41 +08:00
if (package == null)
{
return;
}
data.name = string.IsNullOrEmpty(package.name) ? data.name : package.name;
data.displayName = string.IsNullOrEmpty(package.displayName) ? data.name : package.displayName;
2025-02-20 10:48:57 +08:00
data.version = package.version;
data.unityVersion = package.unity;
data.category = package.category;
data.packageDescription = package.description;
2026-04-17 14:21:41 +08:00
data.authorName = package.author?.name;
}
private static string DecodeContent(PackageJsonContentResponse contentResponse)
{
if (contentResponse == null || string.IsNullOrEmpty(contentResponse.content))
{
return null;
}
if (!string.Equals(contentResponse.encoding, "base64", StringComparison.OrdinalIgnoreCase))
{
return contentResponse.content;
}
2025-02-20 10:48:57 +08:00
2026-04-17 14:21:41 +08:00
try
{
var normalized = contentResponse.content.Replace("\n", string.Empty).Replace("\r", string.Empty);
var bytes = Convert.FromBase64String(normalized);
return Encoding.UTF8.GetString(bytes);
}
catch (Exception e)
2025-02-20 10:48:57 +08:00
{
2026-04-17 14:21:41 +08:00
Debug.LogError($"Decode package json content failed: {e.Message}");
return null;
2025-02-20 10:48:57 +08:00
}
}
}
}