
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Gumc</title>
    <description>Here is Gu Mincong&apos;s Personal Blog, Life is nothing but an experience, To leave something behind.</description>
    <link>http://gumcstronger.github.io/</link>
    <atom:link href="http://gumcstronger.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Thu, 10 Sep 2026 16:08:33 +0000</pubDate>
    <lastBuildDate>Thu, 10 Sep 2026 16:08:33 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Cocos 转 Spine</title>
        <description>&lt;p&gt;前提：近期将Lost Journey从Cocos2d-x迁移到Unity，最初的动画是由Cocos制作的，因为只有Spine在一直维护，所以计划将Cocos动画转化为spine动画，在Unity中使用。Dragonbone可以直接读取数据。但导出Spine后会有问题，要么节点位置不对，要么有奇怪的bug。&lt;/p&gt;

&lt;p&gt;结论：最后总结出动画的转化步骤如下&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;1. 使用Dragonbone导入cocos的动画数据，并使用Dragonbone导出Dragonbone 5.5格式的动画，以hero为例，包括hero_ske.json, hero_tex.json, hero_tex.png
2. 使用DragonBoneToSpineData，将hero_ske.json, hero_tex.json, hero_tex.png整个文件夹拖入DragonBoneToSpineData转化为spine数据的armatureName.json(spine3.3支持的格式)， hero_ske.atlas.txt，hero_tex.png
3. 使用spine 4.3的纹理解包器，导入hero_ske.atlas.txt可以解包出所有的图片资源
4. 使用spine3.3导入armatureName.json，设置图片的路径为前面解压的图片资源的文件夹。然后保存项目，然后用spine4.3重新打开项目，然后导出armatureName.json(即spine4.3版本的数据)
4. armatureName.json会存在问题：关键帧的alpha错误,duration错误,还有bone缩放错误。需要重建 slot 颜色轨道和 bone 缩放轨道，还有等等各种各样的问题。以下提供的Unity代码用于根据cocos动画重建armatureName.json后导出armatureName_output.json
5. 使用spine4.3导入armatureName_output.json，然后将图片文件夹设置为解包后的Image文件夹，将骨架的名字修改为旧的名字，然后导出即可。
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;附带4的Unity代码:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-C#&quot;&gt;#pragma warning disable ET0004
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;

/// &amp;lt;summary&amp;gt;
/// 依据 Cocos Studio ExportJson 修复转换后的 Spine 4.3 动画数据。
/// &amp;lt;/summary&amp;gt;
public static class CocosExportJsonToSpine43RepairEditor
{
    private const string MenuPath = &quot;GameFramework/Project/Spine/5.(final) 修复 Cocos ExportJson 转 Spine4.3 动画数据&quot;;
    private const double CocosRuntimeFrameRate = 60d;
    private const double ValueTolerance = 0.000001d;
    private const double MatrixTolerance = 0.0001d;
    private const double EasingApproximationTolerance = 0.0001d;
    private const int MaxEasingSubdivisionDepth = 10;
    private const double RadiansToDegrees = 180d / Math.PI;
    private const double SineControlX1 = 0.36432160614986847d;
    private const double SineControlX2 = 0.6356762953556379d;

