chore: test deploy snapshot

This commit is contained in:
2026-05-27 16:35:12 +09:00
parent e8a628ac34
commit 10805d242e
7 changed files with 1032 additions and 72 deletions

View File

@@ -50,6 +50,7 @@ import {
hasPendingAttentionVerificationRequest,
} from '../services/chat-room-service.js';
import {
archiveChatShareTokenRoomMap,
ensureDefaultChatShareTokenRoomMap,
getChatShareTokenRoomMap,
resolveChatShareTokenRoomSessionIds,
@@ -1142,12 +1143,13 @@ async function buildChatShareSnapshot(
}
const childRequestIdsByParentRequestId = buildChildRequestIdsByParentRequestId(requests);
const isManagedShareRoomPlaceholder =
tokenPayload.kind === 'request-bundle' && normalizedSessionId.startsWith(MANAGED_CHAT_SHARE_SESSION_PREFIX) && !targetRequestFromStore;
const isManagedShareRoomSession =
tokenPayload.kind === 'request-bundle' && normalizedSessionId.startsWith(MANAGED_CHAT_SHARE_SESSION_PREFIX);
const isManagedShareRoomPlaceholder = isManagedShareRoomSession && !targetRequestFromStore;
const rootRequestId = isManagedShareRoomPlaceholder
? targetRequestId
: resolveShareRootRequestId(targetRequestId, requestMap, tokenPayload.kind);
const scopeRequestIds = isManagedShareRoomPlaceholder
const scopeRequestIds = isManagedShareRoomSession
? requests.map((request) => request.requestId.trim()).filter(Boolean)
: collectShareScopeRequestIds(rootRequestId, childRequestIdsByParentRequestId);
const scopeRequestIdSet = new Set(scopeRequestIds);
@@ -2524,6 +2526,101 @@ export async function registerChatRoutes(app: FastifyInstance) {
};
});
app.delete(`${CHAT_SHARE_ROUTE_PREFIX}/:token/rooms/:sessionId`, async (request, reply) => {
const params = z.object({
token: z.string().trim().min(1).max(16000),
sessionId: z.string().trim().min(1).max(120),
}).parse(request.params ?? {});
const managedContext = await resolveManagedChatShareContext(params.token);
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
if (!tokenPayload) {
return reply.code(404).send({
message: '공유 링크가 유효하지 않습니다.',
});
}
if (tokenPayload.kind === 'prompt') {
return reply.code(400).send({
message: 'prompt 공유 링크에서는 채팅방을 삭제할 수 없습니다.',
});
}
const unavailableMessage = resolveManagedShareUnavailableMessage(managedContext.managedResource);
if (unavailableMessage) {
return reply.code(403).send({
message: unavailableMessage,
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
return;
}
if (!managedContext.managedResource || managedContext.managedResource.token.resourceType !== 'chat-share') {
return reply.code(404).send({
message: '공유 채팅방 정보를 찾을 수 없습니다.',
});
}
if (!managedContext.managedResource.token.permissions.includes('manage')) {
return reply.code(403).send({
message: '이 공유 링크에는 채팅방을 삭제할 권한이 없습니다.',
});
}
if (!hasManagedShareAllowedApp(managedContext.managedResource, 'chat-room-settings')) {
return reply.code(403).send({
message: '이 공유 링크에는 채팅방 설정 앱 권한이 없습니다.',
});
}
const resolvedRoomContext = await resolveActiveManagedShareRoom({
managedResource: managedContext.managedResource,
tokenPayload,
requestedSessionId: params.sessionId,
});
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
return reply.code(403).send({
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
});
}
if (resolvedRoomContext.rooms.length <= 1) {
return reply.code(400).send({
message: '마지막 채팅방은 삭제할 수 없습니다.',
});
}
getActiveChatService()?.forgetSession(params.sessionId);
chatRuntimeService.clearSession(params.sessionId);
const deleted = await deleteChatConversation(params.sessionId);
const archived = await archiveChatShareTokenRoomMap(managedContext.managedResource.token.id, params.sessionId);
if (!deleted || !archived.archived) {
return reply.code(404).send({
message: '삭제할 채팅방을 찾을 수 없습니다.',
});
}
if (managedContext.managedResource) {
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
actorLabel: 'share-manager',
summary: '공유 채팅방을 삭제했습니다.',
detail: params.sessionId,
});
}
return {
ok: true,
deleted: true,
deletedSessionId: params.sessionId,
nextRoomSessionId: archived.nextDefaultRoom?.sessionId ?? null,
};
});
app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/messages`, async (request, reply) => {
const params = z.object({
token: z.string().trim().min(1).max(16000),

View File

@@ -453,6 +453,63 @@ test('buildAgenticCodexPrompt keeps the chat type label provided by the client c
assert.doesNotMatch(prompt, /- label: 코드 수정/);
});
test('buildAgenticCodexPrompt pins the explicitly referenced answer ahead of recent history', () => {
const prompt = buildAgenticCodexPrompt(
{
pageId: null,
pageTitle: 'Codex Live',
topMenu: 'chat',
focusedComponentId: null,
pageUrl: 'https://preview.sm-home.cloud/chat/live',
chatTypeLabel: '일반 요청',
chatTypeDescription: '일반 요청 설명',
},
'지금 무슨 답변을 참조했나요?',
'session-reference',
{
recentHistoryLines: [
'[user] 예전 질문',
'[codex] 다른 답변',
],
referencedRequest: {
sessionId: 'session-reference',
requestId: 'request-123',
requesterClientId: null,
chatTypeId: 'general-request',
chatTypeLabel: '일반 요청',
requestOrigin: 'composer',
sharedResourceTokenId: null,
parentRequestId: null,
promptContextRef: null,
status: 'completed',
statusMessage: '요청 처리 완료',
retryCount: 0,
userMessageId: 1,
userText: 'preview서버에서 test서버 채팅이 안열리는데 test서버 배포 해야돼?',
responseMessageId: 2,
responseText: '배포 누락이 아니라 iframe 차단입니다.',
usageSnapshot: null,
totalTokens: null,
hasResponse: true,
canDelete: false,
manualPromptCompletedAt: null,
manualVerificationCompletedAt: null,
createdAt: '2026-05-27T14:50:23.000Z',
updatedAt: '2026-05-27T14:51:00.000Z',
answeredAt: '2026-05-27T14:51:00.000Z',
terminalAt: '2026-05-27T14:51:00.000Z',
},
},
);
assert.match(prompt, /## 답변 참조/);
assert.match(prompt, /참조 requestId: request-123/);
assert.match(prompt, /참조 사용자 요청: preview서버에서 test서버 채팅이 안열리는데 test서버 배포 해야돼\?/);
assert.match(prompt, /참조 답변 본문: 배포 누락이 아니라 iframe 차단입니다\./);
assert.match(prompt, /다른 최근 답변을 임의로 섞지 마세요\./);
assert.ok(prompt.indexOf('## 답변 참조') < prompt.indexOf('최근 대화 문맥(보조 참조)'));
});
test('resolveCodexParticipantsForExecution expands moderator into opening and closing turns', () => {
const participants = resolveCodexParticipantsForExecution({
pageId: null,

View File

@@ -2804,6 +2804,30 @@ function buildChatSessionReferenceInstructionBlock(referenceContent?: string | n
];
}
function buildReferencedRequestInstructionBlock(referencedRequest?: ChatConversationRequestItem | null) {
if (!referencedRequest) {
return [];
}
const requestId = referencedRequest.requestId.trim();
const referencedQuestion = normalizeChatPromptHistoryText(referencedRequest.userText || '');
const referencedAnswer = normalizeChatPromptHistoryText(
referencedRequest.responseText || referencedRequest.statusMessage || '',
);
return [
'## 답변 참조',
'- 현재 요청은 아래 이전 답변을 명시적으로 선택해 이어가는 후속 요청입니다.',
'- 최근 대화 일반 문맥보다 아래 참조 답변을 우선해서 해석하세요.',
requestId ? `- 참조 requestId: ${requestId}` : null,
referencedQuestion ? `- 참조 사용자 요청: ${normalizePromptContextText(referencedQuestion, 1200)}` : null,
referencedAnswer
? `- 참조 답변 본문: ${normalizePromptContextText(referencedAnswer, 4000)}`
: '- 참조 답변 본문: 아직 저장된 답변 본문이 없어 상위 요청만 참조합니다.',
'- 사용자가 "방금 어떤 답변을 참조했냐"처럼 물으면 위 requestId와 참조 본문을 기준으로 답하고, 다른 최근 답변을 임의로 섞지 마세요.',
].filter((line): line is string => Boolean(line));
}
export function buildAgenticCodexPrompt(
context: ChatContext | null,
input: string,
@@ -2814,6 +2838,7 @@ export function buildAgenticCodexPrompt(
omittedHistoryCount?: number;
sessionReferenceResourcePath?: string;
sessionReferenceContent?: string;
referencedRequest?: ChatConversationRequestItem | null;
},
) {
const repoPath = promptContext?.repoPath?.trim() || resolveMainProjectRoot();
@@ -2847,6 +2872,7 @@ export function buildAgenticCodexPrompt(
...buildChatSessionReferenceInstructionBlock(promptContext?.sessionReferenceContent),
'',
...buildChatTypeInstructionBlock(context),
...(promptContext?.referencedRequest ? ['', ...buildReferencedRequestInstructionBlock(promptContext.referencedRequest)] : []),
'',
'응답 규칙:',
'- 사실성보다 추측을 우선하지 마세요. 오늘/최신/건수 질문은 직접 확인하세요.',
@@ -3024,6 +3050,7 @@ async function runAgenticCodexReply(
options?: {
omitPromptHistory?: boolean;
requestedAt?: Date | null;
parentRequestId?: string | null;
},
onProgress?: (text: string) => void,
onActivity?: (line: string) => void,
@@ -3032,6 +3059,9 @@ async function runAgenticCodexReply(
const repoPath = resolveMainProjectRoot();
await validateAgenticCodexRuntime(repoPath, env.PLAN_CODEX_BIN);
const resolvedContext = await resolveCodexLiveChatContext(context, sessionId);
const referencedRequest = options?.parentRequestId?.trim()
? await getChatConversationRequest(sessionId, options.parentRequestId.trim())
: null;
const appConfig = await getAppConfigSnapshot();
const recentHistory = await buildRecentChatPromptHistory(sessionId, requestId, {
maxMessages: appConfig.chat?.maxContextMessages,
@@ -3090,6 +3120,7 @@ async function runAgenticCodexReply(
omittedHistoryCount: recentHistory.omittedCount,
sessionReferenceResourcePath,
sessionReferenceContent,
referencedRequest,
});
let streamedOutput = '';
let stdoutTail = '';
@@ -3840,6 +3871,7 @@ async function buildCodexReply(
options?: {
omitPromptHistory?: boolean;
requestedAt?: Date | null;
parentRequestId?: string | null;
},
onProgress?: (text: string) => void,
onActivity?: (line: string) => void,
@@ -6143,6 +6175,7 @@ export class ChatService {
{
omitPromptHistory: request.omitPromptHistory === true,
requestedAt: new Date(request.requestedAtMs),
parentRequestId: request.parentRequestId,
},
(partialReply) => {
stopProgressTimer();

View File

@@ -310,3 +310,120 @@ export async function resolveChatShareTokenRoomSessionIds(tokenId: string) {
const rooms = await listChatShareTokenRoomMaps(tokenId);
return rooms.map((item) => item.sessionId).filter(Boolean);
}
export async function archiveChatShareTokenRoomMap(tokenId: string, sessionId: string) {
const normalizedTokenId = tokenId.trim();
const normalizedSessionId = sessionId.trim();
if (!normalizedTokenId || !normalizedSessionId) {
return {
archived: false,
archivedRoom: null,
nextDefaultRoom: null,
} as const;
}
await ensureChatShareTokenRoomMapTable();
return db.transaction(async (trx) => {
const current = await trx(`${CHAT_SHARE_TOKEN_ROOM_MAP_TABLE} as room_map`)
.leftJoin(`${CHAT_CONVERSATION_TABLE} as conversation`, 'conversation.session_id', 'room_map.session_id')
.select(
'room_map.shared_resource_token_id',
'room_map.session_id',
'room_map.root_request_id',
'room_map.is_default',
'room_map.sort_order',
'room_map.created_by_client_id',
'room_map.created_at',
'room_map.updated_at',
'conversation.title',
'conversation.request_badge_label',
'conversation.chat_type_id',
'conversation.last_chat_type_id',
'conversation.context_label',
'conversation.context_description',
'conversation.notify_offline',
'conversation.updated_at as conversation_updated_at',
)
.where({
'room_map.shared_resource_token_id': normalizedTokenId,
'room_map.session_id': normalizedSessionId,
})
.whereNull('room_map.archived_at')
.first();
if (!current) {
return {
archived: false,
archivedRoom: null,
nextDefaultRoom: null,
} as const;
}
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
.where({
shared_resource_token_id: normalizedTokenId,
session_id: normalizedSessionId,
})
.whereNull('archived_at')
.update({
archived_at: db.fn.now(),
updated_at: db.fn.now(),
});
let nextDefaultRoom: ChatShareTokenRoomMapItem | null = null;
if (current.is_default) {
const nextDefaultRow = await trx(`${CHAT_SHARE_TOKEN_ROOM_MAP_TABLE} as room_map`)
.leftJoin(`${CHAT_CONVERSATION_TABLE} as conversation`, 'conversation.session_id', 'room_map.session_id')
.select(
'room_map.shared_resource_token_id',
'room_map.session_id',
'room_map.root_request_id',
'room_map.is_default',
'room_map.sort_order',
'room_map.created_by_client_id',
'room_map.created_at',
'room_map.updated_at',
'conversation.title',
'conversation.request_badge_label',
'conversation.chat_type_id',
'conversation.last_chat_type_id',
'conversation.context_label',
'conversation.context_description',
'conversation.notify_offline',
'conversation.updated_at as conversation_updated_at',
)
.where({ 'room_map.shared_resource_token_id': normalizedTokenId })
.whereNull('room_map.archived_at')
.orderBy('room_map.sort_order', 'asc')
.orderBy('room_map.created_at', 'asc')
.first();
if (nextDefaultRow) {
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
.where({
shared_resource_token_id: normalizedTokenId,
session_id: nextDefaultRow.session_id,
})
.whereNull('archived_at')
.update({
is_default: true,
updated_at: db.fn.now(),
});
nextDefaultRoom = mapChatShareTokenRoomRow({
...nextDefaultRow,
is_default: true,
});
}
}
return {
archived: true,
archivedRoom: mapChatShareTokenRoomRow(current),
nextDefaultRoom,
} as const;
});
}