chore: update live chat and work server changes
This commit is contained in:
@@ -24,20 +24,16 @@ const CHAT_NOTIFY_OFFLINE_STORAGE_PREFIX = 'main-chat-panel:notify-offline:';
|
||||
const CHAT_SESSION_LAST_TYPE_STORAGE_PREFIX = 'main-chat-panel:last-chat-type:';
|
||||
const CHAT_INTRO_MESSAGE = '요청은 기본적으로 순차 처리됩니다. 급한 요청만 즉시 실행을 사용하세요.';
|
||||
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
|
||||
const CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
|
||||
const KST_TIME_ZONE = 'Asia/Seoul';
|
||||
const CHAT_CONVERSATION_LIST_CACHE_WINDOW_MS = 1500;
|
||||
const chatSessionLastTypeMemory = new Map<string, string>();
|
||||
const chatLastEventIdMemory = new Map<string, number>();
|
||||
const chatOfflineNotificationMemory = new Map<string, boolean>();
|
||||
let chatClientSessionIdMemory = '';
|
||||
let localMessageSequence = 0;
|
||||
let cachedChatConversationList: ChatConversationSummary[] | null = null;
|
||||
let cachedChatConversationListAt = 0;
|
||||
let chatConversationListRequestPromise: Promise<ChatConversationSummary[]> | null = null;
|
||||
|
||||
export function invalidateChatConversationListCache() {
|
||||
cachedChatConversationList = null;
|
||||
cachedChatConversationListAt = 0;
|
||||
chatConversationListRequestPromise = null;
|
||||
}
|
||||
|
||||
@@ -817,6 +813,16 @@ async function requestChatApi<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
if (method === 'GET') {
|
||||
if (!headers.has('Cache-Control')) {
|
||||
headers.set('Cache-Control', 'no-store, no-cache, max-age=0');
|
||||
}
|
||||
|
||||
if (!headers.has('Pragma')) {
|
||||
headers.set('Pragma', 'no-cache');
|
||||
}
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
@@ -894,16 +900,35 @@ async function readFileAsBase64(file: File) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchChatConversations() {
|
||||
const now = Date.now();
|
||||
const FALLBACK_UPLOAD_MIME_BY_EXTENSION: Record<string, string> = {
|
||||
zip: 'application/zip',
|
||||
heic: 'image/heic',
|
||||
heif: 'image/heif',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp',
|
||||
gif: 'image/gif',
|
||||
pdf: 'application/pdf',
|
||||
};
|
||||
|
||||
if (
|
||||
cachedChatConversationList &&
|
||||
now - cachedChatConversationListAt < CHAT_CONVERSATION_LIST_CACHE_WINDOW_MS
|
||||
) {
|
||||
return cachedChatConversationList;
|
||||
function resolveUploadMimeType(file: File) {
|
||||
const normalizedName = String(file.name ?? '').trim().toLowerCase();
|
||||
const extension = normalizedName.includes('.') ? normalizedName.split('.').pop()?.trim() ?? '' : '';
|
||||
const normalizedType = String(file.type ?? '').trim().toLowerCase();
|
||||
|
||||
if (normalizedType && normalizedType !== 'application/octet-stream') {
|
||||
return normalizedType;
|
||||
}
|
||||
|
||||
if (extension && FALLBACK_UPLOAD_MIME_BY_EXTENSION[extension]) {
|
||||
return FALLBACK_UPLOAD_MIME_BY_EXTENSION[extension];
|
||||
}
|
||||
|
||||
return normalizedType || 'application/octet-stream';
|
||||
}
|
||||
|
||||
export async function fetchChatConversations() {
|
||||
if (chatConversationListRequestPromise) {
|
||||
return chatConversationListRequestPromise;
|
||||
}
|
||||
@@ -911,16 +936,12 @@ export async function fetchChatConversations() {
|
||||
const clientId = getOrCreateClientId();
|
||||
chatConversationListRequestPromise = requestChatApi<{ ok: boolean; items: ChatConversationSummary[] }>('/conversations')
|
||||
.then((response) => {
|
||||
const items = sortChatConversationSummaries(
|
||||
return sortChatConversationSummaries(
|
||||
response.items.map((item) => ({
|
||||
...item,
|
||||
notifyOffline: resolveSyncedChatOfflineNotificationSetting(item.sessionId, item.notifyOffline, clientId),
|
||||
})),
|
||||
);
|
||||
|
||||
cachedChatConversationList = items;
|
||||
cachedChatConversationListAt = Date.now();
|
||||
return items;
|
||||
})
|
||||
.finally(() => {
|
||||
chatConversationListRequestPromise = null;
|
||||
@@ -1026,23 +1047,75 @@ export async function rollbackChatRuntimeJob(requestId: string, sessionId?: stri
|
||||
|
||||
export async function uploadChatComposerFile(sessionId: string, file: File) {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const resolvedMimeType = resolveUploadMimeType(file);
|
||||
const reportUploadFailure = async (stage: string, error: Error) => {
|
||||
await reportClientError({
|
||||
errorType: 'chat:composer-upload',
|
||||
errorName: error.name,
|
||||
errorMessage: error.message,
|
||||
requestMethod: 'POST',
|
||||
requestPath: '/api/chat/attachments',
|
||||
context: {
|
||||
stage,
|
||||
sessionId: normalizedSessionId || null,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: file.type || null,
|
||||
resolvedMimeType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!normalizedSessionId) {
|
||||
throw new Error('채팅 세션이 준비되지 않았습니다.');
|
||||
const uploadError = new Error('채팅 세션이 준비되지 않았습니다.');
|
||||
await reportUploadFailure('validate-session', uploadError);
|
||||
throw uploadError;
|
||||
}
|
||||
|
||||
const contentBase64 = await readFileAsBase64(file);
|
||||
const response = await requestChatApi<{ ok: boolean; item: ChatComposerAttachment }>('/attachments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionId: normalizedSessionId,
|
||||
fileName: file.name,
|
||||
mimeType: file.type,
|
||||
contentBase64,
|
||||
}),
|
||||
});
|
||||
if (file.size <= 0) {
|
||||
const uploadError = new Error('업로드할 파일 내용을 찾지 못했습니다.');
|
||||
await reportUploadFailure('validate-file', uploadError);
|
||||
throw uploadError;
|
||||
}
|
||||
|
||||
return response.item;
|
||||
if (file.size > CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT) {
|
||||
const uploadError = new Error(`첨부 파일은 10MB 이하만 업로드할 수 있습니다. (${file.name})`);
|
||||
await reportUploadFailure('validate-file', uploadError);
|
||||
throw uploadError;
|
||||
}
|
||||
|
||||
let contentBase64 = '';
|
||||
|
||||
try {
|
||||
contentBase64 = await readFileAsBase64(file);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error && error.message.trim() ? error.message.trim() : '파일 내용을 읽지 못했습니다.';
|
||||
const uploadError = new Error(`${message} (${file.name})`);
|
||||
uploadError.name = error instanceof Error && error.name ? error.name : 'FileReadError';
|
||||
await reportUploadFailure('read-file', uploadError);
|
||||
throw uploadError;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await requestChatApi<{ ok: boolean; item: ChatComposerAttachment }>('/attachments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionId: normalizedSessionId,
|
||||
fileName: file.name,
|
||||
mimeType: resolvedMimeType,
|
||||
contentBase64,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.item;
|
||||
} catch (error) {
|
||||
const uploadError =
|
||||
error instanceof Error && error.message.trim()
|
||||
? error
|
||||
: new Error(`${file.name} 업로드에 실패했습니다.`);
|
||||
await reportUploadFailure('upload-request', uploadError);
|
||||
throw uploadError;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createChatConversationRoom(args: {
|
||||
|
||||
Reference in New Issue
Block a user