chore: test deploy snapshot

This commit is contained in:
2026-05-28 14:34:49 +09:00
parent 82c46f4be4
commit bb275c0534
14 changed files with 531 additions and 193 deletions

File diff suppressed because one or more lines are too long

View File

@@ -66,6 +66,7 @@ import { getTokenSettingById, type TokenSettingRecord } from '../services/token-
const CHAT_ATTACHMENT_FILE_SIZE_LIMIT = 300 * 1024 * 1024;
const CHAT_ATTACHMENT_ROUTE_BODY_LIMIT = 450 * 1024 * 1024;
const CHAT_PUBLIC_ROUTE_PREFIX = '/.codex_chat/';
const CHAT_LEGACY_PUBLIC_ROUTE_PREFIX = '/public/.codex_chat/';
const CHAT_API_RESOURCE_ROUTE_PREFIX = '/api/chat/resources';
const CHAT_SHARE_ROUTE_PREFIX = '/api/chat/shares';
const CHAT_SHARE_TOKEN_VERSION = 1;
@@ -1235,7 +1236,11 @@ async function buildChatShareSnapshot(
tokenPayload.kind === 'request-bundle' && normalizedSessionId.startsWith(MANAGED_CHAT_SHARE_SESSION_PREFIX);
const useInitialManagedShareRoomView = isManagedShareRoomSession && detailLevel === 'initial';
const detailPage = useInitialManagedShareRoomView
? await listChatConversationDetailPage(normalizedSessionId, { limit: 12 })
? await listChatConversationDetailPage(normalizedSessionId, {
limit: 12,
includeActivityLogs: false,
includePagination: false,
})
: null;
const [requests, messages] = detailPage
? [detailPage.requests, detailPage.messages]
@@ -1571,7 +1576,7 @@ async function ensureManagedShareAccessPin(
});
if (pinStatus.status === 'ok' || pinStatus.status === 'not-configured') {
return true;
return pinStatus;
}
if (pinStatus.status === 'required') {
@@ -1579,7 +1584,7 @@ async function ensureManagedShareAccessPin(
code: 'share_pin_required',
message: '이 공유 채팅방은 4자리 비밀번호 입력이 필요합니다.',
});
return false;
return null;
}
if (pinStatus.status === 'invalid') {
@@ -1587,17 +1592,17 @@ async function ensureManagedShareAccessPin(
code: 'share_pin_invalid',
message: '공유 채팅방 비밀번호가 올바르지 않습니다.',
});
return false;
return null;
}
if (pinStatus.status === 'missing') {
reply.code(404).send({
message: '공유 링크를 찾을 수 없습니다.',
});
return false;
return null;
}
return true;
return null;
}
function hasManagedSharePermission(
@@ -1641,6 +1646,11 @@ export async function registerChatRoutes(app: FastifyInstance) {
return serveChatPublicResource(resolveChatAttachmentRepoPath(), wildcard, reply);
});
app.get(`${CHAT_LEGACY_PUBLIC_ROUTE_PREFIX}*`, async (request, reply) => {
const wildcard = String((request.params as { '*': string | undefined })['*'] ?? '').trim();
return serveChatPublicResource(resolveChatAttachmentRepoPath(), wildcard, reply);
});
app.get(`${CHAT_API_RESOURCE_ROUTE_PREFIX}/*`, async (request, reply) => {
const wildcard = String((request.params as { '*': string | undefined })['*'] ?? '').trim();
return serveChatPublicResource(resolveChatAttachmentRepoPath(), wildcard, reply);
@@ -1668,7 +1678,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
const accessPinStatus = await ensureManagedShareAccessPin(request, reply, managedContext.sharePath);
if (!accessPinStatus) {
return;
}
@@ -1898,7 +1910,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
const accessPinStatus = await ensureManagedShareAccessPin(request, reply, managedContext.sharePath);
if (!accessPinStatus) {
return;
}
@@ -2033,7 +2047,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
const accessPinStatus = await ensureManagedShareAccessPin(request, reply, managedContext.sharePath);
if (!accessPinStatus) {
return;
}
@@ -2467,7 +2483,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
const accessPinStatus = await ensureManagedShareAccessPin(request, reply, managedContext.sharePath);
if (!accessPinStatus) {
return;
}
@@ -2497,15 +2515,19 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
const accessPinStatus = await validateSharedResourceAccessPinBySharePath(managedContext.sharePath, getRequestChatSharePin(request), {
clientId: getRequestClientId(request),
});
if (managedContext.managedResource) {
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
void recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
actorLabel: 'share-viewer',
summary: '공유 채팅 링크를 열었습니다.',
detail: managedContext.managedResource.token.resourceLabel,
}).catch((error) => {
request.log.warn(
{
err: error,
managedResourceTokenId: managedContext.managedResource?.token.id,
},
'Failed to record shared chat view usage',
);
});
}

View File