    [MenuItem(MenuPath)]
    private static void RepairAnimationData()
    {
        try
        {
            // 选择输入和独立输出路径。
            string sourcePath = EditorUtility.OpenFilePanel(&quot;选择 Cocos Studio ExportJson&quot;, Application.dataPath, &quot;ExportJson&quot;);
            if (string.IsNullOrEmpty(sourcePath))
            {
                return;
            }

            string spinePath = EditorUtility.OpenFilePanel(&quot;选择未修复的 Spine 4.3 JSON&quot;, Path.GetDirectoryName(sourcePath), &quot;json&quot;);
            if (string.IsNullOrEmpty(spinePath))
            {
                return;
            }

            string outputPath = Path.Combine(Path.GetDirectoryName(spinePath), Path.GetFileNameWithoutExtension(spinePath) + &quot;_fixed.json&quot;);
            if (File.Exists(outputPath) &amp;amp;&amp;amp; !EditorUtility.DisplayDialog(&quot;输出文件已存在&quot;, $&quot;将覆盖已有文件：\n{outputPath}&quot;, &quot;覆盖&quot;, &quot;取消&quot;))
            {
                return;
            }

            // 解析源数据并重建 setup pose、动画轨道和绘制顺序。
            JObject sourceRoot = JObject.Parse(File.ReadAllText(sourcePath));
            JObject spineRoot = JObject.Parse(File.ReadAllText(spinePath));
            JObject originalSpineRoot = spineRoot.DeepClone() as JObject;
            SourceData sourceData = ReadSourceData(sourceRoot);
            EnsureSpine43(spineRoot);
            SetupTransformData setupTransformData = BuildSetupTransformData(sourceData);
            RepairResult result = new RepairResult();
            RepairSetupPose(sourceData, setupTransformData, spineRoot, result);
            RepairAnimations(sourceData, setupTransformData, spineRoot, result);

            // 阻止修复误改 skins、附件和允许范围外的数据。
            EnsureOnlyRepairableDataChanged(originalSpineRoot, spineRoot, sourceData);
            ValidateRepairedSetupPose(sourceData, setupTransformData, spineRoot);
            ValidateRepairedTimelines(spineRoot);
            ValidateWithSpineRuntime(spineRoot);

            // 使用 UTF-8 无 BOM 和 LF 写入结果。
            string outputJson = NormalizeLineEndings(spineRoot.ToString(Formatting.Indented)) + &quot;\n&quot;;
            File.WriteAllText(outputPath, outputJson, new UTF8Encoding(false));
            AssetDatabase.Refresh();

            Debug.Log(
                $&quot;[CocosExportJsonToSpine43RepairEditor] Spine 4.3 数据修复完成。\n&quot;
                    + $&quot;Setup 骨骼: 修复 {result.RepairedSetupBoneCount}\n&quot;
                    + $&quot;Setup 插槽: 修复 {result.RepairedSetupSlotCount}\n&quot;
                    + $&quot;位移轨道: 重建 {result.RebuiltTranslateTimelineCount}，删除误造 {result.RemovedTranslateTimelineCount}\n&quot;
                    + $&quot;旋转轨道: 重建 {result.RebuiltRotateTimelineCount}，删除误造 {result.RemovedRotateTimelineCount}\n&quot;
                    + $&quot;缩放轨道: 重建 {result.RebuiltScaleTimelineCount}，删除误造 {result.RemovedScaleTimelineCount}\n&quot;
                    + $&quot;剪切轨道: 重建 {result.RebuiltShearTimelineCount}，删除误造 {result.RemovedShearTimelineCount}\n&quot;
                    + $&quot;附件轨道: 重建 {result.RebuiltAttachmentTimelineCount}，删除误造 {result.RemovedAttachmentTimelineCount}\n&quot;
                    + $&quot;颜色轨道: 重建 {result.RebuiltColorTimelineCount}，删除误造 {result.RemovedColorTimelineCount}\n&quot;
                    + $&quot;绘制顺序轨道: 重建 {result.RebuiltDrawOrderTimelineCount}，删除误造 {result.RemovedDrawOrderTimelineCount}\n&quot;
                    + $&quot;输出文件: {outputPath}&quot;
            );
            EditorUtility.RevealInFinder(outputPath);
        }
        catch (Exception exception)
        {
            Debug.LogError($&quot;[CocosExportJsonToSpine43RepairEditor] 修复失败: {exception.Message}\n{exception.StackTrace}&quot;);
            EditorUtility.DisplayDialog(&quot;Spine 4.3 数据修复失败&quot;, exception.Message, &quot;确定&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取唯一 Cocos armature、同名 animation_data、setup 骨骼和 movement 索引。
    /// &amp;lt;/summary&amp;gt;
    private static SourceData ReadSourceData(JObject sourceRoot)
    {
        JArray armatures = sourceRoot[&quot;armature_data&quot;] as JArray;
        if (armatures == null || armatures.Count != 1)
        {
            throw new InvalidDataException($&quot;期望 ExportJson 包含 1 个 armature_data，实际为 {armatures?.Count ?? 0} 个。&quot;);
        }

        JObject armature = armatures[0] as JObject;
        string armatureName = armature?.Value&amp;lt;string&amp;gt;(&quot;name&quot;);
        JArray sourceBones = armature?[&quot;bone_data&quot;] as JArray;
        if (string.IsNullOrEmpty(armatureName) || sourceBones == null)
        {
            throw new InvalidDataException(&quot;ExportJson armature_data[0] 缺少 name 或 bone_data。&quot;);
        }

        // 建立 setup 骨骼及显示附件索引。
        Dictionary&amp;lt;string, JObject&amp;gt; sourceBoneMap = BuildNamedObjectMap(sourceBones, &quot;ExportJson setup 骨骼&quot;);
        Dictionary&amp;lt;string, List&amp;lt;string&amp;gt;&amp;gt; displayNameMap = new Dictionary&amp;lt;string, List&amp;lt;string&amp;gt;&amp;gt;(StringComparer.Ordinal);
        foreach (KeyValuePair&amp;lt;string, JObject&amp;gt; bonePair in sourceBoneMap)
        {
            JArray displays = bonePair.Value[&quot;display_data&quot;] as JArray;
            List&amp;lt;string&amp;gt; displayNames = new List&amp;lt;string&amp;gt;();
            if (displays != null)
            {
                for (int displayIndex = 0; displayIndex &amp;lt; displays.Count; displayIndex++)
                {
                    JObject display = displays[displayIndex] as JObject;
                    string displayName = StripExtension(display?.Value&amp;lt;string&amp;gt;(&quot;name&quot;));
                    if (string.IsNullOrEmpty(displayName))
                    {
                        throw new InvalidDataException($&quot;ExportJson 骨骼 {bonePair.Key} 第 {displayIndex} 个 display_data 名称无效。&quot;);
                    }

                    displayNames.Add(displayName);
                }
            }

            displayNameMap.Add(bonePair.Key, displayNames);
        }

        // 选择与 armature 同名的 animation_data。
        JArray animationDataList = sourceRoot[&quot;animation_data&quot;] as JArray;
        JObject selectedAnimationData = null;
        if (animationDataList != null)
        {
            for (int animationDataIndex = 0; animationDataIndex &amp;lt; animationDataList.Count; animationDataIndex++)
            {
                JObject animationData = animationDataList[animationDataIndex] as JObject;
                if (!string.Equals(animationData?.Value&amp;lt;string&amp;gt;(&quot;name&quot;), armatureName, StringComparison.Ordinal))
                {
                    continue;
                }

                if (selectedAnimationData != null)
                {
                    throw new InvalidDataException($&quot;ExportJson 存在重复 animation_data: {armatureName}。&quot;);
                }

                selectedAnimationData = animationData;
            }
        }

        JArray movements = selectedAnimationData?[&quot;mov_data&quot;] as JArray;
        if (movements == null)
        {
            throw new InvalidDataException($&quot;ExportJson 缺少与 armature {armatureName} 同名的 animation_data.mov_data。&quot;);
        }

        Dictionary&amp;lt;string, JObject&amp;gt; movementMap = BuildNamedObjectMap(movements, &quot;ExportJson movement&quot;);
        List&amp;lt;string&amp;gt; sourceBoneOrder = CollectNamedOrder(sourceBones, &quot;ExportJson setup 骨骼&quot;);
        return new SourceData(sourceBoneMap, sourceBoneOrder, displayNameMap, movementMap);
    }

    /// &amp;lt;summary&amp;gt;
    /// 限定目标为当前工具支持的 Spine 4.3 JSON。
    /// &amp;lt;/summary&amp;gt;
    private static void EnsureSpine43(JObject spineRoot)
    {
        string version = spineRoot[&quot;skeleton&quot;]?.Value&amp;lt;string&amp;gt;(&quot;spine&quot;);
        if (string.IsNullOrEmpty(version) || !version.StartsWith(&quot;4.3&quot;, StringComparison.Ordinal))
        {
            throw new InvalidDataException($&quot;当前工具只支持 Spine 4.3 JSON，输入版本为 {version ?? &quot;缺失&quot;}。&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 按 Cocos 双轴角完整重建 Spine setup pose，保留旋转中编码的反射。
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;summary&amp;gt;
    /// 按 sceneext Bone::applyParentTransform 建立 setup 世界变换和 Spine 局部变换。
    /// &amp;lt;/summary&amp;gt;
    private static SetupTransformData BuildSetupTransformData(SourceData sourceData)
    {
        Dictionary&amp;lt;string, CocosWorldTransform&amp;gt; worldTransformMap = new Dictionary&amp;lt;string, CocosWorldTransform&amp;gt;(StringComparer.Ordinal);
        Dictionary&amp;lt;string, SpineLocalTransform&amp;gt; spineLocalTransformMap = new Dictionary&amp;lt;string, SpineLocalTransform&amp;gt;(StringComparer.Ordinal);
        Dictionary&amp;lt;string, double&amp;gt; rotationDirectionMap = new Dictionary&amp;lt;string, double&amp;gt;(StringComparer.Ordinal);
        HashSet&amp;lt;string&amp;gt; visiting = new HashSet&amp;lt;string&amp;gt;(StringComparer.Ordinal);

        // 递归建立 Cocos setup 世界变换，允许源骨骼不是严格父级优先排列。
        for (int boneIndex = 0; boneIndex &amp;lt; sourceData.SourceBoneOrder.Count; boneIndex++)
        {
            string boneName = sourceData.SourceBoneOrder[boneIndex];
            BuildCocosSetupWorldTransform(boneName, sourceData.SourceBoneMap, worldTransformMap, visiting);
        }

        // 将目标世界矩阵反解为 Spine Normal 继承需要的局部矩阵。
        for (int boneIndex = 0; boneIndex &amp;lt; sourceData.SourceBoneOrder.Count; boneIndex++)
        {
            string boneName = sourceData.SourceBoneOrder[boneIndex];
            JObject sourceBone = sourceData.SourceBoneMap[boneName];
            string parentName = sourceBone.Value&amp;lt;string&amp;gt;(&quot;parent&quot;);
            CocosWorldTransform worldTransform = worldTransformMap[boneName];
            Matrix2D localMatrix = string.IsNullOrEmpty(parentName)
                ? worldTransform.Matrix
                : MultiplyInverse(worldTransformMap[parentName].Matrix, worldTransform.Matrix, boneName);
            spineLocalTransformMap.Add(boneName, DecomposeSpineLocalMatrix(localMatrix, boneName));

            double rotationDirection = 1d;
            if (!string.IsNullOrEmpty(parentName) &amp;amp;&amp;amp; worldTransformMap[parentName].Matrix.Determinant &amp;lt; 0d)
            {
                rotationDirection = -1d;
            }

            rotationDirectionMap.Add(boneName, rotationDirection);
        }

        return new SetupTransformData(worldTransformMap, spineLocalTransformMap, rotationDirectionMap);
    }

    /// &amp;lt;summary&amp;gt;
    /// 递归计算单根 Cocos setup 骨骼的世界变换。
    /// &amp;lt;/summary&amp;gt;
    private static CocosWorldTransform BuildCocosSetupWorldTransform(
        string boneName,
        Dictionary&amp;lt;string, JObject&amp;gt; sourceBoneMap,
        Dictionary&amp;lt;string, CocosWorldTransform&amp;gt; worldTransformMap,
        HashSet&amp;lt;string&amp;gt; visiting
    )
    {
        if (worldTransformMap.TryGetValue(boneName, out CocosWorldTransform existingTransform))
        {
            return existingTransform;
        }

        if (!visiting.Add(boneName))
        {
            throw new InvalidDataException($&quot;ExportJson setup 骨骼层级存在循环: {boneName}。&quot;);
        }

        JObject sourceBone = sourceBoneMap[boneName];
        string context = $&quot;ExportJson setup 骨骼 {boneName}&quot;;
        double x = ReadRequiredDouble(sourceBone, &quot;x&quot;, context);
        double y = ReadRequiredDouble(sourceBone, &quot;y&quot;, context);
        double scaleX = ReadRequiredDouble(sourceBone, &quot;cX&quot;, context);
        double scaleY = ReadRequiredDouble(sourceBone, &quot;cY&quot;, context);
        double skewX = ReadRequiredDouble(sourceBone, &quot;kX&quot;, context);
        double skewY = ReadRequiredDouble(sourceBone, &quot;kY&quot;, context);
        string parentName = sourceBone.Value&amp;lt;string&amp;gt;(&quot;parent&quot;);

        // 精确复现 Bone::applyParentTransform 的位置矩阵和分量式继承。
        if (!string.IsNullOrEmpty(parentName))
        {
            if (!sourceBoneMap.ContainsKey(parentName))
            {
                throw new InvalidDataException($&quot;ExportJson setup 骨骼 {boneName} 的父骨骼不存在: {parentName}。&quot;);
            }

            CocosWorldTransform parentTransform = BuildCocosSetupWorldTransform(parentName, sourceBoneMap, worldTransformMap, visiting);
            double localX = x;
            double localY = y;
            x = localX * parentTransform.Matrix.A + localY * parentTransform.Matrix.B + parentTransform.X;
            y = localX * parentTransform.Matrix.C + localY * parentTransform.Matrix.D + parentTransform.Y;
            scaleX *= parentTransform.ScaleX;
            scaleY *= parentTransform.ScaleY;
            skewX += parentTransform.SkewX;
            skewY += parentTransform.SkewY;
        }

        CocosWorldTransform result = new CocosWorldTransform(x, y, scaleX, scaleY, skewX, skewY, CreateCocosMatrix(scaleX, scaleY, skewX, skewY));
        worldTransformMap.Add(boneName, result);
        visiting.Remove(boneName);
        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 按 TransformHelp::nodeToMatrix 创建 Spine 坐标布局的二维矩阵。
    /// &amp;lt;/summary&amp;gt;
    private static Matrix2D CreateCocosMatrix(double scaleX, double scaleY, double skewX, double skewY)
    {
        return new Matrix2D(scaleX * Math.Cos(skewY), scaleY * Math.Sin(skewX), scaleX * Math.Sin(skewY), scaleY * Math.Cos(skewX));
    }

    /// &amp;lt;summary&amp;gt;
    /// 计算 inverse(parent) * world，得到 Spine Normal 继承下的局部矩阵。
    /// &amp;lt;/summary&amp;gt;
    private static Matrix2D MultiplyInverse(Matrix2D parent, Matrix2D world, string boneName)
    {
        double determinant = parent.Determinant;
        if (Math.Abs(determinant) &amp;lt;= ValueTolerance)
        {
            throw new InvalidDataException($&quot;ExportJson setup 骨骼 {boneName} 的父矩阵不可逆。&quot;);
        }

        return new Matrix2D(
            (parent.D * world.A - parent.B * world.C) / determinant,
            (parent.D * world.B - parent.B * world.D) / determinant,
            (-parent.C * world.A + parent.A * world.C) / determinant,
            (-parent.C * world.B + parent.A * world.D) / determinant
        );
    }

    /// &amp;lt;summary&amp;gt;
    /// 将任意可逆二维矩阵分解为 Spine rotation、signed scale 和 shearY。
    /// &amp;lt;/summary&amp;gt;
    private static SpineLocalTransform DecomposeSpineLocalMatrix(Matrix2D matrix, string boneName)
    {
        double scaleX = Math.Sqrt(matrix.A * matrix.A + matrix.C * matrix.C);
        double scaleYLength = Math.Sqrt(matrix.B * matrix.B + matrix.D * matrix.D);
        if (scaleX &amp;lt;= ValueTolerance || scaleYLength &amp;lt;= ValueTolerance)
        {
            throw new InvalidDataException($&quot;ExportJson setup 骨骼 {boneName} 含不可分解的零缩放矩阵。&quot;);
        }

        double rotation = Math.Atan2(matrix.C, matrix.A) * RadiansToDegrees;
        double scaleY = matrix.Determinant &amp;lt; 0d ? -scaleYLength : scaleYLength;
        double yAxisRotation = Math.Atan2(matrix.D / scaleY, matrix.B / scaleY) * RadiansToDegrees;
        double shearY = NormalizeDegrees(yAxisRotation - rotation - 90d);
        return new SpineLocalTransform(NormalizeDegrees(rotation), scaleX, scaleY, shearY);
    }

    /// &amp;lt;summary&amp;gt;
    /// 将角度归一到 (-180, 180]。
    /// &amp;lt;/summary&amp;gt;
    private static double NormalizeDegrees(double value)
    {
        value %= 360d;
        if (value &amp;lt;= -180d)
        {
            value += 360d;
        }
        else if (value &amp;gt; 180d)
        {
            value -= 360d;
        }

        return value;
    }

    /// &amp;lt;summary&amp;gt;
    /// 写入按 Cocos 世界矩阵反解后的 Spine setup 局部变换。
    /// &amp;lt;/summary&amp;gt;
    private static void RepairSetupPose(SourceData sourceData, SetupTransformData setupTransformData, JObject spineRoot, RepairResult result)
    {
        Dictionary&amp;lt;string, JObject&amp;gt; spineBoneMap = BuildNamedObjectMap(spineRoot[&quot;bones&quot;] as JArray, &quot;Spine setup 骨骼&quot;);

        // 位置保持 Cocos 局部坐标；轴矩阵使用层级反解结果。
        for (int boneIndex = 0; boneIndex &amp;lt; sourceData.SourceBoneOrder.Count; boneIndex++)
        {
            string boneName = sourceData.SourceBoneOrder[boneIndex];
            JObject sourceBone = sourceData.SourceBoneMap[boneName];
            if (!spineBoneMap.TryGetValue(boneName, out JObject spineBone))
            {
                throw new InvalidDataException($&quot;Spine setup pose 缺少 Cocos 同名骨骼: {boneName}。&quot;);
            }

            SpineLocalTransform transform = setupTransformData.SpineLocalTransformMap[boneName];
            WriteOptionalDouble(spineBone, &quot;x&quot;, ReadRequiredDouble(sourceBone, &quot;x&quot;, $&quot;setup 骨骼 {boneName}&quot;), 0d);
            WriteOptionalDouble(spineBone, &quot;y&quot;, ReadRequiredDouble(sourceBone, &quot;y&quot;, $&quot;setup 骨骼 {boneName}&quot;), 0d);
            WriteOptionalDouble(spineBone, &quot;rotation&quot;, transform.Rotation, 0d);
            WriteOptionalDouble(spineBone, &quot;scaleX&quot;, transform.ScaleX, 1d);
            WriteOptionalDouble(spineBone, &quot;scaleY&quot;, transform.ScaleY, 1d);
            spineBone.Remove(&quot;shearX&quot;);
            WriteOptionalDouble(spineBone, &quot;shearY&quot;, transform.ShearY, 0d);
            result.RepairedSetupBoneCount++;
        }

        // 按 setup dI 修复 Spine attachment；任意负索引均表示隐藏。
        Dictionary&amp;lt;string, JObject&amp;gt; spineSlotMap = BuildNamedObjectMap(spineRoot[&quot;slots&quot;] as JArray, &quot;Spine setup 插槽&quot;);
        foreach (KeyValuePair&amp;lt;string, List&amp;lt;string&amp;gt;&amp;gt; displayPair in sourceData.DisplayNameMap)
        {
            if (displayPair.Value.Count == 0)
            {
                continue;
            }

            if (!spineSlotMap.TryGetValue(displayPair.Key, out JObject spineSlot))
            {
                throw new InvalidDataException($&quot;Spine setup pose 缺少 Cocos 同名插槽: {displayPair.Key}。&quot;);
            }

            int displayIndex = ReadNormalizedDisplayIndex(
                sourceData.SourceBoneMap[displayPair.Key],
                displayPair.Value.Count,
                $&quot;setup 骨骼 {displayPair.Key}&quot;
            );
            if (displayIndex &amp;lt; 0)
            {
                spineSlot.Remove(&quot;attachment&quot;);
            }
            else
            {
                spineSlot[&quot;attachment&quot;] = displayPair.Value[displayIndex];
            }

            result.RepairedSetupSlotCount++;
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 逐骨骼比较 Cocos 与 Spine 局部仿射矩阵，验证旋转、反射、缩放和位移均未丢失。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateRepairedSetupPose(SourceData sourceData, SetupTransformData setupTransformData, JObject spineRoot)
    {
        Dictionary&amp;lt;string, JObject&amp;gt; spineBoneMap = BuildNamedObjectMap(spineRoot[&quot;bones&quot;] as JArray, &quot;Spine setup 骨骼&quot;);
        Dictionary&amp;lt;string, Matrix2D&amp;gt; spineWorldMatrixMap = new Dictionary&amp;lt;string, Matrix2D&amp;gt;(StringComparer.Ordinal);
        Dictionary&amp;lt;string, Vector2D&amp;gt; spineWorldPositionMap = new Dictionary&amp;lt;string, Vector2D&amp;gt;(StringComparer.Ordinal);

        // 按 Spine Normal 继承重新计算世界矩阵和位置。
        for (int boneIndex = 0; boneIndex &amp;lt; sourceData.SourceBoneOrder.Count; boneIndex++)
        {
            string boneName = sourceData.SourceBoneOrder[boneIndex];
            JObject sourceBone = sourceData.SourceBoneMap[boneName];
            JObject spineBone = spineBoneMap[boneName];
            string parentName = sourceBone.Value&amp;lt;string&amp;gt;(&quot;parent&quot;);
            double x = spineBone.Value&amp;lt;double?&amp;gt;(&quot;x&quot;) ?? 0d;
            double y = spineBone.Value&amp;lt;double?&amp;gt;(&quot;y&quot;) ?? 0d;
            double rotation = spineBone.Value&amp;lt;double?&amp;gt;(&quot;rotation&quot;) ?? 0d;
            double scaleX = spineBone.Value&amp;lt;double?&amp;gt;(&quot;scaleX&quot;) ?? 1d;
            double scaleY = spineBone.Value&amp;lt;double?&amp;gt;(&quot;scaleY&quot;) ?? 1d;
            double shearX = spineBone.Value&amp;lt;double?&amp;gt;(&quot;shearX&quot;) ?? 0d;
            double shearY = spineBone.Value&amp;lt;double?&amp;gt;(&quot;shearY&quot;) ?? 0d;
            double xAxis = (rotation + shearX) / RadiansToDegrees;
            double yAxis = (rotation + 90d + shearY) / RadiansToDegrees;
            Matrix2D localMatrix = new Matrix2D(Math.Cos(xAxis) * scaleX, Math.Cos(yAxis) * scaleY, Math.Sin(xAxis) * scaleX, Math.Sin(yAxis) * scaleY);

            Matrix2D worldMatrix = localMatrix;
            Vector2D worldPosition = new Vector2D(x, y);
            if (!string.IsNullOrEmpty(parentName))
            {
                Matrix2D parentMatrix = spineWorldMatrixMap[parentName];
                Vector2D parentPosition = spineWorldPositionMap[parentName];
                worldMatrix = Matrix2D.Multiply(parentMatrix, localMatrix);
                worldPosition = new Vector2D(
                    parentMatrix.A * x + parentMatrix.B * y + parentPosition.X,
                    parentMatrix.C * x + parentMatrix.D * y + parentPosition.Y
                );
            }

            spineWorldMatrixMap.Add(boneName, worldMatrix);
            spineWorldPositionMap.Add(boneName, worldPosition);

            CocosWorldTransform expected = setupTransformData.WorldTransformMap[boneName];
            if (
                Math.Abs(expected.X - worldPosition.X) &amp;gt; MatrixTolerance
                || Math.Abs(expected.Y - worldPosition.Y) &amp;gt; MatrixTolerance
                || Math.Abs(expected.Matrix.A - worldMatrix.A) &amp;gt; MatrixTolerance
                || Math.Abs(expected.Matrix.B - worldMatrix.B) &amp;gt; MatrixTolerance
                || Math.Abs(expected.Matrix.C - worldMatrix.C) &amp;gt; MatrixTolerance
                || Math.Abs(expected.Matrix.D - worldMatrix.D) &amp;gt; MatrixTolerance
            )
            {
                throw new InvalidDataException($&quot;Spine setup 骨骼 {boneName} 未保持 Cocos sceneext 世界变换。&quot;);
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验两侧 setup 和动画集合，并按每个 Cocos movement 重建动画数据。
    /// &amp;lt;/summary&amp;gt;
    private static void RepairAnimations(SourceData sourceData, SetupTransformData setupTransformData, JObject spineRoot, RepairResult result)
    {
        JObject spineAnimations = spineRoot[&quot;animations&quot;] as JObject;
        if (spineAnimations == null)
        {
            throw new InvalidDataException(&quot;Spine 文件缺少 animations。&quot;);
        }

        // 校验 setup 骨骼、slot 和 attachment 映射。
        Dictionary&amp;lt;string, JObject&amp;gt; spineBoneMap = BuildNamedObjectMap(spineRoot[&quot;bones&quot;] as JArray, &quot;Spine setup 骨骼&quot;);
        Dictionary&amp;lt;string, JObject&amp;gt; spineSlotMap = BuildNamedObjectMap(spineRoot[&quot;slots&quot;] as JArray, &quot;Spine setup 插槽&quot;);
        foreach (KeyValuePair&amp;lt;string, JObject&amp;gt; sourceBonePair in sourceData.SourceBoneMap)
        {
            if (!spineBoneMap.ContainsKey(sourceBonePair.Key))
            {
                throw new InvalidDataException($&quot;Spine setup pose 缺少 Cocos 同名骨骼: {sourceBonePair.Key}。&quot;);
            }

            List&amp;lt;string&amp;gt; displayNames = sourceData.DisplayNameMap[sourceBonePair.Key];
            if (displayNames.Count == 0)
            {
                continue;
            }

            if (!spineSlotMap.TryGetValue(sourceBonePair.Key, out JObject spineSlot))
            {
                throw new InvalidDataException($&quot;Spine setup pose 缺少 Cocos 同名插槽: {sourceBonePair.Key}。&quot;);
            }

            int sourceDisplayIndex = ReadNormalizedDisplayIndex(
                sourceBonePair.Value,
                displayNames.Count,
                $&quot;setup 骨骼 {sourceBonePair.Key}&quot;
            );
            string expectedAttachment = sourceDisplayIndex &amp;lt; 0 ? null : displayNames[sourceDisplayIndex];
            string setupAttachment = spineSlot.Value&amp;lt;string&amp;gt;(&quot;attachment&quot;);
            if (!string.Equals(setupAttachment, expectedAttachment, StringComparison.Ordinal))
            {
                throw new InvalidDataException(
                    $&quot;Spine setup attachment 与 Cocos dI 不一致: {sourceBonePair.Key}，&quot;
                        + $&quot;{setupAttachment ?? &quot;隐藏&quot;} / {expectedAttachment ?? &quot;隐藏&quot;}。&quot;
                );
            }
        }

        // 要求 movement 与 Spine animation 集合完全一致。
        foreach (JProperty spineAnimationProperty in spineAnimations.Properties())
        {
            if (!sourceData.MovementMap.ContainsKey(spineAnimationProperty.Name))
            {
                throw new InvalidDataException($&quot;ExportJson 缺少 Spine 同名 movement: {spineAnimationProperty.Name}。&quot;);
            }
        }

        if (sourceData.MovementMap.Count != spineAnimations.Count)
        {
            throw new InvalidDataException($&quot;ExportJson movement 与 Spine animation 数量不一致: {sourceData.MovementMap.Count} / {spineAnimations.Count}。&quot;);
        }

        // 逐 movement 重建骨骼、附件、颜色和绘制顺序轨道。
        List&amp;lt;string&amp;gt; setupSlotOrder = CollectNamedOrder(spineRoot[&quot;slots&quot;] as JArray, &quot;Spine setup 插槽&quot;);
        foreach (KeyValuePair&amp;lt;string, JObject&amp;gt; movementPair in sourceData.MovementMap)
        {
            JObject spineAnimation = spineAnimations[movementPair.Key] as JObject;
            if (spineAnimation == null)
            {
                throw new InvalidDataException($&quot;Spine 动画 {movementPair.Key} 不是有效对象。&quot;);
            }

            MovementData movementData = ReadMovementData(
                movementPair.Key,
                movementPair.Value,
                sourceData.SourceBoneMap,
                sourceData.SourceBoneOrder,
                setupSlotOrder,
                setupTransformData.RotationDirectionMap
            );
            RepairBoneTimelines(sourceData, setupTransformData, movementData, spineAnimation, result);
            RepairSlotTimelines(sourceData, movementData, spineAnimation, result);
            RepairDrawOrderTimeline(sourceData, movementData, setupSlotOrder, spineAnimation, result);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取 movement 播放速度、时长和骨骼帧，并拒绝无法无损映射的运行时功能。
    /// &amp;lt;/summary&amp;gt;
    private static MovementData ReadMovementData(
        string movementName,
        JObject movement,
        Dictionary&amp;lt;string, JObject&amp;gt; sourceBoneMap,
        List&amp;lt;string&amp;gt; sourceBoneOrder,
        List&amp;lt;string&amp;gt; setupSlotOrder,
        Dictionary&amp;lt;string, double&amp;gt; rotationDirectionMap
    )
    {
        int duration = ReadRequiredInt(movement, &quot;dr&quot;, $&quot;movement {movementName}&quot;);
        double movementScale = ReadRequiredDouble(movement, &quot;sc&quot;, $&quot;movement {movementName}&quot;);
        if (duration &amp;lt; 0 || !IsFinite(movementScale) || movementScale &amp;lt;= 0d)
        {
            throw new InvalidDataException($&quot;ExportJson movement {movementName} 的 dr 或 sc 无效。&quot;);
        }

        EnsureZeroInt(movement, &quot;to&quot;, $&quot;movement {movementName}&quot;);
        EnsureZeroInt(movement, &quot;drTW&quot;, $&quot;movement {movementName}&quot;);
        EnsureZeroInt(movement, &quot;twE&quot;, $&quot;movement {movementName}&quot;);
        double secondsPerFrame = 1d / (CocosRuntimeFrameRate * movementScale);

        JArray movementBones = movement[&quot;mov_bone_data&quot;] as JArray;
        if (movementBones == null)
        {
            throw new InvalidDataException($&quot;ExportJson movement {movementName} 缺少 mov_bone_data。&quot;);
        }

        Dictionary&amp;lt;string, JObject&amp;gt; movementBoneMap = BuildNamedObjectMap(movementBones, $&quot;ExportJson movement {movementName} 骨骼&quot;);
        List&amp;lt;string&amp;gt; movementBoneOrder = CollectNamedOrder(movementBones, $&quot;ExportJson movement {movementName} 骨骼&quot;);

        // 校验骨骼引用和动态 z-order 语义。
        ValidateMovementZOrder(movementName, sourceBoneMap, movementBoneMap, setupSlotOrder);

        // 校验全部关键帧字段。
        foreach (KeyValuePair&amp;lt;string, JObject&amp;gt; movementBonePair in movementBoneMap)
        {
            if (!sourceBoneMap.ContainsKey(movementBonePair.Key))
            {
                throw new InvalidDataException($&quot;ExportJson movement {movementName} 引用了 setup 中不存在的骨骼: {movementBonePair.Key}。&quot;);
            }

            double delay = movementBonePair.Value.Value&amp;lt;double?&amp;gt;(&quot;dl&quot;) ?? 0d;
            if (!IsFinite(delay) || Math.Abs(delay) &amp;gt; ValueTolerance)
            {
                throw new InvalidDataException($&quot;ExportJson movement {movementName}.{movementBonePair.Key} 使用未支持的 delay={delay}。&quot;);
            }

            JArray frames = movementBonePair.Value[&quot;frame_data&quot;] as JArray;
            if (frames == null || frames.Count == 0)
            {
                throw new InvalidDataException($&quot;ExportJson movement {movementName}.{movementBonePair.Key} 缺少 frame_data。&quot;);
            }

            int previousFrameIndex = -1;
            for (int frameIndex = 0; frameIndex &amp;lt; frames.Count; frameIndex++)
            {
                JObject frame = RequireFrame(frames, movementName, movementBonePair.Key, frameIndex);
                int sourceFrameIndex = ReadRequiredInt(frame, &quot;fi&quot;, $&quot;movement {movementName}.{movementBonePair.Key}.frame_data[{frameIndex}]&quot;);
                if (sourceFrameIndex &amp;lt;= previousFrameIndex || sourceFrameIndex &amp;gt; duration)
                {
                    throw new InvalidDataException(
                        $&quot;ExportJson {movementName}.{movementBonePair.Key}.frame_data[{frameIndex}] fi 无效: &quot;
                            + $&quot;{sourceFrameIndex}，前一帧 {previousFrameIndex}，movement dr={duration}。&quot;
                    );
                }

                ValidateFrame(movementName, movementBonePair.Key, frameIndex, frame);
                previousFrameIndex = sourceFrameIndex;
            }
        }

        return new MovementData(
            movementName,
            duration,
            secondsPerFrame,
            sourceBoneMap,
            sourceBoneOrder,
            movementBoneMap,
            movementBoneOrder,
            rotationDirectionMap
        );
    }

    /// &amp;lt;summary&amp;gt;
    /// 按 Cocos setup z 与帧 z 验证 movement 全部关键时刻的稳定绘制顺序。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateMovementZOrder(
        string movementName,
        Dictionary&amp;lt;string, JObject&amp;gt; sourceBoneMap,
        Dictionary&amp;lt;string, JObject&amp;gt; movementBoneMap,
        List&amp;lt;string&amp;gt; setupSlotOrder
    )
    {
        // Spine slot 必须存在同名 Cocos setup 骨骼，才能计算动态绘制顺序。
        for (int slotIndex = 0; slotIndex &amp;lt; setupSlotOrder.Count; slotIndex++)
        {
            string slotName = setupSlotOrder[slotIndex];
            if (!sourceBoneMap.ContainsKey(slotName))
            {
                throw new InvalidDataException($&quot;ExportJson setup 骨骼缺少 Spine slot 同名骨骼: {slotName}。&quot;);
            }
        }

        // movement 骨骼必须来自同一 setup；具体 z 变化由 drawOrder 重建阶段处理。
        foreach (string boneName in movementBoneMap.Keys)
        {
            if (!sourceBoneMap.ContainsKey(boneName))
            {
                throw new InvalidDataException($&quot;ExportJson movement {movementName} 引用了 setup 中不存在的骨骼: {boneName}。&quot;);
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 比较 Cocos 绘制顺序；z 相同时保留 Spine setup slot 次序。
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;summary&amp;gt;
    /// 读取指定时刻 setup z 与最新帧 z 的和。
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;summary&amp;gt;
    /// 校验单个 Cocos 帧的变换、显示、颜色和补间字段。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateFrame(string movementName, string boneName, int frameIndex, JObject frame)
    {
        string context = $&quot;movement {movementName}.{boneName}.frame_data[{frameIndex}]&quot;;
        double x = ReadRequiredDouble(frame, &quot;x&quot;, context);
        double y = ReadRequiredDouble(frame, &quot;y&quot;, context);
        double scaleX = ReadRequiredDouble(frame, &quot;cX&quot;, context);
        double scaleY = ReadRequiredDouble(frame, &quot;cY&quot;, context);
        double skewX = ReadRequiredDouble(frame, &quot;kX&quot;, context);
        double skewY = ReadRequiredDouble(frame, &quot;kY&quot;, context);
        ReadRequiredInt(frame, &quot;dI&quot;, context);
        ReadRequiredInt(frame, &quot;z&quot;, context);
        int tweenEasing = ReadRequiredInt(frame, &quot;twE&quot;, context);
        ReadRequiredBoolean(frame, &quot;tweenFrame&quot;, context);

        if (!IsFinite(x) || !IsFinite(y) || !IsFinite(scaleX) || !IsFinite(scaleY) || !IsFinite(skewX) || !IsFinite(skewY))
        {
            throw new InvalidDataException($&quot;ExportJson {context} 含无效数值。&quot;);
        }

        EnsureZeroInt(frame, &quot;twR&quot;, context);
        ValidateEasingParameters(frame, tweenEasing, context);
        EnsureEmptyProperty(frame, &quot;evt&quot;, context);
        EnsureEmptyProperty(frame, &quot;sd&quot;, context);
        EnsureEmptyProperty(frame, &quot;sdE&quot;, context);
        EnsureEmptyProperty(frame, &quot;mov&quot;, context);

        JObject color = frame[&quot;color&quot;] as JObject;
        if (frame[&quot;color&quot;] != null &amp;amp;&amp;amp; color == null)
        {
            throw new InvalidDataException($&quot;ExportJson {context}.color 不是有效对象。&quot;);
        }

        if (color != null)
        {
            ReadColorByte(color, &quot;r&quot;, context);
            ReadColorByte(color, &quot;g&quot;, context);
            ReadColorByte(color, &quot;b&quot;, context);
            ReadColorByte(color, &quot;a&quot;, context);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验自定义缓动参数，并拒绝普通缓动携带无效参数。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateEasingParameters(JObject frame, int tweenEasing, string context)
    {
        if (tweenEasing != -1)
        {
            EnsureEmptyProperty(frame, &quot;twEP&quot;, context);
            return;
        }

        JArray parameters = RequireCustomEasingParameters(frame, context);
        if (Math.Abs(parameters[1].Value&amp;lt;double&amp;gt;()) &amp;gt; ValueTolerance || Math.Abs(parameters[7].Value&amp;lt;double&amp;gt;() - 1d) &amp;gt; ValueTolerance)
        {
            throw new InvalidDataException($&quot;ExportJson {context}.twEP 必须从进度 0 连续过渡到 1。&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取 Cocos CUSTOM_EASING 使用的 4 个二维 Bezier 点。
    /// &amp;lt;/summary&amp;gt;
    private static JArray RequireCustomEasingParameters(JObject frame, string context)
    {
        JArray parameters = frame[&quot;twEP&quot;] as JArray;
        if (parameters == null || parameters.Count != 8)
        {
            throw new InvalidDataException($&quot;ExportJson {context}.twEP 必须包含 8 个数值。&quot;);
        }

        for (int parameterIndex = 0; parameterIndex &amp;lt; parameters.Count; parameterIndex++)
        {
            double value = parameters[parameterIndex].Value&amp;lt;double&amp;gt;();
            if (!IsFinite(value))
            {
                throw new InvalidDataException($&quot;ExportJson {context}.twEP[{parameterIndex}] 数值无效。&quot;);
            }
        }

        return parameters;
    }

    /// &amp;lt;summary&amp;gt;
    /// 删除无源语义的变换轨道，并按 Cocos 关键帧重建位移、旋转、缩放和剪切。
    /// &amp;lt;/summary&amp;gt;
    private static void RepairBoneTimelines(
        SourceData sourceData,
        SetupTransformData setupTransformData,
        MovementData movementData,
        JObject spineAnimation,
        RepairResult result
    )
    {
        JObject spineBones = spineAnimation[&quot;bones&quot;] as JObject;
        Dictionary&amp;lt;string, BakedTransformTimelines&amp;gt; bakedTimelineMap = BuildBakedTransformTimelines(sourceData, setupTransformData, movementData);

        // 先删除无源通道，以及将由逐帧反解完全替换的旧轨道。
        if (spineBones != null)
        {
            List&amp;lt;JProperty&amp;gt; emptyBoneProperties = new List&amp;lt;JProperty&amp;gt;();
            foreach (JProperty boneProperty in spineBones.Properties())
            {
                JObject targetTimelines = boneProperty.Value as JObject;
                if (bakedTimelineMap.ContainsKey(boneProperty.Name))
                {
                    RemoveBoneTimeline(targetTimelines, TimelineKind.Translate, result);
                    RemoveBoneTimeline(targetTimelines, TimelineKind.Rotate, result);
                    RemoveBoneTimeline(targetTimelines, TimelineKind.Scale, result);
                    RemoveBoneTimeline(targetTimelines, TimelineKind.Shear, result);
                }
                else
                {
                    movementData.MovementBoneMap.TryGetValue(boneProperty.Name, out JObject movementBone);
                    JArray frames = movementBone?[&quot;frame_data&quot;] as JArray;
                    RemoveUnexpectedBoneTimeline(targetTimelines, frames, TimelineKind.Translate, result);
                    RemoveUnexpectedBoneTimeline(targetTimelines, frames, TimelineKind.Rotate, result);
                    RemoveUnexpectedBoneTimeline(targetTimelines, frames, TimelineKind.Scale, result);
                    RemoveUnexpectedBoneTimeline(targetTimelines, frames, TimelineKind.Shear, result);
                }

                if (targetTimelines != null &amp;amp;&amp;amp; !targetTimelines.HasValues)
                {
                    emptyBoneProperties.Add(boneProperty);
                }
            }

            for (int propertyIndex = 0; propertyIndex &amp;lt; emptyBoneProperties.Count; propertyIndex++)
            {
                emptyBoneProperties[propertyIndex].Remove();
            }
        }

        // 普通骨骼继续按源关键帧和原始缓动重建轨道。
        foreach (KeyValuePair&amp;lt;string, JObject&amp;gt; movementBonePair in movementData.MovementBoneMap)
        {
            if (bakedTimelineMap.ContainsKey(movementBonePair.Key))
            {
                continue;
            }

            JArray frames = movementBonePair.Value[&quot;frame_data&quot;] as JArray;
            bool hasTranslate = HasNonDefaultTimeline(frames, TimelineKind.Translate);
            bool hasRotate = HasNonDefaultTimeline(frames, TimelineKind.Rotate);
            bool hasScale = HasNonDefaultTimeline(frames, TimelineKind.Scale);
            bool hasShear = HasNonDefaultTimeline(frames, TimelineKind.Shear);
            if (!hasTranslate &amp;amp;&amp;amp; !hasRotate &amp;amp;&amp;amp; !hasScale &amp;amp;&amp;amp; !hasShear)
            {
                continue;
            }

            if (spineBones == null)
            {
                spineBones = new JObject();
                spineAnimation[&quot;bones&quot;] = spineBones;
            }

            JObject targetTimelines = GetOrCreateObject(spineBones, movementBonePair.Key);
            WriteRebuiltTimeline(targetTimelines, hasTranslate ? BuildTimeline(movementData, movementBonePair.Key, frames, TimelineKind.Translate) : null, TimelineKind.Translate, result);
            WriteRebuiltTimeline(targetTimelines, hasRotate ? BuildTimeline(movementData, movementBonePair.Key, frames, TimelineKind.Rotate) : null, TimelineKind.Rotate, result);
            WriteRebuiltTimeline(targetTimelines, hasScale ? BuildTimeline(movementData, movementBonePair.Key, frames, TimelineKind.Scale) : null, TimelineKind.Scale, result);
            WriteRebuiltTimeline(targetTimelines, hasShear ? BuildTimeline(movementData, movementBonePair.Key, frames, TimelineKind.Shear) : null, TimelineKind.Shear, result);
        }

        // 受动态非等比缩放影响的后代使用逐帧世界矩阵反解结果。
        foreach (KeyValuePair&amp;lt;string, BakedTransformTimelines&amp;gt; bakedPair in bakedTimelineMap)
        {
            if (spineBones == null)
            {
                spineBones = new JObject();
                spineAnimation[&quot;bones&quot;] = spineBones;
            }

            JObject targetTimelines = GetOrCreateObject(spineBones, bakedPair.Key);
            WriteRebuiltTimeline(targetTimelines, bakedPair.Value.Translate, TimelineKind.Translate, result);
            WriteRebuiltTimeline(targetTimelines, bakedPair.Value.Rotate, TimelineKind.Rotate, result);
            WriteRebuiltTimeline(targetTimelines, bakedPair.Value.Scale, TimelineKind.Scale, result);
            WriteRebuiltTimeline(targetTimelines, bakedPair.Value.Shear, TimelineKind.Shear, result);
            if (!targetTimelines.HasValues)
            {
                targetTimelines.Parent?.Remove();
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 写入重建轨道并更新对应统计。
    /// &amp;lt;/summary&amp;gt;
    private static void WriteRebuiltTimeline(JObject targetTimelines, JArray timeline, TimelineKind timelineKind, RepairResult result)
    {
        if (timeline == null)
        {
            return;
        }

        targetTimelines[GetTimelineName(timelineKind)] = timeline;
        if (timelineKind == TimelineKind.Translate)
        {
            result.RebuiltTranslateTimelineCount++;
        }
        else if (timelineKind == TimelineKind.Rotate)
        {
            result.RebuiltRotateTimelineCount++;
        }
        else if (timelineKind == TimelineKind.Scale)
        {
            result.RebuiltScaleTimelineCount++;
        }
        else if (timelineKind == TimelineKind.Shear)
        {
            result.RebuiltShearTimelineCount++;
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 找出动态非等比缩放后代，并按每个 Cocos 帧反解 Spine 局部变换轨道。
    /// &amp;lt;/summary&amp;gt;
    private static Dictionary&amp;lt;string, BakedTransformTimelines&amp;gt; BuildBakedTransformTimelines(
        SourceData sourceData,
        SetupTransformData setupTransformData,
        MovementData movementData
    )
    {
        HashSet&amp;lt;string&amp;gt; nonUniformScaleBoneNames = CollectNonUniformScaleBoneNames(movementData);
        HashSet&amp;lt;string&amp;gt; bakedBoneNames = CollectDescendantsOfBones(sourceData, nonUniformScaleBoneNames);
        Dictionary&amp;lt;string, BakedTransformTimelines&amp;gt; result = new Dictionary&amp;lt;string, BakedTransformTimelines&amp;gt;(StringComparer.Ordinal);
        if (bakedBoneNames.Count == 0)
        {
            return result;
        }

        // 按 setup 顺序建立稳定输出和角度连续状态。
        Dictionary&amp;lt;string, double&amp;gt; previousRotationMap = new Dictionary&amp;lt;string, double&amp;gt;(StringComparer.Ordinal);
        Dictionary&amp;lt;string, double&amp;gt; previousShearMap = new Dictionary&amp;lt;string, double&amp;gt;(StringComparer.Ordinal);
        for (int boneIndex = 0; boneIndex &amp;lt; sourceData.SourceBoneOrder.Count; boneIndex++)
        {
            string boneName = sourceData.SourceBoneOrder[boneIndex];
            if (bakedBoneNames.Contains(boneName))
            {
                result.Add(boneName, new BakedTransformTimelines());
            }
        }

        // 逐 Cocos 动画帧计算完整世界变换，再相对父世界矩阵反解 Spine 局部值。
        for (int sourceFrameIndex = 0; sourceFrameIndex &amp;lt;= movementData.Duration; sourceFrameIndex++)
        {
            Dictionary&amp;lt;string, CocosWorldTransform&amp;gt; worldTransformMap = BuildAnimatedCocosWorldTransforms(sourceData, movementData, sourceFrameIndex);
            foreach (KeyValuePair&amp;lt;string, BakedTransformTimelines&amp;gt; bakedPair in result)
            {
                string boneName = bakedPair.Key;
                JObject sourceBone = sourceData.SourceBoneMap[boneName];
                string parentName = sourceBone.Value&amp;lt;string&amp;gt;(&quot;parent&quot;);
                CocosWorldTransform worldTransform = worldTransformMap[boneName];
                Matrix2D localMatrix;
                Vector2D localPosition;
                if (string.IsNullOrEmpty(parentName))
                {
                    localMatrix = worldTransform.Matrix;
                    localPosition = new Vector2D(worldTransform.X, worldTransform.Y);
                }
                else
                {
                    CocosWorldTransform parentTransform = worldTransformMap[parentName];
                    localMatrix = MultiplyInverse(parentTransform.Matrix, worldTransform.Matrix, boneName);
                    localPosition = TransformPointByInverseParent(parentTransform, worldTransform, boneName);
                }

                SpineLocalTransform localTransform = DecomposeSpineLocalMatrix(localMatrix, boneName);
                SpineLocalTransform setupTransform = setupTransformData.SpineLocalTransformMap[boneName];
                double translateX = localPosition.X - ReadRequiredDouble(sourceBone, &quot;x&quot;, $&quot;setup 骨骼 {boneName}&quot;);
                double translateY = localPosition.Y - ReadRequiredDouble(sourceBone, &quot;y&quot;, $&quot;setup 骨骼 {boneName}&quot;);
                double rotation = NormalizeDegrees(localTransform.Rotation - setupTransform.Rotation);
                double shearY = NormalizeDegrees(localTransform.ShearY - setupTransform.ShearY);
                if (previousRotationMap.TryGetValue(boneName, out double previousRotation))
                {
                    rotation = UnwrapDegrees(rotation, previousRotation);
                }

                if (previousShearMap.TryGetValue(boneName, out double previousShear))
                {
                    shearY = UnwrapDegrees(shearY, previousShear);
                }

                previousRotationMap[boneName] = rotation;
                previousShearMap[boneName] = shearY;
                double scaleX = localTransform.ScaleX / setupTransform.ScaleX;
                double scaleY = localTransform.ScaleY / setupTransform.ScaleY;
                ValidateBakedLocalTransform(
                    movementData.Name,
                    boneName,
                    sourceFrameIndex,
                    localMatrix,
                    setupTransform,
                    rotation,
                    scaleX,
                    scaleY,
                    shearY
                );
                double time = sourceFrameIndex * movementData.SecondsPerFrame;
                bakedPair.Value.AddFrame(time, translateX, translateY, rotation, scaleX, scaleY, shearY);
            }
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验烘焙后的 Spine setup 加动画值能重建目标局部轴矩阵。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateBakedLocalTransform(
        string movementName,
        string boneName,
        int sourceFrameIndex,
        Matrix2D expectedMatrix,
        SpineLocalTransform setupTransform,
        double rotation,
        double scaleX,
        double scaleY,
        double shearY
    )
    {
        double xAxis = (setupTransform.Rotation + rotation) / RadiansToDegrees;
        double yAxis = (setupTransform.Rotation + rotation + 90d + setupTransform.ShearY + shearY) / RadiansToDegrees;
        Matrix2D actualMatrix = new Matrix2D(
            Math.Cos(xAxis) * setupTransform.ScaleX * scaleX,
            Math.Cos(yAxis) * setupTransform.ScaleY * scaleY,
            Math.Sin(xAxis) * setupTransform.ScaleX * scaleX,
            Math.Sin(yAxis) * setupTransform.ScaleY * scaleY
        );
        if (
            Math.Abs(expectedMatrix.A - actualMatrix.A) &amp;gt; MatrixTolerance
            || Math.Abs(expectedMatrix.B - actualMatrix.B) &amp;gt; MatrixTolerance
            || Math.Abs(expectedMatrix.C - actualMatrix.C) &amp;gt; MatrixTolerance
            || Math.Abs(expectedMatrix.D - actualMatrix.D) &amp;gt; MatrixTolerance
        )
        {
            throw new InvalidDataException(
                $&quot;ExportJson movement {movementName}.{boneName} 第 {sourceFrameIndex} 帧层级矩阵反解校验失败。&quot;
            );
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 收集 movement 中相对 setup 比例为非等比缩放的骨骼。
    /// &amp;lt;/summary&amp;gt;
    private static HashSet&amp;lt;string&amp;gt; CollectNonUniformScaleBoneNames(MovementData movementData)
    {
        HashSet&amp;lt;string&amp;gt; result = new HashSet&amp;lt;string&amp;gt;(StringComparer.Ordinal);
        foreach (KeyValuePair&amp;lt;string, JObject&amp;gt; movementBonePair in movementData.MovementBoneMap)
        {
            JObject sourceBone = movementData.SourceBoneMap[movementBonePair.Key];
            JArray frames = movementBonePair.Value[&quot;frame_data&quot;] as JArray;
            for (int frameIndex = 0; frameIndex &amp;lt; frames.Count; frameIndex++)
            {
                JObject frame = RequireFrame(frames, movementData.Name, movementBonePair.Key, frameIndex);
                double[] scaleRatios = ReadScaleTimelineValues(
                    sourceBone,
                    frame,
                    $&quot;movement {movementData.Name}.{movementBonePair.Key}.frame_data[{frameIndex}]&quot;
                );
                if (Math.Abs(scaleRatios[0] - scaleRatios[1]) &amp;gt; ValueTolerance)
                {
                    result.Add(movementBonePair.Key);
                    break;
                }
            }
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 收集指定骨骼集合的全部后代，不包含非等比缩放骨骼自身。
    /// &amp;lt;/summary&amp;gt;
    private static HashSet&amp;lt;string&amp;gt; CollectDescendantsOfBones(SourceData sourceData, HashSet&amp;lt;string&amp;gt; ancestorNames)
    {
        HashSet&amp;lt;string&amp;gt; result = new HashSet&amp;lt;string&amp;gt;(StringComparer.Ordinal);
        for (int boneIndex = 0; boneIndex &amp;lt; sourceData.SourceBoneOrder.Count; boneIndex++)
        {
            string boneName = sourceData.SourceBoneOrder[boneIndex];
            string parentName = sourceData.SourceBoneMap[boneName].Value&amp;lt;string&amp;gt;(&quot;parent&quot;);
            while (!string.IsNullOrEmpty(parentName))
            {
                if (ancestorNames.Contains(parentName))
                {
                    result.Add(boneName);
                    break;
                }

                parentName = sourceData.SourceBoneMap[parentName].Value&amp;lt;string&amp;gt;(&quot;parent&quot;);
            }
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 计算 movement 指定帧的全部 Cocos sceneext 世界变换。
    /// &amp;lt;/summary&amp;gt;
    private static Dictionary&amp;lt;string, CocosWorldTransform&amp;gt; BuildAnimatedCocosWorldTransforms(
        SourceData sourceData,
        MovementData movementData,
        int sourceFrameIndex
    )
    {
        Dictionary&amp;lt;string, CocosWorldTransform&amp;gt; result = new Dictionary&amp;lt;string, CocosWorldTransform&amp;gt;(StringComparer.Ordinal);
        HashSet&amp;lt;string&amp;gt; visiting = new HashSet&amp;lt;string&amp;gt;(StringComparer.Ordinal);
        for (int boneIndex = 0; boneIndex &amp;lt; sourceData.SourceBoneOrder.Count; boneIndex++)
        {
            BuildAnimatedCocosWorldTransform(sourceData.SourceBoneOrder[boneIndex], sourceData, movementData, sourceFrameIndex, result, visiting);
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 递归计算 movement 指定帧的单根 Cocos sceneext 世界变换。
    /// &amp;lt;/summary&amp;gt;
    private static CocosWorldTransform BuildAnimatedCocosWorldTransform(
        string boneName,
        SourceData sourceData,
        MovementData movementData,
        int sourceFrameIndex,
        Dictionary&amp;lt;string, CocosWorldTransform&amp;gt; worldTransformMap,
        HashSet&amp;lt;string&amp;gt; visiting
    )
    {
        if (worldTransformMap.TryGetValue(boneName, out CocosWorldTransform existingTransform))
        {
            return existingTransform;
        }

        if (!visiting.Add(boneName))
        {
            throw new InvalidDataException($&quot;ExportJson movement {movementData.Name} 骨骼层级存在循环: {boneName}。&quot;);
        }

        JObject sourceBone = sourceData.SourceBoneMap[boneName];
        CocosFrameTransform frameTransform = SampleCocosFrameTransform(movementData, boneName, sourceFrameIndex);
        double x = ReadRequiredDouble(sourceBone, &quot;x&quot;, $&quot;setup 骨骼 {boneName}&quot;) + frameTransform.X;
        double y = ReadRequiredDouble(sourceBone, &quot;y&quot;, $&quot;setup 骨骼 {boneName}&quot;) + frameTransform.Y;
        double scaleX = ReadRequiredDouble(sourceBone, &quot;cX&quot;, $&quot;setup 骨骼 {boneName}&quot;) + frameTransform.ScaleX - 1d;
        double scaleY = ReadRequiredDouble(sourceBone, &quot;cY&quot;, $&quot;setup 骨骼 {boneName}&quot;) + frameTransform.ScaleY - 1d;
        double skewX = ReadRequiredDouble(sourceBone, &quot;kX&quot;, $&quot;setup 骨骼 {boneName}&quot;) + frameTransform.SkewX;
        double skewY = ReadRequiredDouble(sourceBone, &quot;kY&quot;, $&quot;setup 骨骼 {boneName}&quot;) + frameTransform.SkewY;
        string parentName = sourceBone.Value&amp;lt;string&amp;gt;(&quot;parent&quot;);

        // 完整复现 Bone::applyParentTransform 的位置矩阵和分量式轴继承。
        if (!string.IsNullOrEmpty(parentName))
        {
            CocosWorldTransform parentTransform = BuildAnimatedCocosWorldTransform(
                parentName,
                sourceData,
                movementData,
                sourceFrameIndex,
                worldTransformMap,
                visiting
            );
            double localX = x;
            double localY = y;
            x = localX * parentTransform.Matrix.A + localY * parentTransform.Matrix.B + parentTransform.X;
            y = localX * parentTransform.Matrix.C + localY * parentTransform.Matrix.D + parentTransform.Y;
            scaleX *= parentTransform.ScaleX;
            scaleY *= parentTransform.ScaleY;
            skewX += parentTransform.SkewX;
            skewY += parentTransform.SkewY;
        }

        CocosWorldTransform result = new CocosWorldTransform(x, y, scaleX, scaleY, skewX, skewY, CreateCocosMatrix(scaleX, scaleY, skewX, skewY));
        worldTransformMap.Add(boneName, result);
        visiting.Remove(boneName);
        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 按 BoneTweenController 插值规则读取 movement 指定帧的局部增量。
    /// &amp;lt;/summary&amp;gt;
    private static CocosFrameTransform SampleCocosFrameTransform(MovementData movementData, string boneName, int sourceFrameIndex)
    {
        if (!movementData.MovementBoneMap.TryGetValue(boneName, out JObject movementBone))
        {
            return CocosFrameTransform.Identity;
        }

        JArray frames = movementBone[&quot;frame_data&quot;] as JArray;
        JObject firstFrame = RequireFrame(frames, movementData.Name, boneName, 0);
        int firstFrameIndex = ReadRequiredInt(firstFrame, &quot;fi&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[0]&quot;);
        if (sourceFrameIndex &amp;lt;= firstFrameIndex)
        {
            return ReadCocosFrameTransform(firstFrame, $&quot;movement {movementData.Name}.{boneName}.frame_data[0]&quot;);
        }

        // 精确关键帧进入下一段；关键帧之后使用当前帧到下一帧的补间。
        for (int frameIndex = 0; frameIndex &amp;lt; frames.Count - 1; frameIndex++)
        {
            JObject fromFrame = RequireFrame(frames, movementData.Name, boneName, frameIndex);
            JObject toFrame = RequireFrame(frames, movementData.Name, boneName, frameIndex + 1);
            int fromFrameIndex = ReadRequiredInt(fromFrame, &quot;fi&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;);
            int toFrameIndex = ReadRequiredInt(toFrame, &quot;fi&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex + 1}]&quot;);
            if (sourceFrameIndex &amp;gt;= toFrameIndex)
            {
                continue;
            }

            CocosFrameTransform fromTransform = ReadCocosFrameTransform(fromFrame, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;);
            CocosFrameTransform toTransform = ReadCocosFrameTransform(toFrame, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex + 1}]&quot;);
            int fromDisplayIndex = ReadRequiredInt(fromFrame, &quot;dI&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;);
            int toDisplayIndex = ReadRequiredInt(toFrame, &quot;dI&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex + 1}]&quot;);
            if (fromDisplayIndex &amp;lt; 0 &amp;amp;&amp;amp; toDisplayIndex &amp;gt;= 0)
            {
                return toTransform;
            }

            if (toDisplayIndex &amp;lt; 0 &amp;amp;&amp;amp; fromDisplayIndex &amp;gt;= 0)
            {
                return fromTransform;
            }

            bool tweenFrame = ReadRequiredBoolean(fromFrame, &quot;tweenFrame&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;);
            if (!tweenFrame)
            {
                return fromTransform;
            }

            double percent = (sourceFrameIndex - fromFrameIndex) / (double)(toFrameIndex - fromFrameIndex);
            int tweenEasing = ReadRequiredInt(fromFrame, &quot;twE&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;);
            double easedPercent = EvaluateCocosEasing(
                tweenEasing,
                percent,
                fromFrame,
                $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;
            );
            return CocosFrameTransform.Interpolate(fromTransform, toTransform, easedPercent);
        }

        JObject lastFrame = RequireFrame(frames, movementData.Name, boneName, frames.Count - 1);
        return ReadCocosFrameTransform(lastFrame, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frames.Count - 1}]&quot;);
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取单个 Cocos movement 帧的变换分量。
    /// &amp;lt;/summary&amp;gt;
    private static CocosFrameTransform ReadCocosFrameTransform(JObject frame, string context)
    {
        return new CocosFrameTransform(
            ReadRequiredDouble(frame, &quot;x&quot;, context),
            ReadRequiredDouble(frame, &quot;y&quot;, context),
            ReadRequiredDouble(frame, &quot;cX&quot;, context),
            ReadRequiredDouble(frame, &quot;cY&quot;, context),
            ReadRequiredDouble(frame, &quot;kX&quot;, context),
            ReadRequiredDouble(frame, &quot;kY&quot;, context)
        );
    }

    /// &amp;lt;summary&amp;gt;
    /// 将 Cocos 世界位置反解到父骨骼局部坐标。
    /// &amp;lt;/summary&amp;gt;
    private static Vector2D TransformPointByInverseParent(
        CocosWorldTransform parentTransform,
        CocosWorldTransform worldTransform,
        string boneName
    )
    {
        double determinant = parentTransform.Matrix.Determinant;
        if (Math.Abs(determinant) &amp;lt;= ValueTolerance)
        {
            throw new InvalidDataException($&quot;ExportJson movement 骨骼 {boneName} 的父矩阵不可逆。&quot;);
        }

        double deltaX = worldTransform.X - parentTransform.X;
        double deltaY = worldTransform.Y - parentTransform.Y;
        return new Vector2D(
            (parentTransform.Matrix.D * deltaX - parentTransform.Matrix.B * deltaY) / determinant,
            (-parentTransform.Matrix.C * deltaX + parentTransform.Matrix.A * deltaY) / determinant
        );
    }

    /// &amp;lt;summary&amp;gt;
    /// 将角度展开到最接近上一帧的等价值。
    /// &amp;lt;/summary&amp;gt;
    private static double UnwrapDegrees(double value, double previousValue)
    {
        while (value - previousValue &amp;gt; 180d)
        {
            value -= 360d;
        }

        while (value - previousValue &amp;lt;= -180d)
        {
            value += 360d;
        }

        return value;
    }

    /// &amp;lt;summary&amp;gt;
    /// 按 Cocos 显示索引、缺失骨骼隐藏和颜色插值语义重建 slot 轨道。
    /// &amp;lt;/summary&amp;gt;
    private static void RepairSlotTimelines(SourceData sourceData, MovementData movementData, JObject spineAnimation, RepairResult result)
    {
        JObject spineSlots = spineAnimation[&quot;slots&quot;] as JObject;

        // 每个有 display_data 的 Cocos 骨骼对应一个同名 Spine slot。
        foreach (KeyValuePair&amp;lt;string, List&amp;lt;string&amp;gt;&amp;gt; displayPair in sourceData.DisplayNameMap)
        {
            if (displayPair.Value.Count == 0)
            {
                continue;
            }

            JObject movementBone = null;
            movementData.MovementBoneMap.TryGetValue(displayPair.Key, out movementBone);
            JArray frames = movementBone?[&quot;frame_data&quot;] as JArray;
            JArray attachmentTimeline = BuildAttachmentTimeline(movementData, displayPair.Key, displayPair.Value, frames);
            JArray colorTimeline = HasColorTimeline(frames) ? BuildTimeline(movementData, displayPair.Key, frames, TimelineKind.Rgba) : null;

            JObject targetTimelines = spineSlots?[displayPair.Key] as JObject;
            bool needsTarget = attachmentTimeline != null || colorTimeline != null;
            if (targetTimelines == null &amp;amp;&amp;amp; needsTarget)
            {
                if (spineSlots == null)
                {
                    spineSlots = new JObject();
                    spineAnimation[&quot;slots&quot;] = spineSlots;
                }

                targetTimelines = new JObject();
                spineSlots[displayPair.Key] = targetTimelines;
            }

            // 重建离散 attachment 轨道。
            if (attachmentTimeline == null)
            {
                if (targetTimelines != null &amp;amp;&amp;amp; targetTimelines.Remove(&quot;attachment&quot;))
                {
                    result.RemovedAttachmentTimelineCount++;
                }
            }
            else
            {
                targetTimelines[&quot;attachment&quot;] = attachmentTimeline;
                result.RebuiltAttachmentTimelineCount++;
            }

            // 重建 Spine 4.3 rgba 轨道。
            if (colorTimeline == null)
            {
                if (targetTimelines != null &amp;amp;&amp;amp; targetTimelines.Remove(&quot;rgba&quot;))
                {
                    result.RemovedColorTimelineCount++;
                }
            }
            else
            {
                targetTimelines[&quot;rgba&quot;] = colorTimeline;
                result.RebuiltColorTimelineCount++;
            }

            if (targetTimelines != null &amp;amp;&amp;amp; !targetTimelines.HasValues)
            {
                targetTimelines.Parent?.Remove();
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 模拟 Cocos local z 与 order-of-arrival，重建 Spine drawOrder 轨道。
    /// &amp;lt;/summary&amp;gt;
    private static void RepairDrawOrderTimeline(
        SourceData sourceData,
        MovementData movementData,
        List&amp;lt;string&amp;gt; setupSlotOrder,
        JObject spineAnimation,
        RepairResult result
    )
    {
        Dictionary&amp;lt;string, int&amp;gt; setupZMap = new Dictionary&amp;lt;string, int&amp;gt;(StringComparer.Ordinal);
        Dictionary&amp;lt;string, int&amp;gt; effectiveZMap = new Dictionary&amp;lt;string, int&amp;gt;(StringComparer.Ordinal);
        Dictionary&amp;lt;string, long&amp;gt; arrivalOrderMap = new Dictionary&amp;lt;string, long&amp;gt;(StringComparer.Ordinal);

        // 建立 setup z 和初始到达顺序。
        for (int slotIndex = 0; slotIndex &amp;lt; setupSlotOrder.Count; slotIndex++)
        {
            string slotName = setupSlotOrder[slotIndex];
            if (!sourceData.SourceBoneMap.TryGetValue(slotName, out JObject sourceBone))
            {
                throw new InvalidDataException($&quot;ExportJson setup 骨骼缺少 Spine slot 同名骨骼: {slotName}。&quot;);
            }

            int setupZ = sourceBone.Value&amp;lt;int?&amp;gt;(&quot;z&quot;) ?? 0;
            setupZMap.Add(slotName, setupZ);
            effectiveZMap.Add(slotName, setupZ);
            arrivalOrderMap.Add(slotName, slotIndex);
        }

        // 收集所有可能重置或改变 z 的关键时刻。
        SortedSet&amp;lt;int&amp;gt; sourceFrameIndices = new SortedSet&amp;lt;int&amp;gt;();
        foreach (string boneName in movementData.MovementBoneOrder)
        {
            JArray frames = movementData.MovementBoneMap[boneName][&quot;frame_data&quot;] as JArray;
            for (int frameIndex = 0; frameIndex &amp;lt; frames.Count; frameIndex++)
            {
                JObject frame = RequireFrame(frames, movementData.Name, boneName, frameIndex);
                sourceFrameIndices.Add(ReadRequiredInt(frame, &quot;fi&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;));
            }
        }

        // Cocos 只在 effective z 真正改变时刷新 order-of-arrival。
        long nextArrivalOrder = setupSlotOrder.Count;
        List&amp;lt;string&amp;gt; previousOrder = new List&amp;lt;string&amp;gt;(setupSlotOrder);
        JArray drawOrderTimeline = new JArray();
        foreach (int sourceFrameIndex in sourceFrameIndices)
        {
            for (int boneOrderIndex = 0; boneOrderIndex &amp;lt; movementData.MovementBoneOrder.Count; boneOrderIndex++)
            {
                string boneName = movementData.MovementBoneOrder[boneOrderIndex];
                if (!setupZMap.ContainsKey(boneName))
                {
                    continue;
                }

                JObject frame = FindFrameAt(movementData.MovementBoneMap[boneName][&quot;frame_data&quot;] as JArray, sourceFrameIndex);
                if (frame == null)
                {
                    continue;
                }

                int effectiveZ = setupZMap[boneName] + (frame.Value&amp;lt;int?&amp;gt;(&quot;z&quot;) ?? 0);
                if (effectiveZ == effectiveZMap[boneName])
                {
                    continue;
                }

                effectiveZMap[boneName] = effectiveZ;
                arrivalOrderMap[boneName] = nextArrivalOrder++;
            }

            List&amp;lt;string&amp;gt; currentOrder = new List&amp;lt;string&amp;gt;(setupSlotOrder);
            currentOrder.Sort(
                (left, right) =&amp;gt;
                {
                    int zComparison = effectiveZMap[left].CompareTo(effectiveZMap[right]);
                    return zComparison != 0 ? zComparison : arrivalOrderMap[left].CompareTo(arrivalOrderMap[right]);
                }
            );
            if (AreOrdersEqual(previousOrder, currentOrder))
            {
                continue;
            }

            JObject drawOrderFrame = new JObject();
            WriteOptionalDouble(drawOrderFrame, &quot;time&quot;, sourceFrameIndex * movementData.SecondsPerFrame, 0d);
            JArray offsets = new JArray();
            for (int setupIndex = 0; setupIndex &amp;lt; setupSlotOrder.Count; setupIndex++)
            {
                string slotName = setupSlotOrder[setupIndex];
                offsets.Add(new JObject { [&quot;slot&quot;] = slotName, [&quot;offset&quot;] = currentOrder.IndexOf(slotName) - setupIndex });
            }

            drawOrderFrame[&quot;offsets&quot;] = offsets;
            drawOrderTimeline.Add(drawOrderFrame);
            previousOrder = currentOrder;
        }

        if (drawOrderTimeline.Count == 0)
        {
            if (spineAnimation.Remove(&quot;drawOrder&quot;))
            {
                result.RemovedDrawOrderTimelineCount++;
            }

            return;
        }

        spineAnimation[&quot;drawOrder&quot;] = drawOrderTimeline;
        result.RebuiltDrawOrderTimelineCount++;
    }

    /// &amp;lt;summary&amp;gt;
    /// 查找指定 Cocos 帧号的关键帧。
    /// &amp;lt;/summary&amp;gt;
    private static JObject FindFrameAt(JArray frames, int sourceFrameIndex)
    {
        if (frames == null)
        {
            return null;
        }

        for (int frameIndex = 0; frameIndex &amp;lt; frames.Count; frameIndex++)
        {
            JObject frame = frames[frameIndex] as JObject;
            int framePosition = frame?.Value&amp;lt;int?&amp;gt;(&quot;fi&quot;) ?? int.MinValue;
            if (framePosition == sourceFrameIndex)
            {
                return frame;
            }

            if (framePosition &amp;gt; sourceFrameIndex)
            {
                break;
            }
        }

        return null;
    }

    /// &amp;lt;summary&amp;gt;
    /// 判断两个 slot 顺序是否完全一致。
    /// &amp;lt;/summary&amp;gt;
    private static bool AreOrdersEqual(List&amp;lt;string&amp;gt; left, List&amp;lt;string&amp;gt; right)
    {
        if (left.Count != right.Count)
        {
            return false;
        }

        for (int index = 0; index &amp;lt; left.Count; index++)
        {
            if (!string.Equals(left[index], right[index], StringComparison.Ordinal))
            {
                return false;
            }
        }

        return true;
    }

    /// &amp;lt;summary&amp;gt;
    /// 删除没有对应源通道的目标骨骼轨道。
    /// &amp;lt;/summary&amp;gt;
    private static void RemoveUnexpectedBoneTimeline(JObject targetTimelines, JArray sourceFrames, TimelineKind timelineKind, RepairResult result)
    {
        if (targetTimelines == null || HasNonDefaultTimeline(sourceFrames, timelineKind))
        {
            return;
        }

        RemoveBoneTimeline(targetTimelines, timelineKind, result);
    }

    /// &amp;lt;summary&amp;gt;
    /// 删除指定目标骨骼轨道并更新统计。
    /// &amp;lt;/summary&amp;gt;
    private static void RemoveBoneTimeline(JObject targetTimelines, TimelineKind timelineKind, RepairResult result)
    {
        if (targetTimelines == null || !targetTimelines.Remove(GetTimelineName(timelineKind)))
        {
            return;
        }

        if (timelineKind == TimelineKind.Translate)
        {
            result.RemovedTranslateTimelineCount++;
        }
        else if (timelineKind == TimelineKind.Rotate)
        {
            result.RemovedRotateTimelineCount++;
        }
        else if (timelineKind == TimelineKind.Scale)
        {
            result.RemovedScaleTimelineCount++;
        }
        else if (timelineKind == TimelineKind.Shear)
        {
            result.RemovedShearTimelineCount++;
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 将一条 Cocos 连续轨道转为 Spine 4.3 关键帧，并在二次缓动中点拆段。
    /// &amp;lt;/summary&amp;gt;
    private static JArray BuildTimeline(MovementData movementData, string targetName, JArray sourceFrames, TimelineKind timelineKind)
    {
        JArray result = new JArray();
        double rotationDirection = movementData.RotationDirectionMap[targetName];
        for (int frameIndex = 0; frameIndex &amp;lt; sourceFrames.Count; frameIndex++)
        {
            JObject sourceFrame = RequireFrame(sourceFrames, movementData.Name, targetName, frameIndex);
            double sourceFrameIndex = ReadRequiredInt(sourceFrame, &quot;fi&quot;, $&quot;movement {movementData.Name}.{targetName}.frame_data[{frameIndex}]&quot;);
            double time = sourceFrameIndex * movementData.SecondsPerFrame;
            double[] values = ReadSpineTimelineValues(movementData, targetName, sourceFrame, timelineKind, rotationDirection, frameIndex);

            // sceneext 在 movement 开始时立即应用首帧，即使首帧 fi 大于 0。
            if (frameIndex == 0 &amp;amp;&amp;amp; sourceFrameIndex &amp;gt; 0d)
            {
                result.Add(CreateTimelineFrame(0d, values, timelineKind));
            }

            JObject spineFrame = CreateTimelineFrame(time, values, timelineKind);
            if (frameIndex &amp;gt;= sourceFrames.Count - 1)
            {
                result.Add(spineFrame);
                continue;
            }

            JObject nextSourceFrame = RequireFrame(sourceFrames, movementData.Name, targetName, frameIndex + 1);
            double nextSourceFrameIndex = ReadRequiredInt(nextSourceFrame, &quot;fi&quot;, $&quot;movement {movementData.Name}.{targetName}.frame_data[{frameIndex + 1}]&quot;);
            double nextTime = nextSourceFrameIndex * movementData.SecondsPerFrame;
            double[] nextValues = ReadSpineTimelineValues(
                movementData,
                targetName,
                nextSourceFrame,
                timelineKind,
                rotationDirection,
                frameIndex + 1
            );
            bool tweenFrame = ReadRequiredBoolean(sourceFrame, &quot;tweenFrame&quot;, $&quot;movement {movementData.Name}.{targetName}.frame_data[{frameIndex}]&quot;);
            int tweenEasing = ReadRequiredInt(sourceFrame, &quot;twE&quot;, $&quot;movement {movementData.Name}.{targetName}.frame_data[{frameIndex}]&quot;);

            // stepped 优先于缓动类型。
            if (!tweenFrame)
            {
                spineFrame[&quot;curve&quot;] = &quot;stepped&quot;;
                result.Add(spineFrame);
                continue;
            }

            if (tweenEasing == 0)
            {
                result.Add(spineFrame);
                continue;
            }

            if (tweenEasing == 3)
            {
                spineFrame[&quot;curve&quot;] = CreateBezierCurve(time, nextTime, values, nextValues, SineControlX1, 0d, SineControlX2, 1d);
                result.Add(spineFrame);
                continue;
            }

            if (tweenEasing == 7)
            {
                spineFrame[&quot;curve&quot;] = CreateBezierCurve(time, nextTime, values, nextValues, 1d / 3d, 0d, 2d / 3d, 0d);
                result.Add(spineFrame);
                continue;
            }

            if (tweenEasing == 6)
            {
                double middleTime = (time + nextTime) * 0.5d;
                double[] middleValues = InterpolateValues(values, nextValues, 0.5d);
                spineFrame[&quot;curve&quot;] = CreateBezierCurve(time, middleTime, values, middleValues, 1d / 3d, 0d, 2d / 3d, 1d / 3d);
                result.Add(spineFrame);

                JObject middleFrame = CreateTimelineFrame(middleTime, middleValues, timelineKind);
                middleFrame[&quot;curve&quot;] = CreateBezierCurve(middleTime, nextTime, middleValues, nextValues, 1d / 3d, 2d / 3d, 2d / 3d, 1d);
                result.Add(middleFrame);
                continue;
            }

            string easingContext = $&quot;movement {movementData.Name}.{targetName}.frame_data[{frameIndex}]&quot;;
            if (tweenEasing == 21)
            {
                AppendCircularEaseInOutFrames(result, time, nextTime, values, nextValues, timelineKind);
                continue;
            }

            if (IsBezierApproximationEasing(tweenEasing))
            {
                AppendApproximatedEasingFrames(
                    result,
                    time,
                    nextTime,
                    values,
                    nextValues,
                    timelineKind,
                    tweenEasing,
                    sourceFrame,
                    easingContext
                );
                continue;
            }

            throw new InvalidDataException($&quot;ExportJson {easingContext} 使用未支持的 twE={tweenEasing}。&quot;);
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 判断缓动是否使用自适应三次 Bezier 分段逼近。
    /// &amp;lt;/summary&amp;gt;
    private static bool IsBezierApproximationEasing(int tweenEasing)
    {
        return tweenEasing == -1
            || tweenEasing == 1
            || tweenEasing == 2
            || tweenEasing == 4
            || tweenEasing == 9
            || tweenEasing == 13
            || tweenEasing == 27;
    }

    /// &amp;lt;summary&amp;gt;
    /// 使用自适应三次 Bezier 分段重建 Cocos 缓动。
    /// &amp;lt;/summary&amp;gt;
    private static void AppendApproximatedEasingFrames(
        JArray result,
        double time,
        double nextTime,
        double[] values,
        double[] nextValues,
        TimelineKind timelineKind,
        int tweenEasing,
        JObject sourceFrame,
        string context
    )
    {
        List&amp;lt;EasingBezierSegment&amp;gt; segments = new List&amp;lt;EasingBezierSegment&amp;gt;();

        // 自适应拆分缓动，三次及以下多项式会自然收敛为最少分段。
        BuildEasingBezierSegments(tweenEasing, sourceFrame, context, 0d, 1d, 0, segments);

        // 每段写入起点和绝对控制点；终点由下一段或源关键帧提供。
        for (int segmentIndex = 0; segmentIndex &amp;lt; segments.Count; segmentIndex++)
        {
            EasingBezierSegment segment = segments[segmentIndex];
            double segmentTime = time + (nextTime - time) * segment.Start;
            double[] segmentValues = InterpolateValues(values, nextValues, segment.StartProgress);
            JObject segmentFrame = CreateTimelineFrame(segmentTime, segmentValues, timelineKind);
            segmentFrame[&quot;curve&quot;] = CreateEasingBezierCurve(time, nextTime, values, nextValues, segment);
            result.Add(segmentFrame);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 使用四段圆弧 Bezier 重建 Circ_EaseInOut，避免中点无限斜率失真。
    /// &amp;lt;/summary&amp;gt;
    private static void AppendCircularEaseInOutFrames(
        JArray result,
        double time,
        double nextTime,
        double[] values,
        double[] nextValues,
        TimelineKind timelineKind
    )
    {
        const double arcControl = 0.265216489839544d;
        double quarterAngle = Math.PI * 0.25d;
        List&amp;lt;EasingArcSegment&amp;gt; segments = new List&amp;lt;EasingArcSegment&amp;gt;();

        // 前半段沿圆心 (0, 0.5) 的四分之一圆拆成两段。
        for (int segmentIndex = 0; segmentIndex &amp;lt; 2; segmentIndex++)
        {
            double startAngle = segmentIndex * quarterAngle;
            double endAngle = startAngle + quarterAngle;
            Vector2D startPoint = new Vector2D(0.5d * Math.Sin(startAngle), 0.5d * (1d - Math.Cos(startAngle)));
            Vector2D endPoint = new Vector2D(0.5d * Math.Sin(endAngle), 0.5d * (1d - Math.Cos(endAngle)));
            Vector2D startTangent = new Vector2D(0.5d * Math.Cos(startAngle), 0.5d * Math.Sin(startAngle));
            Vector2D endTangent = new Vector2D(0.5d * Math.Cos(endAngle), 0.5d * Math.Sin(endAngle));
            segments.Add(
                new EasingArcSegment(
                    startPoint,
                    new Vector2D(startPoint.X + arcControl * startTangent.X, startPoint.Y + arcControl * startTangent.Y),
                    new Vector2D(endPoint.X - arcControl * endTangent.X, endPoint.Y - arcControl * endTangent.Y)
                )
            );
        }

        // 后半段沿圆心 (1, 0.5) 的四分之一圆拆成两段。
        for (int segmentIndex = 0; segmentIndex &amp;lt; 2; segmentIndex++)
        {
            double startAngle = -Math.PI * 0.5d + segmentIndex * quarterAngle;
            double endAngle = startAngle + quarterAngle;
            Vector2D startPoint = new Vector2D(1d + 0.5d * Math.Sin(startAngle), 0.5d + 0.5d * Math.Cos(startAngle));
            Vector2D endPoint = new Vector2D(1d + 0.5d * Math.Sin(endAngle), 0.5d + 0.5d * Math.Cos(endAngle));
            Vector2D startTangent = new Vector2D(0.5d * Math.Cos(startAngle), -0.5d * Math.Sin(startAngle));
            Vector2D endTangent = new Vector2D(0.5d * Math.Cos(endAngle), -0.5d * Math.Sin(endAngle));
            segments.Add(
                new EasingArcSegment(
                    startPoint,
                    new Vector2D(startPoint.X + arcControl * startTangent.X, startPoint.Y + arcControl * startTangent.Y),
                    new Vector2D(endPoint.X - arcControl * endTangent.X, endPoint.Y - arcControl * endTangent.Y)
                )
            );
        }

        // 写入每段起点及绝对时间和值控制点。
        for (int segmentIndex = 0; segmentIndex &amp;lt; segments.Count; segmentIndex++)
        {
            EasingArcSegment segment = segments[segmentIndex];
            double segmentTime = time + (nextTime - time) * segment.Start.X;
            JObject segmentFrame = CreateTimelineFrame(
                segmentTime,
                InterpolateValues(values, nextValues, segment.Start.Y),
                timelineKind
            );
            segmentFrame[&quot;curve&quot;] = CreateAbsoluteEasingBezierCurve(
                time,
                nextTime,
                values,
                nextValues,
                segment.FirstControl,
                segment.SecondControl
            );
            result.Add(segmentFrame);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 递归生成满足误差阈值的 Cocos 缓动 Bezier 分段。
    /// &amp;lt;/summary&amp;gt;
    private static void BuildEasingBezierSegments(
        int tweenEasing,
        JObject sourceFrame,
        string context,
        double start,
        double end,
        int depth,
        List&amp;lt;EasingBezierSegment&amp;gt; result
    )
    {
        double length = end - start;
        double startProgress = EvaluateCocosEasing(tweenEasing, start, sourceFrame, context);
        double endProgress = EvaluateCocosEasing(tweenEasing, end, sourceFrame, context);
        double firstSample = EvaluateCocosEasing(tweenEasing, start + length / 3d, sourceFrame, context);
        double secondSample = EvaluateCocosEasing(tweenEasing, start + length * 2d / 3d, sourceFrame, context);
        double firstEquation = 27d * firstSample - 8d * startProgress - endProgress;
        double secondEquation = 27d * secondSample - startProgress - 8d * endProgress;
        double firstControlProgress = (2d * firstEquation - secondEquation) / 18d;
        double secondControlProgress = (2d * secondEquation - firstEquation) / 18d;

        // 检查分段内部误差，超限时从中点继续拆分。
        double maximumError = 0d;
        double[] samples = { 1d / 6d, 0.5d, 5d / 6d };
        for (int sampleIndex = 0; sampleIndex &amp;lt; samples.Length; sampleIndex++)
        {
            double localPercent = samples[sampleIndex];
            double expected = EvaluateCocosEasing(tweenEasing, start + length * localPercent, sourceFrame, context);
            double actual = EvaluateCubicBezier(startProgress, firstControlProgress, secondControlProgress, endProgress, localPercent);
            maximumError = Math.Max(maximumError, Math.Abs(expected - actual));
        }

        if (maximumError &amp;gt; EasingApproximationTolerance &amp;amp;&amp;amp; depth &amp;lt; MaxEasingSubdivisionDepth)
        {
            double middle = (start + end) * 0.5d;
            BuildEasingBezierSegments(tweenEasing, sourceFrame, context, start, middle, depth + 1, result);
            BuildEasingBezierSegments(tweenEasing, sourceFrame, context, middle, end, depth + 1, result);
            return;
        }

        result.Add(new EasingBezierSegment(start, end, startProgress, firstControlProgress, secondControlProgress));
    }

    /// &amp;lt;summary&amp;gt;
    /// 计算 Cocos TweenFunction 对应的归一化缓动进度。
    /// &amp;lt;/summary&amp;gt;
    private static double EvaluateCocosEasing(int tweenEasing, double time, JObject sourceFrame, string context)
    {
        if (tweenEasing == 0)
        {
            return time;
        }

        if (tweenEasing == -1)
        {
            JArray parameters = RequireCustomEasingParameters(sourceFrame, context);
            double inverse = 1d - time;
            return parameters[1].Value&amp;lt;double&amp;gt;() * inverse * inverse * inverse
                + 3d * parameters[3].Value&amp;lt;double&amp;gt;() * time * inverse * inverse
                + 3d * parameters[5].Value&amp;lt;double&amp;gt;() * time * time * inverse
                + parameters[7].Value&amp;lt;double&amp;gt;() * time * time * time;
        }

        if (tweenEasing == 1)
        {
            return -Math.Cos(time * Math.PI * 0.5d) + 1d;
        }

        if (tweenEasing == 2)
        {
            return Math.Sin(time * Math.PI * 0.5d);
        }

        if (tweenEasing == 3)
        {
            return -0.5d * (Math.Cos(Math.PI * time) - 1d);
        }

        if (tweenEasing == 4)
        {
            return time * time;
        }

        if (tweenEasing == 6)
        {
            double doubled = time * 2d;
            if (doubled &amp;lt; 1d)
            {
                return 0.5d * doubled * doubled;
            }

            doubled -= 1d;
            return -0.5d * (doubled * (doubled - 2d) - 1d);
        }

        if (tweenEasing == 7)
        {
            return time * time * time;
        }

        if (tweenEasing == 9)
        {
            double doubled = time * 2d;
            if (doubled &amp;lt; 1d)
            {
                return 0.5d * doubled * doubled * doubled;
            }

            doubled -= 2d;
            return 0.5d * (doubled * doubled * doubled + 2d);
        }

        if (tweenEasing == 13)
        {
            return time * time * time * time * time;
        }

        if (tweenEasing == 21)
        {
            double doubled = time * 2d;
            if (doubled &amp;lt; 1d)
            {
                return -0.5d * (Math.Sqrt(1d - doubled * doubled) - 1d);
            }

            doubled -= 2d;
            return 0.5d * (Math.Sqrt(1d - doubled * doubled) + 1d);
        }

        if (tweenEasing == 27)
        {
            const double overshoot = 1.70158d * 1.525d;
            double doubled = time * 2d;
            if (doubled &amp;lt; 1d)
            {
                return doubled * doubled * ((overshoot + 1d) * doubled - overshoot) * 0.5d;
            }

            doubled -= 2d;
            return doubled * doubled * ((overshoot + 1d) * doubled + overshoot) * 0.5d + 1d;
        }

        throw new InvalidDataException($&quot;ExportJson {context} 使用未支持的 twE={tweenEasing}。&quot;);
    }

    /// &amp;lt;summary&amp;gt;
    /// 计算三次 Bezier 在指定参数处的数值。
    /// &amp;lt;/summary&amp;gt;
    private static double EvaluateCubicBezier(double start, double control1, double control2, double end, double time)
    {
        double inverse = 1d - time;
        return inverse * inverse * inverse * start
            + 3d * inverse * inverse * time * control1
            + 3d * inverse * time * time * control2
            + time * time * time * end;
    }

    /// &amp;lt;summary&amp;gt;
    /// 使用归一化绝对时间和进度创建 Spine Bezier 控制点。
    /// &amp;lt;/summary&amp;gt;
    private static JArray CreateAbsoluteEasingBezierCurve(
        double time,
        double nextTime,
        double[] values,
        double[] nextValues,
        Vector2D firstControl,
        Vector2D secondControl
    )
    {
        JArray curve = new JArray();
        double duration = nextTime - time;
        for (int valueIndex = 0; valueIndex &amp;lt; values.Length; valueIndex++)
        {
            double valueDelta = nextValues[valueIndex] - values[valueIndex];
            curve.Add(time + duration * firstControl.X);
            curve.Add(values[valueIndex] + valueDelta * firstControl.Y);
            curve.Add(time + duration * secondControl.X);
            curve.Add(values[valueIndex] + valueDelta * secondControl.Y);
        }

        return curve;
    }

    /// &amp;lt;summary&amp;gt;
    /// 创建单个缓动分段的 Spine 绝对时间和值控制点。
    /// &amp;lt;/summary&amp;gt;
    private static JArray CreateEasingBezierCurve(
        double time,
        double nextTime,
        double[] values,
        double[] nextValues,
        EasingBezierSegment segment
    )
    {
        JArray curve = new JArray();
        double duration = nextTime - time;
        double segmentLength = segment.End - segment.Start;
        double firstControlTime = time + duration * (segment.Start + segmentLength / 3d);
        double secondControlTime = time + duration * (segment.Start + segmentLength * 2d / 3d);
        for (int valueIndex = 0; valueIndex &amp;lt; values.Length; valueIndex++)
        {
            double valueDelta = nextValues[valueIndex] - values[valueIndex];
            curve.Add(firstControlTime);
            curve.Add(values[valueIndex] + valueDelta * segment.FirstControlProgress);
            curve.Add(secondControlTime);
            curve.Add(values[valueIndex] + valueDelta * segment.SecondControlProgress);
        }

        return curve;
    }

    /// &amp;lt;summary&amp;gt;
    /// 按 Cocos display index 重建附件切换；movement 未包含骨骼时在 0 秒隐藏。
    /// &amp;lt;/summary&amp;gt;
    private static JArray BuildAttachmentTimeline(MovementData movementData, string boneName, List&amp;lt;string&amp;gt; displayNames, JArray sourceFrames)
    {
        if (sourceFrames == null)
        {
            return new JArray(new JObject());
        }

        JArray result = new JArray();
        int currentDisplayIndex = ReadNormalizedDisplayIndex(
            movementData.SourceBoneMap[boneName],
            displayNames.Count,
            $&quot;setup 骨骼 {boneName}&quot;
        );
        for (int frameIndex = 0; frameIndex &amp;lt; sourceFrames.Count; frameIndex++)
        {
            JObject sourceFrame = RequireFrame(sourceFrames, movementData.Name, boneName, frameIndex);
            int displayIndex = ReadNormalizedDisplayIndex(
                sourceFrame,
                displayNames.Count,
                $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;
            );
            if (displayIndex == currentDisplayIndex)
            {
                continue;
            }

            // sceneext 播放 movement 时立即应用首帧 displayIndex，后续帧才使用 fi 时间。
            double time = frameIndex == 0
                ? 0d
                : ReadRequiredInt(sourceFrame, &quot;fi&quot;, $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;)
                    * movementData.SecondsPerFrame;
            JObject attachmentFrame = new JObject();
            WriteOptionalDouble(attachmentFrame, &quot;time&quot;, time, 0d);
            if (displayIndex &amp;gt;= 0)
            {
                attachmentFrame[&quot;name&quot;] = displayNames[displayIndex];
            }

            result.Add(attachmentFrame);
            currentDisplayIndex = displayIndex;
        }

        return result.Count == 0 ? null : result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取转换为 Spine 相对 setup 语义后的轨道值。
    /// &amp;lt;/summary&amp;gt;
    private static double[] ReadSpineTimelineValues(
        MovementData movementData,
        string boneName,
        JObject sourceFrame,
        TimelineKind timelineKind,
        double rotationDirection,
        int frameIndex
    )
    {
        if (timelineKind != TimelineKind.Scale)
        {
            return ReadTimelineValues(sourceFrame, timelineKind, rotationDirection);
        }

        return ReadScaleTimelineValues(
            movementData.SourceBoneMap[boneName],
            sourceFrame,
            $&quot;movement {movementData.Name}.{boneName}.frame_data[{frameIndex}]&quot;
        );
    }

    /// &amp;lt;summary&amp;gt;
    /// 将 Cocos 的 setup 加帧偏移缩放转换为 Spine 相对 setup 倍率。
    /// &amp;lt;/summary&amp;gt;
    private static double[] ReadScaleTimelineValues(JObject sourceBone, JObject sourceFrame, string context)
    {
        // 校验 setup 缩放可作为 Spine 相对倍率分母。
        double setupScaleX = ReadRequiredDouble(sourceBone, &quot;cX&quot;, $&quot;{context} 对应 setup 骨骼&quot;);
        double setupScaleY = ReadRequiredDouble(sourceBone, &quot;cY&quot;, $&quot;{context} 对应 setup 骨骼&quot;);
        if (Math.Abs(setupScaleX) &amp;lt;= ValueTolerance || Math.Abs(setupScaleY) &amp;lt;= ValueTolerance)
        {
            throw new InvalidDataException($&quot;ExportJson {context} 对应 setup 骨骼含零缩放，无法转换 Spine 相对缩放。&quot;);
        }

        // 按 Cocos 加法语义还原缩放，再转换为 Spine setup 倍率。
        double frameScaleX = ReadRequiredDouble(sourceFrame, &quot;cX&quot;, context);
        double frameScaleY = ReadRequiredDouble(sourceFrame, &quot;cY&quot;, context);
        double spineScaleX = Math.Abs(setupScaleX - 1d) &amp;lt;= ValueTolerance ? frameScaleX : (setupScaleX + frameScaleX - 1d) / setupScaleX;
        double spineScaleY = Math.Abs(setupScaleY - 1d) &amp;lt;= ValueTolerance ? frameScaleY : (setupScaleY + frameScaleY - 1d) / setupScaleY;
        return new[] { spineScaleX, spineScaleY };
    }

    /// &amp;lt;summary&amp;gt;
    /// 判断 Cocos 帧列表是否包含颜色信息。
    /// &amp;lt;/summary&amp;gt;
    private static bool HasColorTimeline(JArray sourceFrames)
    {
        if (sourceFrames == null)
        {
            return false;
        }

        for (int frameIndex = 0; frameIndex &amp;lt; sourceFrames.Count; frameIndex++)
        {
            if (sourceFrames[frameIndex]?[&quot;color&quot;] != null)
            {
                return true;
            }
        }

        return false;
    }

    /// &amp;lt;summary&amp;gt;
    /// 判断 Cocos 帧列表的指定变换通道是否偏离默认值。
    /// &amp;lt;/summary&amp;gt;
    private static bool HasNonDefaultTimeline(JArray sourceFrames, TimelineKind timelineKind)
    {
        if (sourceFrames == null || timelineKind == TimelineKind.Rgba)
        {
            return false;
        }

        for (int frameIndex = 0; frameIndex &amp;lt; sourceFrames.Count; frameIndex++)
        {
            JObject sourceFrame = sourceFrames[frameIndex] as JObject;
            if (sourceFrame == null)
            {
                return false;
            }

            double[] values = ReadTimelineValues(sourceFrame, timelineKind, 1d);
            double defaultValue = timelineKind == TimelineKind.Scale ? 1d : 0d;
            for (int valueIndex = 0; valueIndex &amp;lt; values.Length; valueIndex++)
            {
                if (Math.Abs(values[valueIndex] - defaultValue) &amp;gt; ValueTolerance)
                {
                    return true;
                }
            }
        }

        return false;
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取 Cocos 帧在指定 Spine 轨道中的值。
    /// &amp;lt;/summary&amp;gt;
    private static double[] ReadTimelineValues(JObject sourceFrame, TimelineKind timelineKind, double rotationDirection)
    {
        if (timelineKind == TimelineKind.Translate)
        {
            return new[] { sourceFrame.Value&amp;lt;double?&amp;gt;(&quot;x&quot;) ?? 0d, sourceFrame.Value&amp;lt;double?&amp;gt;(&quot;y&quot;) ?? 0d };
        }

        if (timelineKind == TimelineKind.Rotate)
        {
            double skewY = sourceFrame.Value&amp;lt;double?&amp;gt;(&quot;kY&quot;) ?? 0d;
            return new[] { skewY * RadiansToDegrees * rotationDirection };
        }

        if (timelineKind == TimelineKind.Scale)
        {
            return new[] { sourceFrame.Value&amp;lt;double?&amp;gt;(&quot;cX&quot;) ?? 1d, sourceFrame.Value&amp;lt;double?&amp;gt;(&quot;cY&quot;) ?? 1d };
        }

        if (timelineKind == TimelineKind.Shear)
        {
            double skewX = sourceFrame.Value&amp;lt;double?&amp;gt;(&quot;kX&quot;) ?? 0d;
            double skewY = sourceFrame.Value&amp;lt;double?&amp;gt;(&quot;kY&quot;) ?? 0d;
            return new[] { 0d, -(skewX + skewY) * RadiansToDegrees * rotationDirection };
        }

        JObject color = sourceFrame[&quot;color&quot;] as JObject;
        if (color == null)
        {
            return new[] { 1d, 1d, 1d, 1d };
        }

        return new[]
        {
            ReadColorByte(color, &quot;r&quot;, &quot;颜色帧&quot;) / 255d,
            ReadColorByte(color, &quot;g&quot;, &quot;颜色帧&quot;) / 255d,
            ReadColorByte(color, &quot;b&quot;, &quot;颜色帧&quot;) / 255d,
            ReadColorByte(color, &quot;a&quot;, &quot;颜色帧&quot;) / 255d,
        };
    }

    /// &amp;lt;summary&amp;gt;
    /// 创建 Spine 4.3 关键帧并省略默认数值字段。
    /// &amp;lt;/summary&amp;gt;
    private static JObject CreateTimelineFrame(double time, double[] values, TimelineKind timelineKind)
    {
        JObject frame = new JObject();
        WriteOptionalDouble(frame, &quot;time&quot;, time, 0d);
        if (timelineKind == TimelineKind.Translate)
        {
            WriteOptionalDouble(frame, &quot;x&quot;, values[0], 0d);
            WriteOptionalDouble(frame, &quot;y&quot;, values[1], 0d);
        }
        else if (timelineKind == TimelineKind.Rotate)
        {
            WriteOptionalDouble(frame, &quot;value&quot;, values[0], 0d);
        }
        else if (timelineKind == TimelineKind.Scale)
        {
            WriteOptionalDouble(frame, &quot;x&quot;, values[0], 1d);
            WriteOptionalDouble(frame, &quot;y&quot;, values[1], 1d);
        }
        else if (timelineKind == TimelineKind.Shear)
        {
            WriteOptionalDouble(frame, &quot;x&quot;, values[0], 0d);
            WriteOptionalDouble(frame, &quot;y&quot;, values[1], 0d);
        }
        else
        {
            frame[&quot;color&quot;] = ConvertColorValues(values);
        }

        return frame;
    }

    /// &amp;lt;summary&amp;gt;
    /// 将归一化 Bezier 控制点转换为 Spine 4.3 使用的绝对时间和值控制点。
    /// &amp;lt;/summary&amp;gt;
    private static JArray CreateBezierCurve(
        double time1,
        double time2,
        double[] values1,
        double[] values2,
        double controlX1,
        double controlY1,
        double controlX2,
        double controlY2
    )
    {
        JArray curve = new JArray();
        double duration = time2 - time1;
        for (int valueIndex = 0; valueIndex &amp;lt; values1.Length; valueIndex++)
        {
            double valueDelta = values2[valueIndex] - values1[valueIndex];
            curve.Add(time1 + duration * controlX1);
            curve.Add(values1[valueIndex] + valueDelta * controlY1);
            curve.Add(time1 + duration * controlX2);
            curve.Add(values1[valueIndex] + valueDelta * controlY2);
        }

        return curve;
    }

    /// &amp;lt;summary&amp;gt;
    /// 线性计算多通道中间值。
    /// &amp;lt;/summary&amp;gt;
    private static double[] InterpolateValues(double[] from, double[] to, double percent)
    {
        double[] result = new double[from.Length];
        for (int valueIndex = 0; valueIndex &amp;lt; from.Length; valueIndex++)
        {
            result[valueIndex] = from[valueIndex] + (to[valueIndex] - from[valueIndex]) * percent;
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 将 RGBA 数值转换为 Spine RRGGBBAA。
    /// &amp;lt;/summary&amp;gt;
    private static string ConvertColorValues(double[] values)
    {
        byte red = ConvertColorByte(values[0] * 255d);
        byte green = ConvertColorByte(values[1] * 255d);
        byte blue = ConvertColorByte(values[2] * 255d);
        byte alpha = ConvertColorByte(values[3] * 255d);
        return $&quot;{red:X2}{green:X2}{blue:X2}{alpha:X2}&quot;;
    }

    /// &amp;lt;summary&amp;gt;
    /// 将颜色数值限制并转换为字节。
    /// &amp;lt;/summary&amp;gt;
    private static byte ConvertColorByte(double value)
    {
        if (!IsFinite(value))
        {
            throw new InvalidDataException($&quot;颜色数值无效: {value}。&quot;);
        }

        double clamped = Math.Max(0d, Math.Min(255d, value));
        return (byte)Math.Round(clamped, MidpointRounding.AwayFromZero);
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取 0～255 的必需颜色分量。
    /// &amp;lt;/summary&amp;gt;
    private static int ReadColorByte(JObject color, string propertyName, string context)
    {
        int value = ReadRequiredInt(color, propertyName, context + &quot;.color&quot;);
        if (value &amp;lt; 0 || value &amp;gt; 255)
        {
            throw new InvalidDataException($&quot;ExportJson {context}.color.{propertyName} 越界: {value}。&quot;);
        }

        return value;
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验修复前后只改变允许重建的动画轨道。
    /// &amp;lt;/summary&amp;gt;
    private static void EnsureOnlyRepairableDataChanged(JObject before, JObject after, SourceData sourceData)
    {
        if (before == null)
        {
            throw new InvalidDataException(&quot;无法复制修复前 Spine JSON。&quot;);
        }

        JObject strippedBefore = before.DeepClone() as JObject;
        JObject strippedAfter = after.DeepClone() as JObject;
        StripRepairableSetupPose(strippedBefore, sourceData);
        StripRepairableSetupPose(strippedAfter, sourceData);
        StripRepairableTimelines(strippedBefore);
        StripRepairableTimelines(strippedAfter);
        if (!JToken.DeepEquals(strippedBefore, strippedAfter))
        {
            throw new InvalidDataException(&quot;结构保护失败：修复修改了 skins、附件 setup、骨骼层级或允许范围外的动画数据。&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 从 JSON 副本移除允许变化的源骨骼 setup 变换字段。
    /// &amp;lt;/summary&amp;gt;
    private static void StripRepairableSetupPose(JObject spineRoot, SourceData sourceData)
    {
        Dictionary&amp;lt;string, JObject&amp;gt; spineBoneMap = BuildNamedObjectMap(spineRoot[&quot;bones&quot;] as JArray, &quot;Spine setup 骨骼&quot;);
        string[] transformNames = { &quot;x&quot;, &quot;y&quot;, &quot;rotation&quot;, &quot;scaleX&quot;, &quot;scaleY&quot;, &quot;shearX&quot;, &quot;shearY&quot; };
        foreach (string boneName in sourceData.SourceBoneMap.Keys)
        {
            if (!spineBoneMap.TryGetValue(boneName, out JObject spineBone))
            {
                continue;
            }

            for (int propertyIndex = 0; propertyIndex &amp;lt; transformNames.Length; propertyIndex++)
            {
                spineBone.Remove(transformNames[propertyIndex]);
            }
        }

        // setup attachment 由 Cocos dI 决定，属于允许修复范围。
        Dictionary&amp;lt;string, JObject&amp;gt; spineSlotMap = BuildNamedObjectMap(spineRoot[&quot;slots&quot;] as JArray, &quot;Spine setup 插槽&quot;);
        foreach (KeyValuePair&amp;lt;string, List&amp;lt;string&amp;gt;&amp;gt; displayPair in sourceData.DisplayNameMap)
        {
            if (displayPair.Value.Count &amp;gt; 0 &amp;amp;&amp;amp; spineSlotMap.TryGetValue(displayPair.Key, out JObject spineSlot))
            {
                spineSlot.Remove(&quot;attachment&quot;);
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 从 JSON 副本移除允许变化的六类轨道和 drawOrder，用于结构保护比较。
    /// &amp;lt;/summary&amp;gt;
    private static void StripRepairableTimelines(JObject spineRoot)
    {
        JObject animations = spineRoot?[&quot;animations&quot;] as JObject;
        if (animations == null)
        {
            return;
        }

        foreach (JProperty animationProperty in animations.Properties())
        {
            JObject animation = animationProperty.Value as JObject;
            JObject bones = animation?[&quot;bones&quot;] as JObject;
            RemoveTimelineNamesAndEmptyTargets(bones, new[] { &quot;translate&quot;, &quot;rotate&quot;, &quot;scale&quot;, &quot;shear&quot; });
            if (bones != null &amp;amp;&amp;amp; !bones.HasValues)
            {
                animation.Remove(&quot;bones&quot;);
            }

            JObject slots = animation?[&quot;slots&quot;] as JObject;
            RemoveTimelineNamesAndEmptyTargets(slots, new[] { &quot;attachment&quot;, &quot;rgba&quot; });
            if (slots != null &amp;amp;&amp;amp; !slots.HasValues)
            {
                animation.Remove(&quot;slots&quot;);
            }

            animation?.Remove(&quot;drawOrder&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 删除目标对象中的指定轨道，并清理因此变空的目标节点。
    /// &amp;lt;/summary&amp;gt;
    private static void RemoveTimelineNamesAndEmptyTargets(JObject container, string[] timelineNames)
    {
        if (container == null)
        {
            return;
        }

        List&amp;lt;JProperty&amp;gt; emptyProperties = new List&amp;lt;JProperty&amp;gt;();
        foreach (JProperty targetProperty in container.Properties())
        {
            JObject timelines = targetProperty.Value as JObject;
            if (timelines == null)
            {
                continue;
            }

            for (int timelineIndex = 0; timelineIndex &amp;lt; timelineNames.Length; timelineIndex++)
            {
                timelines.Remove(timelineNames[timelineIndex]);
            }

            if (!timelines.HasValues)
            {
                emptyProperties.Add(targetProperty);
            }
        }

        for (int propertyIndex = 0; propertyIndex &amp;lt; emptyProperties.Count; propertyIndex++)
        {
            emptyProperties[propertyIndex].Remove();
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验修复轨道帧结构、严格递增时间、字段和 Spine 4.3 曲线格式。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateRepairedTimelines(JObject spineRoot)
    {
        JObject animations = spineRoot[&quot;animations&quot;] as JObject;
        if (animations == null)
        {
            throw new InvalidDataException(&quot;Spine 文件缺少 animations。&quot;);
        }

        // 校验骨骼连续轨道。
        foreach (JProperty animationProperty in animations.Properties())
        {
            JObject bones = animationProperty.Value?[&quot;bones&quot;] as JObject;
            if (bones != null)
            {
                foreach (JProperty boneProperty in bones.Properties())
                {
                    JObject timelines = boneProperty.Value as JObject;
                    ValidateTimeline(animationProperty.Name, boneProperty.Name, &quot;translate&quot;, timelines?[&quot;translate&quot;] as JArray, 2);
                    ValidateTimeline(animationProperty.Name, boneProperty.Name, &quot;rotate&quot;, timelines?[&quot;rotate&quot;] as JArray, 1);
                    ValidateTimeline(animationProperty.Name, boneProperty.Name, &quot;scale&quot;, timelines?[&quot;scale&quot;] as JArray, 2);
                    ValidateTimeline(animationProperty.Name, boneProperty.Name, &quot;shear&quot;, timelines?[&quot;shear&quot;] as JArray, 2);
                }
            }

            // 校验 slot 离散轨道和颜色轨道。
            JObject slots = animationProperty.Value?[&quot;slots&quot;] as JObject;
            if (slots == null)
            {
                continue;
            }

            foreach (JProperty slotProperty in slots.Properties())
            {
                JObject timelines = slotProperty.Value as JObject;
                ValidateAttachmentTimeline(animationProperty.Name, slotProperty.Name, timelines?[&quot;attachment&quot;] as JArray);
                ValidateTimeline(animationProperty.Name, slotProperty.Name, &quot;rgba&quot;, timelines?[&quot;rgba&quot;] as JArray, 4);
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验一条连续数值或颜色轨道。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateTimeline(string animationName, string targetName, string timelineName, JArray timeline, int valueCount)
    {
        if (timeline == null)
        {
            return;
        }

        if (timeline.Count == 0)
        {
            throw new InvalidDataException($&quot;Spine 轨道为空: {animationName}.{targetName}.{timelineName}。&quot;);
        }

        double previousTime = -1d;
        for (int frameIndex = 0; frameIndex &amp;lt; timeline.Count; frameIndex++)
        {
            JObject frame = timeline[frameIndex] as JObject;
            double time = frame?.Value&amp;lt;double?&amp;gt;(&quot;time&quot;) ?? 0d;
            if (frame == null || !IsFinite(time) || time &amp;lt;= previousTime)
            {
                throw new InvalidDataException($&quot;Spine 轨道时间无效: {animationName}.{targetName}.{timelineName}[{frameIndex}]。&quot;);
            }

            if (timelineName == &quot;rgba&quot;)
            {
                string color = frame.Value&amp;lt;string&amp;gt;(&quot;color&quot;);
                if (string.IsNullOrEmpty(color) || color.Length != 8 || !IsHexColor(color))
                {
                    throw new InvalidDataException($&quot;Spine rgba 颜色无效: {animationName}.{targetName}.rgba[{frameIndex}]。&quot;);
                }
            }
            else
            {
                ValidateNumericFrame(animationName, targetName, timelineName, frameIndex, frame);
            }

            ValidateCurve(animationName, targetName, timelineName, frameIndex, frame[&quot;curve&quot;], valueCount);
            previousTime = time;
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验骨骼数值关键帧字段。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateNumericFrame(string animationName, string targetName, string timelineName, int frameIndex, JObject frame)
    {
        double firstValue;
        double secondValue;
        if (timelineName == &quot;rotate&quot;)
        {
            firstValue = frame.Value&amp;lt;double?&amp;gt;(&quot;value&quot;) ?? 0d;
            secondValue = 0d;
        }
        else
        {
            double defaultValue = timelineName == &quot;scale&quot; ? 1d : 0d;
            firstValue = frame.Value&amp;lt;double?&amp;gt;(&quot;x&quot;) ?? defaultValue;
            secondValue = frame.Value&amp;lt;double?&amp;gt;(&quot;y&quot;) ?? defaultValue;
        }

        if (!IsFinite(firstValue) || !IsFinite(secondValue))
        {
            throw new InvalidDataException($&quot;Spine 轨道数值无效: {animationName}.{targetName}.{timelineName}[{frameIndex}]。&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验 Spine 4.3 stepped 或绝对控制点 Bezier 曲线。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateCurve(string animationName, string targetName, string timelineName, int frameIndex, JToken curve, int valueCount)
    {
        if (curve == null)
        {
            return;
        }

        if (curve.Type == JTokenType.String &amp;amp;&amp;amp; string.Equals(curve.Value&amp;lt;string&amp;gt;(), &quot;stepped&quot;, StringComparison.Ordinal))
        {
            return;
        }

        JArray controlPoints = curve as JArray;
        if (controlPoints == null || controlPoints.Count != valueCount * 4)
        {
            throw new InvalidDataException($&quot;Spine 曲线格式无效: {animationName}.{targetName}.{timelineName}[{frameIndex}]。&quot;);
        }

        for (int pointIndex = 0; pointIndex &amp;lt; controlPoints.Count; pointIndex++)
        {
            double value = controlPoints[pointIndex].Value&amp;lt;double&amp;gt;();
            if (!IsFinite(value))
            {
                throw new InvalidDataException($&quot;Spine 曲线控制点无效: {animationName}.{targetName}.{timelineName}[{frameIndex}]。&quot;);
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 校验 attachment 关键帧时间和可选名称。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateAttachmentTimeline(string animationName, string slotName, JArray timeline)
    {
        if (timeline == null)
        {
            return;
        }

        double previousTime = -1d;
        for (int frameIndex = 0; frameIndex &amp;lt; timeline.Count; frameIndex++)
        {
            JObject frame = timeline[frameIndex] as JObject;
            double time = frame?.Value&amp;lt;double?&amp;gt;(&quot;time&quot;) ?? 0d;
            JToken name = frame?[&quot;name&quot;];
            if (frame == null || !IsFinite(time) || time &amp;lt;= previousTime || (name != null &amp;amp;&amp;amp; name.Type != JTokenType.String &amp;amp;&amp;amp; name.Type != JTokenType.Null))
            {
                throw new InvalidDataException($&quot;Spine attachment 轨道无效: {animationName}.{slotName}.attachment[{frameIndex}]。&quot;);
            }

            previousTime = time;
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 使用项目当前 Spine 4.3 runtime 完整解析输出 JSON。
    /// &amp;lt;/summary&amp;gt;
    private static void ValidateWithSpineRuntime(JObject spineRoot)
    {
        string json = spineRoot.ToString(Formatting.None);
        Spine.Unity.RegionlessAttachmentLoader attachmentLoader = new Spine.Unity.RegionlessAttachmentLoader();
        Spine.SkeletonJson skeletonJson = new Spine.SkeletonJson(attachmentLoader);
        using (StringReader reader = new StringReader(json))
        {
            Spine.SkeletonData skeletonData = skeletonJson.ReadSkeletonData(reader);
            if (skeletonData == null)
            {
                throw new InvalidDataException(&quot;Spine runtime 未能生成 SkeletonData。&quot;);
            }
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 将对象数组转换为名称唯一的索引。
    /// &amp;lt;/summary&amp;gt;
    private static Dictionary&amp;lt;string, JObject&amp;gt; BuildNamedObjectMap(JArray sourceItems, string context)
    {
        if (sourceItems == null)
        {
            throw new InvalidDataException($&quot;{context} 数组缺失。&quot;);
        }

        Dictionary&amp;lt;string, JObject&amp;gt; result = new Dictionary&amp;lt;string, JObject&amp;gt;(StringComparer.Ordinal);
        for (int itemIndex = 0; itemIndex &amp;lt; sourceItems.Count; itemIndex++)
        {
            JObject sourceItem = sourceItems[itemIndex] as JObject;
            string itemName = sourceItem?.Value&amp;lt;string&amp;gt;(&quot;name&quot;);
            if (string.IsNullOrEmpty(itemName) || !result.TryAdd(itemName, sourceItem))
            {
                throw new InvalidDataException($&quot;{context} 第 {itemIndex} 项名称无效或重复: {itemName}。&quot;);
            }
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 收集名称数组的稳定顺序。
    /// &amp;lt;/summary&amp;gt;
    private static List&amp;lt;string&amp;gt; CollectNamedOrder(JArray sourceItems, string context)
    {
        Dictionary&amp;lt;string, JObject&amp;gt; itemMap = BuildNamedObjectMap(sourceItems, context);
        List&amp;lt;string&amp;gt; result = new List&amp;lt;string&amp;gt;(itemMap.Count);
        foreach (KeyValuePair&amp;lt;string, JObject&amp;gt; itemPair in itemMap)
        {
            result.Add(itemPair.Key);
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取并校验 Cocos 帧对象。
    /// &amp;lt;/summary&amp;gt;
    private static JObject RequireFrame(JArray sourceFrames, string movementName, string boneName, int frameIndex)
    {
        JObject sourceFrame = sourceFrames[frameIndex] as JObject;
        if (sourceFrame == null)
        {
            throw new InvalidDataException($&quot;ExportJson {movementName}.{boneName}.frame_data[{frameIndex}] 不是有效对象。&quot;);
        }

        return sourceFrame;
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取必需整数属性。
    /// &amp;lt;/summary&amp;gt;
    private static int ReadRequiredInt(JObject source, string propertyName, string context)
    {
        JToken token = source[propertyName];
        if (token == null || token.Type != JTokenType.Integer)
        {
            throw new InvalidDataException($&quot;ExportJson {context} 缺少整数 {propertyName}。&quot;);
        }

        return token.Value&amp;lt;int&amp;gt;();
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取必需浮点属性。
    /// &amp;lt;/summary&amp;gt;
    private static double ReadRequiredDouble(JObject source, string propertyName, string context)
    {
        JToken token = source[propertyName];
        if (token == null || (token.Type != JTokenType.Integer &amp;amp;&amp;amp; token.Type != JTokenType.Float))
        {
            throw new InvalidDataException($&quot;ExportJson {context} 缺少数值 {propertyName}。&quot;);
        }

        return token.Value&amp;lt;double&amp;gt;();
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取必需布尔属性。
    /// &amp;lt;/summary&amp;gt;
    private static bool ReadRequiredBoolean(JObject source, string propertyName, string context)
    {
        JToken token = source[propertyName];
        if (token == null || token.Type != JTokenType.Boolean)
        {
            throw new InvalidDataException($&quot;ExportJson {context} 缺少布尔值 {propertyName}。&quot;);
        }

        return token.Value&amp;lt;bool&amp;gt;();
    }

    /// &amp;lt;summary&amp;gt;
    /// 要求可选整数属性缺失或为零。
    /// &amp;lt;/summary&amp;gt;
    private static void EnsureZeroInt(JObject source, string propertyName, string context)
    {
        JToken token = source[propertyName];
        if (token == null)
        {
            return;
        }

        if (token.Type != JTokenType.Integer || token.Value&amp;lt;int&amp;gt;() != 0)
        {
            throw new InvalidDataException($&quot;ExportJson {context} 使用未支持的 {propertyName}={token.ToString(Formatting.None)}。&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 要求可选扩展属性缺失、null、空字符串或空数组。
    /// &amp;lt;/summary&amp;gt;
    private static void EnsureEmptyProperty(JObject source, string propertyName, string context)
    {
        JToken token = source[propertyName];
        if (token == null || token.Type == JTokenType.Null)
        {
            return;
        }

        if (token.Type == JTokenType.String &amp;amp;&amp;amp; string.IsNullOrEmpty(token.Value&amp;lt;string&amp;gt;()))
        {
            return;
        }

        JArray array = token as JArray;
        if (array != null &amp;amp;&amp;amp; array.Count == 0)
        {
            return;
        }

        throw new InvalidDataException($&quot;ExportJson {context} 使用未支持的 {propertyName}={token.ToString(Formatting.None)}。&quot;);
    }

    /// &amp;lt;summary&amp;gt;
    /// 读取 Cocos displayIndex，并将任意负值归一为隐藏状态 -1。
    /// &amp;lt;/summary&amp;gt;
    private static int ReadNormalizedDisplayIndex(JObject source, int displayCount, string context)
    {
        int sourceDisplayIndex = ReadRequiredInt(source, &quot;dI&quot;, context);
        if (sourceDisplayIndex &amp;gt;= displayCount)
        {
            throw new InvalidDataException($&quot;ExportJson {context} dI 越界: {sourceDisplayIndex}，display 数量 {displayCount}。&quot;);
        }

        return sourceDisplayIndex &amp;lt; 0 ? -1 : sourceDisplayIndex;
    }

    /// &amp;lt;summary&amp;gt;
    /// 获取或创建 JObject 子节点。
    /// &amp;lt;/summary&amp;gt;
    private static JObject GetOrCreateObject(JObject parent, string propertyName)
    {
        JObject result = parent[propertyName] as JObject;
        if (result == null)
        {
            result = new JObject();
            parent[propertyName] = result;
        }

        return result;
    }

    /// &amp;lt;summary&amp;gt;
    /// 返回轨道对应的 Spine 字段名。
    /// &amp;lt;/summary&amp;gt;
    private static string GetTimelineName(TimelineKind timelineKind)
    {
        if (timelineKind == TimelineKind.Translate)
        {
            return &quot;translate&quot;;
        }

        if (timelineKind == TimelineKind.Rotate)
        {
            return &quot;rotate&quot;;
        }

        if (timelineKind == TimelineKind.Scale)
        {
            return &quot;scale&quot;;
        }

        if (timelineKind == TimelineKind.Shear)
        {
            return &quot;shear&quot;;
        }

        return &quot;rgba&quot;;
    }

    /// &amp;lt;summary&amp;gt;
    /// 去除 Cocos display 名称末尾扩展名并统一路径分隔符。
    /// &amp;lt;/summary&amp;gt;
    private static string StripExtension(string path)
    {
        if (string.IsNullOrEmpty(path))
        {
            return string.Empty;
        }

        string normalized = path.Replace(&apos;\\&apos;, &apos;/&apos;);
        int slashIndex = normalized.LastIndexOf(&apos;/&apos;);
        int extensionIndex = normalized.LastIndexOf(&apos;.&apos;);
        if (extensionIndex &amp;gt; slashIndex)
        {
            return normalized.Substring(0, extensionIndex);
        }

        return normalized;
    }

    /// &amp;lt;summary&amp;gt;
    /// 写入非默认浮点字段。
    /// &amp;lt;/summary&amp;gt;
    private static void WriteOptionalDouble(JObject target, string propertyName, double value, double defaultValue)
    {
        if (!IsFinite(value))
        {
            throw new InvalidDataException($&quot;待写入数值无效: {propertyName}={value}。&quot;);
        }

        if (Math.Abs(value - defaultValue) &amp;lt;= ValueTolerance)
        {
            target.Remove(propertyName);
            return;
        }

        target[propertyName] = value;
    }

    /// &amp;lt;summary&amp;gt;
    /// 将弧度归一到 (-π, π]。
    /// &amp;lt;/summary&amp;gt;
    private static double NormalizeRadians(double value)
    {
        double twoPi = Math.PI * 2d;
        value %= twoPi;
        if (value &amp;lt;= -Math.PI)
        {
            value += twoPi;
        }
        else if (value &amp;gt; Math.PI)
        {
            value -= twoPi;
        }

        return value;
    }

    /// &amp;lt;summary&amp;gt;
    /// 判断字符串是否为十六进制颜色。
    /// &amp;lt;/summary&amp;gt;
    private static bool IsHexColor(string color)
    {
        for (int index = 0; index &amp;lt; color.Length; index++)
        {
            char value = color[index];
            bool isDigit = value &amp;gt;= &apos;0&apos; &amp;amp;&amp;amp; value &amp;lt;= &apos;9&apos;;
            bool isUpper = value &amp;gt;= &apos;A&apos; &amp;amp;&amp;amp; value &amp;lt;= &apos;F&apos;;
            bool isLower = value &amp;gt;= &apos;a&apos; &amp;amp;&amp;amp; value &amp;lt;= &apos;f&apos;;
            if (!isDigit &amp;amp;&amp;amp; !isUpper &amp;amp;&amp;amp; !isLower)
            {
                return false;
            }
        }

        return true;
    }

    /// &amp;lt;summary&amp;gt;
    /// 判断双精度数是否有效。
    /// &amp;lt;/summary&amp;gt;
    private static bool IsFinite(double value)
    {
        return !double.IsNaN(value) &amp;amp;&amp;amp; !double.IsInfinity(value);
    }

    /// &amp;lt;summary&amp;gt;
    /// 将文本行尾统一为 LF。
    /// &amp;lt;/summary&amp;gt;
    private static string NormalizeLineEndings(string value)
    {
        return value.Replace(&quot;\r\n&quot;, &quot;\n&quot;).Replace(&apos;\r&apos;, &apos;\n&apos;);
    }

    private readonly struct EasingArcSegment
    {
        internal readonly Vector2D Start;
        internal readonly Vector2D FirstControl;
        internal readonly Vector2D SecondControl;

        /// &amp;lt;summary&amp;gt;
        /// 创建归一化圆弧 Bezier 分段。
        /// &amp;lt;/summary&amp;gt;
        internal EasingArcSegment(Vector2D start, Vector2D firstControl, Vector2D secondControl)
        {
            Start = start;
            FirstControl = firstControl;
            SecondControl = secondControl;
        }
    }

    private readonly struct EasingBezierSegment
    {
        internal readonly double Start;
        internal readonly double End;
        internal readonly double StartProgress;
        internal readonly double FirstControlProgress;
        internal readonly double SecondControlProgress;

        /// &amp;lt;summary&amp;gt;
        /// 创建归一化缓动 Bezier 分段。
        /// &amp;lt;/summary&amp;gt;
        internal EasingBezierSegment(
            double start,
            double end,
            double startProgress,
            double firstControlProgress,
            double secondControlProgress
        )
        {
            Start = start;
            End = end;
            StartProgress = startProgress;
            FirstControlProgress = firstControlProgress;
            SecondControlProgress = secondControlProgress;
        }
    }

    private enum TimelineKind
    {
        Translate,
        Rotate,
        Scale,
        Shear,
        Rgba,
    }

    private sealed class SourceData
    {
        /// &amp;lt;summary&amp;gt;
        /// Cocos setup 骨骼索引。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, JObject&amp;gt; SourceBoneMap;

        /// &amp;lt;summary&amp;gt;
        /// ExportJson 中 bone_data 的原始顺序。
        /// &amp;lt;/summary&amp;gt;
        internal readonly List&amp;lt;string&amp;gt; SourceBoneOrder;

        /// &amp;lt;summary&amp;gt;
        /// 每根骨骼按 display index 排列的附件名称。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, List&amp;lt;string&amp;gt;&amp;gt; DisplayNameMap;

        /// &amp;lt;summary&amp;gt;
        /// Cocos movement 索引。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, JObject&amp;gt; MovementMap;

        /// &amp;lt;summary&amp;gt;
        /// 创建完成校验的 Cocos 源数据索引。
        /// &amp;lt;/summary&amp;gt;
        internal SourceData(
            Dictionary&amp;lt;string, JObject&amp;gt; sourceBoneMap,
            List&amp;lt;string&amp;gt; sourceBoneOrder,
            Dictionary&amp;lt;string, List&amp;lt;string&amp;gt;&amp;gt; displayNameMap,
            Dictionary&amp;lt;string, JObject&amp;gt; movementMap
        )
        {
            SourceBoneMap = sourceBoneMap;
            SourceBoneOrder = sourceBoneOrder;
            DisplayNameMap = displayNameMap;
            MovementMap = movementMap;
        }
    }

    private sealed class SetupTransformData
    {
        /// &amp;lt;summary&amp;gt;
        /// 按 sceneext 规则计算的 Cocos setup 世界变换。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, CocosWorldTransform&amp;gt; WorldTransformMap;

        /// &amp;lt;summary&amp;gt;
        /// 反解后的 Spine setup 局部变换。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, SpineLocalTransform&amp;gt; SpineLocalTransformMap;

        /// &amp;lt;summary&amp;gt;
        /// 每根骨骼动画旋转增量的方向；父世界为反射时取 -1。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, double&amp;gt; RotationDirectionMap;

        /// &amp;lt;summary&amp;gt;
        /// 创建层级变换数据。
        /// &amp;lt;/summary&amp;gt;
        internal SetupTransformData(
            Dictionary&amp;lt;string, CocosWorldTransform&amp;gt; worldTransformMap,
            Dictionary&amp;lt;string, SpineLocalTransform&amp;gt; spineLocalTransformMap,
            Dictionary&amp;lt;string, double&amp;gt; rotationDirectionMap
        )
        {
            WorldTransformMap = worldTransformMap;
            SpineLocalTransformMap = spineLocalTransformMap;
            RotationDirectionMap = rotationDirectionMap;
        }
    }

    private sealed class CocosWorldTransform
    {
        internal readonly double X;
        internal readonly double Y;
        internal readonly double ScaleX;
        internal readonly double ScaleY;
        internal readonly double SkewX;
        internal readonly double SkewY;
        internal readonly Matrix2D Matrix;

        /// &amp;lt;summary&amp;gt;
        /// 创建 Cocos 世界变换。
        /// &amp;lt;/summary&amp;gt;
        internal CocosWorldTransform(double x, double y, double scaleX, double scaleY, double skewX, double skewY, Matrix2D matrix)
        {
            X = x;
            Y = y;
            ScaleX = scaleX;
            ScaleY = scaleY;
            SkewX = skewX;
            SkewY = skewY;
            Matrix = matrix;
        }
    }

    private readonly struct SpineLocalTransform
    {
        internal readonly double Rotation;
        internal readonly double ScaleX;
        internal readonly double ScaleY;
        internal readonly double ShearY;

        /// &amp;lt;summary&amp;gt;
        /// 创建 Spine 局部变换。
        /// &amp;lt;/summary&amp;gt;
        internal SpineLocalTransform(double rotation, double scaleX, double scaleY, double shearY)
        {
            Rotation = rotation;
            ScaleX = scaleX;
            ScaleY = scaleY;
            ShearY = shearY;
        }
    }

    private readonly struct Matrix2D
    {
        internal readonly double A;
        internal readonly double B;
        internal readonly double C;
        internal readonly double D;

        internal double Determinant =&amp;gt; A * D - B * C;

        /// &amp;lt;summary&amp;gt;
        /// 创建二维轴矩阵。
        /// &amp;lt;/summary&amp;gt;
        internal Matrix2D(double a, double b, double c, double d)
        {
            A = a;
            B = b;
            C = c;
            D = d;
        }

        /// &amp;lt;summary&amp;gt;
        /// 矩阵相乘。
        /// &amp;lt;/summary&amp;gt;
        internal static Matrix2D Multiply(Matrix2D left, Matrix2D right)
        {
            return new Matrix2D(
                left.A * right.A + left.B * right.C,
                left.A * right.B + left.B * right.D,
                left.C * right.A + left.D * right.C,
                left.C * right.B + left.D * right.D
            );
        }
    }

    private readonly struct Vector2D
    {
        internal readonly double X;
        internal readonly double Y;

        /// &amp;lt;summary&amp;gt;
        /// 创建二维位置。
        /// &amp;lt;/summary&amp;gt;
        internal Vector2D(double x, double y)
        {
            X = x;
            Y = y;
        }
    }

    private sealed class BakedTransformTimelines
    {
        private readonly JArray translate = new JArray();
        private readonly JArray rotate = new JArray();
        private readonly JArray scale = new JArray();
        private readonly JArray shear = new JArray();
        private bool hasTranslate;
        private bool hasRotate;
        private bool hasScale;
        private bool hasShear;

        /// &amp;lt;summary&amp;gt;
        /// 非默认位移轨道；全部帧为默认值时返回 null。
        /// &amp;lt;/summary&amp;gt;
        internal JArray Translate =&amp;gt; hasTranslate ? translate : null;

        /// &amp;lt;summary&amp;gt;
        /// 非默认旋转轨道；全部帧为默认值时返回 null。
        /// &amp;lt;/summary&amp;gt;
        internal JArray Rotate =&amp;gt; hasRotate ? rotate : null;

        /// &amp;lt;summary&amp;gt;
        /// 非默认缩放轨道；全部帧为默认值时返回 null。
        /// &amp;lt;/summary&amp;gt;
        internal JArray Scale =&amp;gt; hasScale ? scale : null;

        /// &amp;lt;summary&amp;gt;
        /// 非默认剪切轨道；全部帧为默认值时返回 null。
        /// &amp;lt;/summary&amp;gt;
        internal JArray Shear =&amp;gt; hasShear ? shear : null;

        /// &amp;lt;summary&amp;gt;
        /// 追加一帧逐层级反解后的 Spine 局部变换。
        /// &amp;lt;/summary&amp;gt;
        internal void AddFrame(
            double time,
            double translateX,
            double translateY,
            double rotation,
            double scaleX,
            double scaleY,
            double shearY
        )
        {
            translate.Add(CreateTimelineFrame(time, new[] { translateX, translateY }, TimelineKind.Translate));
            rotate.Add(CreateTimelineFrame(time, new[] { rotation }, TimelineKind.Rotate));
            scale.Add(CreateTimelineFrame(time, new[] { scaleX, scaleY }, TimelineKind.Scale));
            shear.Add(CreateTimelineFrame(time, new[] { 0d, shearY }, TimelineKind.Shear));
            hasTranslate |= Math.Abs(translateX) &amp;gt; ValueTolerance || Math.Abs(translateY) &amp;gt; ValueTolerance;
            hasRotate |= Math.Abs(rotation) &amp;gt; ValueTolerance;
            hasScale |= Math.Abs(scaleX - 1d) &amp;gt; ValueTolerance || Math.Abs(scaleY - 1d) &amp;gt; ValueTolerance;
            hasShear |= Math.Abs(shearY) &amp;gt; ValueTolerance;
        }
    }

    private readonly struct CocosFrameTransform
    {
        internal static readonly CocosFrameTransform Identity = new CocosFrameTransform(0d, 0d, 1d, 1d, 0d, 0d);
        internal readonly double X;
        internal readonly double Y;
        internal readonly double ScaleX;
        internal readonly double ScaleY;
        internal readonly double SkewX;
        internal readonly double SkewY;

        /// &amp;lt;summary&amp;gt;
        /// 创建 Cocos movement 局部增量。
        /// &amp;lt;/summary&amp;gt;
        internal CocosFrameTransform(double x, double y, double scaleX, double scaleY, double skewX, double skewY)
        {
            X = x;
            Y = y;
            ScaleX = scaleX;
            ScaleY = scaleY;
            SkewX = skewX;
            SkewY = skewY;
        }

        /// &amp;lt;summary&amp;gt;
        /// 按 Cocos BoneTweenController 分量线性插值两个 movement 帧。
        /// &amp;lt;/summary&amp;gt;
        internal static CocosFrameTransform Interpolate(CocosFrameTransform from, CocosFrameTransform to, double percent)
        {
            return new CocosFrameTransform(
                from.X + (to.X - from.X) * percent,
                from.Y + (to.Y - from.Y) * percent,
                from.ScaleX + (to.ScaleX - from.ScaleX) * percent,
                from.ScaleY + (to.ScaleY - from.ScaleY) * percent,
                from.SkewX + (to.SkewX - from.SkewX) * percent,
                from.SkewY + (to.SkewY - from.SkewY) * percent
            );
        }
    }

    private sealed class MovementData
    {
        /// &amp;lt;summary&amp;gt;
        /// movement 名称。
        /// &amp;lt;/summary&amp;gt;
        internal readonly string Name;

        /// &amp;lt;summary&amp;gt;
        /// movement 总帧数。
        /// &amp;lt;/summary&amp;gt;
        internal readonly int Duration;

        /// &amp;lt;summary&amp;gt;
        /// Cocos 运行时每动画帧对应的秒数。
        /// &amp;lt;/summary&amp;gt;
        internal readonly double SecondsPerFrame;

        /// &amp;lt;summary&amp;gt;
        /// Cocos setup 骨骼索引，用于转换 movement 相对缩放。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, JObject&amp;gt; SourceBoneMap;

        /// &amp;lt;summary&amp;gt;
        /// ExportJson setup 骨骼原始顺序。
        /// &amp;lt;/summary&amp;gt;
        internal readonly List&amp;lt;string&amp;gt; SourceBoneOrder;

        /// &amp;lt;summary&amp;gt;
        /// movement 骨骼轨道索引。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, JObject&amp;gt; MovementBoneMap;

        /// &amp;lt;summary&amp;gt;
        /// ExportJson 中 mov_bone_data 的原始顺序。
        /// &amp;lt;/summary&amp;gt;
        internal readonly List&amp;lt;string&amp;gt; MovementBoneOrder;

        /// &amp;lt;summary&amp;gt;
        /// 每根骨骼动画旋转增量的方向。
        /// &amp;lt;/summary&amp;gt;
        internal readonly Dictionary&amp;lt;string, double&amp;gt; RotationDirectionMap;

        /// &amp;lt;summary&amp;gt;
        /// 创建完成校验的 movement 数据。
        /// &amp;lt;/summary&amp;gt;
        internal MovementData(
            string name,
            int duration,
            double secondsPerFrame,
            Dictionary&amp;lt;string, JObject&amp;gt; sourceBoneMap,
            List&amp;lt;string&amp;gt; sourceBoneOrder,
            Dictionary&amp;lt;string, JObject&amp;gt; movementBoneMap,
            List&amp;lt;string&amp;gt; movementBoneOrder,
            Dictionary&amp;lt;string, double&amp;gt; rotationDirectionMap
        )
        {
            Name = name;
            Duration = duration;
            SecondsPerFrame = secondsPerFrame;
            SourceBoneMap = sourceBoneMap;
            SourceBoneOrder = sourceBoneOrder;
            MovementBoneMap = movementBoneMap;
            MovementBoneOrder = movementBoneOrder;
            RotationDirectionMap = rotationDirectionMap;
        }
    }

    private sealed class RepairResult
    {
        /// &amp;lt;summary&amp;gt;
        /// 修复的 setup 骨骼数。
        /// &amp;lt;/summary&amp;gt;
        internal int RepairedSetupBoneCount;

        /// &amp;lt;summary&amp;gt;
        /// 修复的 setup 插槽数。
        /// &amp;lt;/summary&amp;gt;
        internal int RepairedSetupSlotCount;

        /// &amp;lt;summary&amp;gt;
        /// 重建的位移轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RebuiltTranslateTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 删除的误造位移轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RemovedTranslateTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 重建的旋转轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RebuiltRotateTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 删除的误造旋转轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RemovedRotateTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 重建的缩放轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RebuiltScaleTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 删除的误造缩放轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RemovedScaleTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 重建的剪切轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RebuiltShearTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 删除的误造剪切轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RemovedShearTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 重建的附件轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RebuiltAttachmentTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 删除的误造附件轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RemovedAttachmentTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 重建的颜色轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RebuiltColorTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 删除的误造颜色轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RemovedColorTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 重建的绘制顺序轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RebuiltDrawOrderTimelineCount;

        /// &amp;lt;summary&amp;gt;
        /// 删除的误造绘制顺序轨道数。
        /// &amp;lt;/summary&amp;gt;
        internal int RemovedDrawOrderTimelineCount;
    }
}
#pragma warning restore ET0004

&lt;/code&gt;&lt;/pre&gt;
</description>
        <pubDate>Mon, 31 Aug 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/08/31/dragonbone-2-spine/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/08/31/dragonbone-2-spine/</guid>
        
        <category>Game Development</category>
        
        
      </item>
    
      <item>
        <title>思考</title>
        <description>&lt;p&gt;整个国家是水管，每个垄断企业都是水龙头，不能说水龙头的水多就可以随便浪费。&lt;/p&gt;
</description>
        <pubDate>Tue, 23 Jun 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/06/23/think/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/06/23/think/</guid>
        
        <category>Game Development</category>
        
        
      </item>
    
      <item>
        <title>Google Play 成就导入失败的坑</title>
        <description>&lt;p&gt;最近为公司通用游戏框架增加成就的功能，想着更新到旧游戏中进行猜测。
无奈，无论如何导入，GooglePlay后台都会提示：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;语言区域不受支持 - 请使用游戏支持的语言区域。请修改以下成就的语言区域值：xxx
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;检查测试了多次，最开始猜测是官方文档的语言代码与后台语言代码不同，全部改为后台配置时显示的语言代码，例如ru改为ru-RU。还是错误。
即使将语言减少为只有一种zh-CN一种也不行。&lt;/p&gt;

&lt;p&gt;在最后发现坑呀，虽然游戏详情中设置设置了多种语言，但实际上导入成就时，是根据Play Service中配置的语言的。果断删了AchievementsLocalizations.csv中的所有多语言，成功导入。&lt;/p&gt;

&lt;p&gt;嗯，坑。&lt;/p&gt;
</description>
        <pubDate>Tue, 23 Jun 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/06/23/google-play-achievement/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/06/23/google-play-achievement/</guid>
        
        <category>Game Development</category>
        
        
      </item>
    
      <item>
        <title>Github Desktop无法打开</title>
        <description>&lt;p&gt;前几天突然无法打开Github Desktop了，查了下有人是安装了Antigravity也无法打开了。真是坑呀谷歌，又垃圾还搞坏环境。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;使用powershell查询：&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 输入&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;icacls&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 应该输出&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;C:\Users\xxxx\AppData\Local&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;NT&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AUTHORITY\SYSTEM:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BUILTIN\Administrators:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xxxx\xxx:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xxxx\CodexSandboxUsers:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#实际输出&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;C:\Users\xxxx\AppData\Local&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;xxxx\CodexSandboxUsers:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;S-1-15-2-4283406991-4294444083-2861539546-3594594304-3831838268-676399724-3246962861:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;S-1-15-2-4283406991-4294444083-2861539546-3594594304-3831838268-676399724-3246962861:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NT&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AUTHORITY\SYSTEM:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BUILTIN\Administrators:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xxxx\xxxx:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# S-1-15-2-*这段就是Antigravity留下的坑了&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;使用Powershell管理员模式运行&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 定义路径和 SID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$sidString&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;S-1-15-2-4283406991-4294444083-2861539546-3594594304-3831838268-676399724-3246962861&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 获取当前 ACL&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$acl&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get-Acl&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 将 SID 字符串转换为 SecurityIdentifier 对象&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$identity&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;System.Security.Principal.SecurityIdentifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$sidString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 找出所有匹配该 SID 的访问规则&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rulesToRemove&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$acl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Access&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Where-Object&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;$_&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;IdentityReference&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-eq&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$identity&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rulesToRemove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;foreach&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rule&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rulesToRemove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 移除该规则&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;$null&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$acl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;RemoveAccessRule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write-Host&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;移除了规则：&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rule&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 应用更改&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Set-Acl&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-Path&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-AclObject&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$acl&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write-Host&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;权限已更新。&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write-Host&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;未找到匹配该 SID 的权限规则。&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 验证结果&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;icacls&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Tue, 16 Jun 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/06/16/github-desktop/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/06/16/github-desktop/</guid>
        
        <category>System</category>
        
        
      </item>
    
      <item>
        <title>黑苹果</title>
        <description>&lt;p&gt;前阵子不小心误删了PC的系统内容导致无法开机，所以就考虑试试黑苹果，虽然手头的mac air和mini能用，但如何能统一使用苹果环境那就更好了。不过最后测试后能使用，N卡也有驱动。但Unity开发时Unity Editor的UI上的部分shader显示有问题，所以最终放弃,此处仅作记录。&lt;/p&gt;

&lt;p&gt;工具:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/lzhoang2801/OpCore-Simplify&quot;&gt;OpCore-Simplify&lt;/a&gt;
OpenCore-Simplify用于获取PC的硬件信息并生成EFI引导文件。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/corpnewt/UnPlugged&quot;&gt;UnPlugged&lt;/a&gt;
UnPlugged构建和运行离线安装程序的 Bash 脚本&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注意：&lt;/p&gt;

&lt;p&gt;独立显卡和集成显卡同时存在会出问题，所以我只保留了独立显卡，通过BIOS关闭集成显卡。
兼容模式也需要注意，是否要开启或关闭。
安装完再使用OpenCore-Legacy-Patcher更新驱动&lt;/p&gt;
</description>
        <pubDate>Thu, 09 Apr 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/04/09/mac-hack/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/04/09/mac-hack/</guid>
        
        <category>System</category>
        
        
      </item>
    
      <item>
        <title>AI Cli</title>
        <description>&lt;p&gt;需求：使用v2rayN实现A走A代理，B走B代理。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v2rayN 真正的转发逻辑在底部的&lt;/strong&gt; &lt;strong&gt;“路由”&lt;/strong&gt; &lt;strong&gt;里。&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;点击上方菜单的&lt;/strong&gt; &lt;strong&gt;“设置”&lt;/strong&gt; &lt;strong&gt;-&amp;gt;&lt;/strong&gt; &lt;strong&gt;“路由设置”&lt;/strong&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;点击&lt;/strong&gt;  &lt;strong&gt;-&amp;gt;&lt;/strong&gt; &lt;strong&gt;“添加”&lt;/strong&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;起个名字（比如&lt;/strong&gt; &lt;strong&gt;A_B_Split&lt;/strong&gt;）。&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;添加第一条规则&lt;/strong&gt;：&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;outboundTag&lt;/strong&gt;：选 &lt;strong&gt;proxy&lt;/strong&gt;（这代表走你当前&lt;strong&gt;激活&lt;/strong&gt;的那个服务器）。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Domain&lt;/strong&gt;：填入域名 A。&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;添加第二条规则&lt;/strong&gt;：&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;outboundTag&lt;/strong&gt;：选 &lt;strong&gt;direct&lt;/strong&gt; &lt;strong&gt;或者你定义的另一个出口。&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;难点&lt;/strong&gt;：v2rayN 的 GUI 界面在处理“两个不同的远程代理”时非常笨拙。它默认只支持 &lt;strong&gt;proxy&lt;/strong&gt;（当前的）和 &lt;strong&gt;direct&lt;/strong&gt;（直连）。&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 09 Apr 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/04/09/v2rayn-two-proxy/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/04/09/v2rayn-two-proxy/</guid>
        
        <category>System</category>
        
        
      </item>
    
      <item>
        <title>AI Cli</title>
        <description>&lt;ol&gt;
  &lt;li&gt;ds2api (&lt;a href=&quot;https://github.com/CJackHwang/ds2api&quot;&gt;https://github.com/CJackHwang/ds2api&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;{
  “env”: {
    “ANTHROPIC_BASE_URL”: “&lt;a href=&quot;http://localhost:5001&quot;&gt;http://localhost:5001&lt;/a&gt;”,
    “ANTHROPIC_AUTH_TOKEN”: “sk-xxx”,
    “ANTHROPIC_MODEL”: “claude-opus-4-6”,
    “CLAUDE_CODE_SUBAGENT_MODEL”: “claude-haiku-4-5”,
    “ANTHROPIC_DEFAULT_HAIKU_MODEL”: “claude-opus-4-6”,
    “ANTHROPIC_DEFAULT_SONNET_MODEL”: “claude-opus-4-6”,
    “ANTHROPIC_DEFAULT_OPUS_MODEL”: “claude-opus-4-6”
  },
  “model”: “claude-opus-4-6”,
  “autoUpdatesChannel”: “latest”,
  “theme”: “dark”,
  “hasCompletedOnboarding”: true
}&lt;/p&gt;

&lt;p&gt;// 模型映射
{
  “claude-3-5-haiku-20241022”: “deepseek-v4-flash”,
  “claude-3-5-haiku-latest”: “deepseek-v4-flash”,
  “claude-3-5-sonnet-20241022”: “deepseek-v4-flash”,
  “claude-3-7-sonnet-20250219”: “deepseek-v4-flash”,
  “claude-haiku-4-5”: “deepseek-v4-flash-nothinking”,
  “claude-haiku-4-5-20251001”: “deepseek-v4-flash-nothinking”,
  “claude-haiku-4-5-latest”: “deepseek-v4-flash-nothinking”,
  “claude-haiku-4-5-nothinking”: “deepseek-v4-flash-nothinking”,
  “claude-opus-4-5-20251124”: “deepseek-v4-pro”,
  “claude-opus-4-6”: “deepseek-v4-pro”,
  “claude-opus-4-6-20260205”: “deepseek-v4-pro”,
  “claude-opus-4-6-latest”: “deepseek-v4-pro”,
  “claude-opus-4-6-nothinking”: “deepseek-v4-pro-nothinking”,
  “claude-opus-4-6[1m]”: “deepseek-v4-pro”,
  “claude-opus-4-7”: “deepseek-v4-pro”,
  “claude-opus-4-7-20260416”: “deepseek-v4-pro”,
  “claude-sonnet-4-5-20250929”: “deepseek-v4-flash”,
  “claude-sonnet-4-6”: “deepseek-v4-flash”,
  “claude-sonnet-4-6-20260217”: “deepseek-v4-flash”,
  “claude-sonnet-4-6-latest”: “deepseek-v4-flash”,
  “claude-sonnet-4-6-nothinking”: “deepseek-v4-flash-nothinking”,
  “claude-sonnet-4-6[1m]”: “deepseek-v4-flash”,
  “gpt-4o”: “deepseek-v4-flash”,
  “gpt-5.3-codex”: “deepseek-v4-pro”,
  “gpt-5.5”: “deepseek-v4-flash”,
  “o3”: “deepseek-v4-pro”
}&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;qwen2api(&lt;a href=&quot;https://github.com/YuJunZhiXue/qwen2API&quot;&gt;https://github.com/YuJunZhiXue/qwen2API&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;{
  “env”: {
    “ANTHROPIC_BASE_URL”: “&lt;a href=&quot;http://127.0.0.1:7860&quot;&gt;http://127.0.0.1:7860&lt;/a&gt;”,
    “ANTHROPIC_AUTH_TOKEN”: “sk-xxx”,
    “ANTHROPIC_MODEL”: “claude-opus-4-6”,
    “CLAUDE_CODE_SUBAGENT_MODEL”: “claude-opus-4-6”,
    “ANTHROPIC_DEFAULT_HAIKU_MODEL”: “claude-opus-4-6”,
    “ANTHROPIC_DEFAULT_SONNET_MODEL”: “claude-opus-4-6”,
    “ANTHROPIC_DEFAULT_OPUS_MODEL”: “claude-opus-4-6”,
    “DISABLE_TELEMETRY”: “1”,
    “CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC”: “1”,
    “CLAUDE_CODE_ENABLE_FEEDBACK_SURVEY_FOR_OTEL”: “0”
  },
  “model”: “claude-opus-4-6”,
  “autoUpdatesChannel”: “latest”,
  “theme”: “dark”,
  “hasCompletedOnboarding”: true
}&lt;/p&gt;

&lt;p&gt;模型映射
{
“claude-3-5-haiku-20241022”: “qwen3.5-flash”,
  “claude-3-5-haiku-latest”: “qwen3.5-flash”,
  “claude-3-5-sonnet-20241022”: “qwen3.5-flash”,
  “claude-3-7-sonnet-20250219”: “qwen3.5-flash”,
  “claude-haiku-4-5”: “qwen3.5-flash”,
  “claude-haiku-4-5-20251001”: “qwen3.5-flash”,
  “claude-haiku-4-5-latest”: “qwen3.5-flash”,
  “claude-haiku-4-5-nothinking”: “qwen3.5-flash”,
  “claude-opus-4-5-20251124”: “qwen3.6-plus”,
  “claude-opus-4-6”: “qwen3.6-plus”,
  “claude-opus-4-6-20260205”: “qwen3.6-plus”,
  “claude-opus-4-6-latest”: “qwen3.6-plus”,
  “claude-opus-4-6-nothinking”: “qwen3.6-plus”,
  “claude-opus-4-7”: “qwen3.6-plus”,
  “claude-opus-4-7-20260416”: “qwen3.6-plus”,
  “claude-opus-4-6-20250929”: “qwen3.5-flash”,
  “claude-sonnet-4-6”: “qwen3.5-flash”,
  “claude-sonnet-4-6-20260217”: “qwen3.5-flash”,
  “claude-sonnet-4-6-latest”: “qwen3.5-flash”,
  “claude-sonnet-4-6-nothinking”: “qwen3.5-flash”,
}&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;❌ chat2api
我以为是9router或claude code router将Anthropic格式转化为OpenAI Chat/Completion格式导致工具调用出问题，后面改用cline直接使用Chat/Completion格式，还是无法进行工具调用，确认Chat2api无法使用工具。&lt;/li&gt;
&lt;/ol&gt;
</description>
        <pubDate>Thu, 09 Apr 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/04/09/ai-cli/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/04/09/ai-cli/</guid>
        
        <category>AI</category>
        
        
      </item>
    
      <item>
        <title>AI Code助手配置</title>
        <description>&lt;h2 id=&quot;ai-coding-必备&quot;&gt;AI Coding 必备&lt;/h2&gt;

&lt;h3 id=&quot;powershell-7&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/powershell/scripting/install/install-powershell-on-windows&quot;&gt;PowerShell 7&lt;/a&gt;&lt;/h3&gt;

&lt;h3 id=&quot;cockpit-tools&quot;&gt;&lt;a href=&quot;https://github.com/jlcodes99/cockpit-tools&quot;&gt;cockpit-tools&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;通用 AI IDE 账号管理工具，用于切换不同账号Auth&lt;/p&gt;

&lt;h3 id=&quot;cc-switch&quot;&gt;&lt;a href=&quot;https://github.com/farion1231/cc-switch&quot;&gt;CC Switch&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;用于：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;切换第三方API&lt;/li&gt;
  &lt;li&gt;添加管理MCP服务器&lt;/li&gt;
  &lt;li&gt;添加管理Skill&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;jq&quot;&gt;&lt;a href=&quot;https://jqlang.org/download/&quot;&gt;jq&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;下载后添加到PATH&lt;/p&gt;

&lt;h3 id=&quot;rtk&quot;&gt;&lt;a href=&quot;https://github.com/rtk-ai/rtk&quot;&gt;rtk&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;降低token&lt;/p&gt;

&lt;h3 id=&quot;ripgrep&quot;&gt;&lt;a href=&quot;https://github.com/burntsushi/ripgrep&quot;&gt;Ripgrep&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;似乎AI助手的grep底层使用的都是ripgrep。但如果我们配置了通过vscode进行shell绕过权限，这时候需要支持rg所以这里手动安装。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;winget &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;BurntSushi.ripgrep.MSVC
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;cc-connectfor-discordwechat等&quot;&gt;cc-connect(for Discord/Wechat等)&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;安装&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 安装cc-connect（wechat需要使用beta版本cc-connect@beta)&lt;/span&gt;
npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; cc-connect
&lt;span class=&quot;c&quot;&gt;# 创建配置文件&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 创建C:\Users\Gumc\.cc-connect&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 将官方的config.example.toml复制到.cc-connect/config.toml&lt;/span&gt;


&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;[cc-connect配置文件]&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-conf highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;  &lt;span class=&quot;c&quot;&gt;# 配置config.toml，以下是我的配置(从discord获取token)
&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;data_dir&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;language&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;zh&quot;&lt;/span&gt;

  [[&lt;span class=&quot;n&quot;&gt;projects&lt;/span&gt;]]
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;framework&quot;&lt;/span&gt;
    [&lt;span class=&quot;n&quot;&gt;projects&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;agent&lt;/span&gt;]
      &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;gemini&quot;&lt;/span&gt;
      [&lt;span class=&quot;n&quot;&gt;projects&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;agent&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;options&lt;/span&gt;]
        &lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;yolo&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;gemini-3.1-flash-lite-preview&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;work_dir&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;C:\\Users\\Gumc\\Desktop\\WorkSpace\\framework&quot;&lt;/span&gt;

    [[&lt;span class=&quot;n&quot;&gt;projects&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;platforms&lt;/span&gt;]]
      &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;discord&quot;&lt;/span&gt;
      [&lt;span class=&quot;n&quot;&gt;projects&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;platforms&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;options&lt;/span&gt;]
        &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;token&quot;&lt;/span&gt;

  [&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;]
    &lt;span class=&quot;n&quot;&gt;level&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;info&quot;&lt;/span&gt;

  [&lt;span class=&quot;n&quot;&gt;speech&lt;/span&gt;]
    &lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;provider&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;language&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
    [&lt;span class=&quot;n&quot;&gt;speech&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;openai&lt;/span&gt;]
      &lt;span class=&quot;n&quot;&gt;api_key&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;base_url&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
    [&lt;span class=&quot;n&quot;&gt;speech&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;groq&lt;/span&gt;]
      &lt;span class=&quot;n&quot;&gt;api_key&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
    [&lt;span class=&quot;n&quot;&gt;speech&lt;/span&gt;.&lt;span class=&quot;n&quot;&gt;qwen&lt;/span&gt;]
      &lt;span class=&quot;n&quot;&gt;api_key&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;base_url&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; = &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;

  [&lt;span class=&quot;n&quot;&gt;display&lt;/span&gt;]
  &lt;span class=&quot;n&quot;&gt;thinking_messages&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Show/hide thinking messages (default: true) / 是否显示思考消息（默认 true）
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;thinking_max_len&lt;/span&gt; = &lt;span class=&quot;m&quot;&gt;1000&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;# Max chars for thinking messages (default: 300) / 思考消息最大字符数（默认 300）
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;tool_max_len&lt;/span&gt; = &lt;span class=&quot;m&quot;&gt;1000&lt;/span&gt;       &lt;span class=&quot;c&quot;&gt;# Max chars for tool use messages (default: 500) / 工具调用消息最大字符数（默认 500）
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;tool_messages&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;# Show/hide tool progress messages (default: true) / 是否显示工具进度消息（默认 true）
&lt;/span&gt;
  [&lt;span class=&quot;n&quot;&gt;stream_preview&lt;/span&gt;]
  &lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;            &lt;span class=&quot;c&quot;&gt;# Enable/disable streaming preview (default: true) / 启用/禁用流式预览（默认 true）
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;interval_ms&lt;/span&gt; = &lt;span class=&quot;m&quot;&gt;1500&lt;/span&gt;        &lt;span class=&quot;c&quot;&gt;# Min ms between updates (default: 1500) / 更新最小间隔毫秒数（默认 1500）
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;min_delta_chars&lt;/span&gt; = &lt;span class=&quot;m&quot;&gt;30&lt;/span&gt;      &lt;span class=&quot;c&quot;&gt;# Min new chars before sending update (default: 30) / 发送更新前最少新增字符数（默认 30）
&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;max_chars&lt;/span&gt; = &lt;span class=&quot;m&quot;&gt;2000&lt;/span&gt;          &lt;span class=&quot;c&quot;&gt;# Max preview length (default: 2000) / 预览最大长度（默认 2000）
&lt;/span&gt;

  [&lt;span class=&quot;n&quot;&gt;rate_limit&lt;/span&gt;]
  &lt;span class=&quot;n&quot;&gt;max_messages&lt;/span&gt; = &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt;         &lt;span class=&quot;c&quot;&gt;# Max messages per window; 0 = disabled (default: 20) / 窗口内最大消息数；0 = 禁用（默认 20）
&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# window_secs = 60          # Window size in seconds (default: 60) / 窗口时间秒数（默认 60）
&lt;/span&gt;
  [&lt;span class=&quot;n&quot;&gt;cron&lt;/span&gt;]
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/chenhg5/cc-connect/blob/main/docs/usage.zh-CN.md&quot;&gt;cc-connect使用指南&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;配置开机启动&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;gemini&quot;&gt;Gemini&lt;/h2&gt;

&lt;h3 id=&quot;安装&quot;&gt;安装&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Gemini Code Assist(VSCode插件) 用处不大，仅在头脑风暴时进行。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;只要开启Agent就会显示：There was a problem getting a response.猜测是免费用户会被限制在 Flash 模型中，而Flash 用不来 Agent。&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;vscode&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;配置&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;geminicodeassist.enableTelemetry&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;geminicodeassist.chat.changeView&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Default diff view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;geminicodeassist.inlineSuggestions.enableAuto&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;geminicodeassist.project&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;xxxx&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;free&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;无效&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;geminicodeassist.agentYoloMode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;             &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;开启&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;Yolo&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;模式，自动执行&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;不要停下来请求权限&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;Gemini Cli （npm安装）&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://geminicli.com/docs/reference/configuration/&quot;&gt;所有配置链接&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;skill--mcp&quot;&gt;Skill &amp;amp; MCP&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;superpowers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;复制superpowers的skill到项目.agents/skills下（brainstorming、dispatching-parallel-agents、executing-plans、receiving-code-review、requesting-code-review、subagent-driven-development、using-superpowers、writing-plans、writing-skills）&lt;/p&gt;

&lt;!-- * (弃用) 通过CC Switch 安装[vscode-mcp](https://github.com/tjx666/vscode-mcp)

```
{
    &quot;vscode-mcp&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@vscode-mcp/vscode-mcp-server@latest&quot;],
      &quot;env&quot;: {},
      &quot;includeTools&quot;: [
        &quot;get_symbol_lsp_info&quot;,
        &quot;get_diagnostics&quot;,
        &quot;get_references&quot;,
        &quot;health_check&quot;,
        &quot;rename_symbol&quot;
      ]
    }
}
``` --&gt;

&lt;ul&gt;
  &lt;li&gt;通过CC Switch 安装&lt;a href=&quot;https://github.com/guillehr2/Excel-MCP-Server-Master&quot;&gt;excel-master&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;excel-master&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;npx&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;-y&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;@guillehr2/excel-mcp-server@latest&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;timeout&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;60000&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;通过CC Switch 安装&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=JuehangQin.vscode-mcp-server&quot;&gt;vscode-mcp-servr&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;vscode-mcp-server&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;npx&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mcp-remote@next&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://localhost:3000/mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/CoplayDev/unity-mcp&quot;&gt;Unity MCP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# unity通过github安装：https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 进入unity - window - mcp for unity - toggle mcp window&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 进入connect，选择传递方式为stdio, 复制configuratioon到项目目录配置文件，工具只开启execute_menu_item和read_console和manage_prefabs&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 可以禁用手机数据&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;&quot;unityMCP&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;command&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;uvx&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;args&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;--from&quot;&lt;/span&gt;,
        &lt;span class=&quot;s2&quot;&gt;&quot;mcpforunityserver==9.6.6&quot;&lt;/span&gt;,
        &lt;span class=&quot;s2&quot;&gt;&quot;mcp-for-unity&quot;&lt;/span&gt;,
        &lt;span class=&quot;s2&quot;&gt;&quot;--transport&quot;&lt;/span&gt;,
        &lt;span class=&quot;s2&quot;&gt;&quot;stdio&quot;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;stdio&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;env&quot;&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;DISABLE_TELEMETRY&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;true&quot;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 让AI自己写markdown,工具的参数见源码：https://github.com/CoplayDev/unity-mcp/tree/beta/Server/src/services/tools&lt;/span&gt;

&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/AnkleBreaker-Studio/unity-mcp-server&quot;&gt;unity-mcp-server&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/MinishLab/semble&quot;&gt;semble&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://github.com/johnhuang316/code-index-mcp/&quot;&gt;code-index-mcp&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/0xmariowu/AgentLint&quot;&gt;AgentLint&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/leighstillard/poormansadvisor&quot;&gt;poormansadvisor&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/ndpvt-web/prompt-improver&quot;&gt;prompt-improver&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;!-- ### (已弃用)clibot(for Discord/Wechat)

消息会被截断，所以已弃用

  * 安装[Go](https://go.dev/dl/)
  * 安装clibot

```bash
git clone https://github.com/keepmind9/clibot.git
cd clibot
go build -o clibot.exe ./cmd/clibot
# 复制config.yaml
cp configs/config.mini.yaml configs\config.yaml
# 配置具体的信息
# 可以启动
clibot serve --config configs\config.yaml
# 设置开机启动
# 第一步：打开启动文件夹,按下键盘上的 Win + R 键。在弹出的对话框中输入 shell:startup 并按回车。这会打开一个名为“启动”的文件夹。
# 第二步：创建快捷方式,在文件夹空白处点击 右键 -&gt; 新建 -&gt; 快捷方式。在“请键入对象的位置”框中，直接复制并粘贴以下完整命令：(如没有安装pwsh则使用powershell.exe)
pwsh.exe -NoExit -Command &quot;cd &apos;D:\Pack\AI\clibot&apos;; clibot serve --config  .\configs\config.yaml&quot;
=# 具体命令
slist                              # 列出所有会话
suse &lt;session&gt;                     # 切换到指定会话
snew &lt;name&gt; &lt;type&gt; &lt;dir&gt; [cmd]     # 创建新会话（仅管理员）
sdel &lt;name&gt;                        # 删除会话（仅管理员）
sclose [name]                      # 关闭会话
sstatus [name]                     # 显示会话状态
whoami                             # 显示你的信息
status                             # 显示所有会话状态
echo                               # 显示你的 IM 信息
help                               # 显示帮助
``` --&gt;

&lt;h2 id=&quot;codex&quot;&gt;Codex&lt;/h2&gt;

&lt;h3 id=&quot;安装-1&quot;&gt;安装&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Codex Cli&lt;/li&gt;
  &lt;li&gt;&lt;del&gt;Codex桌面版&lt;/del&gt;&lt;/li&gt;
  &lt;li&gt;&lt;del&gt;VSCode Codex插件&lt;/del&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;vscode codex插件如果开启WSL就会使用WSL的配置，需要进入/home/xx/.condex中修改配置，所以不开启WSL&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.openai.com/codex/config-sample&quot;&gt;codex全部配置&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;skill--mcp-1&quot;&gt;Skill &amp;amp; MCP&lt;/h3&gt;

&lt;p&gt;同Gemini Cli&lt;/p&gt;

&lt;!-- * [VSCode MCP](https://github.com/tjx666/vscode-mcp)

不支持partial class，但能获取诊断信息，用于获取诊断信息并修复。

MCP（模型上下文协议）客户端能够实时访问丰富的 VSCode 上下文信息

```bash
# vscode安装vsc-lsp-mcp插件
# .condex/config.toml添加配置：
[mcp_servers.vscode-mcp]
command = &quot;bunx&quot;
args = [&quot;-y&quot;, &quot;@vscode-mcp/vscode-mcp-server@latest&quot;]
env = { &quot;VSCODE_MCP_DISABLED_TOOLS&quot; = &quot;health_check,list_workspaces,open_files&quot; }
startup_timeout_ms = 16_000
``` --&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;claude-code&quot;&gt;Claude Code&lt;/h2&gt;

&lt;h3 id=&quot;安装-2&quot;&gt;安装&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;安装&lt;a href=&quot;https://github.com/anthropics/claude-code&quot;&gt;Claude Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;  &lt;span class=&quot;c&quot;&gt;# 管理员权限运行powershell (备注：旧的npm安装方式已弃用)&lt;/span&gt;
  irm https://claude.ai/install.ps1 | iex
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;del&gt;安装&lt;a href=&quot;vscode:extension/anthropic.claude-code&quot;&gt;Claude Code for VS Code&lt;/a&gt;&lt;/del&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;del&gt;VS Code插件&lt;/del&gt;&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;err&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;VSCode的Setting.json必须显性禁用所有非核心功能的网络请求，包括遥测上报和自动更新检查。不然会一直等待遥感失败，导致等到几分钟才能进入AI的请求。&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;claudeCode.environmentVariables&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DISABLE_TELEMETRY&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;caludemd&quot;&gt;CALUDE.md&lt;/h3&gt;

&lt;h3 id=&quot;skill--mcp-2&quot;&gt;Skill &amp;amp; MCP&lt;/h3&gt;

&lt;p&gt;同Gemini Cli&lt;/p&gt;

&lt;!-- * [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)

```bash
# 安装
irm https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/scripts/setup-windows.ps1 | iex
# 启用 MCP 会话启动时的自动索引
codebase-memory-mcp config set auto_index true
# 保持最新状态
codebase-memory-mcp update
# 告诉claude测试codebase-memory-mcp的使用会进行项目索引
```

  * (弃用)[~~Superpowers~~](https://github.com/obra/superpowers)
  * (弃用)~~claude-md-management~~
  * (弃用)~~planning-with-files~~
  * (弃用)~~Remember~~
  * (弃用)LSP

  放弃LSP方案，因为C# LSP无法正确处理partial。
  * (弃用) LSP- [VSC-LSP-MCP](https://github.com/beixiyo/vsc-lsp-mcp)

使用VSCode LSP的MCP，MCP（模型上下文协议）客户端能够实时访问丰富的 VSCode 上下文信息

```md
  1. vscode安装vsc-lsp-mcp插件
  2. 通过CC Switch给Claude添加MCP：
  &quot;lsp-mcp&quot;: {
    &quot;type&quot;: &quot;http&quot;,
    &quot;url&quot;: &quot;http://127.0.0.1:9527/mcp&quot;
  },
  3. .claude/rules/TOOLS.LSP.md要求Claude Code生成对VSCode-MCP的使用。
```

  * [LSP-VSCode MCP](https://github.com/tjx666/vscode-mcp)

不支持partial class，但能获取诊断信息，用于获取诊断信息并修复。

MCP（模型上下文协议）客户端能够实时访问丰富的 VSCode 上下文信息

```md
  1. vscode安装vsc-lsp-mcp插件
  2. 通过CC Switch给Claude添加MCP：
  &quot;vscode-mcp&quot;: {
    &quot;args&quot;: [
      &quot;/c&quot;,
      &quot;npx&quot;,
      &quot;-y&quot;,
      &quot;@vscode-mcp/vscode-mcp-server@latest&quot;
    ],
    &quot;command&quot;: &quot;cmd&quot;,
    &quot;type&quot;: &quot;stdio&quot;
  }
  3. .claude/rules/TOOLS.md要求Claude Code生成对VSCode-MCP的使用。
```

  * [弃用]LSP-Claude Code官方CSharp-lsp

dotnet安装[csharp-ls](https://github.com/razzmatazz/csharp-language-server)
Claude Code VS Code插件市场安装csharp-ls Plugin

  * (弃用)LSP-[VSCode LSP MCP Server](https://marketplace.visualstudio.com/items?itemName=trademe.vscode-lsp-mcp)(作者：Trad Me)

这个最简单，安装vscode插件。
运行VSCode命令: &quot;LSP MCP: Install for Claude Code&quot;
需要运行

```bash
# 确保mcp-proxy安装
uv tool install mcp-proxy
# 确保加入PATH，可能会输出Executable directory C:\Users\Gumc\.local\bin is already in PATH
uv tool update-shell
```

```json
{
  &quot;mcpServers&quot;: {
    &quot;vscode-lsp&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;http://localhost:37140/mcp&quot;
    }
  }
}
```

  * (弃用)LSP-roslyn-refactor

感觉很慢 --&gt;

&lt;h3 id=&quot;opencode&quot;&gt;OpenCode&lt;/h3&gt;

&lt;h2 id=&quot;antigravity&quot;&gt;Antigravity&lt;/h2&gt;

&lt;p&gt;复杂功能特别是需要分析现有的代码且使用Gemini 3 Pro时，则使用Antigravity。&lt;/p&gt;

&lt;h2 id=&quot;vscode插件暂时不使用&quot;&gt;VSCode插件(暂时不使用)&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Copilot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copilot Pro无限使用GPT-5 Mini是不错的，可惜只有首月免费。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Codex插件&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;某鱼某淘可购买business或Plus也有25元，可能有风险。
免费额度也很慷慨。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Gemini Code Assist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;复杂问题使用，Gemini 3的思考和代码能力最强。当然Geimin Code Assist经常会自动切换到Gemini Pro 2.5会降智的。
一般遇到复杂功能、不确定如何实现的需求或找Bug，则在aistudio使用Gemini 3讨论。如需要与代码交互(如找Bug)则使用Gemini Code Assist或Antigravity。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Trae&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;基本废了，基本作为补全使用，不会用来写代码。
trae使用梯子通过trae.ai登录海外账号，似乎可以无限使用Gemini 2.5,不过现在经常出错，似乎海外账号不支持vscode插件了。
目前使用起来很慢，估计很快就可以弃用了。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Code Web Chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;相当于合法通过vscode将上下文发送到web页面，然后获取web页面结果返回到vscode，自动进行editor等操作。
相比于很多逆向API(违规），这个合规的，自动帮忙提交上下文到网页版后并自动获取结果来对比。
但是，”codeWebChat.reuseLastTab”: true,这个配置似乎不生效，每次都重开一个标签，有毛病。&lt;/p&gt;

&lt;p&gt;备注：与Code Web Chat类似的有个：&lt;a href=&quot;https://github.com/afumu/openlink&quot;&gt;openlink&lt;/a&gt;(&lt;a href=&quot;https://www.bilibili.com/video/BV17Yw3z7EJd/?spm_id_from=333.1391.0.0&amp;amp;vd_source=f355063fe070b37905b1cec42ccf5c6c&quot;&gt;视频&lt;/a&gt;)，但还需要自己解决gemini外的前端适配和skill。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Claude Code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除非用Claude官方模型，不然第三方其他AI支持很差。&lt;/p&gt;

&lt;h2 id=&quot;ai思考&quot;&gt;AI思考&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;经常只改动他知道的，不理解上下文和改动的代码的涉及意图：
 例如：原本代码逻辑是A, AI加了个逻辑if判断后跳转到B，后面觉得B并不合适需要去掉，AI会基于B的判断去改为判断后走A逻辑。而不是按原来那样，直接就走A逻辑了。
 例如：Tween动画，需要调用OnComplete，原本的逻辑是加了个包装器在OnComplete后也进行RemoveTween。但如果不需要RemoveTween,AI只会删掉RemoveTween，而不会理解到包装器也是为了OnComplete而存在的。需要把包装器也删掉。
 我加了文档，不确定最后是否会按文档来，慢慢等待测试情况。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;✅ AI的大部分时间花费在找代码,要通过read去读取。想想我们自己是如何理解代码的，1个是整套代码框架有基本理解。2是vscode直接搜索能很快匹配大部分代码信息。但AI这个过程会很慢，特别遇到高峰期基本30分钟干不了人10分钟的事情。
 测试&lt;a href=&quot;https://github.com/MinishLab/semble&quot;&gt;https://github.com/MinishLab/semble&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/johnhuang316/code-index-mcp&quot;&gt;https://github.com/johnhuang316/code-index-mcp&lt;/a&gt;, 结合用感觉还可以，似乎理解变快，但经常回退到Grep。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;cli无法自动匹配任务去使用模型，例如简单的模型和复杂的模型
 skill可以设定模型。
   子代理模式，但目前似乎还没发挥子代理的优势，因为spec和plan还是使用haiku，而且superpowers作用不大，头脑风暴似乎没什么意义。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;花很多时间在决策和修正AI，AI的方案总是不理想，需要给它提供正确的信息和方案。所以所有时间都在给AI做决策和修正Ai的错误方案上。
   我觉得有几个问题，1是弱AI会有很大问题，得不断加无穷无尽的限制，还是不够聪明。2.即使聪明的，也还是会给出错误决策。就像重建新的Tutorial系统，说了旧系统会删掉，他还是用了旧系统的功能。说了需要跑完全部测试才能停下来，还是跑不完测的（是否没有测试的规矩）
    &lt;ul&gt;
      &lt;li&gt;✅ 或者能否不要把AI当做一次性完成的工具，他就是新人，需要不断纠正。而你的工作就是让AI新人帮你干活。不要期待他在没有约束和纠正的情况下干好。你的工作就是约束和纠正AI新人。不要为此生气，气坏了没得赔。&lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;⬜️ Unity似乎没有正确的测试方法。&lt;/p&gt;
      &lt;/li&gt;
      &lt;li&gt;我觉得应该Antigravity给计划，讲这个计划落盘。
我审核通过后没问题再让gpt-5.4-mini进行落盘。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;没有免费AI
 想法1：使用Gemma 31B + ds2API测试效果解决5
 想法2：使用Gemma 31B + GPT5.4-mini合作检查方案并且一个执行一个审查看效果解决4
 目前使用 gpt-5.4-mini然后用coder-spark做审查，还行但是还是用不到coder-spark的能力，推理思考还是很弱。还可以使用gemma31b做审查。&lt;/p&gt;

    &lt;p&gt;想法3: 使用类似&lt;a href=&quot;https://github.com/ypollak2/llm-router的功能来做匹配模型，解决3&quot;&gt;https://github.com/ypollak2/llm-router的功能来做匹配模型，解决3&lt;/a&gt;
 想法4：RAG本地向量索引是否能优化2。解决2。trae是如何实现的？
 想法5： 如何解决1？C# .Net是否有好的方案。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;AI还是不够聪明，我让他重新做一个系统，他确在做一个系统去引用旧系统，或者把旧系统作为入口引导到新系统。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;备用：
改进Todo：
&lt;a href=&quot;https://github.com/lethain/library-mcp/tree/main&quot;&gt;https://github.com/lethain/library-mcp/tree/main&lt;/a&gt;
以下两个不确定是否有作用：
&lt;a href=&quot;https://github.com/Horilla/claudectx&quot;&gt;https://github.com/Horilla/claudectx&lt;/a&gt;
&lt;a href=&quot;https://github.com/azkhh/cchubber&quot;&gt;https://github.com/azkhh/cchubber&lt;/a&gt; 可以作为开启启动&lt;/p&gt;
</description>
        <pubDate>Thu, 09 Apr 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/04/09/ai-code-helper-config/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/04/09/ai-code-helper-config/</guid>
        
        <category>AI</category>
        
        
      </item>
    
      <item>
        <title>Free AI</title>
        <description>&lt;h2 id=&quot;gemini&quot;&gt;Gemini&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;入口&lt;/th&gt;
      &lt;th&gt;额度&lt;/th&gt;
      &lt;th&gt;是否值得试用&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;VS Code 插件 (OAuth 登录)&lt;/td&gt;
      &lt;td&gt;Gemini 3 Flash 1,000 次&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Gemini CLI (OAuth 登录)&lt;/td&gt;
      &lt;td&gt;Gemini 3 Flash 1,000 次&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Antigravity&lt;/td&gt;
      &lt;td&gt;信用点，&lt;a href=&quot;https://gemini.google/subscriptions/&quot;&gt;每日50点&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;codexclaude-使用9router接入所有ai效果并不好&quot;&gt;Codex/Claude (使用9Router接入所有AI，效果并不好)&lt;/h2&gt;

&lt;p&gt;备注：效果并不好，Claude、OpenAI和Gemini还有其他大模型之间的虽然能进行格式转化，但无法配合工具调用，例如claude code如果使用其他模型，会经常出现tool不存在。
有个大胆的想法，如果为Claude配置足够多的MCP作为tool，是否就可以使用第三方API了。待实现…&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;类型&lt;/th&gt;
      &lt;th&gt;名称&lt;/th&gt;
      &lt;th&gt;是否接入&lt;/th&gt;
      &lt;th&gt;Advance&lt;/th&gt;
      &lt;th&gt;Daily(稳定)&lt;/th&gt;
      &lt;th&gt;Weekly&lt;/th&gt;
      &lt;th&gt;Monthly&lt;/th&gt;
      &lt;th&gt;Free(不稳定)&lt;/th&gt;
      &lt;th&gt;免费额度说明&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;OAuth&lt;/td&gt;
      &lt;td&gt;&lt;del&gt;claude code&lt;/del&gt;&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;基本无，需要海外手机，不使用&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;del&gt;Antigravity&lt;/del&gt;&lt;/td&gt;
      &lt;td&gt;❌不合规&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;不符合谷歌要求，避免封号，不使用&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;OpenAI Codex&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt;✅ cx/gpt-5.3-codex&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;del&gt;cx/gpt-5.1-codex-mini&lt;/del&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://chatgpt.com/zh-Hans-CN/pricing/&quot;&gt;有限额度 GPT-5.3&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Github Copilot&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅gh/gpt-5-mini&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/get-started/plans&quot;&gt;每月50次GPT-5 Mini&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Cursor&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅ cu/default&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://cursor.com/docs/models-and-pricing&quot;&gt;Premium模型请求每月50次&lt;br /&gt;Cursor-small模型每月200次&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Kilo&lt;/td&gt;
      &lt;td&gt;❌Grep挂起&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;del&gt;kc/kilo-auto/free&lt;/del&gt;&lt;/td&gt;
      &lt;td&gt;免费模型都报错导致挂起&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;cline&lt;/td&gt;
      &lt;td&gt;❌Grep挂起&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;del&gt;cl/minimax/minimax-m2.5:free&lt;/del&gt;&lt;/td&gt;
      &lt;td&gt;Qwen免费是因为用Qwen Code Cli&lt;br /&gt;官方能选择2.5free但无法测试通过且&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Free/Free试用&lt;/td&gt;
      &lt;td&gt;Kiro（AWS亚马逊）&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt;✅kr/claude-sonnet-4.5&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅kr/claude-sonnet-4.5&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;新用户送500个credits,首月可用Claude Opus 4.6&lt;br /&gt;&lt;a href=&quot;https://kiro.dev/pricing/&quot;&gt;每月50 个 credits&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Qwen Code&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;del&gt;每日2000次请求 （中国大陆用户）&lt;/del&gt;&lt;a href=&quot;https://qwen-ai.com/qwen-code/#free-tier&quot;&gt;&lt;del&gt;每日1000次（海外用户）&lt;/del&gt;&lt;/a&gt;免费额度100次每天&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Open Code&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅&lt;br /&gt;oc/minimax-m2.5-free&lt;br /&gt;&lt;del&gt;oc/nemotron-3-super-free&lt;/del&gt; 会挂起&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;内置免费模型&lt;br /&gt;不确定重置时间&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Gemini CLI&lt;/td&gt;
      &lt;td&gt;❌可能不合规&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;iFlow&lt;/td&gt;
      &lt;td&gt;❌即将关闭&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Open Router&lt;/td&gt;
      &lt;td&gt;❌openrouter/free&lt;br /&gt;但claude code使用会挂起&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;似乎在Claude Code中不能正确使用&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Nvidia NIM&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;✅&lt;br /&gt;nvidia/&lt;strong&gt;minimaxai/minimax-m2.7&lt;/strong&gt;&lt;br /&gt;nvidia/z-ai/glm5&lt;br /&gt;nvidia/&lt;strong&gt;qwen/qwen3.5-122b-a10b&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;没有额度限制，缺点是慢。&lt;br /&gt;添加3个避免有的模型挂掉&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Ollama Cloud&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅ollama/glm-5&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;每5小时重置一次&lt;br /&gt;一周重置一次&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Gemini&lt;/td&gt;
      &lt;td&gt;❌Key只能在Gemini Cli中用&lt;/td&gt;
      &lt;td&gt;&lt;del&gt;gemini/gemini-3-flash-preview&lt;/del&gt;&lt;/td&gt;
      &lt;td&gt;&lt;del&gt;gemini/gemini-3-flash-preview&lt;/del&gt;&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;del&gt;AI Studio后台显示:&lt;br /&gt;Gemini 3 Flash 每日20次&lt;br /&gt;Gemini 3 Flash Lite 每日500次&lt;/del&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;API&lt;/td&gt;
      &lt;td&gt;GLM Coding&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅glm/glm-4.7-flash&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://docs.z.ai/guides/overview/pricing&quot;&gt;GLM-4.7-Flash&lt;/a&gt;永久免费，都不稳定&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;GLN(china)&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅glm-cn/glm-4.7-flash&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://bigmodel.cn/pricing&quot;&gt;GLM-4.7-Flash&lt;/a&gt;永久免费，都不稳定&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Groq&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅groq/openai/gpt-oss-120b&lt;/td&gt;
      &lt;td&gt;每日14,400次请求&lt;br /&gt;不支持VPN&lt;br /&gt;据说当前免费,但似乎没有说明，先试用Free&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Mistral&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;✅mistral/codestral-latest&lt;/td&gt;
      &lt;td&gt;Experiment计划，模型免费，&lt;br /&gt;但数据用于训练&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;cerebras&lt;/td&gt;
      &lt;td&gt;❌没有任何模型可用&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;每日14,400次请求&lt;br /&gt;可能不支持VPN&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Free&lt;/td&gt;
      &lt;td&gt;modelscope.cn/API-Inference&lt;/td&gt;
      &lt;td&gt;❌免费额度很少了&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;br /&gt;&lt;del&gt;modelscope/Qwen/Qwen3-Coder-480B-A35B-Instruct&lt;br /&gt;modelscope/Qwen/Qwen3-Coder-30B-A3B-Instruct&lt;br /&gt;modelscope/Qwen/Qwen3.5-397B-A17B&lt;br /&gt;modelscope/Qwen/Qwen3-235B-A22B-Thinking-2507&lt;br /&gt;modelscope/Qwen/Qwen3-Next-80B-A3B-Thinking&lt;/del&gt;&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;del&gt;每日2000次，每个模型不超过500次&lt;/del&gt;&lt;br /&gt;&lt;del&gt;只有千问算力够，其他很慢&lt;/del&gt;&lt;br /&gt;实际免费额度100次&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;!--
## (已弃用)Nvidia NIM/Open Reouter

### 前提:cc switcher/ free claude core / 9router

* 使用[CC Switcher](https://github.com/farion1231/cc-switch), 用于配置free-cluade-code的模型即指向localhost.
* 使用[9router](https://github.com/decolua/9router) 自动切换所有api
* 使用[free-claude-code](https://github.com/Alishahryar1/free-claude-code)用于转发请求到Nvidial NIM和OpenRouter
* free-claude-code配置Discord方便远程操作Claude code. (20260405的free-claude-code有bug,需要手动修复cli/session.py中ANTHROPIC_API_KEY的设置), 需要npm安装mcp-server-discord然后通过ccswitcher配置mcp-server-discord和DISCORD_TOKEN

```conf
    MODEL_OPUS=&quot;nvidia_nim/deepseek-ai/deepseek-v3.2&quot;         # opus最复杂最慢
    MODEL_SONNET=&quot;open_router/qwen/qwen3.6-plus:free&quot;
    MODEL_HAIKU=&quot;open_router/minimax/minimax-m2.5:free&quot;       # haiku最简单最快
    MODEL=&quot;nvidia_nim/openai/gpt-oss-120b&quot;                     # fallback

    # 可在vscode中/switch model来切换默认模型
```

* free-claude-code开机启动（win10）

```conf
  # 第一步：打开启动文件夹,按下键盘上的 Win + R 键。在弹出的对话框中输入 shell:startup 并按回车。这会打开一个名为“启动”的文件夹。
  # 第二步：创建快捷方式,在文件夹空白处点击 右键 -&gt; 新建 -&gt; 快捷方式。在“请键入对象的位置”框中，直接复制并粘贴以下完整命令：
  powershell.exe -NoExit -Command &quot;cd &apos;D:\Pack\AI\free-claude-code&apos;; uv run uvicorn server:app --host 0.0.0.0 --port 8082&quot;
  # 双击测试是否成功
```

### Nvidia NIM

* 使用free-claude-code来转发请求到Nvidia NIM.

### openRouter

* 使用free-claude-code来转发请求到OpenRouter

### ollama

* 3小时的量很快使用完, 但可以使用Gemini, 目前不使用.

### z.ai的GLM-4.7-Flash

* z.ai和智谱国内站的GLM-4.7-Flash免费使用, 可用于处理简单或明确的功能,在cc switcher配置了,其他不可用时切换到z.ai.

### claude进阶:mcp和skill等

* mcp

  * [vsc-ls-mcp](https://github.com/beixiyo/vsc-lsp-mcp) claude code使用vscode提供的lsp语言服务器
  * [mcp-server-discord] claude code 与 discord通信
* skill

```md
  # Claude Code 项目规范

  ## 语义化代码导航 (Semantic Navigation)

  项目已连接 LSP-MCP 服务器 (127.0.0.1:9527)。在处理 C# 代码时，必须优先使用语义工具，禁止仅依赖文本搜索。

  ### 1. 查找与定位逻辑 (Finding Code)

    - **查找类/方法/变量的定义**：严禁使用 `glob` 或 `grep` 查找符号位置。必须直接调用 `get_definition`。
    - **理解代码含义**：在修改不熟悉的符号前，先调用 `get_hover` 获取该符号的完整类型信息和文档注释（尤其在处理 ET 框架的复杂泛型时）。
    - **处理编译错误**：若修改后出现编译问题，优先使用 `get_completions` 获取补全建议，辅助修复语法错误。

  ### 2. 影响分析逻辑 (Impact Analysis)

    - **重构与修改前**：在修改任何 `Component`、`System` 或公共接口前，必须执行 `get_references`。
    - **评估范围**：根据 `get_references` 返回的列表，评估修改对项目其他模块的影响，确保不会破坏 ET 框架的事件分发或组件生命周期。

  ### 3. 全局重命名 (Refactoring)

    - **跨文件更名**：严禁手动在多个文件中使用 `sed` 或 `edit_file` 替换名称。必须使用 `rename_symbol` 以确保所有语义引用（包括注释和不同文件中的引用）同步更新。

  ### 4. 故障退避机制 (Fallback)

    - 只有在以下情况方可使用 `ls` / `grep` / `glob`：
      - 查找非 C# 符号的字符串内容（如 Log 文本、JSON 键名）。
      - `lsp-mcp` 返回“未找到定义”或服务器连接异常时。
      - 需要模糊匹配文件名而非代码符号时。

  ## 开发工作流示例 (SOP)

  当你被要求“修改 UIModule 的初始化逻辑”时：

  1. **第一步**：调用 `get_definition` 定位 `UIModule` 类及其初始化方法。
  2. **第二步**：调用 `get_hover` 确认参数类型。
  3. **第三步**：调用 `get_references` 查看哪些 System 正在调用此初始化。
  4. **第四步**：执行修改。

  ## 缩进与格式要求 (Indentation &amp; Formatting)

  本项目严格使用 **空格 (Spaces)** 缩进，禁止使用 Tab。

  1. **缩进标准**：全文使用 **4 个空格** 缩进。
  2. **编辑准则**：在执行 `edit_file` 操作前，必须先通过 `read_file` 确认目标代码块的精确缩进级别。
  3. **匹配要求**：生成的 `oldText` 必须与磁盘文件的空格数量完全一致。如果无法确定空格数量，请使用 `grep` 或查看该行前后的空格分布。
  4. **禁止混用**：严禁在一次编辑中引入任何 Tab 字符或不一致的空格缩进。

  ## 处理 C# 转义字符串 (Escaped Strings)

  当修改包含 `\n` 或 `$&quot;{...}&quot;` 的 C# 代码时，经常会发生编辑冲突。请执行以下规避策略：

  1. **编辑策略**：如果一行代码包含 `\n`，在 `edit_file` 时尽量不要把 `\n` 放在匹配字符串的边界，或者尝试仅替换该行中不含转义符的部分。
  2. **重构优先**：如果编辑失败，请尝试将该行重写。例如：
    - 原始：`string.Format($&quot;\nError: {msg}&quot;)`
    - 改为：`$&quot;{System.Environment.NewLine}Error: {msg}&quot;` 或使用拼接。
  3. **匹配验证**：在定位 `oldText` 时，如果包含反斜杠，必须意识到 JSON 传输层可能导致的转义失效。如果匹配不到，请尝试使用 `grep` 先确认磁盘上的精确字节。

``` --&gt;

&lt;h2 id=&quot;image&quot;&gt;Image&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://copilot.microsoft.com/&quot;&gt;copilot.microsoft&lt;/a&gt; 据说每次免费图片不限制&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 28 Mar 2026 11:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/03/28/free-ai-code/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/03/28/free-ai-code/</guid>
        
        <category>AI</category>
        
        
      </item>
    
      <item>
        <title>解决MacOS Sequoia 15连接蓝牙/无线鼠标后导致强制进入安全模式</title>
        <description>&lt;h2 id=&quot;状况&quot;&gt;状况&lt;/h2&gt;

&lt;p&gt;Macbook Air 2015出问题的情况:&lt;/p&gt;

&lt;p&gt;系统支持版本太低，使用OpenCore  Legacy Patcher升级到MacOs Sequoia。能顺利使用新系统。&lt;/p&gt;

&lt;p&gt;无线网卡损坏，使用USB网卡并且安装Wireless-USB-OC-Big-Sur-Adapter第三方驱动。能顺利使用无线网络。&lt;/p&gt;

&lt;p&gt;但当插入DOAL-MODE MOUSE(可以同时使用WIFI和蓝牙的鼠标)的USB模块且连接蓝牙后，下次重启就会强制进入安全模式。之后无论怎么重启都只会进入安全模式。如果删除蓝牙后放置一段时间不开机后，再次开机就可以正常进入。&lt;/p&gt;

&lt;h2 id=&quot;分析问题&quot;&gt;分析问题&lt;/h2&gt;

&lt;p&gt;从 Big Sur 开始，苹果正式宣布废弃传统的内核扩展（Kernel Extensions，即 .kext 文件），要求所有网卡、鼠标、键盘驱动开发者改用运行在用户空间的 NetworkExtension 和 DriverKit 框架。
在这个阶段，Realtek 等厂商直接宣布停止开发 Mac 驱动[&lt;a href=&quot;https://www.google.com/url?sa=E&amp;amp;q=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fgrounding-api-redirect%2FAUZIYQH9vQdFyxHSlKpXo7bEo0LOBFSWtMLFq0p8df0lW6wfZNpmFp8DD4EFmPRntNXsTqUJM9lKS7m5eetq4fSs_iMB-_b0sWRsgpGJTFT_v8YLtbkYGMatcLN6ozsmUh_N8sao5N8djPiLowfNQna4ZQBir-2oQijRpLW471zwW4HYqyfa4xwjuyK6z10eG-xI6YJZa_ljR8j7dcWmnA%3D%3D&quot;&gt;1&lt;/a&gt;]。但由于底层还没完全封死，像 chris1111 这样的民间大神还能通过关闭 SIP，把旧驱动“硬塞”进内核里，此时勉强能用，只是偶尔不稳定[&lt;a href=&quot;https://www.google.com/url?sa=E&amp;amp;q=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fgrounding-api-redirect%2FAUZIYQH9vQdFyxHSlKpXo7bEo0LOBFSWtMLFq0p8df0lW6wfZNpmFp8DD4EFmPRntNXsTqUJM9lKS7m5eetq4fSs_iMB-_b0sWRsgpGJTFT_v8YLtbkYGMatcLN6ozsmUh_N8sao5N8djPiLowfNQna4ZQBir-2oQijRpLW471zwW4HYqyfa4xwjuyK6z10eG-xI6YJZa_ljR8j7dcWmnA%3D%3D&quot;&gt;1&lt;/a&gt;][&lt;a href=&quot;https://www.google.com/url?sa=E&amp;amp;q=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Fgrounding-api-redirect%2FAUZIYQFfrhZqlsT0Xonukhzpfex5AJ8_QUEJevTfJtOBlCygiVemoX7pAa1UeJMg02R1ynGawfGodiGnHLoqR6KTmcGut-Bd0vOVSNqlNfjDBVNRaHeqMuDm6IL_zZumcgBcJJk9_m2N5atoyso2QAeXj_68G_Pf&quot;&gt;2&lt;/a&gt;]。&lt;/p&gt;

&lt;p&gt;到了 Sonoma，苹果做了一个大动作：彻底删除了系统内核中老旧的 IO80211FamilyLegacy（传统无线网卡堆栈），并大幅修改了 USB 栈。为了让你电脑自带的老网卡能用，OCLP 必须强行向 Sonoma 注入一个叫 IOSkywalkFamily 的底层核心文件。当你安装了第三方的 USB 网卡驱动（比如 Wireless-USB-OC-Big-Sur-Adapter），这个驱动在工作时，会和 OCLP 注入的 IOSkywalkFamily 在争夺底层网络控制权时发生严重冲突。但这种可能，我看Wireless-USB-OC-Big-Sur-Adapter是支持OpenCore Legacy Patcher和Sequoia的，大概率不是这个问题，那么我们分析其他原因：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如果是USB网卡的问题，但我安装Wireless-USB-OC-Big-Sur-Adapter驱动后一直能正常使用。（有一种可能使用Wireless-USB-OC-Big-Sur-Adapter驱动的WIFI图标修改软件的导致的冲突）但这种可能性较低，因为我安装后一直能正常使用。&lt;/li&gt;
  &lt;li&gt;DUAL MODE MOUSE的2.4G接收器的问题：当插入接收器，macOS 会通过设备的Vendor ID (VID) 和 Product ID (PID) 来判断这是什么设备。如果系统有内置驱动 ：它会直接加载 macOS 内建的 Kext 或 DriverKit 驱动。如果是新设备/旧设备：对于老 Mac 上的 OCLP 环境，它会加载 OCLP 为了支持你这个老 Mac 的 USB 接口而提前注入的通用 USB 驱动补丁。OCLP 必须确保 Mac 的所有 USB 接口能工作，所以它注入了一些底层补丁。点击“重新启动”时，系统需要“安全地”卸载所有正在运行的 Kext 和 DriverKit 扩展。2.4G 接收器的驱动（可能是 OCLP 注入的通用 USB 驱动，或者是设备内部的 HID 描述符被旧补丁错误识别了）在被强制卸载时，没有正确响应内核指令，导致内核崩溃 (Kernel Panic)。系统底层自动识别了该 USB 设备的 PID/VID，并强制加载了 OCLP 帮你打上的、用来支持老 USB 接口的底层补丁/Kext。正是这个补丁在重启卸载时崩溃了&lt;/li&gt;
  &lt;li&gt;DUAL MODE MOUSE的蓝牙协议问题：旧蓝牙鼠标”可能使用的是一个非常老的蓝牙协议版本。当 OCLP 注入的补丁尝试用 Sonoma 的新蓝牙框架（IOBluetoothFamily）去和这个老协议握手时，如果补丁本身没有完全适配 Sonoma 的新接口，就会在握手完成的瞬间引发一个无法恢复的内核崩溃。死锁机制触发：系统检测到“鼠标连接”这个行为引发了崩溃，它会记录下来。下次重启时，系统（特别是启动到一定程度时）会自动尝试重新初始化蓝牙设备（即使你把鼠标关了，它也会尝试连接已配对设备），一旦初始化这个老旧的蓝牙芯片模块，就立即触发崩溃，再次进入安全模式。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;解决问题思路&quot;&gt;解决问题思路&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;先使用USB启动盘降级到Ventura(按Atl，进入时选择EFI BOOT图标的)&lt;/li&gt;
  &lt;li&gt;安装第三方USB驱动，确定可以运行和访问WIFI，重启是否进入安全模式。&lt;/li&gt;
  &lt;li&gt;连接蓝牙鼠标和键盘，查看是否有问题。&lt;/li&gt;
  &lt;li&gt;插入2.4G接收机蓝牙和键盘，查看是否有问题。&lt;/li&gt;
  &lt;li&gt;升级到Sequoia系统&lt;/li&gt;
  &lt;li&gt;先连接蓝牙鼠标（不要插入2.4G接收器）&lt;/li&gt;
  &lt;li&gt;如果以上哪一步出了问题，就能确定是哪一步的问题。如果以上都没问题，那么也不用插入2.4G接收器了，确定是接收器问题。&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 09 Mar 2026 01:05:00 +0000</pubDate>
        <link>http://gumcstronger.github.io/2026/03/09/mac-sequoia-bluetooth/</link>
        <guid isPermaLink="true">http://gumcstronger.github.io/2026/03/09/mac-sequoia-bluetooth/</guid>
        
        <category>Mac</category>
        
        
      </item>
    
  </channel>
</rss>
