590
docs/html/story_renderer_extract.js
Normal file
590
docs/html/story_renderer_extract.js
Normal file
@@ -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 =
|
||||
'<li class="text-gray-400"><div class="flex items-center"><div class="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-primary mr-2"></div>加载选项中...</div></li>';
|
||||
}
|
||||
|
||||
// 查找根节点
|
||||
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 = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span>`;
|
||||
|
||||
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 = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span>`;
|
||||
|
||||
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 = `<span class="font-medium text-primary">${this.formatNodeName(key)}:</span> ${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 `<div class="text-center py-4 text-gray-500 theme-transition">
|
||||
<i class="fa fa-info-circle mr-1"></i>${message}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
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 = `
|
||||
<div class="bg-red-900/20 border border-red-800/30 text-red-400 px-4 py-3 rounded relative" role="alert">
|
||||
<strong class="font-bold">状态栏渲染失败: </strong>
|
||||
<span class="block sm:inline">${error.message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 在行动选项区域也显示错误信息
|
||||
this.elements.optionsList.innerHTML = `
|
||||
<li class="text-red-400 bg-red-900/20 border border-red-800/30 rounded p-3 theme-transition">
|
||||
<div class="flex items-start">
|
||||
<i class="fa fa-exclamation-triangle mr-2 mt-1"></i>
|
||||
<div>
|
||||
<div class="font-semibold mb-1">行动选项加载失败</div>
|
||||
<div class="text-sm text-red-300">请检查YAML格式是否正确</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
$(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(/<maintext>([\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(/<Status_block>\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;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
pm
|
||||
</body></html>
|
||||
@@ -448,41 +448,12 @@ func (s *ConversationService) SendMessage(userID, conversationID uint, req *requ
|
||||
global.GVA_LOG.Info(fmt.Sprintf("替换了 {{getvar::}} 变量"))
|
||||
}
|
||||
|
||||
// 提取 <Status_block> 和 <maintext>,保护它们不被正则脚本修改
|
||||
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>" + maintext + "</maintext>\n\n" + finalContent
|
||||
}
|
||||
if statusBlock != "" {
|
||||
finalContent = finalContent + "\n\n<Status_block>\n" + statusBlock + "\n</Status_block>"
|
||||
}
|
||||
|
||||
global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 最终返回内容长度: %d", len(finalContent)))
|
||||
// 注意:此时 displayContent 中的 <Status_block> 已经被 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 函数来提取和保护 <Status_block> 和 <maintext>
|
||||
// 前端会负责渲染这些标签
|
||||
global.GVA_LOG.Info("[AI回复] 保留原始内容,不应用输出阶段正则脚本")
|
||||
// 应用输出阶段的正则脚本 (Placement 1)
|
||||
// 这里会把 <Status_block> 替换成 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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 特殊处理:如果正则匹配 <Status_block>,需要提取 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 替换 <Status_block> 为 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(`<Status_block>\s*([\s\S]*?)\s*</Status_block>`)
|
||||
yamlMatches := yamlRegex.FindStringSubmatch(match)
|
||||
|
||||
if len(yamlMatches) < 2 {
|
||||
global.GVA_LOG.Warn("无法提取 Status_block 中的 YAML 数据")
|
||||
return match
|
||||
}
|
||||
|
||||
yamlData := strings.TrimSpace(yamlMatches[1])
|
||||
|
||||
// 在 HTML 模板中查找 <script id="yaml-data-source" type="text/yaml"></script>
|
||||
// 并将 YAML 数据注入其中
|
||||
injectedHTML := strings.Replace(
|
||||
htmlTemplate,
|
||||
`<script id="yaml-data-source" type="text/yaml"></script>`,
|
||||
fmt.Sprintf(`<script id="yaml-data-source" type="text/yaml">%s</script>`, 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
|
||||
|
||||
@@ -508,8 +508,10 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
||||
<body>
|
||||
`)
|
||||
|
||||
// 2. 创建 YAML script 标签并直接设置 textContent(避免 HTML 转义)
|
||||
doc.write('<script id="yaml-data-source" type="text/yaml"><\/script>')
|
||||
// 2. 写入 YAML 数据(使用 textContent 方式,避免 HTML 转义)
|
||||
// 注意:我们需要在 doc.close() 之前写入所有内容
|
||||
const yamlScriptContent = `<script id="yaml-data-source" type="text/yaml">${statusYaml.replace(/</g, '\\x3C').replace(/>/g, '\\x3E')}<\/script>`
|
||||
doc.write(yamlScriptContent)
|
||||
|
||||
// 3. 写入渲染脚本
|
||||
doc.write(`
|
||||
@@ -624,17 +626,9 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
||||
|
||||
doc.close()
|
||||
|
||||
// 4. 关键步骤:直接设置 YAML 数据到 script 标签的 textContent
|
||||
// 这样可以避免 HTML 转义问题
|
||||
const yamlScript = doc.getElementById('yaml-data-source')
|
||||
if (yamlScript) {
|
||||
yamlScript.textContent = statusYaml
|
||||
console.log('[MessageContent] YAML 数据已注入,长度:', statusYaml.length)
|
||||
} else {
|
||||
console.error('[MessageContent] 找不到 yaml-data-source 元素')
|
||||
}
|
||||
console.log('[MessageContent] YAML 数据已注入,长度:', statusYaml.length)
|
||||
|
||||
// 5. 自动调整 iframe 高度
|
||||
// 4. 自动调整 iframe 高度
|
||||
setTimeout(() => {
|
||||
if (doc.body) {
|
||||
const height = doc.body.scrollHeight
|
||||
|
||||
Reference in New Issue
Block a user