@@ -0,0 +1,39 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { extractChatMessageParts, parseChatMessageParts } from './chat-message-parts.js';
test('extractChatMessageParts normalizes absolute legacy dot-codex prompt preview urls to api chat resource urls', () => {
const input = [
'문서 미리보기',
'[[prompt:{"title":"확인","options":[{"label":"legacy","value":"legacy","preview":{"type":"resource","url":"https://preview.sm-home.cloud/public/.codex_chat/chat-room/resource/source/chat-room-reference.md"}}]}]]',
].join('\n');
const parsed = extractChatMessageParts(input);
const prompt = parsed.parts.find((part): part is Extract<(typeof parsed.parts)[number], { type: 'prompt' }> => part.type === 'prompt');
assert.ok(prompt);
assert.equal(
prompt.options[0]?.preview?.url,
'/api/chat/resources/chat-room/resource/source/chat-room-reference.md',
);
});
test('parseChatMessageParts normalizes absolute legacy link card urls to api chat resource urls', () => {
const parsed = parseChatMessageParts([
{
type: 'link_card',
title: 'legacy resource',
url: 'https://preview.sm-home.cloud/.codex_chat/chat-room/resource/uploads/spec.png',
actionLabel: '열기',
},
]);
assert.deepEqual(parsed, [
{
type: 'link_card',
title: 'legacy resource',
url: '/api/chat/resources/chat-room/resource/uploads/spec.png',
actionLabel: '열기',
},
]);
});

View File

@@ -237,6 +237,14 @@ function normalizeUrl(value: string) {
if (pathname.startsWith(CHAT_API_RESOURCE_MARKER) || pathname.startsWith(RESOURCE_MANAGER_PREVIEW_MARKER)) {
return normalizePreviewPathHash(pathname);
}
if (pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_MARKER)) {
return `${CHAT_API_RESOURCE_MARKER}${pathname.slice(CHAT_PUBLIC_DOT_CODEX_MARKER.length)}`;
}
if (pathname.startsWith(CHAT_DOT_CODEX_MARKER)) {
return `${CHAT_API_RESOURCE_MARKER}${pathname.slice(CHAT_DOT_CODEX_MARKER.length)}`;
}
} catch {
// Fall through to handle relative and embedded resource paths below.
}

View File

@@ -3570,12 +3570,16 @@ export async function listChatConversationDetailPage(
options: {
limit?: number;
beforeMessageId?: number | null;
includeActivityLogs?: boolean;
includePagination?: boolean;
} = {},
): Promise<ChatConversationDetailPage> {
const normalizedSessionId = sessionId.trim();
await getChatConversation(normalizedSessionId, null);
const conversation = await db(CHAT_CONVERSATION_TABLE).where({ session_id: normalizedSessionId }).first();
const normalizedLimit = Math.max(1, Math.min(100, Math.round(options.limit ?? 10)));
const includeActivityLogs = options.includeActivityLogs !== false;
const includePagination = options.includePagination !== false;
const normalizedBeforeMessageId =
Number.isFinite(options.beforeMessageId) && (options.beforeMessageId ?? 0) > 0
? Math.trunc(options.beforeMessageId as number)
@@ -3629,21 +3633,26 @@ export async function listChatConversationDetailPage(
.orderBy('message_id', 'asc')
.orderBy('id', 'asc');
const messages = messageRows.map((row: Parameters<typeof mapMessageRow>[0]) => mapMessageRow(row));
const activityLogs = await listChatConversationActivityLogsByRequestIds(normalizedSessionId, requestIds);
const oldestLoadedMessageId =
requests.reduce<number | null>((oldestId, request) => {
const candidateIds = [request.userMessageId, request.responseMessageId].filter(
(value): value is number => typeof value === 'number' && Number.isInteger(value) && value > 0,
);
const activityLogs = includeActivityLogs
? await listChatConversationActivityLogsByRequestIds(normalizedSessionId, requestIds)
: [];
const oldestLoadedMessageId = includePagination
? (
requests.reduce<number | null>((oldestId, request) => {
const candidateIds = [request.userMessageId, request.responseMessageId].filter(
(value): value is number => typeof value === 'number' && Number.isInteger(value) && value > 0,
);
if (candidateIds.length === 0) {
return oldestId;
}
if (candidateIds.length === 0) {
return oldestId;
}
const nextCandidateId = Math.min(...candidateIds);
return oldestId == null ? nextCandidateId : Math.min(oldestId, nextCandidateId);
}, null) ?? messages[0]?.id ?? null;
const oldestRequest = requests[0] ?? null;
const nextCandidateId = Math.min(...candidateIds);
return oldestId == null ? nextCandidateId : Math.min(oldestId, nextCandidateId);
}, null) ?? messages[0]?.id ?? null
)
: null;
const oldestRequest = includePagination ? requests[0] ?? null : null;
const hasOlderMessages = oldestRequest
? Boolean(
await db(CHAT_CONVERSATION_REQUEST_TABLE)