使用 `trim_messages` 优化聊天消息长度

在使用聊天模型进行对话时,每个模型都有其有限的上下文窗口,限制了能够接收的最大文本长度。因此,在处理长消息或持续累积的聊天记录时,我们需要有效管理消息的长度。本文将介绍如何使用 trim_messages 工具来管理消息长度,以确保它们适合特定模型的上下文窗口。

技术背景介绍

在与聊天模型交互时,信息的上下文窗口限制意味着我们不能无限制地传递长消息。为了提升对话效率并避免超出上下文窗口的限制,我们可以通过不同的策略修剪消息列表。

核心原理解析

  • 上下文窗口限制:模型处理的文本长度有限,超出则无法处理。
  • 消息修剪策略:包括保留最后、最前的消息等策略。
  • 自定义计数器:可以编写自定义函数来计算消息中的token(令牌)数量,以更精确地修剪消息。

代码实现演示

以下代码示例展示了如何使用 trim_messages 进行消息修剪。

# 安装必要的库
# pip install -U langchain-openai
# pip install tiktoken

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, trim_messages
from langchain_openai import ChatOpenAI

# 定义消息列表
messages = [
    SystemMessage("you're a good assistant, you always respond with a joke."),
    HumanMessage("i wonder why it's called langchain"),
    AIMessage("Well, I guess they thought ‘WordRope’ and ‘SentenceString’ just didn’t have the same ring to it!"),
    HumanMessage("and who is harrison chasing anyways"),
    AIMessage("Hmmm let me think.\n\nWhy, he's probably chasing after the last cup of coffee in the office!"),
    HumanMessage("what do you call a speechless parrot"),
]

# 使用 OpenAI API 进行消息修剪
trimmed_messages = trim_messages(
    messages,
    max_tokens=45,
    strategy="last",
    token_counter=ChatOpenAI(model="gpt-4o"),
    include_system=True,
)

print(trimmed_messages)

上述代码使用了 trim_messages 方法来修剪消息,只保留最后的45个tokens(令牌)。通过包含 SystemMessage,确保保持对话的上下文。

应用场景分析

  1. 长对话管理:在需要管理长对话的情况下,确保对话的某些重要部分始终在模型的上下文中。
  2. 提高响应效率:通过修剪无关信息,提升模型响应速度。
  3. 资源节约:减少无用token的传递,在长时间对话中节省计算资源。

实践建议

  1. 选择合适的策略:根据对话的性质选择合适的消息修剪策略。
  2. 编写自定义计数器:在需要高精度时,可以定义自定义token计数器。
  3. 测试不同配置:在实际应用中多次测试修剪效果,找到最适合的设置。

结束语:如果遇到问题欢迎在评论区交流。

—END—

