Files
upage-git/app/.client/hooks/useProject.ts
史悦 177f15a136
Some checks failed
CI/CD / Test (push) Has been cancelled
Docker Publish / docker-build-and-push (push) Has been cancelled
避免 Remix revalidation:使用原生 fetch 而非 fetcher.submit,避免触发 Remix 的数据重新验证机制,这是导致流式请求被中止的主要原因。
增加延迟时间:将项目保存延迟增加到2秒,确保流式请求完全结束后再执行保存操作,减少时间窗口内的冲突。

改进错误处理:添加了更详细的错误处理和日志记录,便于后续调试。
2025-10-15 09:55:47 +08:00

136 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEditorStorage } from '~/.client/persistence/editor';
import { webBuilderStore } from '~/.client/stores/web-builder';
import type { ApiResponse } from '~/types/global';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('useGrapesProject');
export function useProject() {
const { saveEditorProject } = useEditorStorage();
/**
* 保存项目数据到后端数据库
*
* @param messageId 消息ID
* @param projectData GrapesJS项目数据
* @param sections 页面区块数据
* @returns 保存是否成功
*/
async function saveProject(messageId: string) {
if (!messageId) {
logger.error('保存项目失败: 消息ID不能为空');
return false;
}
// 保存之前,先保存所有页面
await webBuilderStore.saveAllPages('auto-save');
const projectPages = Object.values(webBuilderStore.pagesStore.pages.get()).filter((page) => page !== undefined);
const projectSections = Object.values(webBuilderStore.pagesStore.sections.get())
.filter((section) => section !== undefined)
.map((section) => ({
...section,
actionId: section.id,
}));
if (projectPages.length === 0 || projectSections.length === 0) {
logger.error('保存项目失败: 页面或 Section 不能为空');
return false;
}
const isConsistent = projectPages.every((page) => {
const actionIds = page.actionIds;
const content = page.content;
if (actionIds.length === 0) {
return true;
}
if (!content) {
return false;
}
return true;
});
if (!isConsistent) {
logger.error(
'保存项目失败: 页面内容与 actions 不一致',
JSON.stringify({
projectPages,
projectSections,
}),
);
return false;
}
try {
// 先保存在本地数据中
saveEditorProject(messageId, projectPages, projectSections);
// 再调用远程接口保存到后端数据库
// 使用原生 fetch 而非 Remix fetcher避免触发 revalidation 导致流式请求中断
const response = await fetch('/api/project', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messageId,
pages: projectPages,
sections: projectSections,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
logger.error('保存项目失败:', errorData.message || '服务器错误');
return false;
}
logger.info('项目保存成功');
return true;
} catch (error) {
logger.error('保存GrapesJS项目失败:', error);
return false;
}
}
/**
* 复制聊天及其相关内容消息、GrapesJS项目数据和区块
*
* @param chatId 要复制的聊天ID
* @param messageId 可选参数,当提供时只复制到该消息为止的消息(包含该消息);不提供时复制整个聊天
* @returns 成功时返回新聊天的ID失败时返回undefined
*/
async function forkChat(chatId: string, messageId?: string) {
if (!chatId) {
logger.error('复制聊天失败: 聊天ID不能为空');
return undefined;
}
try {
// 调用后端API复制聊天
const response = await fetch('/api/chat/fork', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sourceChatId: chatId,
messageId,
}),
});
const { data, success, message } = (await response.json()) as ApiResponse<string>;
if (!response.ok || !success) {
logger.error('复制聊天失败:', message);
return undefined;
}
logger.info(`成功复制聊天 ${chatId}新聊天ID: ${data}`);
return data;
} catch (error) {
logger.error('复制聊天过程中发生错误:', error);
return undefined;
}
}
return {
saveProject,
forkChat,
};
}