chore: sync local workspace changes
This commit is contained in:
@@ -9,6 +9,7 @@ export const CHAT_CONVERSATION_CLIENT_TABLE = 'chat_conversation_clients';
|
||||
export const CHAT_CONVERSATION_REQUEST_TABLE = 'chat_conversation_requests';
|
||||
export const CHAT_CONVERSATION_ACTIVITY_TABLE = 'chat_conversation_request_activities';
|
||||
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
|
||||
export const CHAT_CONTEXT_DESCRIPTION_MAX_LENGTH = 10000;
|
||||
const STALE_CHAT_REQUEST_TIMEOUT_MS = 2 * 60 * 1000;
|
||||
|
||||
const conversationPayloadSchema = z.object({
|
||||
@@ -17,8 +18,9 @@ const conversationPayloadSchema = z.object({
|
||||
title: z.string().trim().max(200).nullable().optional(),
|
||||
chatTypeId: z.string().trim().max(120).nullable().optional(),
|
||||
lastChatTypeId: z.string().trim().max(120).nullable().optional(),
|
||||
generalSectionName: z.string().trim().max(120).nullable().optional(),
|
||||
contextLabel: z.string().trim().max(200).nullable().optional(),
|
||||
contextDescription: z.string().trim().max(2000).nullable().optional(),
|
||||
contextDescription: z.string().trim().max(CHAT_CONTEXT_DESCRIPTION_MAX_LENGTH).nullable().optional(),
|
||||
notifyOffline: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -38,6 +40,7 @@ export type ChatConversationItem = {
|
||||
title: string;
|
||||
chatTypeId: string | null;
|
||||
lastChatTypeId: string | null;
|
||||
generalSectionName: string | null;
|
||||
contextLabel: string | null;
|
||||
contextDescription: string | null;
|
||||
notifyOffline: boolean;
|
||||
@@ -47,7 +50,9 @@ export type ChatConversationItem = {
|
||||
currentJobMessage: string | null;
|
||||
currentQueueSize: number;
|
||||
currentStatusUpdatedAt: string | null;
|
||||
lastRequestPreview: string;
|
||||
lastMessagePreview: string;
|
||||
lastResponsePreview: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastMessageAt: string | null;
|
||||
@@ -95,6 +100,22 @@ export type ChatConversationActivityLogItem = {
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type RecoverableChatConversationRequestItem = {
|
||||
sessionId: string;
|
||||
clientId: string | null;
|
||||
chatTypeId: string | null;
|
||||
lastChatTypeId: string | null;
|
||||
generalSectionName: string | null;
|
||||
contextLabel: string | null;
|
||||
contextDescription: string | null;
|
||||
currentRequestId: string | null;
|
||||
currentJobStatus: ChatConversationItem['currentJobStatus'];
|
||||
requestId: string;
|
||||
status: ChatConversationRequestStatus;
|
||||
userText: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type ChatConversationDetailPage = {
|
||||
messages: StoredChatMessage[];
|
||||
requests: ChatConversationRequestItem[];
|
||||
@@ -152,6 +173,75 @@ function createPreview(text: string) {
|
||||
return normalized.length > 140 ? `${normalized.slice(0, 137).trimEnd()}...` : normalized;
|
||||
}
|
||||
|
||||
const CONTEXT_DEPENDENT_REQUEST_PATTERNS = [
|
||||
/이전\s*(채팅|대화|문맥)/u,
|
||||
/이전\s*요청/u,
|
||||
/마지막\s*요청/u,
|
||||
/요청내역/u,
|
||||
/두\s*단어/u,
|
||||
/최근\s*작업\s*(뱃지|badge|라벨)/iu,
|
||||
/(?:이어서|이어진|이어가|계속|추가로|연달아|후속|마저)/u,
|
||||
/^(?:그리고|그럼|그러면|또|또한|근데|그런데|여기도|여기서도|이것도|그것도|저것도|이거|그거|저거)/u,
|
||||
/\b(?:also|continue|continued|follow[\s-]?up|same|again)\b/i,
|
||||
] as const;
|
||||
|
||||
function normalizeRequestPreviewText(text: string) {
|
||||
return String(text ?? '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isContextDependentRequestPreview(text: string) {
|
||||
const normalized = normalizeRequestPreviewText(text);
|
||||
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CONTEXT_DEPENDENT_REQUEST_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalized.length <= 16) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function buildLatestRequestPreview(
|
||||
requests: Array<{ text: string; createdAt: string | null }>,
|
||||
): { text: string; createdAt: string | null } | null {
|
||||
const normalizedRequests = requests
|
||||
.map((request) => ({
|
||||
text: normalizeRequestPreviewText(request.text),
|
||||
createdAt: request.createdAt,
|
||||
}))
|
||||
.filter((request) => Boolean(request.text));
|
||||
|
||||
const latestRequest = normalizedRequests[0];
|
||||
|
||||
if (!latestRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isContextDependentRequestPreview(latestRequest.text)) {
|
||||
return latestRequest;
|
||||
}
|
||||
|
||||
const previousRequest =
|
||||
normalizedRequests.slice(1).find((request) => !isContextDependentRequestPreview(request.text)) ??
|
||||
normalizedRequests[1] ??
|
||||
null;
|
||||
|
||||
if (!previousRequest) {
|
||||
return latestRequest;
|
||||
}
|
||||
|
||||
return {
|
||||
text: `${previousRequest.text} ${latestRequest.text}`.trim(),
|
||||
createdAt: latestRequest.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function isPreviewableConversationMessage(row: { author?: unknown; text?: unknown }) {
|
||||
const author = String(row.author ?? '');
|
||||
const text = String(row.text ?? '').trim();
|
||||
@@ -179,6 +269,7 @@ function mapConversationRow(row: Record<string, unknown>): ChatConversationItem
|
||||
title: String(row.title ?? '새 대화'),
|
||||
chatTypeId: row.chat_type_id == null ? null : String(row.chat_type_id),
|
||||
lastChatTypeId: row.last_chat_type_id == null ? null : String(row.last_chat_type_id),
|
||||
generalSectionName: row.general_section_name == null ? null : String(row.general_section_name),
|
||||
contextLabel: row.context_label == null ? null : String(row.context_label),
|
||||
contextDescription: row.context_description == null ? null : String(row.context_description),
|
||||
notifyOffline: Boolean(row.notify_offline),
|
||||
@@ -188,7 +279,9 @@ function mapConversationRow(row: Record<string, unknown>): ChatConversationItem
|
||||
currentJobMessage: row.current_job_message == null ? null : String(row.current_job_message),
|
||||
currentQueueSize: Number(row.current_queue_size ?? 0),
|
||||
currentStatusUpdatedAt: normalizeDateTimeValue(row.current_status_updated_at),
|
||||
lastRequestPreview: '',
|
||||
lastMessagePreview: String(row.last_message_preview ?? ''),
|
||||
lastResponsePreview: '',
|
||||
createdAt: normalizeDateTimeValue(row.created_at) ?? '',
|
||||
updatedAt: normalizeDateTimeValue(row.updated_at) ?? '',
|
||||
lastMessageAt: normalizeDateTimeValue(row.last_message_at),
|
||||
@@ -322,6 +415,30 @@ function isConversationRequestActive(
|
||||
return currentJobStatus === 'queued' || currentJobStatus === 'started';
|
||||
}
|
||||
|
||||
function hasConversationMetadata(
|
||||
conversation: {
|
||||
title?: unknown;
|
||||
chat_type_id?: unknown;
|
||||
last_chat_type_id?: unknown;
|
||||
general_section_name?: unknown;
|
||||
context_label?: unknown;
|
||||
context_description?: unknown;
|
||||
current_request_id?: unknown;
|
||||
current_job_status?: unknown;
|
||||
} | null | undefined,
|
||||
) {
|
||||
return [
|
||||
conversation?.title,
|
||||
conversation?.chat_type_id,
|
||||
conversation?.last_chat_type_id,
|
||||
conversation?.general_section_name,
|
||||
conversation?.context_label,
|
||||
conversation?.context_description,
|
||||
conversation?.current_request_id,
|
||||
conversation?.current_job_status,
|
||||
].some((value) => String(value ?? '').trim().length > 0);
|
||||
}
|
||||
|
||||
function normalizeStaleRequestItem(
|
||||
item: ChatConversationRequestItem,
|
||||
conversation: {
|
||||
@@ -626,24 +743,81 @@ async function getLatestRequestPreviewMap(sessionIds: string[]) {
|
||||
.orderBy('request_id', 'desc');
|
||||
|
||||
const requestMap = new Map<string, { text: string; createdAt: string | null }>();
|
||||
const requestRowsBySession = new Map<string, Array<{ text: string; createdAt: string | null }>>();
|
||||
const completedSessionIds = new Set<string>();
|
||||
|
||||
for (const row of rows) {
|
||||
const sessionId = String(row.session_id ?? '').trim();
|
||||
const userText = String(row.user_text ?? '').trim();
|
||||
|
||||
if (!sessionId || requestMap.has(sessionId) || !userText) {
|
||||
if (!sessionId || completedSessionIds.has(sessionId) || !userText) {
|
||||
continue;
|
||||
}
|
||||
|
||||
requestMap.set(sessionId, {
|
||||
const requestRows = requestRowsBySession.get(sessionId) ?? [];
|
||||
requestRows.push({
|
||||
text: userText,
|
||||
createdAt: normalizeDateTimeValue(row.created_at),
|
||||
});
|
||||
|
||||
if (requestRows.length >= 5) {
|
||||
completedSessionIds.add(sessionId);
|
||||
}
|
||||
|
||||
requestRowsBySession.set(sessionId, requestRows);
|
||||
|
||||
if (completedSessionIds.size >= normalizedSessionIds.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const sessionId of normalizedSessionIds) {
|
||||
const preview = buildLatestRequestPreview(requestRowsBySession.get(sessionId) ?? []);
|
||||
|
||||
if (!preview) {
|
||||
continue;
|
||||
}
|
||||
|
||||
requestMap.set(sessionId, preview);
|
||||
}
|
||||
|
||||
return requestMap;
|
||||
}
|
||||
|
||||
async function getLatestResponsePreviewMap(sessionIds: string[]) {
|
||||
const normalizedSessionIds = Array.from(new Set(sessionIds.map((sessionId) => sessionId.trim()).filter(Boolean)));
|
||||
|
||||
if (normalizedSessionIds.length === 0) {
|
||||
return new Map<string, { text: string; createdAt: string | null }>();
|
||||
}
|
||||
|
||||
const rows = await db(CHAT_CONVERSATION_REQUEST_TABLE)
|
||||
.select('session_id', 'response_text', 'answered_at', 'updated_at', 'status', 'request_id')
|
||||
.whereIn('session_id', normalizedSessionIds)
|
||||
.whereNot('status', 'removed')
|
||||
.orderBy('session_id', 'asc')
|
||||
.orderByRaw('COALESCE(answered_at, updated_at, created_at) desc')
|
||||
.orderBy('request_id', 'desc');
|
||||
|
||||
const responseMap = new Map<string, { text: string; createdAt: string | null }>();
|
||||
|
||||
for (const row of rows) {
|
||||
const sessionId = String(row.session_id ?? '').trim();
|
||||
const responseText = String(row.response_text ?? '').trim();
|
||||
|
||||
if (!sessionId || responseMap.has(sessionId) || !responseText) {
|
||||
continue;
|
||||
}
|
||||
|
||||
responseMap.set(sessionId, {
|
||||
text: responseText,
|
||||
createdAt: normalizeDateTimeValue(row.answered_at ?? row.updated_at),
|
||||
});
|
||||
}
|
||||
|
||||
return responseMap;
|
||||
}
|
||||
|
||||
function resolveConversationPreviewOverride(
|
||||
mapped: ChatConversationItem,
|
||||
latestMessage: { text: string; createdAt: string | null } | undefined,
|
||||
@@ -717,6 +891,7 @@ export async function ensureChatConversationTables() {
|
||||
table.string('title', 200).notNullable().defaultTo('새 대화');
|
||||
table.string('chat_type_id', 120).nullable();
|
||||
table.string('last_chat_type_id', 120).nullable();
|
||||
table.string('general_section_name', 120).nullable();
|
||||
table.string('context_label', 200).nullable();
|
||||
table.text('context_description').nullable();
|
||||
table.boolean('notify_offline').notNullable().defaultTo(false);
|
||||
@@ -737,6 +912,7 @@ export async function ensureChatConversationTables() {
|
||||
['title', (table) => table.string('title', 200).notNullable().defaultTo('새 대화')],
|
||||
['chat_type_id', (table) => table.string('chat_type_id', 120).nullable()],
|
||||
['last_chat_type_id', (table) => table.string('last_chat_type_id', 120).nullable()],
|
||||
['general_section_name', (table) => table.string('general_section_name', 120).nullable()],
|
||||
['context_label', (table) => table.string('context_label', 200).nullable()],
|
||||
['context_description', (table) => table.text('context_description').nullable()],
|
||||
['notify_offline', (table) => table.boolean('notify_offline').notNullable().defaultTo(false)],
|
||||
@@ -1024,6 +1200,7 @@ export async function createChatConversation(payload: z.input<typeof conversatio
|
||||
title: parsed.title?.trim() || '새 대화',
|
||||
chat_type_id: parsed.chatTypeId?.trim() || null,
|
||||
last_chat_type_id: parsed.lastChatTypeId?.trim() || parsed.chatTypeId?.trim() || null,
|
||||
general_section_name: parsed.generalSectionName?.trim() || null,
|
||||
context_label: parsed.contextLabel?.trim() || null,
|
||||
context_description: parsed.contextDescription?.trim() || null,
|
||||
notify_offline: notifyOffline,
|
||||
@@ -1064,6 +1241,7 @@ export async function updateChatConversationContext(
|
||||
clientId?: string | null;
|
||||
chatTypeId?: string | null;
|
||||
lastChatTypeId?: string | null;
|
||||
generalSectionName?: string | null;
|
||||
contextLabel?: string | null;
|
||||
contextDescription?: string | null;
|
||||
notifyOffline?: boolean | null;
|
||||
@@ -1089,6 +1267,7 @@ export async function updateChatConversationContext(
|
||||
client_id: normalizedClientId || current.client_id || null,
|
||||
chat_type_id: nextChatTypeId,
|
||||
last_chat_type_id: nextChatTypeId || payload.lastChatTypeId?.trim() || current.last_chat_type_id || null,
|
||||
general_section_name: resolveNextConversationContextValue(current.general_section_name, payload.generalSectionName),
|
||||
context_label: resolveNextConversationContextValue(current.context_label, requestedContextLabel),
|
||||
context_description: resolveNextConversationContextValue(current.context_description, requestedContextDescription),
|
||||
notify_offline:
|
||||
@@ -1235,12 +1414,33 @@ export async function listChatConversations(
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length > 0) {
|
||||
const candidateSessionIds = rows.map((row) => String(row.session_id ?? '').trim()).filter(Boolean);
|
||||
const [messageSessionRows, requestSessionRows] = await Promise.all([
|
||||
db(CHAT_CONVERSATION_MESSAGE_TABLE).distinct('session_id').whereIn('session_id', candidateSessionIds),
|
||||
db(CHAT_CONVERSATION_REQUEST_TABLE).distinct('session_id').whereIn('session_id', candidateSessionIds),
|
||||
]);
|
||||
const visibleSessionIds = new Set(
|
||||
[...messageSessionRows, ...requestSessionRows]
|
||||
.map((row) => String(row.session_id ?? '').trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
rows = rows.filter((row) => {
|
||||
const sessionId = String(row.session_id ?? '').trim();
|
||||
return visibleSessionIds.has(sessionId) || hasConversationMetadata(row);
|
||||
});
|
||||
}
|
||||
|
||||
const latestPreviewMessageMap = await getLatestPreviewableMessageMap(
|
||||
rows.map((row) => String(row.session_id ?? '')),
|
||||
);
|
||||
const latestRequestPreviewMap = await getLatestRequestPreviewMap(
|
||||
rows.map((row) => String(row.session_id ?? '')),
|
||||
);
|
||||
const latestResponsePreviewMap = await getLatestResponsePreviewMap(
|
||||
rows.map((row) => String(row.session_id ?? '')),
|
||||
);
|
||||
const latestResponseMessageIdMap = await getLatestResponseMessageIdMap(
|
||||
rows.map((row) => String(row.session_id ?? '')),
|
||||
);
|
||||
@@ -1255,6 +1455,8 @@ export async function listChatConversations(
|
||||
latestPreviewMessageMap.get(mapped.sessionId),
|
||||
latestRequestPreviewMap.get(mapped.sessionId),
|
||||
),
|
||||
lastRequestPreview: createPreview(latestRequestPreviewMap.get(mapped.sessionId)?.text ?? ''),
|
||||
lastResponsePreview: createPreview(latestResponsePreviewMap.get(mapped.sessionId)?.text ?? ''),
|
||||
hasUnreadResponse: false,
|
||||
};
|
||||
})
|
||||
@@ -1294,6 +1496,8 @@ export async function listChatConversations(
|
||||
latestPreviewMessage,
|
||||
latestRequestPreviewMap.get(mapped.sessionId),
|
||||
),
|
||||
lastRequestPreview: createPreview(latestRequestPreviewMap.get(mapped.sessionId)?.text ?? ''),
|
||||
lastResponsePreview: createPreview(latestResponsePreviewMap.get(mapped.sessionId)?.text ?? ''),
|
||||
clientId: normalizedUnreadStateClientId,
|
||||
notifyOffline: preference?.notifyOffline ?? mapped.notifyOffline,
|
||||
hasUnreadResponse:
|
||||
@@ -1781,6 +1985,71 @@ export async function listChatConversationActivityLogs(
|
||||
return requestIds.map((requestId) => activityMap.get(requestId)).filter(Boolean) as ChatConversationActivityLogItem[];
|
||||
}
|
||||
|
||||
export async function listRecoverableChatConversationRequests(): Promise<RecoverableChatConversationRequestItem[]> {
|
||||
await ensureChatConversationTables();
|
||||
|
||||
const rows = await db(`${CHAT_CONVERSATION_REQUEST_TABLE} as request`)
|
||||
.join(`${CHAT_CONVERSATION_TABLE} as conversation`, 'conversation.session_id', 'request.session_id')
|
||||
.select(
|
||||
'request.session_id',
|
||||
'request.request_id',
|
||||
'request.status',
|
||||
'request.user_text',
|
||||
'request.created_at',
|
||||
'conversation.client_id',
|
||||
'conversation.chat_type_id',
|
||||
'conversation.last_chat_type_id',
|
||||
'conversation.general_section_name',
|
||||
'conversation.context_label',
|
||||
'conversation.context_description',
|
||||
'conversation.current_request_id',
|
||||
'conversation.current_job_status',
|
||||
)
|
||||
.whereIn('request.status', ['accepted', 'queued', 'started'])
|
||||
.andWhere((builder) => {
|
||||
builder.whereNull('request.terminal_at');
|
||||
})
|
||||
.andWhere((builder) => {
|
||||
builder.whereNull('request.response_message_id').orWhere('request.response_message_id', 0);
|
||||
})
|
||||
.orderByRaw(
|
||||
"case when request.request_id = conversation.current_request_id then 0 else 1 end asc",
|
||||
)
|
||||
.orderBy('request.session_id', 'asc')
|
||||
.orderBy('request.created_at', 'asc')
|
||||
.orderBy('request.request_id', 'asc');
|
||||
|
||||
return rows
|
||||
.map((row) => {
|
||||
const sessionId = String(row.session_id ?? '').trim();
|
||||
const requestId = String(row.request_id ?? '').trim();
|
||||
const userText = String(row.user_text ?? '').trim();
|
||||
const createdAt = normalizeDateTimeValue(row.created_at) ?? '';
|
||||
|
||||
if (!sessionId || !requestId || !userText || !createdAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
clientId: row.client_id == null ? null : String(row.client_id),
|
||||
chatTypeId: row.chat_type_id == null ? null : String(row.chat_type_id),
|
||||
lastChatTypeId: row.last_chat_type_id == null ? null : String(row.last_chat_type_id),
|
||||
generalSectionName: row.general_section_name == null ? null : String(row.general_section_name),
|
||||
contextLabel: row.context_label == null ? null : String(row.context_label),
|
||||
contextDescription: row.context_description == null ? null : String(row.context_description),
|
||||
currentRequestId: row.current_request_id == null ? null : String(row.current_request_id),
|
||||
currentJobStatus:
|
||||
row.current_job_status == null ? null : (String(row.current_job_status) as ChatConversationItem['currentJobStatus']),
|
||||
requestId,
|
||||
status: String(row.status ?? 'accepted') as ChatConversationRequestStatus,
|
||||
userText,
|
||||
createdAt,
|
||||
} satisfies RecoverableChatConversationRequestItem;
|
||||
})
|
||||
.filter(Boolean) as RecoverableChatConversationRequestItem[];
|
||||
}
|
||||
|
||||
export async function updateChatConversationJobState(
|
||||
sessionId: string,
|
||||
payload: {
|
||||
|
||||
Reference in New Issue
Block a user