chore: test deploy snapshot

This commit is contained in:
2026-05-28 12:45:36 +09:00
parent 983887dc05
commit 82c46f4be4
21 changed files with 4163 additions and 449 deletions

View File

@@ -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: {