chore: test deploy snapshot
This commit is contained in:
@@ -14,6 +14,7 @@ import type {
|
||||
ChatPromptContextRef,
|
||||
ChatConversationRequest,
|
||||
ChatConversationSummary,
|
||||
ChatShareRoomLinkContext,
|
||||
ChatSourceChangeSnapshot,
|
||||
ChatSourceChangeSnapshotListResponse,
|
||||
ChatJobEvent,
|
||||
@@ -35,7 +36,7 @@ const CHAT_INTRO_MESSAGE =
|
||||
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
|
||||
const CHAT_MISSING_REQUEST_MESSAGE_PREFIX = '[[missing-request]]';
|
||||
const CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX = '[[execution-failure]]';
|
||||
const CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
|
||||
const CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT = 300 * 1024 * 1024;
|
||||
const KST_TIME_ZONE = 'Asia/Seoul';
|
||||
const chatSessionLastTypeMemory = new Map<string, string>();
|
||||
const chatLastEventIdMemory = new Map<string, number>();
|
||||
@@ -1691,8 +1692,20 @@ async function requestChatApi<T>(
|
||||
window.clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
||||
const text = await response.text();
|
||||
|
||||
if (response.status === 413) {
|
||||
throw new ChatApiError(
|
||||
'첨부 업로드 크기가 현재 허용 한도를 초과했습니다. 300MB 이하 파일로 다시 시도해 주세요.',
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
|
||||
if (contentType.includes('text/html') && text.trim().startsWith('<')) {
|
||||
throw new ChatApiError('채팅 API가 HTML 오류 페이지를 반환했습니다. 프록시 업로드 한도를 확인해 주세요.', response.status);
|
||||
}
|
||||
|
||||
if (text.trim()) {
|
||||
try {
|
||||
const payload = JSON.parse(text) as { message?: string; code?: string };
|
||||
@@ -1728,26 +1741,8 @@ async function requestChatApi<T>(
|
||||
}
|
||||
}
|
||||
|
||||
async function readFileAsBase64(file: File) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result !== 'string') {
|
||||
reject(new Error('파일 내용을 읽지 못했습니다.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const commaIndex = reader.result.indexOf(',');
|
||||
resolve(commaIndex >= 0 ? reader.result.slice(commaIndex + 1) : reader.result);
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(reader.error ?? new Error('파일 내용을 읽지 못했습니다.'));
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
function encodeChatAttachmentHeaderValue(value: string) {
|
||||
return encodeURIComponent(value);
|
||||
}
|
||||
|
||||
const FALLBACK_UPLOAD_MIME_BY_EXTENSION: Record<string, string> = {
|
||||
@@ -2036,6 +2031,38 @@ function normalizeChatSourceChangeSnapshot(item: ChatSourceChangeSnapshot): Chat
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadChatAttachmentBinary(
|
||||
path: string,
|
||||
file: File,
|
||||
args: {
|
||||
sessionId: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
allowUnauthenticated?: boolean;
|
||||
sharePin?: string | null;
|
||||
},
|
||||
) {
|
||||
const response = await requestChatApi<{ ok: boolean; item: ChatComposerAttachment }>(
|
||||
path,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Chat-Attachment-Session-Id': encodeChatAttachmentHeaderValue(args.sessionId),
|
||||
'X-Chat-Attachment-File-Name': encodeChatAttachmentHeaderValue(args.fileName),
|
||||
'X-Chat-Attachment-Mime-Type': encodeChatAttachmentHeaderValue(args.mimeType),
|
||||
},
|
||||
body: file,
|
||||
},
|
||||
{
|
||||
allowUnauthenticated: args.allowUnauthenticated,
|
||||
sharePin: args.sharePin,
|
||||
},
|
||||
);
|
||||
|
||||
return response.item;
|
||||
}
|
||||
|
||||
export async function uploadChatComposerFile(sessionId: string, file: File) {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const resolvedMimeType = resolveUploadMimeType(file);
|
||||
@@ -2071,35 +2098,17 @@ export async function uploadChatComposerFile(sessionId: string, file: File) {
|
||||
}
|
||||
|
||||
if (file.size > CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT) {
|
||||
const uploadError = new Error(`첨부 파일은 10MB 이하만 업로드할 수 있습니다. (${resolvedFileName})`);
|
||||
const uploadError = new Error(`첨부 파일은 300MB 이하만 업로드할 수 있습니다. (${resolvedFileName})`);
|
||||
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} (${resolvedFileName})`);
|
||||
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: resolvedFileName,
|
||||
mimeType: resolvedMimeType,
|
||||
contentBase64,
|
||||
}),
|
||||
return await uploadChatAttachmentBinary('/attachments', file, {
|
||||
sessionId: normalizedSessionId,
|
||||
fileName: resolvedFileName,
|
||||
mimeType: resolvedMimeType,
|
||||
});
|
||||
|
||||
return response.item;
|
||||
} catch (error) {
|
||||
const uploadError =
|
||||
error instanceof Error && error.message.trim()
|
||||
@@ -2153,41 +2162,22 @@ export async function uploadChatShareComposerFile(token: string, sessionId: stri
|
||||
}
|
||||
|
||||
if (file.size > CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT) {
|
||||
const uploadError = new Error(`첨부 파일은 10MB 이하만 업로드할 수 있습니다. (${resolvedFileName})`);
|
||||
const uploadError = new Error(`첨부 파일은 300MB 이하만 업로드할 수 있습니다. (${resolvedFileName})`);
|
||||
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} (${resolvedFileName})`);
|
||||
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 }>(
|
||||
return await uploadChatAttachmentBinary(
|
||||
`/shares/${encodeURIComponent(normalizedToken)}/attachments`,
|
||||
file,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionId: normalizedSessionId,
|
||||
fileName: resolvedFileName,
|
||||
mimeType: resolvedMimeType,
|
||||
contentBase64,
|
||||
}),
|
||||
},
|
||||
{
|
||||
sessionId: normalizedSessionId,
|
||||
fileName: resolvedFileName,
|
||||
mimeType: resolvedMimeType,
|
||||
allowUnauthenticated: true,
|
||||
},
|
||||
);
|
||||
|
||||
return response.item;
|
||||
} catch (error) {
|
||||
const uploadError =
|
||||
error instanceof Error && error.message.trim()
|
||||
@@ -2341,6 +2331,11 @@ export async function createChatShareRoom(
|
||||
title: string;
|
||||
requestBadgeLabel?: string | null;
|
||||
seedMessage: string;
|
||||
linkedSessionId?: string | null;
|
||||
linkedRequestId?: string | null;
|
||||
linkedTitle?: string | null;
|
||||
linkedRequestPreview?: string | null;
|
||||
linkedChatTypeLabel?: string | null;
|
||||
},
|
||||
) {
|
||||
const response = await requestChatApi<{ ok: boolean; room: ChatShareRoomSummary }>(
|
||||
@@ -2366,6 +2361,18 @@ export async function createChatShareRoom(
|
||||
contextLabel: normalizeOptionalText(response.room.contextLabel),
|
||||
contextDescription: normalizeOptionalText(response.room.contextDescription),
|
||||
notifyOffline: response.room.notifyOffline === true,
|
||||
linkContext:
|
||||
response.room.linkContext?.kind === 'linked-session'
|
||||
? {
|
||||
kind: 'linked-session',
|
||||
sourceSessionId: normalizeRequiredText(response.room.linkContext.sourceSessionId),
|
||||
sourceRequestId: normalizeRequiredText(response.room.linkContext.sourceRequestId),
|
||||
sourceTitle: normalizeOptionalText(response.room.linkContext.sourceTitle),
|
||||
sourceRequestPreview: normalizeOptionalText(response.room.linkContext.sourceRequestPreview),
|
||||
sourceChatTypeLabel: normalizeOptionalText(response.room.linkContext.sourceChatTypeLabel),
|
||||
linkedAt: normalizeOptionalText(response.room.linkContext.linkedAt),
|
||||
}
|
||||
: null,
|
||||
createdAt: normalizeOptionalText(response.room.createdAt),
|
||||
updatedAt: normalizeOptionalText(response.room.updatedAt),
|
||||
} satisfies ChatShareRoomSummary;
|
||||
@@ -2516,11 +2523,13 @@ export type ChatShareRoomSummary = {
|
||||
contextLabel?: string | null;
|
||||
contextDescription?: string | null;
|
||||
notifyOffline?: boolean;
|
||||
linkContext?: ChatShareRoomLinkContext | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
};
|
||||
|
||||
export type ChatShareSnapshot = {
|
||||
detailLevel?: 'full' | 'initial';
|
||||
share: {
|
||||
kind: ChatShareKind;
|
||||
sessionId: string;
|
||||
@@ -2759,9 +2768,27 @@ export async function saveChatShareRoomSettings(
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchChatShareSnapshot(token: string, options?: { sharePin?: string | null; sessionId?: string | null }) {
|
||||
export async function fetchChatShareSnapshot(
|
||||
token: string,
|
||||
options?: {
|
||||
sharePin?: string | null;
|
||||
sessionId?: string | null;
|
||||
view?: 'full' | 'initial';
|
||||
},
|
||||
) {
|
||||
const query = new URLSearchParams();
|
||||
|
||||
if (options?.sessionId?.trim()) {
|
||||
query.set('sessionId', options.sessionId.trim());
|
||||
}
|
||||
|
||||
if (options?.view === 'initial') {
|
||||
query.set('view', 'initial');
|
||||
}
|
||||
|
||||
const response = await requestChatApi<{
|
||||
ok: boolean;
|
||||
detailLevel?: ChatShareSnapshot['detailLevel'];
|
||||
share: ChatShareSnapshot['share'];
|
||||
conversation: ChatShareSnapshot['conversation'];
|
||||
rootRequestId: string;
|
||||
@@ -2775,7 +2802,7 @@ export async function fetchChatShareSnapshot(token: string, options?: { sharePin
|
||||
promptTarget?: ChatShareSnapshot['promptTarget'];
|
||||
refreshedAt: string;
|
||||
}>(
|
||||
`/shares/${encodeURIComponent(token)}${options?.sessionId?.trim() ? `?sessionId=${encodeURIComponent(options.sessionId.trim())}` : ''}`,
|
||||
`/shares/${encodeURIComponent(token)}${query.size > 0 ? `?${query.toString()}` : ''}`,
|
||||
undefined,
|
||||
{
|
||||
allowUnauthenticated: true,
|
||||
@@ -2785,6 +2812,7 @@ export async function fetchChatShareSnapshot(token: string, options?: { sharePin
|
||||
);
|
||||
|
||||
return {
|
||||
detailLevel: response.detailLevel === 'initial' ? 'initial' : 'full',
|
||||
share: {
|
||||
...response.share,
|
||||
createdAt: normalizeOptionalText(response.share?.createdAt),
|
||||
@@ -2855,6 +2883,18 @@ export async function fetchChatShareSnapshot(token: string, options?: { sharePin
|
||||
contextLabel: normalizeOptionalText(item.contextLabel),
|
||||
contextDescription: normalizeOptionalText(item.contextDescription),
|
||||
notifyOffline: item.notifyOffline === true,
|
||||
linkContext:
|
||||
item.linkContext?.kind === 'linked-session'
|
||||
? {
|
||||
kind: 'linked-session',
|
||||
sourceSessionId: normalizeRequiredText(item.linkContext.sourceSessionId),
|
||||
sourceRequestId: normalizeRequiredText(item.linkContext.sourceRequestId),
|
||||
sourceTitle: normalizeOptionalText(item.linkContext.sourceTitle),
|
||||
sourceRequestPreview: normalizeOptionalText(item.linkContext.sourceRequestPreview),
|
||||
sourceChatTypeLabel: normalizeOptionalText(item.linkContext.sourceChatTypeLabel),
|
||||
linkedAt: normalizeOptionalText(item.linkContext.linkedAt),
|
||||
}
|
||||
: null,
|
||||
createdAt: normalizeOptionalText(item.createdAt),
|
||||
updatedAt: normalizeOptionalText(item.updatedAt),
|
||||
}))
|
||||
@@ -2897,6 +2937,34 @@ export async function submitChatShareMessage(
|
||||
);
|
||||
}
|
||||
|
||||
export async function submitChatShareOriginReply(
|
||||
token: string,
|
||||
payload: {
|
||||
sessionId?: string | null;
|
||||
sourceSessionId: string;
|
||||
sourceRequestId: string;
|
||||
text: string;
|
||||
mode?: 'queue' | 'direct';
|
||||
},
|
||||
) {
|
||||
return requestChatApi<{ ok: boolean; queuedRequestId: string }>(
|
||||
`/shares/${encodeURIComponent(token)}/origin-reply`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionId: payload.sessionId?.trim() || undefined,
|
||||
sourceSessionId: payload.sourceSessionId.trim(),
|
||||
sourceRequestId: payload.sourceRequestId.trim(),
|
||||
text: payload.text,
|
||||
mode: payload.mode === 'direct' ? 'direct' : 'queue',
|
||||
}),
|
||||
},
|
||||
{
|
||||
allowUnauthenticated: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function submitChatSharePrompt(
|
||||
token: string,
|
||||
payload: {
|
||||
|
||||
Reference in New Issue
Block a user