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 = `
+
+ 状态栏渲染失败:
+ ${error.message}
+
+ `;
+
+ // 在行动选项区域也显示错误信息
+ 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
+