mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 22:03:30 +08:00
feat(godot): 收敛 GodotLogger 宿主能力
- 新增 GodotLog、DeferredLogger 和配置自动发现、热重载接线。 - 修复已缓存 logger 的级别判定与输出路径,使动态配置生效。 - 更新文档与追踪记录,明确当前收敛边界和恢复点。
This commit is contained in:
parent
36e1ae5f32
commit
748bb714fb
@ -6,16 +6,42 @@ namespace GFramework.Core.Logging;
|
|||||||
/// 日志抽象基类,封装日志级别判断、格式化与异常处理逻辑。
|
/// 日志抽象基类,封装日志级别判断、格式化与异常处理逻辑。
|
||||||
/// 平台日志器只需实现 Write 方法即可。
|
/// 平台日志器只需实现 Write 方法即可。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AbstractLogger(
|
public abstract class AbstractLogger : IStructuredLogger
|
||||||
string? name = null,
|
|
||||||
LogLevel minLevel = LogLevel.Info) : IStructuredLogger
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根日志记录器的名称常量
|
/// 根日志记录器的名称常量
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RootLoggerName = "ROOT";
|
public const string RootLoggerName = "ROOT";
|
||||||
|
|
||||||
private readonly string _name = name ?? RootLoggerName;
|
private readonly Func<LogLevel> _minLevelProvider;
|
||||||
|
private readonly string _name;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用固定最小日志级别初始化日志器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">日志器名称。</param>
|
||||||
|
/// <param name="minLevel">最小日志级别。</param>
|
||||||
|
protected AbstractLogger(
|
||||||
|
string? name = null,
|
||||||
|
LogLevel minLevel = LogLevel.Info)
|
||||||
|
: this(name, () => minLevel)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用动态最小日志级别提供器初始化日志器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">日志器名称。</param>
|
||||||
|
/// <param name="minLevelProvider">最小日志级别提供器。</param>
|
||||||
|
protected AbstractLogger(
|
||||||
|
string? name,
|
||||||
|
Func<LogLevel> minLevelProvider)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(minLevelProvider);
|
||||||
|
|
||||||
|
_name = name ?? RootLoggerName;
|
||||||
|
_minLevelProvider = minLevelProvider;
|
||||||
|
}
|
||||||
|
|
||||||
#region Metadata
|
#region Metadata
|
||||||
|
|
||||||
@ -47,7 +73,7 @@ public abstract class AbstractLogger(
|
|||||||
/// <returns>如果指定级别大于等于最小级别则返回true,否则返回false</returns>
|
/// <returns>如果指定级别大于等于最小级别则返回true,否则返回false</returns>
|
||||||
protected bool IsEnabled(LogLevel level)
|
protected bool IsEnabled(LogLevel level)
|
||||||
{
|
{
|
||||||
return level >= minLevel;
|
return level >= _minLevelProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -568,4 +594,4 @@ public abstract class AbstractLogger(
|
|||||||
#region Core Pipeline (Private)
|
#region Core Pipeline (Private)
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,8 @@ public sealed class GodotLogTemplateTests
|
|||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
"Game.Services.Inventory",
|
"Game.Services.Inventory",
|
||||||
"Loaded",
|
"Loaded",
|
||||||
"orange");
|
"orange",
|
||||||
|
string.Empty);
|
||||||
|
|
||||||
var result = template.Render(context);
|
var result = template.Render(context);
|
||||||
|
|
||||||
@ -32,7 +33,8 @@ public sealed class GodotLogTemplateTests
|
|||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
"Game",
|
"Game",
|
||||||
"Ready",
|
"Ready",
|
||||||
"cyan");
|
"cyan",
|
||||||
|
string.Empty);
|
||||||
|
|
||||||
var result = template.Render(context);
|
var result = template.Render(context);
|
||||||
|
|
||||||
@ -48,7 +50,8 @@ public sealed class GodotLogTemplateTests
|
|||||||
LogLevel.Info,
|
LogLevel.Info,
|
||||||
"UI",
|
"UI",
|
||||||
"Ready",
|
"Ready",
|
||||||
"white");
|
"white",
|
||||||
|
string.Empty);
|
||||||
|
|
||||||
var result = template.Render(context);
|
var result = template.Render(context);
|
||||||
|
|
||||||
@ -64,7 +67,8 @@ public sealed class GodotLogTemplateTests
|
|||||||
LogLevel.Info,
|
LogLevel.Info,
|
||||||
"Game",
|
"Game",
|
||||||
"Ready",
|
"Ready",
|
||||||
"white");
|
"white",
|
||||||
|
string.Empty);
|
||||||
|
|
||||||
var result = template.Render(context);
|
var result = template.Render(context);
|
||||||
|
|
||||||
@ -80,13 +84,31 @@ public sealed class GodotLogTemplateTests
|
|||||||
LogLevel.Info,
|
LogLevel.Info,
|
||||||
"Game",
|
"Game",
|
||||||
"Ready",
|
"Ready",
|
||||||
"white");
|
"white",
|
||||||
|
string.Empty);
|
||||||
|
|
||||||
var result = template.Render(context);
|
var result = template.Render(context);
|
||||||
|
|
||||||
Assert.That(result, Is.EqualTo("INFO "));
|
Assert.That(result, Is.EqualTo("INFO "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Render_Should_Append_Properties_Placeholder()
|
||||||
|
{
|
||||||
|
var template = GodotLogTemplate.Parse("{message}{properties}");
|
||||||
|
var context = new GodotLogRenderContext(
|
||||||
|
new DateTime(2026, 5, 2, 1, 2, 3, DateTimeKind.Utc),
|
||||||
|
LogLevel.Info,
|
||||||
|
"Game",
|
||||||
|
"Ready",
|
||||||
|
"white",
|
||||||
|
" | Scene=Boot");
|
||||||
|
|
||||||
|
var result = template.Render(context);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Ready | Scene=Boot"));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Options_ForMinimumLevel_Should_Preserve_Fixed_Minimum_Level()
|
public void Options_ForMinimumLevel_Should_Preserve_Fixed_Minimum_Level()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,4 +8,5 @@ internal readonly record struct GodotLogRenderContext(
|
|||||||
LogLevel Level,
|
LogLevel Level,
|
||||||
string Category,
|
string Category,
|
||||||
string Message,
|
string Message,
|
||||||
string Color);
|
string Color,
|
||||||
|
string Properties);
|
||||||
|
|||||||
@ -91,6 +91,7 @@ internal sealed class GodotLogTemplate
|
|||||||
"color" => static (builder, context) => builder.Append(context.Color),
|
"color" => static (builder, context) => builder.Append(context.Color),
|
||||||
"level" => static (builder, context) => builder.Append(context.Level),
|
"level" => static (builder, context) => builder.Append(context.Level),
|
||||||
"message" => static (builder, context) => builder.Append(context.Message),
|
"message" => static (builder, context) => builder.Append(context.Message),
|
||||||
|
"properties" => static (builder, context) => builder.Append(context.Properties),
|
||||||
"timestamp" => static (builder, context) => builder.Append(context.Timestamp.ToString(
|
"timestamp" => static (builder, context) => builder.Append(context.Timestamp.ToString(
|
||||||
"yyyy-MM-dd HH:mm:ss.fff",
|
"yyyy-MM-dd HH:mm:ss.fff",
|
||||||
CultureInfo.InvariantCulture)),
|
CultureInfo.InvariantCulture)),
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
using Godot;
|
using Godot;
|
||||||
@ -10,7 +13,8 @@ namespace GFramework.Godot.Logging;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GodotLogger : AbstractLogger
|
public sealed class GodotLogger : AbstractLogger
|
||||||
{
|
{
|
||||||
private readonly GodotLoggerOptions _options;
|
private readonly Func<LogLevel> _minLevelProvider;
|
||||||
|
private readonly Func<GodotLoggerOptions> _optionsProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a logger that preserves the historical fixed-format template.
|
/// Initializes a logger that preserves the historical fixed-format template.
|
||||||
@ -18,7 +22,10 @@ public sealed class GodotLogger : AbstractLogger
|
|||||||
/// <param name="name">The logger name.</param>
|
/// <param name="name">The logger name.</param>
|
||||||
/// <param name="minLevel">The minimum enabled log level.</param>
|
/// <param name="minLevel">The minimum enabled log level.</param>
|
||||||
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info)
|
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info)
|
||||||
: this(name, GodotLoggerOptions.ForMinimumLevel(minLevel))
|
: this(
|
||||||
|
name ?? RootLoggerName,
|
||||||
|
() => GodotLoggerOptions.ForMinimumLevel(minLevel),
|
||||||
|
() => minLevel)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,9 +35,21 @@ public sealed class GodotLogger : AbstractLogger
|
|||||||
/// <param name="name">The logger name.</param>
|
/// <param name="name">The logger name.</param>
|
||||||
/// <param name="options">The logger options.</param>
|
/// <param name="options">The logger options.</param>
|
||||||
public GodotLogger(string? name, GodotLoggerOptions options)
|
public GodotLogger(string? name, GodotLoggerOptions options)
|
||||||
: base(name ?? RootLoggerName, (options ?? throw new ArgumentNullException(nameof(options))).GetEffectiveMinLevel())
|
: this(
|
||||||
|
name ?? RootLoggerName,
|
||||||
|
() => options ?? throw new ArgumentNullException(nameof(options)),
|
||||||
|
() => (options ?? throw new ArgumentNullException(nameof(options))).GetEffectiveMinLevel())
|
||||||
{
|
{
|
||||||
_options = options;
|
}
|
||||||
|
|
||||||
|
internal GodotLogger(
|
||||||
|
string name,
|
||||||
|
Func<GodotLoggerOptions> optionsProvider,
|
||||||
|
Func<LogLevel> minLevelProvider)
|
||||||
|
: base(name, minLevelProvider ?? throw new ArgumentNullException(nameof(minLevelProvider)))
|
||||||
|
{
|
||||||
|
_optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider));
|
||||||
|
_minLevelProvider = minLevelProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -41,18 +60,59 @@ public sealed class GodotLogger : AbstractLogger
|
|||||||
/// <param name="exception">The optional exception.</param>
|
/// <param name="exception">The optional exception.</param>
|
||||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
protected override void Write(LogLevel level, string message, Exception? exception)
|
||||||
{
|
{
|
||||||
var templateText = _options.Mode == GodotLoggerMode.Debug
|
WriteEntry(level, message, exception, properties: null);
|
||||||
? _options.DebugOutputTemplate
|
}
|
||||||
: _options.ReleaseOutputTemplate;
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
|
||||||
|
/// </summary>
|
||||||
|
public override void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
|
||||||
|
{
|
||||||
|
if (level < _minLevelProvider())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteEntry(level, message, exception: null, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
|
||||||
|
/// </summary>
|
||||||
|
public override void Log(
|
||||||
|
LogLevel level,
|
||||||
|
string message,
|
||||||
|
Exception? exception,
|
||||||
|
params (string Key, object? Value)[] properties)
|
||||||
|
{
|
||||||
|
if (level < _minLevelProvider())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteEntry(level, message, exception, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteEntry(
|
||||||
|
LogLevel level,
|
||||||
|
string message,
|
||||||
|
Exception? exception,
|
||||||
|
(string Key, object? Value)[]? properties)
|
||||||
|
{
|
||||||
|
var options = _optionsProvider();
|
||||||
|
var templateText = options.Mode == GodotLoggerMode.Debug
|
||||||
|
? options.DebugOutputTemplate
|
||||||
|
: options.ReleaseOutputTemplate;
|
||||||
var context = new GodotLogRenderContext(
|
var context = new GodotLogRenderContext(
|
||||||
DateTime.UtcNow,
|
DateTime.UtcNow,
|
||||||
level,
|
level,
|
||||||
Name(),
|
Name(),
|
||||||
message,
|
message,
|
||||||
_options.GetColor(level));
|
options.GetColor(level),
|
||||||
|
FormatProperties(properties));
|
||||||
var rendered = GodotLogTemplate.Parse(templateText).Render(context);
|
var rendered = GodotLogTemplate.Parse(templateText).Render(context);
|
||||||
|
|
||||||
if (_options.Mode == GodotLoggerMode.Debug)
|
if (options.Mode == GodotLoggerMode.Debug)
|
||||||
{
|
{
|
||||||
WriteDebug(level, rendered);
|
WriteDebug(level, rendered);
|
||||||
}
|
}
|
||||||
@ -67,6 +127,54 @@ public sealed class GodotLogger : AbstractLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FormatProperties((string Key, object? Value)[]? properties)
|
||||||
|
{
|
||||||
|
var merged = MergeProperties(properties);
|
||||||
|
if (merged.Count == 0)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return " | " + string.Join(", ", merged.Select(static pair => $"{pair.Key}={FormatValue(pair.Value)}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, object?> MergeProperties((string Key, object? Value)[]? properties)
|
||||||
|
{
|
||||||
|
var contextProperties = LogContext.Current;
|
||||||
|
if ((properties == null || properties.Length == 0) && contextProperties.Count == 0)
|
||||||
|
{
|
||||||
|
return EmptyProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
var merged = new Dictionary<string, object?>(contextProperties, StringComparer.Ordinal);
|
||||||
|
if (properties != null)
|
||||||
|
{
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
merged[property.Key] = property.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, object?> EmptyProperties =
|
||||||
|
new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private static string FormatValue(object? value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
|
||||||
|
_ => value.ToString() ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static void WriteDebug(LogLevel level, string rendered)
|
private static void WriteDebug(LogLevel level, string rendered)
|
||||||
{
|
{
|
||||||
GD.PrintRich(rendered);
|
GD.PrintRich(rendered);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Logging;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Logging;
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
@ -9,14 +9,15 @@ namespace GFramework.Godot.Logging;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory _cachedFactory;
|
private readonly ConcurrentDictionary<string, ILogger> _loggers = new(StringComparer.Ordinal);
|
||||||
|
private readonly Func<GodotLoggerSettings> _settingsProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a Godot logger provider with the default logger factory.
|
/// Initializes a Godot logger provider with the default logger factory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GodotLoggerFactoryProvider()
|
public GodotLoggerFactoryProvider()
|
||||||
|
: this(static () => GodotLoggerSettings.Default)
|
||||||
{
|
{
|
||||||
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -24,8 +25,13 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options">The logger options.</param>
|
/// <param name="options">The logger options.</param>
|
||||||
public GodotLoggerFactoryProvider(GodotLoggerOptions options)
|
public GodotLoggerFactoryProvider(GodotLoggerOptions options)
|
||||||
|
: this(CreateStaticSettingsProvider(options))
|
||||||
{
|
{
|
||||||
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory(options));
|
}
|
||||||
|
|
||||||
|
internal GodotLoggerFactoryProvider(Func<GodotLoggerSettings> settingsProvider)
|
||||||
|
{
|
||||||
|
_settingsProvider = settingsProvider ?? throw new ArgumentNullException(nameof(settingsProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -40,12 +46,31 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
|||||||
/// <returns>A logger configured with <see cref="MinLevel"/>.</returns>
|
/// <returns>A logger configured with <see cref="MinLevel"/>.</returns>
|
||||||
public ILogger CreateLogger(string name)
|
public ILogger CreateLogger(string name)
|
||||||
{
|
{
|
||||||
return _cachedFactory.GetLogger(name, MinLevel);
|
ArgumentNullException.ThrowIfNull(name);
|
||||||
|
|
||||||
|
return _loggers.GetOrAdd(
|
||||||
|
name,
|
||||||
|
static (loggerName, provider) => new GodotLogger(
|
||||||
|
loggerName,
|
||||||
|
provider.GetOptions,
|
||||||
|
() => provider.GetEffectiveMinLevel(loggerName)),
|
||||||
|
this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ILoggerFactory CreateCachedFactory(ILoggerFactory innerFactory)
|
private GodotLoggerOptions GetOptions()
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(innerFactory);
|
return _settingsProvider().Options;
|
||||||
return new CachedLoggerFactory(innerFactory);
|
}
|
||||||
|
|
||||||
|
private LogLevel GetEffectiveMinLevel(string categoryName)
|
||||||
|
{
|
||||||
|
return _settingsProvider().GetEffectiveMinLevel(categoryName, MinLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<GodotLoggerSettings> CreateStaticSettingsProvider(GodotLoggerOptions options)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
var settings = GodotLoggerSettings.FromOptions(options);
|
||||||
|
return () => settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ public sealed class GodotLoggerOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios.
|
#pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios.
|
||||||
public string DebugOutputTemplate { get; set; } =
|
public string DebugOutputTemplate { get; set; } =
|
||||||
"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [color={color}][{level:u3}][/color] [{category:l16}] {message}";
|
"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [color={color}][{level:u3}][/color] [{category:l16}] {message}{properties}";
|
||||||
#pragma warning restore MA0016
|
#pragma warning restore MA0016
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -47,7 +47,7 @@ public sealed class GodotLoggerOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios.
|
#pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios.
|
||||||
public string ReleaseOutputTemplate { get; set; } =
|
public string ReleaseOutputTemplate { get; set; } =
|
||||||
"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{level:u3}] [{category:l16}] {message}";
|
"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{level:u3}] [{category:l16}] {message}{properties}";
|
||||||
#pragma warning restore MA0016
|
#pragma warning restore MA0016
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -69,8 +69,8 @@ public sealed class GodotLoggerOptions
|
|||||||
Mode = GodotLoggerMode.Debug,
|
Mode = GodotLoggerMode.Debug,
|
||||||
DebugMinLevel = minLevel,
|
DebugMinLevel = minLevel,
|
||||||
ReleaseMinLevel = minLevel,
|
ReleaseMinLevel = minLevel,
|
||||||
DebugOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}",
|
DebugOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}{properties}",
|
||||||
ReleaseOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}",
|
ReleaseOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}{properties}",
|
||||||
Colors = new Dictionary<LogLevel, string>(DefaultColors)
|
Colors = new Dictionary<LogLevel, string>(DefaultColors)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,10 @@ help the current worktree land on the right recovery documents without scanning
|
|||||||
- Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog.
|
- Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog.
|
||||||
- Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md`
|
- Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md`
|
||||||
- Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md`
|
- Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md`
|
||||||
|
- `godot-logging-compliance-polish`
|
||||||
|
- Purpose: continue Godot logging host integration, configuration reload, structured-output polish, and follow-up work without forking the Core logging model.
|
||||||
|
- Tracking: `ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md`
|
||||||
|
- Trace: `ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md`
|
||||||
- `semantic-release-versioning`
|
- `semantic-release-versioning`
|
||||||
- Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow.
|
- Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow.
|
||||||
- Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md`
|
- Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md`
|
||||||
@ -59,6 +63,9 @@ help the current worktree land on the right recovery documents without scanning
|
|||||||
- Branch: `feat/data-repository-persistence`
|
- Branch: `feat/data-repository-persistence`
|
||||||
- Worktree hint: `GFramework-data-repository-persistence`
|
- Worktree hint: `GFramework-data-repository-persistence`
|
||||||
- Priority 1: `data-repository-persistence`
|
- Priority 1: `data-repository-persistence`
|
||||||
|
- Branch: `feat/godot-logging-compliance-polish`
|
||||||
|
- Worktree hint: `GFramework`
|
||||||
|
- Priority 1: `godot-logging-compliance-polish`
|
||||||
- Branch: `feat/semantic-release-versioning`
|
- Branch: `feat/semantic-release-versioning`
|
||||||
- Worktree hint: `GFramework`
|
- Worktree hint: `GFramework`
|
||||||
- Priority 1: `semantic-release-versioning`
|
- Priority 1: `semantic-release-versioning`
|
||||||
|
|||||||
@ -5,11 +5,16 @@ description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准
|
|||||||
|
|
||||||
# Godot 日志系统
|
# Godot 日志系统
|
||||||
|
|
||||||
`GFramework.Godot` 当前的日志能力很收敛:它不是一套独立于 Core 的新日志框架,而是把现有 `ILogger` 调用面接到
|
`GFramework.Godot` 当前的日志能力仍然以 Core 的 `ILogger` 调用面为中心,但已经不再只是一个薄输出适配层。
|
||||||
Godot 控制台。
|
除了把日志写到 Godot 控制台,它现在还补上了 Godot 宿主常见的接入便利层:
|
||||||
|
|
||||||
换句话说,Godot 侧真正新增的是 provider / factory / logger 这层输出适配,而不是新的日志 API。业务代码仍然继续使用
|
- `GodotLog` 静态入口
|
||||||
`LoggerFactoryResolver.Provider.CreateLogger(...)` 或 `[Log]` 生成的 `ILogger` 字段。
|
- 配置文件自动发现
|
||||||
|
- 运行期配置热重载
|
||||||
|
- 延迟 logger 解析,适合 `static readonly` 字段
|
||||||
|
|
||||||
|
业务代码仍然继续使用 `LoggerFactoryResolver.Provider.CreateLogger(...)`、`GodotLog.CreateLogger(...)` 或 `[Log]`
|
||||||
|
生成的 `ILogger` 字段;Godot 侧没有额外引入第二套业务日志 API。
|
||||||
|
|
||||||
## 当前公开入口
|
## 当前公开入口
|
||||||
|
|
||||||
@ -57,11 +62,34 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
它内部用 `CachedLoggerFactory` 包装 `GodotLoggerFactory`。缓存 key 由 `name` 和 `MinLevel` 共同组成,所以:
|
当前 provider 会按 logger 名称缓存实例,但 logger 本身会在写入时读取当前配置快照,所以:
|
||||||
|
|
||||||
- 同名、同 `MinLevel` 的 logger 会复用实例
|
- 同名 logger 会复用实例
|
||||||
- 调整 `MinLevel` 后,新创建的 logger 会走新的缓存 key
|
- 调整 provider 最小级别或热更新配置后,已持有的 logger 会立即看到新行为
|
||||||
- 已经持有的旧 logger 不会被原地改写
|
- 不需要为了刷新模板、颜色或级别而重新创建 logger
|
||||||
|
|
||||||
|
### `GodotLog`
|
||||||
|
|
||||||
|
`GodotLog` 是新增的 Godot 宿主友好入口:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
GodotLog.Configure(options =>
|
||||||
|
{
|
||||||
|
options.Mode = GodotLoggerMode.Debug;
|
||||||
|
});
|
||||||
|
|
||||||
|
GodotLog.UseAsDefaultProvider();
|
||||||
|
|
||||||
|
var logger = GodotLog.CreateLogger<Main>();
|
||||||
|
```
|
||||||
|
|
||||||
|
它提供三件事:
|
||||||
|
|
||||||
|
- 在第一次真正创建 provider 前允许代码覆写 `GodotLoggerOptions`
|
||||||
|
- 自动按 `GODOT_LOGGER_CONFIG` -> 可执行目录 `appsettings.json` -> `res://appsettings.json` 顺序发现配置
|
||||||
|
- 返回延迟解析 logger,避免 `static readonly` 字段过早锁死配置
|
||||||
|
|
||||||
## 最小接入路径
|
## 最小接入路径
|
||||||
|
|
||||||
@ -121,7 +149,7 @@ public partial class SettingsPanel : Control
|
|||||||
```
|
```
|
||||||
|
|
||||||
如果你已经在用 `GFramework.Core.SourceGenerators`,也可以继续让 `[Log]` 生成字段。Godot provider 只改变输出落点,
|
如果你已经在用 `GFramework.Core.SourceGenerators`,也可以继续让 `[Log]` 生成字段。Godot provider 只改变输出落点,
|
||||||
不会改变 `[Log]` 的生成契约。
|
不会改变 `[Log]` 的生成契约。需要静态字段延迟初始化时,也可以直接用 `GodotLog.CreateLogger<T>()`。
|
||||||
|
|
||||||
### 3. Scene / UI 迁移日志会自动复用同一套 provider
|
### 3. Scene / UI 迁移日志会自动复用同一套 provider
|
||||||
|
|
||||||
@ -152,7 +180,9 @@ RegisterHandler(new LoggingTransitionHandler());
|
|||||||
| `Error` | `GD.PrintErr(...)` | 输出到错误流 |
|
| `Error` | `GD.PrintErr(...)` | 输出到错误流 |
|
||||||
| `Fatal` | `GD.PushError(...)` | 进入 Godot 错误通道 |
|
| `Fatal` | `GD.PushError(...)` | 进入 Godot 错误通道 |
|
||||||
|
|
||||||
异常追加格式也来自当前实现本身:
|
结构化属性如果通过 `IStructuredLogger` 或 `LogContext` 传入,也会追加到模板里的 `{properties}` 占位符。
|
||||||
|
|
||||||
|
异常追加格式仍然来自当前实现本身:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[2026-04-22 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
|
[2026-04-22 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
|
||||||
@ -182,14 +212,16 @@ System.IO.IOException: ...
|
|||||||
|
|
||||||
## 当前边界
|
## 当前边界
|
||||||
|
|
||||||
- 当前推荐接法是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;直接赋值
|
- 当前推荐接法仍然是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;如果项目是纯
|
||||||
`LoggerFactoryResolver.Provider` 仍然可用,但不该再写成默认采用路径
|
Godot 宿主,也可以在入口直接调用 `GodotLog.UseAsDefaultProvider()`
|
||||||
- `GFramework.Godot.Logging` 只解决 Godot 控制台输出,不提供文件落盘、JSON formatter、异步 appender 或按 namespace
|
- `GFramework.Godot.Logging` 只解决 Godot 控制台输出,不提供文件落盘、JSON formatter、异步 appender 或按 namespace
|
||||||
的复杂过滤
|
的复杂过滤
|
||||||
- `GodotLogger` 只改变输出方式,不改变 `ILogger` 接口本身;业务代码不需要切换到 Godot 专用日志 API
|
- `GodotLogger` 只改变输出方式,不改变 `ILogger` 接口本身;业务代码不需要切换到 Godot 专用日志 API
|
||||||
- `[Log]`、`[ContextAware]` 这类字段注入能力不属于 `GFramework.Godot.Logging`
|
- `[Log]`、`[ContextAware]` 这类字段注入能力不属于 `GFramework.Godot.Logging`
|
||||||
- Scene / UI 的 `LoggingTransitionHandler` 位于 `GFramework.Game`,Godot 侧只是通过 provider 让它们输出到 Godot 控制台
|
- Scene / UI 的 `LoggingTransitionHandler` 位于 `GFramework.Game`,Godot 侧只是通过 provider 让它们输出到 Godot 控制台
|
||||||
- 当前 `GodotLogger` 使用的是 UTC 时间戳;如果项目需要本地时区展示,需要自定义 provider / logger,而不是假定当前实现会自动转换
|
- 当前 `GodotLogger` 使用的是 UTC 时间戳;如果项目需要本地时区展示,需要自定义 provider / logger,而不是假定当前实现会自动转换
|
||||||
|
- 当前配置热重载只覆盖 Godot logger 自身的模板、颜色、模式和级别;它没有把 `Microsoft.Extensions.Logging` 的整个
|
||||||
|
options / builder 模型搬进来
|
||||||
|
|
||||||
## 继续阅读
|
## 继续阅读
|
||||||
|
|
||||||
|
|||||||
@ -1,154 +1,154 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
更新所有 C# 文件中的命名空间声明和 using 语句
|
更新所有 C# 文件中的命名空间声明和 using 语句
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT_DIR = "/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework"
|
ROOT_DIR = "/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework"
|
||||||
|
|
||||||
# 命名空间替换规则(按优先级排序,长的先匹配)
|
# 命名空间替换规则(按优先级排序,长的先匹配)
|
||||||
NAMESPACE_RULES = [
|
NAMESPACE_RULES = [
|
||||||
# CQRS 子命名空间
|
# CQRS 子命名空间
|
||||||
(r'\.cqrs\.notification\b', '.CQRS.Notification'),
|
(r'\.cqrs\.notification\b', '.CQRS.Notification'),
|
||||||
(r'\.cqrs\.command\b', '.CQRS.Command'),
|
(r'\.cqrs\.command\b', '.CQRS.Command'),
|
||||||
(r'\.cqrs\.request\b', '.CQRS.Request'),
|
(r'\.cqrs\.request\b', '.CQRS.Request'),
|
||||||
(r'\.cqrs\.query\b', '.CQRS.Query'),
|
(r'\.cqrs\.query\b', '.CQRS.Query'),
|
||||||
(r'\.cqrs\.behaviors\b', '.CQRS.Behaviors'),
|
(r'\.cqrs\.behaviors\b', '.CQRS.Behaviors'),
|
||||||
(r'\.cqrs\b', '.CQRS'),
|
(r'\.cqrs\b', '.CQRS'),
|
||||||
|
|
||||||
# 嵌套命名空间
|
# 嵌套命名空间
|
||||||
(r'\.coroutine\.instructions\b', '.Coroutine.Instructions'),
|
(r'\.coroutine\.instructions\b', '.Coroutine.Instructions'),
|
||||||
(r'\.coroutine\.extensions\b', '.Coroutine.Extensions'),
|
(r'\.coroutine\.extensions\b', '.Coroutine.Extensions'),
|
||||||
(r'\.coroutine\b', '.Coroutine'),
|
(r'\.coroutine\b', '.Coroutine'),
|
||||||
|
|
||||||
(r'\.events\.filters\b', '.Events.Filters'),
|
(r'\.events\.filters\b', '.Events.Filters'),
|
||||||
(r'\.events\b', '.Events'),
|
(r'\.events\b', '.Events'),
|
||||||
|
|
||||||
(r'\.logging\.appenders\b', '.Logging.Appenders'),
|
(r'\.logging\.appenders\b', '.Logging.Appenders'),
|
||||||
(r'\.logging\.filters\b', '.Logging.Filters'),
|
(r'\.logging\.filters\b', '.Logging.Filters'),
|
||||||
(r'\.logging\.formatters\b', '.Logging.Formatters'),
|
(r'\.logging\.formatters\b', '.Logging.Formatters'),
|
||||||
(r'\.logging\b', '.Logging'),
|
(r'\.logging\b', '.Logging'),
|
||||||
|
|
||||||
(r'\.functional\.async\b', '.Functional.Async'),
|
(r'\.functional\.async\b', '.Functional.Async'),
|
||||||
(r'\.functional\.control\b', '.Functional.Control'),
|
(r'\.functional\.control\b', '.Functional.Control'),
|
||||||
(r'\.functional\.functions\b', '.Functional.Functions'),
|
(r'\.functional\.functions\b', '.Functional.Functions'),
|
||||||
(r'\.functional\.pipe\b', '.Functional.Pipe'),
|
(r'\.functional\.pipe\b', '.Functional.Pipe'),
|
||||||
(r'\.functional\.result\b', '.Functional.Result'),
|
(r'\.functional\.result\b', '.Functional.Result'),
|
||||||
(r'\.functional\b', '.Functional'),
|
(r'\.functional\b', '.Functional'),
|
||||||
|
|
||||||
(r'\.services\.modules\b', '.Services.Modules'),
|
(r'\.services\.modules\b', '.Services.Modules'),
|
||||||
(r'\.services\b', '.Services'),
|
(r'\.services\b', '.Services'),
|
||||||
|
|
||||||
(r'\.extensions\.signal\b', '.Extensions.Signal'),
|
(r'\.extensions\.signal\b', '.Extensions.Signal'),
|
||||||
(r'\.extensions\b', '.Extensions'),
|
(r'\.extensions\b', '.Extensions'),
|
||||||
|
|
||||||
(r'\.setting\.data\b', '.Setting.Data'),
|
(r'\.setting\.data\b', '.Setting.Data'),
|
||||||
(r'\.setting\.events\b', '.Setting.Events'),
|
(r'\.setting\.events\b', '.Setting.Events'),
|
||||||
(r'\.setting\b', '.Setting'),
|
(r'\.setting\b', '.Setting'),
|
||||||
|
|
||||||
(r'\.scene\.handler\b', '.Scene.Handler'),
|
(r'\.scene\.handler\b', '.Scene.Handler'),
|
||||||
(r'\.scene\b', '.Scene'),
|
(r'\.scene\b', '.Scene'),
|
||||||
|
|
||||||
(r'\.ui\.handler\b', '.UI.Handler'),
|
(r'\.ui\.handler\b', '.UI.Handler'),
|
||||||
(r'\.ui\b', '.UI'),
|
(r'\.ui\b', '.UI'),
|
||||||
|
|
||||||
(r'\.data\.events\b', '.Data.Events'),
|
(r'\.data\.events\b', '.Data.Events'),
|
||||||
(r'\.data\b', '.Data'),
|
(r'\.data\b', '.Data'),
|
||||||
|
|
||||||
# 单层命名空间
|
# 单层命名空间
|
||||||
(r'\.architecture\b', '.Architecture'),
|
(r'\.architecture\b', '.Architecture'),
|
||||||
(r'\.bases\b', '.Bases'),
|
(r'\.bases\b', '.Bases'),
|
||||||
(r'\.command\b', '.Command'),
|
(r'\.command\b', '.Command'),
|
||||||
(r'\.configuration\b', '.Configuration'),
|
(r'\.configuration\b', '.Configuration'),
|
||||||
(r'\.constants\b', '.Constants'),
|
(r'\.constants\b', '.Constants'),
|
||||||
(r'\.enums\b', '.Enums'),
|
(r'\.enums\b', '.Enums'),
|
||||||
(r'\.environment\b', '.Environment'),
|
(r'\.environment\b', '.Environment'),
|
||||||
(r'\.internals\b', '.Internals'),
|
(r'\.internals\b', '.Internals'),
|
||||||
(r'\.ioc\b', '.IoC'),
|
(r'\.ioc\b', '.IoC'),
|
||||||
(r'\.lifecycle\b', '.Lifecycle'),
|
(r'\.lifecycle\b', '.Lifecycle'),
|
||||||
(r'\.model\b', '.Model'),
|
(r'\.model\b', '.Model'),
|
||||||
(r'\.pause\b', '.Pause'),
|
(r'\.pause\b', '.Pause'),
|
||||||
(r'\.pool\b', '.Pool'),
|
(r'\.pool\b', '.Pool'),
|
||||||
(r'\.properties\b', '.Properties'),
|
(r'\.properties\b', '.Properties'),
|
||||||
(r'\.property\b', '.Property'),
|
(r'\.property\b', '.Property'),
|
||||||
(r'\.query\b', '.Query'),
|
(r'\.query\b', '.Query'),
|
||||||
(r'\.registries\b', '.Registries'),
|
(r'\.registries\b', '.Registries'),
|
||||||
(r'\.resource\b', '.Resource'),
|
(r'\.resource\b', '.Resource'),
|
||||||
(r'\.rule\b', '.Rule'),
|
(r'\.rule\b', '.Rule'),
|
||||||
(r'\.serializer\b', '.Serializer'),
|
(r'\.serializer\b', '.Serializer'),
|
||||||
(r'\.state\b', '.State'),
|
(r'\.state\b', '.State'),
|
||||||
(r'\.storage\b', '.Storage'),
|
(r'\.storage\b', '.Storage'),
|
||||||
(r'\.system\b', '.System'),
|
(r'\.system\b', '.System'),
|
||||||
(r'\.time\b', '.Time'),
|
(r'\.time\b', '.Time'),
|
||||||
(r'\.utility\b', '.Utility'),
|
(r'\.utility\b', '.Utility'),
|
||||||
(r'\.versioning\b', '.Versioning'),
|
(r'\.versioning\b', '.Versioning'),
|
||||||
(r'\.asset\b', '.Asset'),
|
(r'\.asset\b', '.Asset'),
|
||||||
(r'\.components\b', '.Components'),
|
(r'\.components\b', '.Components'),
|
||||||
(r'\.systems\b', '.Systems'),
|
(r'\.systems\b', '.Systems'),
|
||||||
(r'\.ecs\b', '.ECS'),
|
(r'\.ecs\b', '.ECS'),
|
||||||
(r'\.integration\b', '.Integration'),
|
(r'\.integration\b', '.Integration'),
|
||||||
(r'\.mediator\b', '.Mediator'),
|
(r'\.mediator\b', '.Mediator'),
|
||||||
(r'\.tests\b', '.Tests'),
|
(r'\.tests\b', '.Tests'),
|
||||||
(r'\.analyzers\b', '.Analyzers'),
|
(r'\.analyzers\b', '.Analyzers'),
|
||||||
(r'\.diagnostics\b', '.Diagnostics'),
|
(r'\.diagnostics\b', '.Diagnostics'),
|
||||||
(r'\.generator\b', '.Generator'),
|
(r'\.generator\b', '.Generator'),
|
||||||
(r'\.info\b', '.Info'),
|
(r'\.info\b', '.Info'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_file(file_path):
|
def update_file(file_path):
|
||||||
"""更新单个文件中的命名空间"""
|
"""更新单个文件中的命名空间"""
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
original_content = content
|
original_content = content
|
||||||
replacements = 0
|
replacements = 0
|
||||||
|
|
||||||
for pattern, replacement in NAMESPACE_RULES:
|
for pattern, replacement in NAMESPACE_RULES:
|
||||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
matches = re.findall(pattern, content, re.IGNORECASE)
|
||||||
if matches:
|
if matches:
|
||||||
content = re.sub(pattern, replacement, content, flags=re.IGNORECASE)
|
content = re.sub(pattern, replacement, content, flags=re.IGNORECASE)
|
||||||
replacements += len(matches)
|
replacements += len(matches)
|
||||||
|
|
||||||
if content != original_content:
|
if content != original_content:
|
||||||
with open(file_path, 'w', encoding='utf-8') as f:
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
return replacements
|
return replacements
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"错误处理文件 {file_path}: {e}")
|
print(f"错误处理文件 {file_path}: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("开始更新命名空间...")
|
print("开始更新命名空间...")
|
||||||
|
|
||||||
# 查找所有 C# 文件
|
# 查找所有 C# 文件
|
||||||
cs_files = []
|
cs_files = []
|
||||||
for root, dirs, files in os.walk(ROOT_DIR):
|
for root, dirs, files in os.walk(ROOT_DIR):
|
||||||
# 跳过 bin, obj, Generated 目录
|
# 跳过 bin, obj, Generated 目录
|
||||||
dirs[:] = [d for d in dirs if d not in ['bin', 'obj', 'Generated', '.git', 'node_modules']]
|
dirs[:] = [d for d in dirs if d not in ['bin', 'obj', 'Generated', '.git', 'node_modules']]
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.endswith('.cs'):
|
if file.endswith('.cs'):
|
||||||
cs_files.append(os.path.join(root, file))
|
cs_files.append(os.path.join(root, file))
|
||||||
|
|
||||||
print(f"找到 {len(cs_files)} 个 C# 文件")
|
print(f"找到 {len(cs_files)} 个 C# 文件")
|
||||||
|
|
||||||
updated_files = 0
|
updated_files = 0
|
||||||
total_replacements = 0
|
total_replacements = 0
|
||||||
|
|
||||||
for file_path in cs_files:
|
for file_path in cs_files:
|
||||||
replacements = update_file(file_path)
|
replacements = update_file(file_path)
|
||||||
if replacements > 0:
|
if replacements > 0:
|
||||||
updated_files += 1
|
updated_files += 1
|
||||||
total_replacements += replacements
|
total_replacements += replacements
|
||||||
print(f"更新: {os.path.basename(file_path)} ({replacements} 处替换)")
|
print(f"更新: {os.path.basename(file_path)} ({replacements} 处替换)")
|
||||||
|
|
||||||
print(f"\n完成!更新了 {updated_files} 个文件,共 {total_replacements} 处替换")
|
print(f"\n完成!更新了 {updated_files} 个文件,共 {total_replacements} 处替换")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user