用户信息发不出去,没有出现在对话框中,但是AI收到了,并且AI的回复正常可见:using UnityEngine.UI; using System.Collections; using DG.Tweening; using System.Collections.Generic; using UnityEngine.EventSystems; using UnityEngine; public class 聊天系统 : MonoBehaviour { // UI组件引用 public GameObject 聊天窗口; public InputField 消息输入框; public Button 发送按钮; public Button 关闭按钮; public Transform 消息容器; // 消息项的父物体(必须是ScrollView的Content) public GameObject 玩家消息预制体; // 玩家消息预制体(靠右) public GameObject 朋友消息预制体; // 朋友消息预制体(靠左) public ScrollRect 聊天滚动视图; // 手动指定滚动视图 public RectTransform 标题栏; // 用于拖拽的标题栏 // 聊天设置 public string 玩家名称 = "我"; public string 朋友名称 = "AI助手"; public Color 玩家消息颜色 = Color.blue; public Color 朋友消息颜色 = Color.green; public int 消息分割长度 = 20; // 消息分割长度 // 显示相关设置 public RectTransform 聊天内容区域; public bool 显示关键错误 = true; // 只保留关键错误显示控制 public Color 调试文本颜色 = Color.red; // 聊天状态 private bool 聊天窗口是否打开 = false; public 聊天邀请管理器 邀请管理器; // 字体相关 public Font 聊天字体; public int 字体大小 = 16; // UI层级相关 public Canvas 聊天画布; public int 消息文本层级 = 5; // 动画设置 public float 动画持续时间 = 0.3f; public float 缩放比例 = 0.8f; public float 弹性系数 = 1.1f; // 消息历史记录 private List<消息数据> 消息历史 = new List<消息数据>(); public int 最大消息数量 = 50; // 限制最大消息数量 // 拖拽相关变量 private bool 正在拖拽 = false; private Vector2 拖拽偏移量; private RectTransform 聊天窗口Rect; // 引用AI管理器 public AIManager ai管理器; public void 收到AI消息(string 内容) { // 使用朋友的名称和颜色显示AI消息 收到消息(朋友名称, 内容); } void Start() { 聊天窗口.SetActive(false); 聊天窗口是否打开 = false; 邀请管理器 = FindObjectOfType<聊天邀请管理器>(); // 获取聊天窗口的RectTransform用于拖拽 聊天窗口Rect = 聊天窗口.GetComponent<RectTransform>(); // 自动查找AI管理器 if (ai管理器 == null) { ai管理器 = FindObjectOfType<AIManager>(); if (ai管理器 != null) { ai管理器.聊天系统 = this; // 双向关联 } else if (显示关键错误) { Debug.LogWarning("场景中未找到AIManager组件,AI功能将不可用"); } } // 确保标题栏存在,若未指定则尝试查找 if (标题栏 == null) { Transform 标题栏Transform = 聊天窗口.transform.Find("标题栏"); if (标题栏Transform != null) { 标题栏 = 标题栏Transform.GetComponent<RectTransform>(); } else { 标题栏 = 聊天窗口Rect; } } // 自动查找滚动视图 if (聊天滚动视图 == null) 聊天滚动视图 = 消息容器.GetComponentInParent<ScrollRect>(); // 验证并修复显示问题 验证消息显示相关组件(); 修复输入框(); 修复消息容器布局(); 修复UI层级问题(); // 绑定事件 发送按钮.onClick.RemoveAllListeners(); 发送按钮.onClick.AddListener(发送消息); 关闭按钮.onClick.RemoveAllListeners(); 关闭按钮.onClick.AddListener(关闭聊天窗口); 消息输入框.onEndEdit.RemoveAllListeners(); 消息输入框.onEndEdit.AddListener(输入框回车发送); // 添加输入框按键监听,支持Ctrl+Enter换行 消息输入框.onValueChanged.AddListener(OnInputValueChanged); // 添加拖拽事件监听 添加拖拽事件监听(); StartCoroutine(监控输入框状态()); } // 添加拖拽事件监听 private void 添加拖拽事件监听() { if (标题栏 == null) return; // 添加事件触发器用于拖拽 EventTrigger trigger = 标题栏.GetComponent<EventTrigger>(); if (trigger == null) { trigger = 标题栏.gameObject.AddComponent<EventTrigger>(); } // 清除现有事件,避免重复添加 trigger.triggers.Clear(); // 鼠标按下事件 EventTrigger.Entry 按下事件 = new EventTrigger.Entry(); 按下事件.eventID = EventTriggerType.PointerDown; 按下事件.callback.AddListener((data) => 开始拖拽((PointerEventData)data)); trigger.triggers.Add(按下事件); // 鼠标拖动事件 EventTrigger.Entry 拖动事件 = new EventTrigger.Entry(); 拖动事件.eventID = EventTriggerType.Drag; 拖动事件.callback.AddListener((data) => 执行拖拽((PointerEventData)data)); trigger.triggers.Add(拖动事件); // 鼠标释放事件 EventTrigger.Entry 释放事件 = new EventTrigger.Entry(); 释放事件.eventID = EventTriggerType.PointerUp; 释放事件.callback.AddListener((data) => 结束拖拽()); trigger.triggers.Add(释放事件); // 鼠标离开事件 EventTrigger.Entry 离开事件 = new EventTrigger.Entry(); 离开事件.eventID = EventTriggerType.PointerExit; 离开事件.callback.AddListener((data) => 结束拖拽()); trigger.triggers.Add(离开事件); } // 开始拖拽 private void 开始拖拽(PointerEventData 事件数据) { 正在拖拽 = true; // 计算鼠标位置与窗口位置的偏移量 RectTransformUtility.ScreenPointToLocalPointInRectangle( 聊天窗口Rect, 事件数据.position, 事件数据.pressEventCamera, out 拖拽偏移量); } // 执行拖拽 private void 执行拖拽(PointerEventData 事件数据) { if (!正在拖拽 || 聊天窗口Rect == null) return; Vector2 本地鼠标位置; if (RectTransformUtility.ScreenPointToLocalPointInRectangle( 聊天窗口Rect.parent.GetComponent<RectTransform>(), 事件数据.position, 事件数据.pressEventCamera, out 本地鼠标位置)) { // 根据偏移量调整窗口位置 聊天窗口Rect.localPosition = 本地鼠标位置 - 拖拽偏移量; } } // 结束拖拽 private void 结束拖拽() { 正在拖拽 = false; } // 处理输入框内容变化,支持Ctrl+Enter换行 private void OnInputValueChanged(string input) { if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) { if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)) { 消息输入框.text += "\n"; 消息输入框.caretPosition = 消息输入框.text.Length; } } } // 修复UI层级问题 private void 修复UI层级问题() { // 确保有Canvas组件 if (聊天画布 == null) { 聊天画布 = GetComponentInParent<Canvas>(); if (聊天画布 == null) { 聊天画布 = 聊天窗口.AddComponent<Canvas>(); 聊天窗口.AddComponent<CanvasScaler>(); 聊天窗口.AddComponent<GraphicRaycaster>(); } } // 确保消息容器在正确层级 if (消息容器 != null) { CanvasRenderer cr = 消息容器.GetComponent<CanvasRenderer>(); if (cr == null) { cr = 消息容器.gameObject.AddComponent<CanvasRenderer>(); } // 设置消息容器的层级 RectTransform rt = 消息容器.GetComponent<RectTransform>(); if (rt != null) { rt.SetAsLastSibling(); // 确保在最上层 } } } // 验证消息显示相关组件 private void 验证消息显示相关组件() { // 验证消息容器 if (消息容器 == null) { if (显示关键错误) Debug.LogError("错误:消息容器未赋值!"); return; } // 验证滚动视图 if (聊天滚动视图 == null) { // 移除布局提示 } else { // 确保消息容器是ScrollView的Content if (聊天滚动视图.content != 消息容器.GetComponent<RectTransform>()) { 聊天滚动视图.content = 消息容器.GetComponent<RectTransform>(); } // 修复滚动视图掩码问题 Mask mask = 聊天滚动视图.GetComponent<Mask>(); if (mask != null && !mask.showMaskGraphic) { mask.showMaskGraphic = true; } // 修复滚动条设置 if (聊天滚动视图.verticalScrollbar != null) { 聊天滚动视图.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHideAndExpandViewport; 聊天滚动视图.verticalScrollbarSpacing = 2; } } // 验证预制体 if (玩家消息预制体 == null) { if (显示关键错误) Debug.LogError("错误:玩家消息预制体未赋值!"); } if (朋友消息预制体 == null) { if (显示关键错误) Debug.LogError("错误:朋友消息预制体未赋值!"); } // 检查预制体是否激活 if (玩家消息预制体 != null && !玩家消息预制体.activeSelf) { 玩家消息预制体.SetActive(true); } if (朋友消息预制体 != null && !朋友消息预制体.activeSelf) { 朋友消息预制体.SetActive(true); } // 验证文本组件 if (玩家消息预制体 != null) { Text 发送者文本 = 玩家消息预制体.transform.Find("发送者文本")?.GetComponent<Text>(); Text 内容文本 = 玩家消息预制体.transform.Find("内容文本")?.GetComponent<Text>(); if (发送者文本 == null || 内容文本 == null) { 修复消息项预制体(玩家消息预制体); } } if (朋友消息预制体 != null) { Text 发送者文本 = 朋友消息预制体.transform.Find("发送者文本")?.GetComponent<Text>(); Text 内容文本 = 朋友消息预制体.transform.Find("内容文本")?.GetComponent<Text>(); if (发送者文本 == null || 内容文本 == null) { 修复消息项预制体(朋友消息预制体); } } } // 修复消息项预制体 private void 修复消息项预制体(GameObject 预制体) { if (预制体 == null) return; // 添加发送者文本 if (预制体.transform.Find("发送者文本") == null) { GameObject 发送者文本Object = new GameObject("发送者文本"); 发送者文本Object.transform.SetParent(预制体.transform); Text 发送者文本 = 发送者文本Object.AddComponent<Text>(); 发送者文本.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); 发送者文本.fontSize = 字体大小; 发送者文本.color = Color.black; 发送者文本.enabled = true; 发送者文本.raycastTarget = false; // 设置位置 RectTransform rt = 发送者文本Object.GetComponent<RectTransform>(); rt.anchorMin = new Vector2(0, 1); rt.anchorMax = new Vector2(0, 1); rt.pivot = new Vector2(0, 1); rt.offsetMin = new Vector2(5, -25); rt.offsetMax = new Vector2(100, 0); } // 添加内容文本 if (预制体.transform.Find("内容文本") == null) { GameObject 内容文本Object = new GameObject("内容文本"); 内容文本Object.transform.SetParent(预制体.transform); Text 内容文本 = 内容文本Object.AddComponent<Text>(); 内容文本.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); 内容文本.fontSize = 字体大小; 内容文本.color = Color.black; 内容文本.enabled = true; 内容文本.raycastTarget = false; // 设置位置 RectTransform rt = 内容文本Object.GetComponent<RectTransform>(); rt.anchorMin = new Vector2(0, 1); rt.anchorMax = new Vector2(1, 1); rt.pivot = new Vector2(0, 1); rt.offsetMin = new Vector2(110, -25); rt.offsetMax = new Vector2(-5, 0); } } // 修复消息容器布局 private void 修复消息容器布局() { if (消息容器 == null) return; // 确保容器激活 消息容器.gameObject.SetActive(true); // 添加垂直布局组 VerticalLayoutGroup layout = 消息容器.GetComponent<VerticalLayoutGroup>(); if (layout == null) { layout = 消息容器.gameObject.AddComponent<VerticalLayoutGroup>(); } // 修复间距和布局冲突问题 layout.spacing = 20; layout.childAlignment = TextAnchor.UpperLeft; layout.childControlWidth = true; layout.childControlHeight = true; layout.childForceExpandWidth = false; layout.childForceExpandHeight = false; layout.padding = new RectOffset(10, 10, 10, 10); // 添加内容大小适配 ContentSizeFitter fitter = 消息容器.GetComponent<ContentSizeFitter>(); if (fitter == null) { fitter = 消息容器.gameObject.AddComponent<ContentSizeFitter>(); } fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; // 滚动视图设置优化 if (聊天滚动视图 != null) { 聊天滚动视图.vertical = true; 聊天滚动视图.horizontal = false; 聊天滚动视图.movementType = ScrollRect.MovementType.Clamped; 聊天滚动视图.content.sizeDelta = new Vector2(0, 0); 聊天滚动视图.scrollSensitivity = 40f; // 确保滚动条正确关联 if (聊天滚动视图.verticalScrollbar != null) { 聊天滚动视图.verticalScrollbar.value = 0; // 初始位置在底部 } } // 修复两个预制体的RectTransform 修复预制体RectTransform(玩家消息预制体, true); 修复预制体RectTransform(朋友消息预制体, false); } // 修复预制体的RectTransform private void 修复预制体RectTransform(GameObject 预制体, bool 是玩家消息) { if (预制体 == null) return; RectTransform rt = 预制体.GetComponent<RectTransform>(); if (rt != null) { rt.anchorMin = new Vector2(0, 1); rt.anchorMax = new Vector2(1, 1); rt.pivot = new Vector2(0.5f, 1); rt.offsetMin = new Vector2(0, 0); rt.offsetMax = new Vector2(0, 0); rt.localScale = Vector3.one; } // 确保文本可见 Text[] texts = 预制体.GetComponentsInChildren<Text>(); foreach (var text in texts) { text.enabled = true; text.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); text.fontSize = 字体大小; text.horizontalOverflow = HorizontalWrapMode.Wrap; text.verticalOverflow = VerticalWrapMode.Overflow; } } // 分割消息的方法 private List<string> 分割消息(string 内容) { List<string> 分割后的消息 = new List<string>(); // 如果消息长度小于等于分割长度,直接返回原消息 if (string.IsNullOrEmpty(内容) || 内容.Length <= 消息分割长度) { 分割后的消息.Add(内容); return 分割后的消息; } // 否则按指定长度分割消息 for (int i = 0; i < 内容.Length; i += 消息分割长度) { int 截取长度 = Mathf.Min(消息分割长度, 内容.Length - i); string 子消息 = 内容.Substring(i, 截取长度); 分割后的消息.Add(子消息); } return 分割后的消息; } // 显示消息项 private void 显示消息项(string 发送者, string 内容, Color 颜色, bool 是玩家消息, bool 显示发送者 = true) { if (消息容器 == null) { if (显示关键错误) Debug.LogError("消息容器未赋值!"); return; } // 根据是否是玩家消息选择不同的预制体 GameObject 消息预制体 = 是玩家消息 ? 玩家消息预制体 : 朋友消息预制体; if (消息预制体 == null) { if (显示关键错误) Debug.LogError("消息项预制体未赋值!"); return; } // 实例化消息项 GameObject 消息项 = Instantiate(消息预制体, 消息容器); 消息项.name = $"{(是玩家消息 ? "玩家" : "朋友")}_消息_{System.DateTime.Now.Ticks}"; 消息项.SetActive(true); 消息项.transform.SetAsLastSibling(); // 初始状态设置为缩放为0,为动画做准备 消息项.transform.localScale = Vector3.zero; // 设置发送者文本 Text 发送者文本 = 消息项.transform.Find("发送者文本")?.GetComponent<Text>(); if (发送者文本 != null) { 发送者文本.text = 发送者 + ":"; 发送者文本.color = 颜色; 发送者文本.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); 发送者文本.fontSize = 字体大小; 发送者文本.enabled = 显示发送者; 发送者文本.rectTransform.sizeDelta = new Vector2(100, 发送者文本.preferredHeight); } else if (显示关键错误) { Debug.LogError("未找到'发送者文本'组件!"); } // 设置内容文本 Text 内容文本 = 消息项.transform.Find("内容文本")?.GetComponent<Text>(); if (内容文本 != null) { 内容文本.text = 内容; 内容文本.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); 内容文本.fontSize = 字体大小; 内容文本.enabled = true; // 计算最大宽度 float 最大宽度 = 聊天滚动视图 != null ? 聊天滚动视图.GetComponent<RectTransform>().rect.width - 120 : 300; 内容文本.rectTransform.sizeDelta = new Vector2(最大宽度, 内容文本.preferredHeight); // 强制更新文本尺寸 Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(消息项.GetComponent<RectTransform>()); } else if (显示关键错误) { Debug.LogError("未找到'内容文本'组件!"); } // 强制刷新布局 LayoutRebuilder.ForceRebuildLayoutImmediate(消息容器.GetComponent<RectTransform>()); if (聊天内容区域 != null) LayoutRebuilder.ForceRebuildLayoutImmediate(聊天内容区域); // 应用Q弹动画效果 应用消息动画(消息项.transform); // 保存消息到历史记录 消息历史.Add(new 消息数据(发送者, 内容, System.DateTime.Now, 是玩家消息)); // 检查并移除超出最大数量的消息 清理旧消息(); // 滚动到最新消息 StartCoroutine(滚动到最新消息()); } // 应用消息弹出的Q弹动画 private void 应用消息动画(Transform 消息变换) { // 确保DOTween可用 if (消息变换 == null) return; // 重置缩放 消息变换.localScale = Vector3.one * 缩放比例; // 使用DOTween创建Q弹效果 消息变换.DOScale(Vector3.one * 弹性系数, 动画持续时间 / 2) .SetEase(Ease.OutQuad) .OnComplete(() => { // 回弹效果 消息变换.DOScale(Vector3.one, 动画持续时间 / 2) .SetEase(Ease.OutElastic); }); } // 滚动到最新消息 - 优化滚动效果 private IEnumerator 滚动到最新消息() { // 等待布局刷新 yield return new WaitForEndOfFrame(); if (聊天滚动视图 != null) { // 强制更新内容大小 ContentSizeFitter fitter = 消息容器.GetComponent<ContentSizeFitter>(); if (fitter != null) { fitter.SetLayoutVertical(); } // 使用动画平滑滚动到底部 float targetPos = 0; // 0表示滚动到底部 float startPos = 聊天滚动视图.verticalNormalizedPosition; float elapsedTime = 0; float duration = 0.2f; while (elapsedTime < duration) { elapsedTime += Time.deltaTime; 聊天滚动视图.verticalNormalizedPosition = Mathf.Lerp(startPos, targetPos, elapsedTime / duration); yield return null; } // 确保最终位置正确 聊天滚动视图.verticalNormalizedPosition = targetPos; } } // 清理超出最大数量的旧消息 private void 清理旧消息() { if (消息历史.Count <= 最大消息数量) return; int 要删除的数量 = 消息历史.Count - 最大消息数量; // 从容器中移除最早的消息对象 for (int i = 0; i < 要删除的数量 && 消息容器.childCount > 0; i++) { Destroy(消息容器.GetChild(0).gameObject); } // 从历史记录中移除 消息历史.RemoveRange(0, 要删除的数量); } private void 验证必要组件() { if (消息输入框 == null && 显示关键错误) Debug.LogError("错误:消息输入框未赋值!"); if (发送按钮 == null && 显示关键错误) Debug.LogError("错误:发送按钮未赋值!"); if (关闭按钮 == null && 显示关键错误) Debug.LogError("错误:关闭按钮未赋值!"); if (聊天字体 == null) { 聊天字体 = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); if (聊天字体 == null && 消息输入框 != null && 消息输入框.textComponent != null) 聊天字体 = 消息输入框.textComponent.font; } } private void 修复输入框() { if (消息输入框 == null) return; 消息输入框.gameObject.SetActive(true); 消息输入框.enabled = true; 消息输入框.interactable = true; if (消息输入框.textComponent == null) { Text textComp = 消息输入框.GetComponentInChildren<Text>(); if (textComp == null) textComp = 消息输入框.gameObject.AddComponent<Text>(); 消息输入框.textComponent = textComp; } if (消息输入框.textComponent != null) { if (聊天字体 != null) 消息输入框.textComponent.font = 聊天字体; 消息输入框.textComponent.fontSize = 字体大小; 消息输入框.textComponent.color = Color.black; } if (消息输入框.placeholder != null) { Text placeholder = 消息输入框.placeholder.GetComponent<Text>(); if (placeholder != null) { if (聊天字体 != null) placeholder.font = 聊天字体; placeholder.text = "输入消息...(Ctrl+Enter换行)"; } } } private IEnumerator 监控输入框状态() { while (true) { if (聊天窗口是否打开 && 消息输入框 != null && !消息输入框.interactable) 消息输入框.interactable = true; yield return new WaitForSeconds(0.5f); } } public void 打开聊天窗口() { if (聊天窗口是否打开) return; 聊天窗口.SetActive(true); 聊天窗口是否打开 = true; if (消息输入框 != null) 消息输入框.text = ""; StartCoroutine(延迟聚焦输入框(0.1f)); if (邀请管理器 != null) 邀请管理器.设置正在聊天(true); } public void 关闭聊天窗口() { if (!聊天窗口是否打开) return; 聊天窗口.SetActive(false); 聊天窗口是否打开 = false; if (邀请管理器 != null) 邀请管理器.设置正在聊天(false); } public void 发送消息() { if (!聊天窗口是否打开 || 消息输入框 == null) return; string 消息内容 = 消息输入框.text.Trim(); if (string.IsNullOrEmpty(消息内容)) return; List<string> 分割后的消息 = 分割消息(消息内容); // 发送分割后的消息 for (int i = 0; i < 分割后的消息.Count; i++) { // 只有第一条消息显示发送者,后续分割的消息隐藏发送者 bool 显示发送者 = (i == 0); 显示消息项(玩家名称, 分割后的消息[i], 玩家消息颜色, true, 显示发送者); } // 发送消息给AI if (ai管理器 != null) { ai管理器.发送消息给AI(消息内容); } else if (显示关键错误) { Debug.LogWarning("AI管理器未找到,无法发送消息给AI"); } 消息输入框.text = ""; 消息输入框.ActivateInputField(); } private IEnumerator 延迟聚焦输入框(float 延迟时间) { yield return new WaitForSeconds(延迟时间); if (消息输入框 != null) { 消息输入框.Select(); 消息输入框.ActivateInputField(); } } private void 输入框回车发送(string 内容) { if ((Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)) && !(Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))) { if (聊天窗口是否打开) 发送消息(); 消息输入框.DeactivateInputField(); 消息输入框.ActivateInputField(); } } public void 收到消息(string 发送者, string 内容) { List<string> 分割后的消息 = 分割消息(内容); for (int i = 0; i < 分割后的消息.Count; i++) { bool 显示发送者 = (i == 0); 显示消息项(发送者, 分割后的消息[i], 朋友消息颜色, false, 显示发送者); } } // 消息数据类,用于存储消息历史 private class 消息数据 { public string 发送者; public string 内容; public System.DateTime 时间; public bool 是玩家消息; public 消息数据(string 发送者, string 内容, System.DateTime 时间, bool 是玩家消息) { this.发送者 = 发送者; this.内容 = 内容; this.时间 = 时间; this.是玩家消息 = 是玩家消息; } } private static 聊天系统 _instance; public static 聊天系统 Instance { get { if (_instance == null) { _instance = FindObjectOfType<聊天系统>(); if (_instance == null) { GameObject go = new GameObject("聊天系统"); _instance = go.AddComponent<聊天系统>(); DontDestroyOnLoad(go); } } return _instance; } } } using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Networking; using System.Threading; using Newtonsoft.Json; // 错误响应模型 public class ErrorResponse { public ErrorDetails error; } public class ErrorDetails { public string message; public string code; } // 聊天响应模型(阻塞模式) public class ChatCompletionResponse { [JsonProperty("event")] public string @event; [JsonProperty("task_id")] public string task_id; [JsonProperty("id")] public string id; [JsonProperty("message_id")] public string message_id; [JsonProperty("conversation_id")] public string conversation_id; [JsonProperty("mode")] public string mode; [JsonProperty("answer")] public string answer; [JsonProperty("metadata")] public Metadata metadata; [JsonProperty("created_at")] public long created_at; } // 元数据模型 public class Metadata { [JsonProperty("usage")] public Usage usage; [JsonProperty("retriever_resources")] public List<RetrieverResource> retriever_resources; } // 用量信息模型 public class Usage { [JsonProperty("prompt_tokens")] public int prompt_tokens; [JsonProperty("prompt_unit_price")] public string prompt_unit_price; [JsonProperty("prompt_price_unit")] public string prompt_price_unit; [JsonProperty("prompt_price")] public string prompt_price; [JsonProperty("completion_tokens")] public int completion_tokens; [JsonProperty("completion_unit_price")] public string completion_unit_price; [JsonProperty("completion_price_unit")] public string completion_price_unit; [JsonProperty("completion_price")] public string completion_price; [JsonProperty("total_tokens")] public int total_tokens; [JsonProperty("total_price")] public string total_price; [JsonProperty("currency")] public string currency; [JsonProperty("latency")] public float latency; } // 检索资源模型 public class RetrieverResource { [JsonProperty("position")] public int position; [JsonProperty("dataset_id")] public string dataset_id; [JsonProperty("dataset_name")] public string dataset_name; [JsonProperty("document_id")] public string document_id; [JsonProperty("document_name")] public string document_name; [JsonProperty("segment_id")] public string segment_id; [JsonProperty("score")] public float score; [JsonProperty("content")] public string content; } // 文件信息模型 public class FileInfo { [JsonProperty("type")] public string type; [JsonProperty("transfer_method")] public string transfer_method; [JsonProperty("url")] public string url; [JsonProperty("upload_file_id")] public string upload_file_id; } // 请求数据模型 public class ChatRequestData { [JsonProperty("inputs")] public Dictionary<string, string> inputs; [JsonProperty("query")] public string query; [JsonProperty("response_mode")] public string response_mode; [JsonProperty("user")] public string user; [JsonProperty("conversation_id")] public string conversation_id; [JsonProperty("files")] public List<FileInfo> files; [JsonProperty("auto_generate_name")] public bool? auto_generate_name; [JsonProperty("workflow_id")] public string workflow_id; [JsonProperty("trace_id")] public string trace_id; } // 简单的错误对象模型(替代dynamic) public class SimpleErrorObject { [JsonProperty("message")] public string message; [JsonProperty("code")] public string code; } public class AIManager : MonoBehaviour { private static AIManager _instance; public static AIManager Instance { get { if (_instance == null) { _instance = FindObjectOfType<AIManager>(); if (_instance == null) { GameObject go = new GameObject("AIManager"); _instance = go.AddComponent<AIManager>(); DontDestroyOnLoad(go); } } return _instance; } } [Header("API设置")] [SerializeField] private string apiUrl = "http://localhost/v1/chat-messages"; [SerializeField] private string apiKey = "aapp-wRDYHNHA5BmBsGsMZjDPqfL1"; [Header("响应模式")] [SerializeField] private string responseMode = "blocking"; // blocking 或 streaming [Header("引用")] public 聊天系统 聊天系统; [Header("标志位")] public bool 是否转发给聊天系统 = true; // ✅ 新增标志位 private string _conversationId = ""; private bool _isProcessing = false; private int _mainThreadId; private readonly Queue<Action> _mainThreadActions = new Queue<Action>(); private string _当前回复 = null; public string 当前回复 { get { return _当前回复; } set { _当前回复 = value; } } private void Awake() { if (_instance != null && _instance != this) { Destroy(gameObject); return; } _mainThreadId = Thread.CurrentThread.ManagedThreadId; if (聊天系统 == null) { 聊天系统 = FindObjectOfType<聊天系统>(); } } private void Update() { lock (_mainThreadActions) { while (_mainThreadActions.Count > 0) { try { _mainThreadActions.Dequeue().Invoke(); } catch (Exception ex) { Debug.LogError($"主线程任务执行错误: {ex.Message}"); } } } } /// <summary> /// 发送消息到AI /// </summary> public void 发送消息给AI(string query, Dictionary<string, string> inputs = null) { if (_isProcessing) { Debug.LogWarning("正在处理前一个请求,请等待完成"); return; } if (string.IsNullOrEmpty(query)) { Debug.LogWarning("发送的消息不能为空"); return; } StartCoroutine(PostChatMessage(query, inputs)); } /// <summary> /// 发送带图片的消息 /// </summary> public void 发送带图片的消息(string query, string imageUrl, string transferMethod = "remote_url") { var inputs = new Dictionary<string, string>(); var files = new List<FileInfo> { new FileInfo { type = "image", transfer_method = transferMethod, url = imageUrl } }; StartCoroutine(PostChatMessageWithFiles(query, inputs, files)); } /// <summary> /// 开始新对话 /// </summary> public void 开始新对话() { _conversationId = ""; Debug.Log("开始新对话,会话ID已重置"); } /// <summary> /// 使用UnityWebRequest发送POST请求 /// </summary> private IEnumerator PostChatMessage(string query, Dictionary<string, string> inputs = null) { _isProcessing = true; // 构建请求数据 - 根据Dify API文档要求 var requestData = new ChatRequestData { inputs = inputs ?? new Dictionary<string, string>(), query = query, response_mode = responseMode, user = SystemInfo.deviceUniqueIdentifier, conversation_id = _conversationId, auto_generate_name = true, files = new List<FileInfo>() // 初始化为空列表 }; // 使用Newtonsoft.Json序列化,忽略null值 string jsonBody = JsonConvert.SerializeObject(requestData, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None }); byte[] rawData = Encoding.UTF8.GetBytes(jsonBody); Debug.Log($"发送请求到: {apiUrl}"); Debug.Log($"请求数据: {jsonBody}"); using (UnityWebRequest request = new UnityWebRequest(apiUrl, "POST")) { request.uploadHandler = new UploadHandlerRaw(rawData); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {apiKey}"); // 设置超时 request.timeout = 60; yield return request.SendWebRequest(); Debug.Log($"响应状态: {request.result}"); Debug.Log($"HTTP状态码: {request.responseCode}"); if (request.result == UnityWebRequest.Result.Success) { Debug.Log($"API响应成功: {request.downloadHandler.text}"); ProcessSuccessResponse(request.downloadHandler.text); } else { Debug.LogError($"API请求失败: {request.error}"); ProcessErrorResponse(request.error, request.downloadHandler.text, request.responseCode); } } _isProcessing = false; } /// <summary> /// 发送带文件的聊天消息 /// </summary> private IEnumerator PostChatMessageWithFiles(string query, Dictionary<string, string> inputs, List<FileInfo> files) { _isProcessing = true; var requestData = new ChatRequestData { inputs = inputs ?? new Dictionary<string, string>(), query = query, response_mode = responseMode, user = SystemInfo.deviceUniqueIdentifier, conversation_id = _conversationId, files = files, auto_generate_name = true }; string jsonBody = JsonConvert.SerializeObject(requestData, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None }); byte[] rawData = Encoding.UTF8.GetBytes(jsonBody); Debug.Log($"发送带文件的请求: {jsonBody}"); using (UnityWebRequest request = new UnityWebRequest(apiUrl, "POST")) { request.uploadHandler = new UploadHandlerRaw(rawData); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {apiKey}"); request.timeout = 60; yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { ProcessSuccessResponse(request.downloadHandler.text); } else { ProcessErrorResponse(request.error, request.downloadHandler.text, request.responseCode); } } _isProcessing = false; } /// <summary> /// 处理成功响应 /// </summary> private void ProcessSuccessResponse(string responseText) { try { if (responseMode == "blocking") { // 阻塞模式响应 ChatCompletionResponse response = JsonConvert.DeserializeObject<ChatCompletionResponse>(responseText); if (response == null) { throw new Exception("解析响应结果为空"); } // 更新会话ID if (!string.IsNullOrEmpty(response.conversation_id)) { _conversationId = response.conversation_id; Debug.Log($"更新会话ID: {_conversationId}"); } string message = string.IsNullOrEmpty(response.answer) ? "收到空消息" : response.answer; _当前回复 = message; // ✅ 仅当标志位为 true 时才转发给聊天系统 if (是否转发给聊天系统) { ExecuteOnMainThread(() => { if (聊天系统 != null) { 聊天系统.收到AI消息(message); } }); } // 记录用量信息 if (response.metadata?.usage != null) { Debug.Log($"用量信息 - Tokens: {response.metadata.usage.total_tokens}, 价格: {response.metadata.usage.total_price} {response.metadata.usage.currency}"); } } else { // 流式模式响应处理(如果需要) Debug.LogWarning("流式模式响应,需要特殊处理"); ProcessStreamingResponse(responseText); } } catch (Exception ex) { Debug.LogError($"解析响应失败: {ex.Message}\n原始响应: {responseText}"); ExecuteOnMainThread(() => { if (聊天系统 != null) { 聊天系统.收到AI消息($"解析响应时发生错误: {ex.Message}"); } }); } } /// <summary> /// 处理流式响应(简化版) /// </summary> private void ProcessStreamingResponse(string responseText) { // 这里简化处理,实际需要解析SSE格式 try { // 尝试直接解析为JSON,如果不是SSE格式 ChatCompletionResponse response = JsonConvert.DeserializeObject<ChatCompletionResponse>(responseText); if (response != null && !string.IsNullOrEmpty(response.answer)) { // ✅ 仅当标志位为 true 时才转发给聊天系统 if (是否转发给聊天系统) { ExecuteOnMainThread(() => { if (聊天系统 != null) { 聊天系统.收到AI消息(response.answer); } }); } } else { // 如果不是标准格式,显示原始内容 if (是否转发给聊天系统) { ExecuteOnMainThread(() => { if (聊天系统 != null) { 聊天系统.收到AI消息($"流式响应: {responseText}"); } }); } } } catch { // 如果不是JSON格式,可能是SSE,这里简单显示原始内容 if (是否转发给聊天系统) { ExecuteOnMainThread(() => { if (聊天系统 != null) { 聊天系统.收到AI消息($"流式响应内容: {responseText}"); } }); } } } /// <summary> /// 处理错误响应 /// </summary> private void ProcessErrorResponse(string error, string responseText, long responseCode) { string errorMessage = $"API调用失败: {error} (HTTP {responseCode})"; try { // 检查是否是HTML响应 if (!string.IsNullOrEmpty(responseText) && (responseText.Contains("<!DOCTYPE html>") || responseText.Contains("<html"))) { errorMessage = $"服务器返回了HTML页面而不是JSON响应 (HTTP {responseCode})\n"; errorMessage += "可能的原因:\n"; errorMessage += "1. API端点URL不正确\n"; errorMessage += "2. 认证失败\n"; errorMessage += "3. 服务器配置问题"; Debug.LogError(errorMessage); // 记录部分响应内容 string shortResponse = responseText.Length > 500 ? responseText.Substring(0, 500) + "..." : responseText; Debug.LogError($"HTML响应内容: {shortResponse}"); } else if (!string.IsNullOrEmpty(responseText)) { // 尝试解析JSON错误响应 ErrorResponse errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(responseText); if (errorResponse?.error != null) { errorMessage = $"API错误: {errorResponse.error.message} (代码: {errorResponse.error.code})"; Debug.LogError(errorMessage); } else { // 尝试解析其他格式的错误 try { // 使用简单的错误对象模型替代dynamic SimpleErrorObject errorObj = JsonConvert.DeserializeObject<SimpleErrorObject>(responseText); if (errorObj != null && !string.IsNullOrEmpty(errorObj.message)) { errorMessage = $"错误: {errorObj.message}"; if (!string.IsNullOrEmpty(errorObj.code)) { errorMessage += $" (代码: {errorObj.code})"; } } else { errorMessage = $"错误响应: {responseText}"; } } catch { errorMessage = $"原始错误: {error}\n响应内容: {responseText}"; } } } } catch (Exception ex) { errorMessage = $"处理错误响应时发生异常: {ex.Message}\n原始错误: {error}"; Debug.LogError(errorMessage); } // ✅ 仅当标志位为 true 时才转发给聊天系统 if (是否转发给聊天系统) { ExecuteOnMainThread(() => { if (聊天系统 != null) { 聊天系统.收到AI消息(errorMessage); } }); } } /// <summary> /// 停止响应(仅流式模式) /// </summary> public void 停止响应(string taskId) { if (responseMode != "streaming") { Debug.LogWarning("停止响应功能仅适用于流式模式"); return; } StartCoroutine(StopChatMessage(taskId)); } private IEnumerator StopChatMessage(string taskId) { string stopUrl = $"http://localhost/v1/chat-messages/{taskId}/stop"; var stopData = new { user = SystemInfo.deviceUniqueIdentifier }; string jsonBody = JsonConvert.SerializeObject(stopData); byte[] rawData = Encoding.UTF8.GetBytes(jsonBody); using (UnityWebRequest request = new UnityWebRequest(stopUrl, "POST")) { request.uploadHandler = new UploadHandlerRaw(rawData); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {apiKey}"); yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { Debug.Log("成功停止响应"); } else { Debug.LogError($"停止响应失败: {request.error}"); } } } /// <summary> /// 在主线程执行操作 /// </summary> private void ExecuteOnMainThread(Action action) { if (action == null) return; if (Thread.CurrentThread.ManagedThreadId == _mainThreadId) { action.Invoke(); } else { lock (_mainThreadActions) { _mainThreadActions.Enqueue(action); } } } /// <summary> /// 测试API连接 /// </summary> public void 测试连接() { StartCoroutine(TestAPIConnection()); } private IEnumerator TestAPIConnection() { Debug.Log("测试API连接..."); using (UnityWebRequest request = UnityWebRequest.Get("http://localhost/v1/")) { request.SetRequestHeader("Authorization", $"Bearer {apiKey}"); request.timeout = 10; yield return request.SendWebRequest(); Debug.Log($"测试URL: http://localhost/v1/"); Debug.Log($"响应状态: {request.responseCode}"); if (request.result == UnityWebRequest.Result.Success) { Debug.Log("API连接测试成功"); Debug.Log($"响应内容: {request.downloadHandler.text}"); } else { Debug.LogError("API连接测试失败"); Debug.LogError($"错误信息: {request.error}"); Debug.LogError($"响应内容: {request.downloadHandler.text}"); } } } /// <summary> /// 获取当前会话ID /// </summary> public string 获取当前会话ID() { return _conversationId; } /// <summary> /// 设置会话ID(用于恢复对话) /// </summary> public void 设置会话ID(string conversationId) { _conversationId = conversationId; Debug.Log($"设置会话ID: {_conversationId}"); } /// <summary> /// 切换响应模式 /// </summary> public void 切换响应模式(string mode) { if (mode == "blocking" || mode == "streaming") { responseMode = mode; Debug.Log($"响应模式已切换为: {responseMode}"); } else { Debug.LogWarning("无效的响应模式,只能使用 'blocking' 或 'streaming'"); } } }
09-01
根据你提供的完整代码,我们来分析 **用户信息发不出去** 的问题。 --- ## 🔍 问题描述 - 用户输入的消息 **没有显示在聊天窗口中**; - 但是 AI **确实收到了请求**,并且 **AI 的回复正常显示**; - 这说明: - `发送消息()` 方法正常执行; - `发送消息给AI()` 正常执行; - 但 `显示消息项()` 没有被调用,或者调用后消息没有显示。 --- ## ✅ 问题定位 ### 1. `发送消息()` 方法中调用了 `显示消息项()`,但未生效 ```csharp for (int i = 0; i < 分割后的消息.Count; i++) { // 只有第一条消息显示发送者 bool 显示发送者 = (i == 0); 显示消息项(玩家名称, 分割后的消息[i], 玩家消息颜色, true, 显示发送者); } ``` 这段代码是正确的,**它确实会调用 `显示消息项()`**,所以问题出在: --- ### 2. `显示消息项()` 方法中消息没有显示出来 我们来看 `显示消息项()` 的关键部分: ```csharp private void 显示消息项(string 发送者, string 内容, Color 颜色, bool 是玩家消息, bool 显示发送者 = true) { // 选择预制体 GameObject 消息预制体 = 是玩家消息 ? 玩家消息预制体 : 朋友消息预制体; // 实例化消息项 GameObject 消息项 = Instantiate(消息预制体, 消息容器); 消息项.name = $"{(是玩家消息 ? "玩家" : "朋友")}_消息_{System.DateTime.Now.Ticks}"; 消息项.SetActive(true); 消息项.transform.SetAsLastSibling(); // 设置发送者文本 Text 发送者文本 = 消息项.transform.Find("发送者文本")?.GetComponent<Text>(); if (发送者文本 != null) { 发送者文本.text = 发送者 + ":"; 发送者文本.color = 颜色; 发送者文本.enabled = 显示发送者; } // 设置内容文本 Text 内容文本 = 消息项.transform.Find("内容文本")?.GetComponent<Text>(); if (内容文本 != null) { 内容文本.text = 内容; 内容文本.enabled = true; } // 动画和布局刷新 应用消息动画(消息项.transform); LayoutRebuilder.ForceRebuildLayoutImmediate(消息容器.GetComponent<RectTransform>()); StartCoroutine(滚动到最新消息()); } ``` --- ## 🔍 问题可能原因 ### ✅ 1. `玩家消息预制体` 没有正确设置或组件缺失 - 检查 `玩家消息预制体` 是否赋值; - 检查其子物体是否有 `发送者文本` 和 `内容文本`; - 如果没有,`Find("发送者文本")` 返回 `null`,导致跳过设置; - 但你代码中 **没有报错**,所以可能 **静默失败**。 #### ✅ 解决方案: 在 `显示消息项()` 中添加调试输出: ```csharp if (发送者文本 == null) { Debug.LogError("未找到'发送者文本'组件!预制体名称:" + 消息预制体.name); } if (内容文本 == null) { Debug.LogError("未找到'内容文本'组件!预制体名称:" + 消息预制体.name); } ``` --- ### ✅ 2. `消息容器` 为空或未正确赋值 检查 `消息容器` 是否赋值: ```csharp if (消息容器 == null) { Debug.LogError("消息容器未赋值!"); return; } ``` 确保你在 Unity 编辑器中将 `ScrollView` 的 `Content` 赋值给了 `消息容器`。 --- ### ✅ 3. `玩家消息预制体` 中的 `Text` 组件被禁用 有时,虽然组件存在,但 `Text.enabled = false`,导致不显示。 在 `显示消息项()` 中强制启用: ```csharp if (内容文本 != null) { 内容文本.enabled = true; // 确保启用 内容文本.text = 内容; } ``` --- ### ✅ 4. `玩家消息预制体` 的 `GameObject` 被禁用 有时预制体本身是 `SetActive(false)` 的,实例化后也要调用: ```csharp 消息项.SetActive(true); ``` 你已经做了,没问题。 --- ### ✅ 5. `玩家消息预制体` 的 `RectTransform` 布局错误 确保 `玩家消息预制体` 的 `RectTransform` 设置正确: - `anchorMin` 和 `anchorMax` 设置为 `0,1` 到 `1,1`; - `pivot` 设置为 `0.5,1`; - `offsetMin` 和 `offsetMax` 设置为 `0,0`。 你已经做了,没问题。 --- ## ✅ 推荐测试流程 1. 在 `发送消息()` 方法中打印消息内容: ```csharp Debug.Log("发送消息内容:" + 消息内容); ``` 2. 在 `显示消息项()` 方法中打印预制体名称: ```csharp Debug.Log("实例化消息项:" + 消息预制体.name); ``` 3. 检查控制台是否输出以下内容: ``` 发送消息内容:XXX 实例化消息项:玩家消息预制体 ``` 如果没有输出,说明 `显示消息项()` 没有被调用。 --- ## ✅ 最终解决方案 ### ✅ 修复 `玩家消息预制体` 的组件 确保 `玩家消息预制体` 上有: - `发送者文本`:`Text` 组件; - `内容文本`:`Text` 组件; - 都启用; - 名称正确; - 没有被 `SetActive(false)`。 ### ✅ 强制刷新布局 在 `显示消息项()` 方法末尾添加: ```csharp Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(消息容器); ``` --- ## ✅ 示例修复代码(在 `显示消息项()` 方法中添加) ```csharp // 强制刷新布局 Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(消息容器.GetComponent<RectTransform>()); if (聊天内容区域 != null) LayoutRebuilder.ForceRebuildLayoutImmediate(聊天内容区域); // 添加调试日志 Debug.Log($"显示消息项:{内容},预制体:{消息预制体.name}"); ``` --- ## ✅ 总结 | 问题 | 原因 | 解决方案 | |------|------|----------| | 用户消息不显示 | `玩家消息预制体` 组件缺失或禁用 | 检查并启用 `发送者文本` 和 `内容文本` | | 消息项未显示 | 布局未刷新 | 强制调用 `LayoutRebuilder` | | 消息项未显示 | `消息容器` 未赋值 | 检查并赋值 `ScrollView` 的 `Content` | | 消息项未显示 | `预制体` 被禁用 | 实例化后调用 `SetActive(true)` | | 消息项未显示 | `Text` 被禁用 | 强制设置 `Text.enabled = true` | --- ##
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值