feat: update codex live chat workflow
This commit is contained in:
@@ -14,6 +14,7 @@ const conversationPayloadSchema = z.object({
|
||||
sessionId: z.string().trim().min(1).max(120),
|
||||
clientId: z.string().trim().max(120).nullable().optional(),
|
||||
title: z.string().trim().max(200).nullable().optional(),
|
||||
chatTypeId: z.string().trim().max(120).nullable().optional(),
|
||||
contextLabel: z.string().trim().max(200).nullable().optional(),
|
||||
contextDescription: z.string().trim().max(2000).nullable().optional(),
|
||||
notifyOffline: z.boolean().optional(),
|
||||
@@ -32,6 +33,7 @@ export type ChatConversationItem = {
|
||||
sessionId: string;
|
||||
clientId: string | null;
|
||||
title: string;
|
||||
chatTypeId: string | null;
|
||||
contextLabel: string | null;
|
||||
contextDescription: string | null;
|
||||
notifyOffline: boolean;
|
||||
@@ -88,6 +90,14 @@ export type ChatConversationActivityLogItem = {
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type ChatConversationDetailPage = {
|
||||
messages: StoredChatMessage[];
|
||||
requests: ChatConversationRequestItem[];
|
||||
activityLogs: ChatConversationActivityLogItem[];
|
||||
oldestLoadedMessageId: number | null;
|
||||
hasOlderMessages: boolean;
|
||||
};
|
||||
|
||||
type ChatConversationRequestStatusPatch = {
|
||||
requestId: string;
|
||||
status?: ChatConversationRequestStatus;
|
||||
@@ -113,6 +123,25 @@ type ChatConversationClientPreference = {
|
||||
lastReadResponseMessageId: number | null;
|
||||
};
|
||||
|
||||
function normalizeDateTimeValue(value: unknown) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
const normalized = String(value).trim();
|
||||
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = new Date(normalized);
|
||||
return Number.isNaN(parsed.getTime()) ? normalized : parsed.toISOString();
|
||||
}
|
||||
|
||||
function createPreview(text: string) {
|
||||
const normalized = String(text ?? '').replace(/\s+/g, ' ').trim();
|
||||
return normalized.length > 140 ? `${normalized.slice(0, 137).trimEnd()}...` : normalized;
|
||||
@@ -143,6 +172,7 @@ function mapConversationRow(row: Record<string, unknown>): ChatConversationItem
|
||||
sessionId: String(row.session_id ?? ''),
|
||||
clientId: row.client_id == null ? null : String(row.client_id),
|
||||
title: String(row.title ?? '새 대화'),
|
||||
chatTypeId: row.chat_type_id == null ? null : String(row.chat_type_id),
|
||||
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),
|
||||
@@ -151,11 +181,11 @@ function mapConversationRow(row: Record<string, unknown>): ChatConversationItem
|
||||
currentJobStatus: row.current_job_status == null ? null : String(row.current_job_status) as ChatConversationItem['currentJobStatus'],
|
||||
currentJobMessage: row.current_job_message == null ? null : String(row.current_job_message),
|
||||
currentQueueSize: Number(row.current_queue_size ?? 0),
|
||||
currentStatusUpdatedAt: row.current_status_updated_at == null ? null : String(row.current_status_updated_at),
|
||||
currentStatusUpdatedAt: normalizeDateTimeValue(row.current_status_updated_at),
|
||||
lastMessagePreview: String(row.last_message_preview ?? ''),
|
||||
createdAt: String(row.created_at ?? ''),
|
||||
updatedAt: String(row.updated_at ?? ''),
|
||||
lastMessageAt: row.last_message_at == null ? null : String(row.last_message_at),
|
||||
createdAt: normalizeDateTimeValue(row.created_at) ?? '',
|
||||
updatedAt: normalizeDateTimeValue(row.updated_at) ?? '',
|
||||
lastMessageAt: normalizeDateTimeValue(row.last_message_at),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -169,7 +199,7 @@ function mapMessageRow(row: Record<string, unknown>): StoredChatMessage {
|
||||
};
|
||||
}
|
||||
|
||||
function isVisibleConversationMessage(message: StoredChatMessage) {
|
||||
export function isVisibleConversationMessage(message: StoredChatMessage) {
|
||||
if (message.author !== 'system') {
|
||||
return true;
|
||||
}
|
||||
@@ -177,6 +207,12 @@ function isVisibleConversationMessage(message: StoredChatMessage) {
|
||||
return message.text.startsWith(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function applyVisibleConversationMessageCondition(builder: any) {
|
||||
builder.whereNot('author', 'system').orWhere((nestedBuilder: any) => {
|
||||
nestedBuilder.where('author', '=', 'system').andWhere('text', 'like', `${CHAT_ACTIVITY_MESSAGE_PREFIX}\n%`);
|
||||
});
|
||||
}
|
||||
|
||||
function mapClientPreferenceRow(row: Record<string, unknown>): ChatConversationClientPreference {
|
||||
return {
|
||||
sessionId: String(row.session_id ?? ''),
|
||||
@@ -203,10 +239,10 @@ function mapRequestRow(row: Record<string, unknown>): ChatConversationRequestIte
|
||||
responseText: String(row.response_text ?? ''),
|
||||
hasResponse,
|
||||
canDelete,
|
||||
createdAt: String(row.created_at ?? ''),
|
||||
updatedAt: String(row.updated_at ?? ''),
|
||||
answeredAt: row.answered_at == null ? null : String(row.answered_at),
|
||||
terminalAt: row.terminal_at == null ? null : String(row.terminal_at),
|
||||
createdAt: normalizeDateTimeValue(row.created_at) ?? '',
|
||||
updatedAt: normalizeDateTimeValue(row.updated_at) ?? '',
|
||||
answeredAt: normalizeDateTimeValue(row.answered_at),
|
||||
terminalAt: normalizeDateTimeValue(row.terminal_at),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -560,7 +596,7 @@ async function getLatestPreviewableMessageMap(sessionIds: string[]) {
|
||||
|
||||
messageMap.set(sessionId, {
|
||||
text: String(row.text ?? ''),
|
||||
createdAt: row.created_at == null ? null : String(row.created_at),
|
||||
createdAt: normalizeDateTimeValue(row.created_at),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -594,7 +630,7 @@ async function getLatestRequestPreviewMap(sessionIds: string[]) {
|
||||
|
||||
requestMap.set(sessionId, {
|
||||
text: userText,
|
||||
createdAt: row.created_at == null ? null : String(row.created_at),
|
||||
createdAt: normalizeDateTimeValue(row.created_at),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -672,6 +708,7 @@ export async function ensureChatConversationTables() {
|
||||
table.string('session_id', 120).primary();
|
||||
table.string('client_id', 120).nullable().index();
|
||||
table.string('title', 200).notNullable().defaultTo('새 대화');
|
||||
table.string('chat_type_id', 120).nullable();
|
||||
table.string('context_label', 200).nullable();
|
||||
table.text('context_description').nullable();
|
||||
table.boolean('notify_offline').notNullable().defaultTo(false);
|
||||
@@ -690,6 +727,7 @@ export async function ensureChatConversationTables() {
|
||||
const requiredConversationColumns: Array<[string, (table: any) => void]> = [
|
||||
['client_id', (table) => table.string('client_id', 120).nullable().index()],
|
||||
['title', (table) => table.string('title', 200).notNullable().defaultTo('새 대화')],
|
||||
['chat_type_id', (table) => table.string('chat_type_id', 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)],
|
||||
@@ -864,7 +902,6 @@ export async function ensureChatConversationTables() {
|
||||
}
|
||||
|
||||
export async function getChatConversation(sessionId: string, clientId?: string | null) {
|
||||
await ensureChatConversationTables();
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
let row = await db(CHAT_CONVERSATION_TABLE).where({ session_id: normalizedSessionId }).first();
|
||||
|
||||
@@ -878,7 +915,7 @@ export async function getChatConversation(sessionId: string, clientId?: string |
|
||||
shouldClearConversationJobState({
|
||||
currentRequestId,
|
||||
currentJobStatus: row.current_job_status == null ? null : String(row.current_job_status) as ChatConversationItem['currentJobStatus'],
|
||||
currentStatusUpdatedAt: row.current_status_updated_at == null ? null : String(row.current_status_updated_at),
|
||||
currentStatusUpdatedAt: normalizeDateTimeValue(row.current_status_updated_at),
|
||||
runtimeActive: isRuntimeRequestActive(currentRequestId),
|
||||
request: currentRequestId
|
||||
? await db(CHAT_CONVERSATION_REQUEST_TABLE)
|
||||
@@ -894,8 +931,8 @@ export async function getChatConversation(sessionId: string, clientId?: string |
|
||||
status: String(requestRow.status ?? '') as ChatConversationRequestStatus,
|
||||
responseMessageId: requestRow.response_message_id == null ? null : Number(requestRow.response_message_id),
|
||||
responseText: String(requestRow.response_text ?? ''),
|
||||
terminalAt: requestRow.terminal_at == null ? null : String(requestRow.terminal_at),
|
||||
updatedAt: requestRow.updated_at == null ? null : String(requestRow.updated_at),
|
||||
terminalAt: normalizeDateTimeValue(requestRow.terminal_at),
|
||||
updatedAt: normalizeDateTimeValue(requestRow.updated_at),
|
||||
}
|
||||
: null,
|
||||
)
|
||||
@@ -966,7 +1003,6 @@ export async function getChatConversation(sessionId: string, clientId?: string |
|
||||
}
|
||||
|
||||
export async function createChatConversation(payload: z.input<typeof conversationPayloadSchema>) {
|
||||
await ensureChatConversationTables();
|
||||
const parsed = conversationPayloadSchema.parse(payload);
|
||||
const normalizedClientId = normalizeClientId(parsed.clientId);
|
||||
const notifyOffline = parsed.notifyOffline ?? true;
|
||||
@@ -975,6 +1011,7 @@ export async function createChatConversation(payload: z.input<typeof conversatio
|
||||
session_id: parsed.sessionId,
|
||||
client_id: normalizedClientId,
|
||||
title: parsed.title?.trim() || '새 대화',
|
||||
chat_type_id: parsed.chatTypeId?.trim() || null,
|
||||
context_label: parsed.contextLabel?.trim() || null,
|
||||
context_description: parsed.contextDescription?.trim() || null,
|
||||
notify_offline: notifyOffline,
|
||||
@@ -1013,12 +1050,12 @@ export async function updateChatConversationContext(
|
||||
payload: {
|
||||
title?: string | null;
|
||||
clientId?: string | null;
|
||||
chatTypeId?: string | null;
|
||||
contextLabel?: string | null;
|
||||
contextDescription?: string | null;
|
||||
notifyOffline?: boolean | null;
|
||||
},
|
||||
) {
|
||||
await ensureChatConversationTables();
|
||||
const normalizedClientId = normalizeClientId(payload.clientId);
|
||||
const current = await db(CHAT_CONVERSATION_TABLE).where({ session_id: sessionId.trim() }).first();
|
||||
|
||||
@@ -1031,6 +1068,7 @@ export async function updateChatConversationContext(
|
||||
.update({
|
||||
title: payload.title?.trim() || current.title || '새 대화',
|
||||
client_id: normalizedClientId || current.client_id || null,
|
||||
chat_type_id: payload.chatTypeId?.trim() || null,
|
||||
context_label: payload.contextLabel?.trim() || null,
|
||||
context_description: payload.contextDescription?.trim() || null,
|
||||
notify_offline:
|
||||
@@ -1052,32 +1090,44 @@ export async function listChatConversations(
|
||||
limit = 50,
|
||||
unreadStateClientId?: string | null,
|
||||
) {
|
||||
await ensureChatConversationTables();
|
||||
const normalizedClientId = normalizeClientId(clientId);
|
||||
const normalizedUnreadStateClientId = normalizeClientId(unreadStateClientId ?? clientId);
|
||||
const query = db(CHAT_CONVERSATION_TABLE)
|
||||
.select('*')
|
||||
.orderByRaw('COALESCE(last_message_at, updated_at, created_at) DESC NULLS LAST')
|
||||
.orderByRaw('last_message_at DESC NULLS LAST')
|
||||
.orderByRaw('updated_at DESC NULLS LAST')
|
||||
.orderByRaw('created_at DESC NULLS LAST')
|
||||
.limit(Math.max(1, Math.min(200, Math.round(limit))));
|
||||
const normalizedLimit = Math.max(1, Math.min(200, Math.round(limit)));
|
||||
let conversationListScopeClientId = normalizedClientId;
|
||||
const buildConversationListQuery = (targetClientId?: string | null) => {
|
||||
const query = db(CHAT_CONVERSATION_TABLE)
|
||||
.select('*')
|
||||
.orderByRaw('COALESCE(last_message_at, updated_at, created_at) DESC NULLS LAST')
|
||||
.orderByRaw('last_message_at DESC NULLS LAST')
|
||||
.orderByRaw('updated_at DESC NULLS LAST')
|
||||
.orderByRaw('created_at DESC NULLS LAST')
|
||||
.limit(normalizedLimit);
|
||||
|
||||
if (normalizedClientId) {
|
||||
query.where((builder) => {
|
||||
builder
|
||||
.where({ client_id: normalizedClientId })
|
||||
.orWhereExists(
|
||||
db(CHAT_CONVERSATION_CLIENT_TABLE)
|
||||
.select(db.raw('1'))
|
||||
.whereRaw(`${CHAT_CONVERSATION_CLIENT_TABLE}.session_id = ${CHAT_CONVERSATION_TABLE}.session_id`)
|
||||
.andWhere({ client_id: normalizedClientId }),
|
||||
);
|
||||
});
|
||||
if (targetClientId) {
|
||||
query.where((builder) => {
|
||||
builder
|
||||
.where({ client_id: targetClientId })
|
||||
.orWhereExists(
|
||||
db(CHAT_CONVERSATION_CLIENT_TABLE)
|
||||
.select(db.raw('1'))
|
||||
.whereRaw(`${CHAT_CONVERSATION_CLIENT_TABLE}.session_id = ${CHAT_CONVERSATION_TABLE}.session_id`)
|
||||
.andWhere({ client_id: targetClientId }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
let rows = await buildConversationListQuery(normalizedClientId);
|
||||
|
||||
// Browser storage reset can regenerate client_id and hide existing rooms.
|
||||
// When that happens, fall back to the recent global list so the user can recover.
|
||||
if (normalizedClientId && rows.length === 0) {
|
||||
conversationListScopeClientId = null;
|
||||
rows = await buildConversationListQuery(null);
|
||||
}
|
||||
|
||||
let rows = await query;
|
||||
|
||||
const sessionIds = rows.map((row) => String(row.session_id ?? '')).filter(Boolean);
|
||||
const currentRequestIds = Array.from(
|
||||
new Set(rows.map((row) => String(row.current_request_id ?? '').trim()).filter(Boolean)),
|
||||
@@ -1096,7 +1146,7 @@ export async function listChatConversations(
|
||||
status: String(requestRow.status ?? '') as ChatConversationRequestStatus,
|
||||
responseMessageId: requestRow.response_message_id == null ? null : Number(requestRow.response_message_id),
|
||||
responseText: String(requestRow.response_text ?? ''),
|
||||
terminalAt: requestRow.terminal_at == null ? null : String(requestRow.terminal_at),
|
||||
terminalAt: normalizeDateTimeValue(requestRow.terminal_at),
|
||||
},
|
||||
]),
|
||||
);
|
||||
@@ -1105,7 +1155,7 @@ export async function listChatConversations(
|
||||
shouldClearConversationJobState({
|
||||
currentRequestId: String(row.current_request_id ?? ''),
|
||||
currentJobStatus: row.current_job_status == null ? null : String(row.current_job_status) as ChatConversationItem['currentJobStatus'],
|
||||
currentStatusUpdatedAt: row.current_status_updated_at == null ? null : String(row.current_status_updated_at),
|
||||
currentStatusUpdatedAt: normalizeDateTimeValue(row.current_status_updated_at),
|
||||
runtimeActive: isRuntimeRequestActive(String(row.current_request_id ?? '')),
|
||||
request:
|
||||
requestMap.get(`${String(row.session_id ?? '').trim()}:${String(row.current_request_id ?? '').trim()}`) ?? null,
|
||||
@@ -1149,7 +1199,7 @@ export async function listChatConversations(
|
||||
current_status_updated_at: db.fn.now(),
|
||||
updated_at: db.fn.now(),
|
||||
});
|
||||
rows = await query.clone();
|
||||
rows = await buildConversationListQuery(conversationListScopeClientId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1231,7 +1281,6 @@ export async function listChatConversationMessages(
|
||||
beforeMessageId?: number | null;
|
||||
} = {},
|
||||
) {
|
||||
await ensureChatConversationTables();
|
||||
const normalizedLimit = Math.max(1, Math.min(1000, Math.round(options.limit ?? 200)));
|
||||
const normalizedBeforeMessageId =
|
||||
Number.isFinite(options.beforeMessageId) && (options.beforeMessageId ?? 0) > 0
|
||||
@@ -1244,20 +1293,226 @@ export async function listChatConversationMessages(
|
||||
if (normalizedBeforeMessageId !== null) {
|
||||
builder.where('message_id', '<', normalizedBeforeMessageId);
|
||||
}
|
||||
|
||||
builder.andWhere((visibilityBuilder) => {
|
||||
applyVisibleConversationMessageCondition(visibilityBuilder);
|
||||
});
|
||||
})
|
||||
.orderBy('message_id', 'desc')
|
||||
.orderBy('id', 'desc')
|
||||
.limit(normalizedLimit);
|
||||
const latestRows = await query;
|
||||
|
||||
return latestRows
|
||||
.reverse()
|
||||
.map((row: Parameters<typeof mapMessageRow>[0]) => mapMessageRow(row))
|
||||
.filter((message: ReturnType<typeof mapMessageRow>) => isVisibleConversationMessage(message));
|
||||
return latestRows.reverse().map((row: Parameters<typeof mapMessageRow>[0]) => mapMessageRow(row));
|
||||
}
|
||||
|
||||
async function listChatConversationActivityLogsByRequestIds(
|
||||
sessionId: string,
|
||||
requestIds: string[],
|
||||
): Promise<ChatConversationActivityLogItem[]> {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const normalizedRequestIds = Array.from(new Set(requestIds.map((item) => item.trim()).filter(Boolean)));
|
||||
|
||||
if (!normalizedSessionId || normalizedRequestIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows = await db(CHAT_CONVERSATION_ACTIVITY_TABLE)
|
||||
.select('session_id', 'request_id', 'text', 'line_no', 'created_at')
|
||||
.where({ session_id: normalizedSessionId })
|
||||
.whereIn('request_id', normalizedRequestIds)
|
||||
.orderBy('request_id', 'asc')
|
||||
.orderBy('line_no', 'asc')
|
||||
.orderBy('id', 'asc');
|
||||
|
||||
const activityMap = new Map<string, ChatConversationActivityLogItem>();
|
||||
|
||||
for (const row of rows) {
|
||||
const requestId = String(row.request_id ?? '').trim();
|
||||
|
||||
if (!requestId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = activityMap.get(requestId);
|
||||
|
||||
if (existing) {
|
||||
existing.lines.push(String(row.text ?? ''));
|
||||
existing.updatedAt = normalizeDateTimeValue(row.created_at) ?? existing.updatedAt;
|
||||
continue;
|
||||
}
|
||||
|
||||
activityMap.set(requestId, {
|
||||
sessionId: String(row.session_id ?? normalizedSessionId),
|
||||
requestId,
|
||||
lines: [String(row.text ?? '')],
|
||||
updatedAt: normalizeDateTimeValue(row.created_at),
|
||||
});
|
||||
}
|
||||
|
||||
return normalizedRequestIds
|
||||
.map((requestId) => activityMap.get(requestId))
|
||||
.filter(Boolean) as ChatConversationActivityLogItem[];
|
||||
}
|
||||
|
||||
async function resolveConversationRequestCursor(sessionId: string, beforeMessageId: number) {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const normalizedBeforeMessageId = Math.trunc(beforeMessageId);
|
||||
|
||||
if (!normalizedSessionId || normalizedBeforeMessageId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const directRequestRow = await db(CHAT_CONVERSATION_REQUEST_TABLE)
|
||||
.select('request_id', 'created_at')
|
||||
.where({ session_id: normalizedSessionId })
|
||||
.andWhere((builder) => {
|
||||
builder.where('user_message_id', normalizedBeforeMessageId).orWhere('response_message_id', normalizedBeforeMessageId);
|
||||
})
|
||||
.first();
|
||||
|
||||
if (directRequestRow) {
|
||||
return {
|
||||
requestId: String(directRequestRow.request_id ?? '').trim(),
|
||||
createdAt: normalizeDateTimeValue(directRequestRow.created_at) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const messageRow = await db(CHAT_CONVERSATION_MESSAGE_TABLE)
|
||||
.select('client_request_id')
|
||||
.where({
|
||||
session_id: normalizedSessionId,
|
||||
message_id: normalizedBeforeMessageId,
|
||||
})
|
||||
.first();
|
||||
const linkedRequestId = String(messageRow?.client_request_id ?? '').trim();
|
||||
|
||||
if (!linkedRequestId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const linkedRequestRow = await db(CHAT_CONVERSATION_REQUEST_TABLE)
|
||||
.select('request_id', 'created_at')
|
||||
.where({
|
||||
session_id: normalizedSessionId,
|
||||
request_id: linkedRequestId,
|
||||
})
|
||||
.first();
|
||||
|
||||
if (!linkedRequestRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
requestId: String(linkedRequestRow.request_id ?? '').trim(),
|
||||
createdAt: normalizeDateTimeValue(linkedRequestRow.created_at) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export async function listChatConversationDetailPage(
|
||||
sessionId: string,
|
||||
options: {
|
||||
limit?: number;
|
||||
beforeMessageId?: number | null;
|
||||
} = {},
|
||||
): Promise<ChatConversationDetailPage> {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const conversation = await db(CHAT_CONVERSATION_TABLE).where({ session_id: normalizedSessionId }).first();
|
||||
const normalizedLimit = Math.max(1, Math.min(100, Math.round(options.limit ?? 6)));
|
||||
const normalizedBeforeMessageId =
|
||||
Number.isFinite(options.beforeMessageId) && (options.beforeMessageId ?? 0) > 0
|
||||
? Math.trunc(options.beforeMessageId as number)
|
||||
: null;
|
||||
const requestCursor =
|
||||
normalizedBeforeMessageId == null
|
||||
? null
|
||||
: await resolveConversationRequestCursor(normalizedSessionId, normalizedBeforeMessageId);
|
||||
|
||||
const requestRows = await db(CHAT_CONVERSATION_REQUEST_TABLE)
|
||||
.where({ session_id: normalizedSessionId })
|
||||
.whereNot('status', 'removed')
|
||||
.modify((builder) => {
|
||||
if (!requestCursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
builder.andWhere((cursorBuilder) => {
|
||||
cursorBuilder
|
||||
.where('created_at', '<', requestCursor.createdAt)
|
||||
.orWhere((sameTimeBuilder) => {
|
||||
sameTimeBuilder.where('created_at', '=', requestCursor.createdAt).andWhere('request_id', '<', requestCursor.requestId);
|
||||
});
|
||||
});
|
||||
})
|
||||
.orderBy('created_at', 'desc')
|
||||
.orderBy('request_id', 'desc')
|
||||
.limit(normalizedLimit);
|
||||
|
||||
const orderedRequestRows = [...requestRows].reverse();
|
||||
const requests = orderedRequestRows.map((row) => normalizeStaleRequestItem(mapRequestRow(row), conversation));
|
||||
const requestIds = requests.map((item) => item.requestId.trim()).filter(Boolean);
|
||||
|
||||
if (requestIds.length === 0) {
|
||||
return {
|
||||
messages: [],
|
||||
requests,
|
||||
activityLogs: [],
|
||||
oldestLoadedMessageId: null,
|
||||
hasOlderMessages: false,
|
||||
};
|
||||
}
|
||||
|
||||
const messageRows = await db(CHAT_CONVERSATION_MESSAGE_TABLE)
|
||||
.select('*')
|
||||
.where({ session_id: normalizedSessionId })
|
||||
.whereIn('client_request_id', requestIds)
|
||||
.andWhere((builder) => {
|
||||
applyVisibleConversationMessageCondition(builder);
|
||||
})
|
||||
.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,
|
||||
);
|
||||
|
||||
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 hasOlderMessages = oldestRequest
|
||||
? Boolean(
|
||||
await db(CHAT_CONVERSATION_REQUEST_TABLE)
|
||||
.where({ session_id: normalizedSessionId })
|
||||
.whereNot('status', 'removed')
|
||||
.andWhere((builder) => {
|
||||
builder
|
||||
.where('created_at', '<', oldestRequest.createdAt)
|
||||
.orWhere((sameTimeBuilder) => {
|
||||
sameTimeBuilder.where('created_at', '=', oldestRequest.createdAt).andWhere('request_id', '<', oldestRequest.requestId);
|
||||
});
|
||||
})
|
||||
.first(),
|
||||
)
|
||||
: false;
|
||||
|
||||
return {
|
||||
messages,
|
||||
requests,
|
||||
activityLogs,
|
||||
oldestLoadedMessageId,
|
||||
hasOlderMessages,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listChatConversationRequests(sessionId: string, limit = 200) {
|
||||
await ensureChatConversationTables();
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const conversation = await db(CHAT_CONVERSATION_TABLE).where({ session_id: normalizedSessionId }).first();
|
||||
|
||||
@@ -1270,8 +1525,6 @@ export async function listChatConversationRequests(sessionId: string, limit = 20
|
||||
}
|
||||
|
||||
export async function getChatConversationRequest(sessionId: string, requestId: string) {
|
||||
await ensureChatConversationTables();
|
||||
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const normalizedRequestId = requestId.trim();
|
||||
|
||||
@@ -1313,7 +1566,6 @@ export async function appendChatConversationMessage(
|
||||
conversationPayload: z.input<typeof conversationPayloadSchema>,
|
||||
messagePayload: z.input<typeof conversationMessagePayloadSchema>,
|
||||
) {
|
||||
await ensureChatConversationTables();
|
||||
const conversation = conversationPayloadSchema.parse(conversationPayload);
|
||||
const message = conversationMessagePayloadSchema.parse(messagePayload);
|
||||
|
||||
@@ -1353,6 +1605,7 @@ export async function appendChatConversationMessage(
|
||||
.update({
|
||||
client_id: normalizeClientId(conversation.clientId) || currentConversation?.client_id || null,
|
||||
title: nextTitle,
|
||||
chat_type_id: conversation.chatTypeId?.trim() || currentConversation?.chat_type_id || null,
|
||||
context_label: conversation.contextLabel?.trim() || currentConversation?.context_label || null,
|
||||
context_description: conversation.contextDescription?.trim() || currentConversation?.context_description || null,
|
||||
notify_offline:
|
||||
@@ -1390,7 +1643,6 @@ export async function appendChatConversationMessage(
|
||||
}
|
||||
|
||||
export async function appendChatConversationActivityLine(sessionId: string, requestId: string, line: string) {
|
||||
await ensureChatConversationTables();
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const normalizedRequestId = requestId.trim();
|
||||
const normalizedLine = line.trim();
|
||||
@@ -1426,7 +1678,6 @@ export async function listChatConversationActivityLogs(
|
||||
sessionId: string,
|
||||
limitRequests = 500,
|
||||
): Promise<ChatConversationActivityLogItem[]> {
|
||||
await ensureChatConversationTables();
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
|
||||
if (!normalizedSessionId) {
|
||||
@@ -1469,7 +1720,7 @@ export async function listChatConversationActivityLogs(
|
||||
|
||||
if (existing) {
|
||||
existing.lines.push(String(row.text ?? ''));
|
||||
existing.updatedAt = row.created_at == null ? existing.updatedAt : String(row.created_at);
|
||||
existing.updatedAt = normalizeDateTimeValue(row.created_at) ?? existing.updatedAt;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1477,7 +1728,7 @@ export async function listChatConversationActivityLogs(
|
||||
sessionId: String(row.session_id ?? normalizedSessionId),
|
||||
requestId,
|
||||
lines: [String(row.text ?? '')],
|
||||
updatedAt: row.created_at == null ? null : String(row.created_at),
|
||||
updatedAt: normalizeDateTimeValue(row.created_at),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1494,8 +1745,6 @@ export async function updateChatConversationJobState(
|
||||
clear?: boolean;
|
||||
},
|
||||
) {
|
||||
await ensureChatConversationTables();
|
||||
|
||||
const current = await db(CHAT_CONVERSATION_TABLE).where({ session_id: sessionId.trim() }).first();
|
||||
|
||||
if (!current) {
|
||||
@@ -1547,8 +1796,6 @@ export async function upsertChatConversationRequest(
|
||||
responseText?: string | null;
|
||||
},
|
||||
) {
|
||||
await ensureChatConversationTables();
|
||||
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const normalizedRequestId = payload.requestId.trim();
|
||||
|
||||
@@ -1698,7 +1945,7 @@ export async function repairChatConversationRequestLinks(sessionId?: string | nu
|
||||
author: String(row.author ?? 'codex') as StoredChatMessage['author'],
|
||||
text: String(row.text ?? ''),
|
||||
clientRequestId: row.client_request_id == null ? null : String(row.client_request_id),
|
||||
createdAt: row.created_at == null ? null : String(row.created_at),
|
||||
createdAt: normalizeDateTimeValue(row.created_at),
|
||||
}));
|
||||
|
||||
for (let index = 0; index < requestRows.length; index += 1) {
|
||||
@@ -1713,12 +1960,12 @@ export async function repairChatConversationRequestLinks(sessionId?: string | nu
|
||||
const candidate = selectChatConversationResponseCandidate(
|
||||
{
|
||||
requestId,
|
||||
createdAt: String(requestRow.created_at ?? ''),
|
||||
createdAt: normalizeDateTimeValue(requestRow.created_at) ?? '',
|
||||
responseMessageId: requestRow.response_message_id == null ? null : Number(requestRow.response_message_id),
|
||||
},
|
||||
nextRequestRow
|
||||
? {
|
||||
createdAt: String(nextRequestRow.created_at ?? ''),
|
||||
createdAt: normalizeDateTimeValue(nextRequestRow.created_at) ?? '',
|
||||
}
|
||||
: undefined,
|
||||
responseMessages,
|
||||
@@ -1785,8 +2032,6 @@ export async function repairChatConversationRequestLinks(sessionId?: string | nu
|
||||
}
|
||||
|
||||
export async function deleteUnansweredChatConversationRequest(sessionId: string, requestId: string) {
|
||||
await ensureChatConversationTables();
|
||||
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const normalizedRequestId = requestId.trim();
|
||||
const current = await db(CHAT_CONVERSATION_REQUEST_TABLE)
|
||||
@@ -1866,8 +2111,6 @@ export async function clearAllChatConversationJobStates() {
|
||||
}
|
||||
|
||||
export async function deleteChatConversation(sessionId: string) {
|
||||
await ensureChatConversationTables();
|
||||
|
||||
return db.transaction(async (trx) => {
|
||||
await trx(CHAT_CONVERSATION_CLIENT_TABLE).where({ session_id: sessionId.trim() }).del();
|
||||
await trx(CHAT_CONVERSATION_REQUEST_TABLE).where({ session_id: sessionId.trim() }).del();
|
||||
@@ -1878,7 +2121,6 @@ export async function deleteChatConversation(sessionId: string) {
|
||||
}
|
||||
|
||||
export async function getChatConversationClientPreference(sessionId: string, clientId: string) {
|
||||
await ensureChatConversationTables();
|
||||
const row = await db(CHAT_CONVERSATION_CLIENT_TABLE)
|
||||
.where({
|
||||
session_id: sessionId.trim(),
|
||||
@@ -1890,8 +2132,6 @@ export async function getChatConversationClientPreference(sessionId: string, cli
|
||||
}
|
||||
|
||||
export async function listChatConversationOfflineNotificationClientIds(sessionId: string) {
|
||||
await ensureChatConversationTables();
|
||||
|
||||
const rows = await db(CHAT_CONVERSATION_CLIENT_TABLE)
|
||||
.where({
|
||||
session_id: sessionId.trim(),
|
||||
@@ -1905,7 +2145,6 @@ export async function listChatConversationOfflineNotificationClientIds(sessionId
|
||||
}
|
||||
|
||||
export async function upsertChatConversationClientPreference(sessionId: string, clientId: string, notifyOffline: boolean) {
|
||||
await ensureChatConversationTables();
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const normalizedClientId = clientId.trim();
|
||||
await db(CHAT_CONVERSATION_CLIENT_TABLE)
|
||||
@@ -1927,8 +2166,6 @@ export async function upsertChatConversationClientPreference(sessionId: string,
|
||||
}
|
||||
|
||||
export async function markChatConversationResponsesRead(sessionId: string, clientId: string) {
|
||||
await ensureChatConversationTables();
|
||||
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const normalizedClientId = clientId.trim();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user