From c267b6c76e52ad019d22422fd4e3ccefab076ec7 Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Tue, 3 Mar 2026 04:28:33 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=8A=9F=E8=83=BD=EF=BC=88html=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=A0=8F=E9=80=9A=E4=BF=A1=E5=AD=98=E5=9C=A8=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Echo <1711788888@qq.com> --- docs/html/story_renderer_extract.js | 590 ++++++++++++++++++++++ server/service/app/conversation.go | 67 ++- server/service/app/regex_script.go | 37 +- web-app/src/components/MessageContent.tsx | 18 +- 4 files changed, 662 insertions(+), 50 deletions(-) create mode 100644 docs/html/story_renderer_extract.js diff --git a/docs/html/story_renderer_extract.js b/docs/html/story_renderer_extract.js new file mode 100644 index 0000000..27b49bf --- /dev/null +++ b/docs/html/story_renderer_extract.js @@ -0,0 +1,590 @@ + class StoryRenderer { + constructor(dataSourceId) { + this.dataSourceId = dataSourceId; + this.yamlData = null; + this.rootNode = null; // 根节点名称 + this.initElements(); + } + + // 初始化DOM元素引用 + initElements() { + this.elements = { + timeDisplay: document.getElementById('time-display'), + locationDisplay: document.getElementById('location-display'), + charactersContainer: document.getElementById('characters-container'), + actionOwner: document.getElementById('action-owner'), + optionsList: document.getElementById('options-list'), + }; + } + + // 初始化方法 + init() { + try { + // 从script标签中加载YAML数据 + this.loadYamlFromScriptTag(); + + // 如果没有有效数据(显示加载状态的情况),直接返回 + if (!this.yamlData) { + this.setupEventListeners(); + return; + } + + // 找到根节点 + this.findRootNode(); + this.renderAll(); + this.setupEventListeners(); + } catch (error) { + this.handleError(error); + } + } + + // 从script标签加载并解析YAML数据 + loadYamlFromScriptTag() { + const scriptElement = document.getElementById(this.dataSourceId); + if (!scriptElement) { + throw new Error('未找到id为"yaml-data-source"的script标签'); + } + + let yamlContent = scriptElement.textContent.trim(); + + // 检查是否为真正的空内容 + if (!yamlContent) { + // 当YAML内容为空时,设置默认的加载状态但不抛出错误 + this.showLoadingState(); + return; // 直接返回,不抛出错误 + } + + // 如果是"加载中..."文本,也显示加载状态 + if (yamlContent === '加载中...') { + this.showLoadingState(); + return; + } + + // 有内容,尝试解析YAML + try { + this.yamlData = jsyaml.load(yamlContent); + } catch (e) { + // YAML格式错误,应该弹出错误对话框 + throw new Error(`YAML格式错误: ${e.message}`); + } + + if (!this.yamlData || Object.keys(this.yamlData).length === 0) { + // 解析成功但数据为空,这是格式问题 + throw new Error('YAML解析成功但数据为空,请检查YAML格式是否正确'); + } + } + + // 显示加载状态的独立方法 + showLoadingState() { + this.elements.timeDisplay.textContent = '⏰ 加载中...'; + this.elements.locationDisplay.textContent = '📍 加载中...'; + this.elements.actionOwner.textContent = '加载中...'; + this.elements.charactersContainer.innerHTML = this.createEmptyState('数据加载中...'); + this.elements.optionsList.innerHTML = + '
  • 加载选项中...
  • '; + } + + // 查找根节点 + findRootNode() { + const rootNodeNames = Object.keys(this.yamlData); + if (rootNodeNames.length === 0) { + throw new Error('YAML数据中未找到任何根节点'); + } + + this.rootNode = rootNodeNames[0]; + } + + // 格式化节点名称,使其更易读 + formatNodeName(name) { + // 提取emoji后面的文本(如果有emoji) + const emojiMatch = name.match(/^(\p{Emoji}\s*)(.*)$/u); + if (emojiMatch && emojiMatch[2]) { + return emojiMatch[2]; + } + return name; + } + + // 渲染所有内容 + renderAll() { + if (!this.rootNode || !this.yamlData[this.rootNode]) { + throw new Error('未找到有效的根节点数据'); + } + + const rootData = this.yamlData[this.rootNode]; + this.renderHeaderInfo(rootData); + this.renderCharacters(rootData); + this.renderActionOptions(rootData); + } + + // 渲染头部信息(日期和时间和地点) + renderHeaderInfo(rootData) { + // 查找日期时间字段 + const dateTimeField = this.findFieldByKeywords(rootData, ['日期', '时间', 'datetime', 'time']); + // 查找地点字段 + const locationField = this.findFieldByKeywords(rootData, ['地点', '位置', 'location', 'place']); + + // 直接使用包含emoji的值 + this.elements.timeDisplay.textContent = dateTimeField ? rootData[dateTimeField] : '⏰ 时间未知'; + this.elements.locationDisplay.textContent = locationField ? rootData[locationField] : '📍 地点未知'; + } + + // 根据关键词查找字段名 + findFieldByKeywords(data, keywords) { + if (!data || typeof data !== 'object') return null; + + const fields = Object.keys(data); + for (const field of fields) { + for (const keyword of keywords) { + if (field.toLowerCase().includes(keyword.toLowerCase())) { + return field; + } + } + } + return null; + } + + // 渲染角色列表 + renderCharacters(rootData) { + // 查找用户列表字段 + const userListField = this.findFieldByKeywords(rootData, ['用户', '角色', '列表', 'user', 'role', 'list']); + const userList = userListField && Array.isArray(rootData[userListField]) ? rootData[userListField] : []; + + this.elements.charactersContainer.innerHTML = ''; + + if (userList.length === 0) { + this.elements.charactersContainer.innerHTML = this.createEmptyState('没有角色数据'); + return; + } + + // 处理每个用户项 + userList.forEach(userItem => { + // 检查是否有外层包装 + let userData = userItem; + + if (typeof userItem === 'object' && userItem !== null) { + const userField = this.findFieldByKeywords(userItem, ['用户', 'user', '角色', 'role']); + if (userField) { + userData = userItem[userField]; + } + } + + const characterCard = this.createCharacterCard(userData); + if (characterCard) { + this.elements.charactersContainer.appendChild(characterCard); + } + }); + } + + // 创建单个角色卡片 + createCharacterCard(userData) { + if (!userData || typeof userData !== 'object') return null; + + const card = document.createElement('div'); + card.className = + 'bg-dark rounded-xl border border-gray-700/30 p-3.5 shadow-sm card-hover character-card theme-transition'; + + // 查找名字字段 + const nameField = this.findFieldByKeywords(userData, ['名字', '姓名', '名称', 'name']); + const userName = nameField ? userData[nameField] : '👤 未知角色'; + + // 创建标题 + const title = document.createElement('h3'); + title.className = 'font-bold text-lg mb-2 pb-1 border-b border-gray-700/30 theme-transition'; + title.textContent = `${this.formatNodeName(userName)}的状态`; + card.appendChild(title); + + // 创建属性列表 + const attributesList = document.createElement('ul'); + attributesList.className = 'space-y-2 text-sm'; + card.appendChild(attributesList); + + // 处理所有属性 + Object.keys(userData).forEach(key => { + // 跳过已经作为标题使用的名字节点 + if (key === nameField) return; + + // 创建属性项,直接使用包含emoji的值 + const attributeItem = this.createAttributeItem(key, userData[key]); + if (attributeItem) { + attributesList.appendChild(attributeItem); + } + }); + + return card; + } + + // 创建属性项 + createAttributeItem(key, value) { + const item = document.createElement('li'); + + // 处理数组类型 + if (Array.isArray(value)) { + item.innerHTML = `${this.formatNodeName(key)}:`; + + const subList = document.createElement('ul'); + subList.className = 'list-disc list-inside ml-4 mt-1 space-y-1 text-gray-400 theme-transition'; + + value.forEach(itemData => { + if (typeof itemData === 'object' && itemData !== null) { + const subKey = Object.keys(itemData)[0]; + const subValue = itemData[subKey]; + const subItem = document.createElement('li'); + subItem.textContent = subValue; + subList.appendChild(subItem); + } else { + const subItem = document.createElement('li'); + subItem.textContent = itemData; + subList.appendChild(subItem); + } + }); + + item.appendChild(subList); + } + // 处理对象类型 + else if (typeof value === 'object' && value !== null) { + item.innerHTML = `${this.formatNodeName(key)}:`; + + const subList = document.createElement('ul'); + subList.className = 'list-disc list-inside ml-4 mt-1 space-y-1 text-gray-400 theme-transition'; + + Object.keys(value).forEach(subKey => { + const subItem = document.createElement('li'); + subItem.textContent = value[subKey]; + subList.appendChild(subItem); + }); + + item.appendChild(subList); + } + // 处理普通文本值 + else if (value !== null && value !== undefined && value.toString().trim() !== '') { + item.innerHTML = `${this.formatNodeName(key)}: ${value}`; + } + + return item; + } + + // 渲染行动选项 + renderActionOptions(rootData) { + // 查找行动选项字段 + const actionOptionsField = this.findFieldByKeywords(rootData, ['行动', '选项', 'action', 'option']); + const actionOptions = + actionOptionsField && typeof rootData[actionOptionsField] === 'object' ? rootData[actionOptionsField] : {}; + + // 设置行动所有者 + const ownerField = this.findFieldByKeywords(actionOptions, ['名字', '姓名', '所有者', 'owner', 'name']); + this.elements.actionOwner.textContent = ownerField + ? this.formatNodeName(actionOptions[ownerField]) + : '未知角色'; + + // 渲染选项列表 + const optionsField = this.findFieldByKeywords(actionOptions, ['选项', '选择', 'option', 'choice']); + const options = optionsField && Array.isArray(actionOptions[optionsField]) ? actionOptions[optionsField] : []; + + this.elements.optionsList.innerHTML = ''; + + if (options.length === 0) { + this.elements.optionsList.innerHTML = this.createEmptyState('没有可用选项'); + return; + } + + options.forEach(optionText => { + const optionItem = document.createElement('li'); + optionItem.className = + 'pl-2 py-1 border-l-2 border-primary/30 ml-1 hover:border-primary/70 transition-colors text-gray-300 theme-transition'; + optionItem.textContent = optionText; + this.elements.optionsList.appendChild(optionItem); + }); + } + + // 创建空状态提示 + createEmptyState(message) { + return `
    + ${message} +
    `; + } + + // 设置事件监听器 + setupEventListeners() { + const detailsElement = document.querySelector('details'); + const contentElement = this.elements.charactersContainer; + + // 初始化高度为0以实现动画效果 + contentElement.style.maxHeight = '0'; + + // 监听详情展开/折叠事件 + detailsElement.addEventListener('toggle', () => { + if (detailsElement.open) { + // 展开时设置实际高度 + setTimeout(() => { + contentElement.style.maxHeight = contentElement.scrollHeight + 'px'; + }, 10); + } else { + // 折叠时设置高度为0 + contentElement.style.maxHeight = '0'; + } + }); + + // 根据自动折叠设置决定默认状态 + const autoCollapseToggle = document.getElementById('auto-collapse-toggle'); + if (autoCollapseToggle) { + // 从本地存储读取设置,默认为true(折叠) + const savedAutoCollapse = localStorage.getItem('autoCollapse'); + const shouldCollapse = savedAutoCollapse === null ? true : savedAutoCollapse === 'true'; + detailsElement.open = !shouldCollapse; + + // 如果默认展开,需要设置正确的高度 + if (!shouldCollapse) { + setTimeout(() => { + contentElement.style.maxHeight = contentElement.scrollHeight + 'px'; + }, 100); + } + } else { + // 如果没有设置切换开关,默认折叠 + detailsElement.open = false; + } + } + + // 错误处理 + handleError(error) { + console.error('渲染错误:', error); + + // 使用美化的错误弹窗 + showErrorModal(error.message); + + // 在角色状态区域显示错误信息 + this.elements.charactersContainer.innerHTML = ` + + `; + + // 在行动选项区域也显示错误信息 + this.elements.optionsList.innerHTML = ` +
  • +
    + +
    +
    行动选项加载失败
    +
    请检查YAML格式是否正确
    +
    +
    +
  • + `; + } + } + + $(document).ready(function () { + /** + * 获取本楼消息 + * @returns {Object|null} 包含本楼消息信息的对象,失败时返回null + */ + function getCurrentMessage() { + try { + if (typeof getCurrentMessageId !== 'function' || typeof getChatMessages !== 'function') { + return null; + } + + const currentMessageId = getCurrentMessageId(); + if (!currentMessageId && currentMessageId !== 0) return null; + + const messageData = getChatMessages(currentMessageId); + if (!messageData) return null; + + return Array.isArray(messageData) && messageData.length > 0 ? messageData[0] : messageData; + } catch (error) { + console.error('获取消息失败:', error); + return null; + } + } + + function extractMaintext(message) { + if (!message || typeof message !== 'string') return ''; + const match = message.match(/([\s\S]*?)<\/maintext>/i); + return match ? match[1].trim() : ''; + } + + /** + * 从消息中提取Status_block内容 + * @param {string} message 消息文本 + * @returns {string} 提取的YAML状态内容 + */ + function extractStatusBlock(message) { + if (!message || typeof message !== 'string') return ''; + + const match = message.match(/\s*([\s\S]*?)\s*<\/Status_block>/i); + return match ? cleanYamlContent(match[1].trim()) : ''; + } + + /** + * 清理YAML内容,修复常见的格式问题 + * @param {string} yamlContent 原始YAML内容 + * @returns {string} 清理后的YAML内容 + */ + function cleanYamlContent(yamlContent) { + if (!yamlContent) return ''; + + return yamlContent + .split('\n') + .map(line => { + if (line.trim() === '' || !line.trim().match(/^-\s*"/)) return line; + + const match = line.match(/^(\s*-\s*)"(.*)"\s*$/); + if (!match) return line; + + const [, indent, content] = match; + return content.includes('"') || content.includes("'") + ? indent + "'" + content.replace(/'/g, "''") + "'" + : indent + '"' + content + '"'; + }) + .join('\n'); + } + + /** + * 更新YAML数据源 + * @param {string} yamlContent YAML格式的状态内容 + */ + function updateYamlDataSource(yamlContent) { + const yamlScript = document.getElementById('yaml-data-source'); + if (!yamlScript) return; + + // 如果内容为空或无效,设置为加载中状态 + if (!yamlContent || typeof yamlContent !== 'string' || !yamlContent.trim()) { + yamlScript.textContent = ''; // 设置为空,让后续处理显示加载状态 + return; + } + + // 先设置内容,让StoryRenderer能处理格式错误 + yamlScript.textContent = yamlContent; + + // 验证YAML格式,如果有错误会被StoryRenderer捕获并处理 + try { + jsyaml.load(yamlContent); + } catch (error) { + // 尝试修复常见的YAML错误 + const fixedYaml = attemptYamlFix(yamlContent, error); + if (fixedYaml) { + try { + jsyaml.load(fixedYaml); + yamlScript.textContent = fixedYaml; + } catch (e) { + console.error('YAML修复失败:', e.message); + // 修复失败时保留原内容,让StoryRenderer显示具体错误 + } + } + // 如果无法修复,保留原内容,让StoryRenderer显示具体错误 + } + } + + /** + * 尝试修复常见的YAML错误 + * @param {string} yamlContent 有问题的YAML内容 + * @param {Error} error YAML解析错误 + * @returns {string|null} 修复后的YAML或null + */ + function attemptYamlFix(yamlContent, error) { + if (!(error.message.includes('bad indentation') || error.message.includes('quote'))) { + return null; + } + + return yamlContent + .split('\n') + .map(line => { + const match = line.match(/^(\s*-\s*)"(.*)"\s*$/); + if (!match) return line; + + const [, indent, content] = match; + return content.includes('"') + ? indent + "'" + content.replace(/'/g, "''") + "'" + : indent + '"' + content + '"'; + }) + .join('\n'); + } + + /** + * 更新maintext内容 + * @param {string} maintextContent maintext内容 + */ + function updateMaintext(maintextContent) { + try { + const maintextElement = document.getElementById('maintext'); + if (!maintextElement) return; + + // 如果内容为空或无效,设置为加载中状态 + if (!maintextContent || typeof maintextContent !== 'string' || !maintextContent.trim()) { + maintextElement.textContent = ''; + } else { + maintextElement.textContent = maintextContent; + } + + formatMainText(); + } catch (error) { + console.error('更新maintext失败:', error); + // 如果更新失败,直接调用formatMainText,它会处理错误 + formatMainText(); + } + } + + /** + * 重新渲染状态栏 + */ + function reRenderStatusBar() { + try { + const yamlScript = document.getElementById('yaml-data-source'); + if (!yamlScript || !yamlScript.textContent) return; + + const storyRenderer = new StoryRenderer('yaml-data-source'); + storyRenderer.init(); + } catch (error) { + console.error('重新渲染状态栏失败:', error); + // 状态栏渲染失败时,错误处理由StoryRenderer.handleError处理 + // 这里不需要额外处理,因为StoryRenderer的init方法已经有handleError调用 + } + } + + /** + * 根据消息数据渲染整个页面 + * @param {Object} messageData 消息数据对象(格式参考test.json) + */ + function renderPageFromMessage(messageData) { + let actualMessageData = Array.isArray(messageData) && messageData.length > 0 ? messageData[0] : messageData; + + if (!actualMessageData || !actualMessageData.message || typeof actualMessageData.message !== 'string') { + return; + } + + const messageContent = actualMessageData.message; + + // 提取并更新maintext内容 + const maintextContent = extractMaintext(messageContent); + if (maintextContent) { + updateMaintext(maintextContent); + } + + // 提取并更新Status_block内容 + const statusContent = extractStatusBlock(messageContent); + if (statusContent) { + updateYamlDataSource(statusContent); + setTimeout(() => reRenderStatusBar(), 100); + } + } + + // 执行获取操作并处理结果 + try { + const currentMessage = getCurrentMessage(); + if (currentMessage && typeof currentMessage === 'object') { + renderPageFromMessage(currentMessage); + } + } catch (error) { + console.error('获取或渲染消息时出错:', error); + } + + window.statusBlockInitialized = true; + }); + + + + + +pm + \ No newline at end of file diff --git a/server/service/app/conversation.go b/server/service/app/conversation.go index 3b30b46..e16e302 100644 --- a/server/service/app/conversation.go +++ b/server/service/app/conversation.go @@ -448,41 +448,12 @@ func (s *ConversationService) SendMessage(userID, conversationID uint, req *requ global.GVA_LOG.Info(fmt.Sprintf("替换了 {{getvar::}} 变量")) } - // 提取 ,保护它们不被正则脚本修改 - statusBlock, contentWithoutStatus := regexService.ExtractStatusBlock(displayContent) - maintext, contentWithoutMaintext := regexService.ExtractMaintext(contentWithoutStatus) - - global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 提取到 Status_block 长度: %d, maintext 长度: %d", len(statusBlock), len(maintext))) - if len(statusBlock) > 0 { - previewLen := len(statusBlock) - if previewLen > 100 { - previewLen = 100 - } - global.GVA_LOG.Info(fmt.Sprintf("[状态栏] Status_block 内容预览: %s", statusBlock[:previewLen])) - } - - // 应用显示阶段的正则脚本 (Placement 3) - 只处理剩余内容 - finalProcessedContent := contentWithoutMaintext - displayScripts, err2 := regexService.GetScriptsForPlacement(userID, 3, &conversation.CharacterID, nil) - if err2 == nil && len(displayScripts) > 0 { - global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 应用正则脚本前的内容长度: %d", len(finalProcessedContent))) - finalProcessedContent = regexService.ExecuteScripts(displayScripts, finalProcessedContent, userName, character.Name) - global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 应用了 %d 个显示阶段正则脚本,处理后内容长度: %d", len(displayScripts), len(finalProcessedContent))) - } - - // 重新组装内容:maintext + Status_block + 处理后的内容 - finalContent := finalProcessedContent - if maintext != "" { - finalContent = "" + maintext + "\n\n" + finalContent - } - if statusBlock != "" { - finalContent = finalContent + "\n\n\n" + statusBlock + "\n" - } - - global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 最终返回内容长度: %d", len(finalContent))) + // 注意:此时 displayContent 中的 已经被 Placement 1 正则脚本 + // 替换成了包含 YAML 数据的 HTML 模板,所以不需要再提取和保护 + // 直接返回给前端即可 resp := response.ToMessageResponse(&assistantMessage) - resp.Content = finalContent // 使用处理后的显示内容 + resp.Content = displayContent // 使用处理后的显示内容 return &resp, nil } @@ -632,10 +603,32 @@ func (s *ConversationService) callAIService(conversation app.Conversation, chara } global.GVA_LOG.Info(fmt.Sprintf("========== AI返回的完整内容 ==========\n%s\n==========================================", aiResponse)) - // 【重要】不再应用 Placement 1 正则脚本,保留 AI 原始回复 - // 让 SendMessage 函数来提取和保护 - // 前端会负责渲染这些标签 - global.GVA_LOG.Info("[AI回复] 保留原始内容,不应用输出阶段正则脚本") + // 应用输出阶段的正则脚本 (Placement 1) + // 这里会把 替换成 HTML 模板,并注入 YAML 数据 + var regexService RegexScriptService + global.GVA_LOG.Info(fmt.Sprintf("查询输出阶段正则脚本: userID=%d, placement=1, charID=%d", conversation.UserID, conversation.CharacterID)) + outputScripts, err := regexService.GetScriptsForPlacement(conversation.UserID, 1, &conversation.CharacterID, nil) + if err != nil { + global.GVA_LOG.Error(fmt.Sprintf("查询输出阶段正则脚本失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("找到 %d 个输出阶段正则脚本", len(outputScripts))) + if len(outputScripts) > 0 { + // 获取用户信息 + var user app.AppUser + err = global.GVA_DB.Where("id = ?", conversation.UserID).First(&user).Error + userName := "" + if err == nil { + userName = user.Username + if userName == "" { + userName = user.NickName + } + } + + originalResponse := aiResponse + aiResponse = regexService.ExecuteScripts(outputScripts, aiResponse, userName, character.Name) + global.GVA_LOG.Info(fmt.Sprintf("应用了 %d 个输出阶段正则脚本,原始长度: %d, 处理后长度: %d", len(outputScripts), len(originalResponse), len(aiResponse))) + } + } return aiResponse, nil } diff --git a/server/service/app/regex_script.go b/server/service/app/regex_script.go index c20d615..f149d04 100644 --- a/server/service/app/regex_script.go +++ b/server/service/app/regex_script.go @@ -222,7 +222,13 @@ func (s *RegexScriptService) ExecuteScript(script *app.RegexScript, text string, global.GVA_LOG.Warn("正则表达式编译失败", zap.String("pattern", script.FindRegex), zap.Error(err)) return text, err } - result = re.ReplaceAllString(result, script.ReplaceWith) + + // 特殊处理:如果正则匹配 ,需要提取 YAML 并注入到 HTML 模板中 + if strings.Contains(script.FindRegex, "Status_block") { + result = s.replaceStatusBlockWithHTML(result, script.ReplaceWith, re) + } else { + result = re.ReplaceAllString(result, script.ReplaceWith) + } } // 3. 修剪字符串 @@ -237,6 +243,35 @@ func (s *RegexScriptService) ExecuteScript(script *app.RegexScript, text string, return result, nil } +// replaceStatusBlockWithHTML 替换 为 HTML 模板,并注入 YAML 数据 +func (s *RegexScriptService) replaceStatusBlockWithHTML(text string, htmlTemplate string, statusBlockRegex *regexp.Regexp) string { + return statusBlockRegex.ReplaceAllStringFunc(text, func(match string) string { + // 提取 YAML 数据 + yamlRegex := regexp.MustCompile(`\s*([\s\S]*?)\s*`) + yamlMatches := yamlRegex.FindStringSubmatch(match) + + if len(yamlMatches) < 2 { + global.GVA_LOG.Warn("无法提取 Status_block 中的 YAML 数据") + return match + } + + yamlData := strings.TrimSpace(yamlMatches[1]) + + // 在 HTML 模板中查找 + // 并将 YAML 数据注入其中 + injectedHTML := strings.Replace( + htmlTemplate, + ``, + fmt.Sprintf(``, yamlData), + 1, + ) + + global.GVA_LOG.Info(fmt.Sprintf("[正则脚本] 已将 Status_block YAML 数据注入到 HTML 模板,YAML 长度: %d", len(yamlData))) + + return injectedHTML + }) +} + // substituteMacros 替换宏变量 func (s *RegexScriptService) substituteMacros(text string, userName string, charName string) string { result := text diff --git a/web-app/src/components/MessageContent.tsx b/web-app/src/components/MessageContent.tsx index dc40eee..f4ce75b 100644 --- a/web-app/src/components/MessageContent.tsx +++ b/web-app/src/components/MessageContent.tsx @@ -508,8 +508,10 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag `) - // 2. 创建 YAML script 标签并直接设置 textContent(避免 HTML 转义) - doc.write('