chore(bridge): 删除桥接包的提交记录和Git属性配置

删除了 .codely-bridge-commit 文件中存储的提交哈希值
移除了 .gitattributes 文件中的行结束符配置规则
```
This commit is contained in:
huadexuan committed 2026-09-24 21:41:14 +08:00
1 parent d26d323912
commit 13d0c80335
392 files changed
+1814 -1895

No files matched your search

@@ -1,127 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityTcp.Editor.Helpers;
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
using Cn.Tuanjie.Codely.Editor;
#endif
namespace UnityTcp.Editor.Notifier
{
/// <summary>
/// Pushes messages from the Editor/engine into the input field of the
/// embedded AI agent (CodelyWindow) as an "input_from_editor" event:
/// user-typed messages, engine errors, stack traces, or any other info the
/// agent should react to. Distinguish the kind of message via the
/// <c>source</c>/<c>type</c> arguments.
///
/// If no CodelyWindow is open, one is opened and the message is queued
/// instead of broadcast; the frontend drains the queue via manage_editor
/// action "drain_agent_input" once its page is fully loaded, so cold-start
/// messages are not lost. With a window already open, the message is
/// broadcast directly.
///
/// Payload shape: <c>{ text, source, type, autoSubmit, timestamp, attachments? }</c>.
/// </summary>
public static class AgentInputNotifier
{
private const string EventType = "input_from_editor";
private const int MaxPendingMessages = 100;
private static readonly object _gate = new object();
private static readonly List<JObject> _pending = new List<JObject>();
/// <summary>
/// Sends <paramref name="text"/> to the embedded agent's input field.
/// Must be called from the main thread (may open an EditorWindow).
/// </summary>
/// <param name="text">Message to place into the agent input field.</param>
/// <param name="source">Origin of the message; defaults to "editor_window".</param>
/// <param name="type">Kind of message; defaults to "user_message".</param>
/// <param name="autoSubmit">True to have the frontend submit the message immediately instead of just appending it.</param>
/// <param name="timestamp">Time the message was created; defaults to now (UTC).</param>
/// <param name="attachments">Optional file paths the frontend attaches (@) to the message; may be null.</param>
/// <returns>
/// True if broadcast to at least one client; false if invalid or queued
/// for the frontend to drain (no CodelyWindow was open yet).
/// </returns>
public static bool Send(
string text,
string source = "editor_window",
string type = "user_message",
bool autoSubmit = false,
DateTime? timestamp = null,
IReadOnlyList<string> attachments = null)
{
if (string.IsNullOrWhiteSpace(text)) return false;
try
{
var payload = new JObject
{
["text"] = text,
["source"] = source,
["type"] = type,
["autoSubmit"] = autoSubmit,
["timestamp"] = (timestamp ?? DateTime.UtcNow).ToString("o"),
};
var attachmentPaths = attachments
?.Select(p => p?.Trim())
.Where(p => !string.IsNullOrWhiteSpace(p))
.ToList();
if (attachmentPaths != null && attachmentPaths.Count > 0)
payload["attachments"] = JArray.FromObject(attachmentPaths);
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
// The CodelyWindow's presence is the one reliable signal here:
// connection state can lie (e.g. a standalone app client makes
// NotifyAll report delivered while the embedded page is still
// loading). No window → open one, queue the message, and let
// the frontend drain the queue once its page is fully loaded.
if (!EditorWindow.HasOpenInstances<CodelyWindow>())
{
CodelyWindow.ShowWindow();
QueuePending(payload);
return false;
}
#endif
return UnityTcpBridge.IsRunning
&& UnityTcpBridge.NotifyAll(EventType, payload);
}
catch (Exception e)
{
try { CodelyLogger.LogError($"[AgentInputNotifier] send failed: {e.Message}"); } catch { }
return false;
}
}
private static void QueuePending(JObject payload)
{
lock (_gate)
{
if (_pending.Count >= MaxPendingMessages)
_pending.RemoveAt(0); // drop the oldest
_pending.Add(payload);
}
}
/// <summary>
/// Returns all queued (undelivered) messages and clears the queue.
/// Called by manage_editor's "drain_agent_input" action on behalf of
/// the frontend once its page is fully loaded.
/// </summary>
public static List<JObject> DrainPending()
{
lock (_gate)
{
var items = new List<JObject>(_pending);
_pending.Clear();
return items;
}
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 414cb485b5676eda8624d218acd342b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,128 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using Codely.Newtonsoft.Json;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers;
namespace UnityTcp.Editor.Notifier
{
/// <summary>
/// Batches new Unity console activity and forwards it to notifier clients as a
/// "console_log_changed" event (one flush per editor tick).
///
/// Source: <see cref="Application.logMessageReceivedThreaded"/>, which fires for
/// every console entry regardless of the thread it was raised on — Debug.* calls,
/// uncaught exceptions, AND C# compiler errors/warnings (Unity routes those through
/// the log system too; their file/line/column are embedded in the message text,
/// e.g. "Assets/Foo.cs(12,3): error CS1002: ; expected"). Subscribing to this one
/// callback covers all of them without duplicates — also hooking
/// CompilationPipeline.assemblyCompilationFinished would re-deliver every compiler
/// message a second time.
///
/// The payload mirrors what <see cref="UnityTcp.Editor.Tools.ReadConsole"/>'s
/// action="get" with types=["all"] returns, i.e.
/// <see cref="Response.Success(string,object,bool,object)"/> wrapping the entry
/// array: <c>{ success, message, rev, data: [ { type, message, file, line, stackTrace }, … ] }</c>.
/// </summary>
// [InitializeOnLoad] // disable this notifier
public static class ConsoleLogChangedNotifier
{
private const string EventType = "console_log_changed";
// Safety valve for pathological single-tick bursts (e.g. Debug.Log in a tight
// loop): never queue more than this many entries per flush.
private const int MaxEntriesPerFlush = 5000;
private static readonly object _gate = new object();
private static readonly List<object> _pending = new List<object>();
// Re-entrancy guard: a log emitted while we're flushing (e.g. CodelyLogger.LogError
// on failure) must not be re-queued and re-flushed forever.
private static volatile bool _flushing;
static ConsoleLogChangedNotifier()
{
Application.logMessageReceivedThreaded -= OnLogMessageThreaded;
Application.logMessageReceivedThreaded += OnLogMessageThreaded;
// Drained on the main thread regardless of which thread enqueued.
EditorApplication.update -= Tick;
EditorApplication.update += Tick;
}
// ---- Source ---------------------------------------------------------- //
private static void OnLogMessageThreaded(string message, string stackTrace, LogType type)
{
if (_flushing) return; // ignore logs caused by our own flush
Enqueue(MakeEntry(
type: type.ToString(),
message: message,
file: null,
line: 0,
stackTrace: string.IsNullOrEmpty(stackTrace) ? null : stackTrace));
}
// Entry shape matches ReadConsole's "detailed" formatting exactly:
// { type, message, file, line, stackTrace }
private static Dictionary<string, object> MakeEntry(
string type, string message, string file, int line, string stackTrace)
{
return new Dictionary<string, object>
{
{ "type", type },
{ "message", message },
{ "file", file },
{ "line", line },
{ "stackTrace", stackTrace },
};
}
// ---- Queue + flush --------------------------------------------------- //
private static void Enqueue(Dictionary<string, object> entry)
{
lock (_gate)
{
if (_pending.Count >= MaxEntriesPerFlush) return;
_pending.Add(entry);
}
}
private static void Tick() => Flush();
private static void Flush()
{
object[] entries;
lock (_gate)
{
if (_pending.Count == 0) return;
entries = _pending.ToArray();
_pending.Clear();
}
_flushing = true;
try
{
if (!UnityTcpBridge.IsRunning) return; // can't deliver — drop the batch
// Same shape as ReadConsole.HandleCommand(action="get", types=["all"]):
// Response.Success("Retrieved N log entries.", <entries array>)
object response = Response.Success($"Retrieved {entries.Length} log entries.", entries);
JObject payload = JObject.FromObject(response);
UnityTcpBridge.NotifyAll(EventType, payload);
}
catch (Exception e)
{
try { CodelyLogger.LogError($"[ConsoleLogChangedNotifier] flush failed: {e.Message}"); } catch { }
}
finally
{
_flushing = false;
}
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 556bc6845e3743828cc9ad3410ac6344
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,273 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityTcp.Editor.Helpers;
namespace UnityTcp.Editor.Notifier
{
/// <summary>
/// Broadcasts the active scene's hierarchy to notifier clients whenever it
/// changes. The emitted payload mirrors the structure returned by
/// <see cref="ManageScene"/>'s get_hierarchy action (including the
/// roots_only fallback for large scenes).
/// </summary>
// [InitializeOnLoad] // disable this notifier
public static class SceneHierarchyNotifier
{
private const string EventType = "scene_hierarchy_changed";
// Matches ManageScene.GetSceneHierarchy cap.
private const int LargeSceneThreshold = 500;
// Debounce window: every event extends the deadline; flush only after this much idle time.
// Keeps flush rate sane during bursts of edits (drag, multi-select rename, scripted spawns).
private const double DebounceSeconds = 0.25;
private static double _flushDeadline;
private static bool _tickRegistered;
static SceneHierarchyNotifier()
{
EditorApplication.hierarchyChanged += OnHierarchyChanged;
EditorSceneManager.sceneOpened += OnSceneOpened;
EditorSceneManager.sceneClosed += OnSceneClosed;
EditorSceneManager.sceneSaved += OnSceneSaved;
EditorSceneManager.activeSceneChangedInEditMode += OnActiveSceneChanged;
}
// ---- Event hooks (debounced) -------------------------------------- //
private static void OnHierarchyChanged() => ScheduleFlush();
private static void OnSceneOpened(Scene s, OpenSceneMode mode) => ScheduleFlush();
private static void OnSceneClosed(Scene s) => ScheduleFlush();
private static void OnSceneSaved(Scene s) => ScheduleFlush();
private static void OnActiveSceneChanged(Scene previous, Scene current) => ScheduleFlush();
private static void ScheduleFlush()
{
_flushDeadline = EditorApplication.timeSinceStartup + DebounceSeconds;
if (_tickRegistered) return;
_tickRegistered = true;
EditorApplication.update += DebounceTick;
}
private static void DebounceTick()
{
if (EditorApplication.timeSinceStartup < _flushDeadline) return;
EditorApplication.update -= DebounceTick;
_tickRegistered = false;
Flush();
}
private static void Flush()
{
try
{
if (!UnityTcpBridge.IsRunning) return;
JObject payload = BuildHierarchyPayload();
if (payload == null) return;
UnityTcpBridge.NotifyAll(EventType, payload);
}
catch (Exception e)
{
try { CodelyLogger.LogError($"[SceneHierarchyNotifier] flush failed: {e.Message}"); } catch { }
}
}
// ---- Payload builder (shape mirrors ManageScene.GetSceneHierarchy) -- //
private static JObject BuildHierarchyPayload()
{
Scene activeScene = EditorSceneManager.GetActiveScene();
if (!activeScene.IsValid() || !activeScene.isLoaded)
{
return JObject.FromObject(new
{
sceneName = string.Empty,
scenePath = string.Empty,
partial = false,
mode = "empty",
totalObjectCount = 0,
rootCount = 0,
hierarchy = new List<object>(),
});
}
GameObject[] rootObjects = activeScene.GetRootGameObjects();
int totalObjectCount = 0;
foreach (var rootObj in rootObjects)
{
totalObjectCount += CountGameObjectsRecursive(rootObj);
}
// Large scene: mirror ManageScene's roots_only fallback.
if (totalObjectCount > LargeSceneThreshold)
{
var roots = rootObjects
.Where(go => go != null)
.Select(go => new Dictionary<string, object>
{
{ "name", go.name },
{ "instanceID", go.GetStableInstanceId() },
{ "activeSelf", go.activeSelf },
{ "activeInHierarchy", go.activeInHierarchy },
{ "tag", go.tag },
{ "layer", go.layer },
{ "isStatic", go.isStatic },
{ "childCount", go.transform != null ? go.transform.childCount : 0 },
{ "children", new List<object>() },
})
.ToList();
return JObject.FromObject(new
{
sceneName = activeScene.name,
scenePath = activeScene.path,
partial = true,
mode = "roots_only",
totalObjectCount = totalObjectCount,
rootCount = roots.Count,
roots = roots,
hints = new[]
{
"Use unity_gameobject action='list_children' on a specific root GameObject to expand its subtree with a depth limit.",
"Use unity_gameobject action='find' with searchMethod='by_path' to locate a specific object (e.g. 'Root/Child') before listing children.",
"If you only need a specific area, pick a root from this list and then drill down incrementally (depth=1..N)."
}
});
}
var hierarchy = rootObjects
.Where(go => go != null)
.Select(go => BuildGameObjectTree(go))
.ToList();
return JObject.FromObject(new
{
sceneName = activeScene.name,
scenePath = activeScene.path,
partial = false,
mode = "full",
totalObjectCount = totalObjectCount,
rootCount = hierarchy.Count,
hierarchy = hierarchy,
});
}
// ---- Helpers (kept in sync with ManageScene's private equivalents) -- //
private static int CountGameObjectsRecursive(GameObject go)
{
if (go == null) return 0;
int count = 0;
var stack = new Stack<Transform>();
if (go.transform != null) stack.Push(go.transform);
while (stack.Count > 0)
{
var tr = stack.Pop();
if (tr == null) continue;
count++;
int cc = tr.childCount;
for (int i = 0; i < cc; i++)
{
var child = tr.GetChild(i);
if (child != null) stack.Push(child);
}
}
return count;
}
// Iterative DFS that builds the tree without recursion, so deeply nested hierarchies
// (e.g. UI layouts hundreds of levels deep) don't blow the managed stack regardless
// of total object count.
private static object BuildGameObjectTree(GameObject root)
{
if (root == null) return null;
var rootTransform = root.transform;
if (rootTransform == null) return null;
var nodes = new Dictionary<Transform, Dictionary<string, object>>();
var stack = new Stack<Transform>();
stack.Push(rootTransform);
while (stack.Count > 0)
{
var tr = stack.Pop();
if (tr == null || nodes.ContainsKey(tr)) continue;
nodes[tr] = BuildGameObjectNode(tr.gameObject);
int cc = tr.childCount;
for (int i = 0; i < cc; i++)
{
var child = tr.GetChild(i);
if (child != null) stack.Push(child);
}
}
// Second pass: link children in original sibling order.
foreach (var kv in nodes)
{
var tr = kv.Key;
var children = (List<object>)kv.Value["children"];
int cc = tr.childCount;
for (int i = 0; i < cc; i++)
{
var child = tr.GetChild(i);
if (child != null && nodes.TryGetValue(child, out var childNode))
children.Add(childNode);
}
}
return nodes[rootTransform];
}
private static Dictionary<string, object> BuildGameObjectNode(GameObject go)
{
var tr = go.transform;
return new Dictionary<string, object>
{
{ "name", go.name },
{ "activeSelf", go.activeSelf },
{ "activeInHierarchy", go.activeInHierarchy },
{ "tag", go.tag },
{ "layer", go.layer },
{ "isStatic", go.isStatic },
{ "instanceID", go.GetStableInstanceId() },
{
"transform",
new
{
position = new
{
x = tr.localPosition.x,
y = tr.localPosition.y,
z = tr.localPosition.z,
},
rotation = new
{
x = tr.localRotation.eulerAngles.x,
y = tr.localRotation.eulerAngles.y,
z = tr.localRotation.eulerAngles.z,
},
scale = new
{
x = tr.localScale.x,
y = tr.localScale.y,
z = tr.localScale.z,
},
}
},
{ "children", new List<object>() },
};
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: dc36c8035d2dd474196149ff776a0472
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,119 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers;
namespace UnityTcp.Editor.Notifier
{
/// <summary>
/// Broadcasts the current Editor selection to notifier clients whenever it
/// changes. The emitted payload mirrors the structure returned by
/// <see cref="UnityTcp.Editor.Tools.ManageEditor"/>'s get_selection action.
/// </summary>
[InitializeOnLoad]
public static class SelectionChangedNotifier
{
private const string EventType = "selection_changed";
private static bool _flushScheduled;
static SelectionChangedNotifier()
{
Selection.selectionChanged += OnSelectionChanged;
}
private static void OnSelectionChanged() => ScheduleFlush();
private static void ScheduleFlush()
{
if (_flushScheduled) return;
_flushScheduled = true;
EditorApplication.delayCall += Flush;
}
private static void Flush()
{
_flushScheduled = false;
try
{
if (!UnityTcpBridge.IsRunning) return;
JObject payload = BuildSelectionPayload();
if (payload == null) return;
UnityTcpBridge.NotifyAll(EventType, payload);
}
catch (Exception e)
{
try { CodelyLogger.LogError($"[SelectionChangedNotifier] flush failed: {e.Message}"); } catch { }
}
}
private static JObject BuildSelectionPayload()
{
var assetGUIDs = Selection.assetGUIDs;
var assetPaths = assetGUIDs?.Select(AssetDatabase.GUIDToAssetPath).ToArray();
string activeAssetPath = Selection.activeObject != null
? AssetDatabase.GetAssetPath(Selection.activeObject)
: null;
string activeAsset = string.IsNullOrEmpty(activeAssetPath)
? null
: Path.GetFullPath(activeAssetPath).Replace('\\', '/');
var payload = new
{
activeObject = Selection.activeObject?.name,
activeAsset = activeAsset,
activeGameObject = Selection.activeGameObject?.name,
activeTransform = Selection.activeTransform?.name,
activeInstanceID = InstanceIdExtensions.ActiveSelectionInstanceId(),
#if UNITY_2020_1_OR_NEWER
count = Selection.count,
#else
count = Selection.objects?.Length ?? 0,
#endif
objects = Selection.objects
.Where(obj => obj != null)
.Select(obj => new
{
name = obj.name,
type = obj.GetType().FullName,
instanceID = obj.GetStableInstanceId(),
})
.ToList(),
gameObjects = Selection.gameObjects
.Where(go => go != null)
.Select(go => new
{
name = go.name,
path = GetGameObjectHierarchyPath(go),
instanceID = go.GetStableInstanceId(),
})
.ToList(),
assetGUIDs = assetGUIDs,
assetPaths = assetPaths,
};
return JObject.FromObject(payload);
}
private static string GetGameObjectHierarchyPath(GameObject obj)
{
if (obj == null) return string.Empty;
var path = obj.name;
var parent = obj.transform != null ? obj.transform.parent : null;
while (parent != null)
{
path = $"{parent.name}/{path}";
parent = parent.parent;
}
return path;
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 7c8e5f3a2d1b4e5a9b6c8d7e1f2a3b4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: