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::}} 变量"))
|
global.GVA_LOG.Info(fmt.Sprintf("替换了 {{getvar::}} 变量"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取 <Status_block> 和 <maintext>,保护它们不被正则脚本修改
|
// 注意:此时 displayContent 中的 <Status_block> 已经被 Placement 1 正则脚本
|
||||||
statusBlock, contentWithoutStatus := regexService.ExtractStatusBlock(displayContent)
|
// 替换成了包含 YAML 数据的 HTML 模板,所以不需要再提取和保护
|
||||||
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)))
|
|
||||||
|
|
||||||
resp := response.ToMessageResponse(&assistantMessage)
|
resp := response.ToMessageResponse(&assistantMessage)
|
||||||
resp.Content = finalContent // 使用处理后的显示内容
|
resp.Content = displayContent // 使用处理后的显示内容
|
||||||
return &resp, nil
|
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))
|
global.GVA_LOG.Info(fmt.Sprintf("========== AI返回的完整内容 ==========\n%s\n==========================================", aiResponse))
|
||||||
|
|
||||||
// 【重要】不再应用 Placement 1 正则脚本,保留 AI 原始回复
|
// 应用输出阶段的正则脚本 (Placement 1)
|
||||||
// 让 SendMessage 函数来提取和保护 <Status_block> 和 <maintext>
|
// 这里会把 <Status_block> 替换成 HTML 模板,并注入 YAML 数据
|
||||||
// 前端会负责渲染这些标签
|
var regexService RegexScriptService
|
||||||
global.GVA_LOG.Info("[AI回复] 保留原始内容,不应用输出阶段正则脚本")
|
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
|
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))
|
global.GVA_LOG.Warn("正则表达式编译失败", zap.String("pattern", script.FindRegex), zap.Error(err))
|
||||||
return text, 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. 修剪字符串
|
// 3. 修剪字符串
|
||||||
@@ -237,6 +243,35 @@ func (s *RegexScriptService) ExecuteScript(script *app.RegexScript, text string,
|
|||||||
return result, nil
|
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 替换宏变量
|
// substituteMacros 替换宏变量
|
||||||
func (s *RegexScriptService) substituteMacros(text string, userName string, charName string) string {
|
func (s *RegexScriptService) substituteMacros(text string, userName string, charName string) string {
|
||||||
result := text
|
result := text
|
||||||
|
|||||||
@@ -508,8 +508,10 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
<body>
|
<body>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// 2. 创建 YAML script 标签并直接设置 textContent(避免 HTML 转义)
|
// 2. 写入 YAML 数据(使用 textContent 方式,避免 HTML 转义)
|
||||||
doc.write('<script id="yaml-data-source" type="text/yaml"><\/script>')
|
// 注意:我们需要在 doc.close() 之前写入所有内容
|
||||||
|
const yamlScriptContent = `<script id="yaml-data-source" type="text/yaml">${statusYaml.replace(/</g, '\\x3C').replace(/>/g, '\\x3E')}<\/script>`
|
||||||
|
doc.write(yamlScriptContent)
|
||||||
|
|
||||||
// 3. 写入渲染脚本
|
// 3. 写入渲染脚本
|
||||||
doc.write(`
|
doc.write(`
|
||||||
@@ -624,17 +626,9 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
|
|
||||||
doc.close()
|
doc.close()
|
||||||
|
|
||||||
// 4. 关键步骤:直接设置 YAML 数据到 script 标签的 textContent
|
console.log('[MessageContent] YAML 数据已注入,长度:', statusYaml.length)
|
||||||
// 这样可以避免 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 元素')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 自动调整 iframe 高度
|
// 4. 自动调整 iframe 高度
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (doc.body) {
|
if (doc.body) {
|
||||||
const height = doc.body.scrollHeight
|
const height = doc.body.scrollHeight
|
||||||
|
|||||||
Reference in New Issue
Block a user