chore: test deploy snapshot
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user