chore: test deploy snapshot
This commit is contained in:
@@ -49,6 +49,13 @@ import {
|
|||||||
updateChatConversationContext,
|
updateChatConversationContext,
|
||||||
hasPendingAttentionVerificationRequest,
|
hasPendingAttentionVerificationRequest,
|
||||||
} from '../services/chat-room-service.js';
|
} from '../services/chat-room-service.js';
|
||||||
|
import {
|
||||||
|
ensureDefaultChatShareTokenRoomMap,
|
||||||
|
getChatShareTokenRoomMap,
|
||||||
|
resolveChatShareTokenRoomSessionIds,
|
||||||
|
upsertChatShareTokenRoomMap,
|
||||||
|
type ChatShareTokenRoomMapItem,
|
||||||
|
} from '../services/chat-share-room-map-service.js';
|
||||||
import { chatRuntimeService } from '../services/chat-runtime-service.js';
|
import { chatRuntimeService } from '../services/chat-runtime-service.js';
|
||||||
import { resolveMainProjectRoot } from '../services/main-project-root-service.js';
|
import { resolveMainProjectRoot } from '../services/main-project-root-service.js';
|
||||||
import { openResourceManagerPreviewStream } from '../services/resource-manager-service.js';
|
import { openResourceManagerPreviewStream } from '../services/resource-manager-service.js';
|
||||||
@@ -228,6 +235,121 @@ function resolveChatShareTokenSettingSnapshot(tokenPayload: ChatShareTokenPayloa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChatShareResolvedRoom = {
|
||||||
|
sessionId: string;
|
||||||
|
requestId: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
title: string;
|
||||||
|
requestBadgeLabel: string | null;
|
||||||
|
chatTypeId: string | null;
|
||||||
|
lastChatTypeId: string | null;
|
||||||
|
contextLabel: string | null;
|
||||||
|
contextDescription: string | null;
|
||||||
|
notifyOffline: boolean;
|
||||||
|
createdAt: string | null;
|
||||||
|
updatedAt: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapResolvedShareRoomItem(room: ChatShareTokenRoomMapItem): ChatShareResolvedRoom {
|
||||||
|
return {
|
||||||
|
sessionId: room.sessionId,
|
||||||
|
requestId: room.rootRequestId,
|
||||||
|
isDefault: room.isDefault,
|
||||||
|
sortOrder: room.sortOrder,
|
||||||
|
title: room.title,
|
||||||
|
requestBadgeLabel: room.requestBadgeLabel,
|
||||||
|
chatTypeId: room.chatTypeId,
|
||||||
|
lastChatTypeId: room.lastChatTypeId,
|
||||||
|
contextLabel: room.contextLabel,
|
||||||
|
contextDescription: room.contextDescription,
|
||||||
|
notifyOffline: room.notifyOffline,
|
||||||
|
createdAt: room.createdAt,
|
||||||
|
updatedAt: room.conversationUpdatedAt ?? room.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveManagedShareRooms(args: {
|
||||||
|
managedResource:
|
||||||
|
| Awaited<ReturnType<typeof resolveManagedChatShareContext>>['managedResource']
|
||||||
|
| null;
|
||||||
|
tokenPayload: ChatShareTokenPayload;
|
||||||
|
}) {
|
||||||
|
const managedResourceTokenId = args.managedResource?.token.id?.trim() || '';
|
||||||
|
|
||||||
|
if (!managedResourceTokenId || args.tokenPayload.kind !== 'request-bundle') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
sessionId: args.tokenPayload.sessionId,
|
||||||
|
requestId: args.tokenPayload.requestId,
|
||||||
|
isDefault: true,
|
||||||
|
sortOrder: 0,
|
||||||
|
title: '공유 채팅방',
|
||||||
|
requestBadgeLabel: null,
|
||||||
|
chatTypeId: null,
|
||||||
|
lastChatTypeId: null,
|
||||||
|
contextLabel: null,
|
||||||
|
contextDescription: null,
|
||||||
|
notifyOffline: false,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
} satisfies ChatShareResolvedRoom,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensuredRooms = await ensureDefaultChatShareTokenRoomMap({
|
||||||
|
tokenId: managedResourceTokenId,
|
||||||
|
sessionId: args.tokenPayload.sessionId,
|
||||||
|
rootRequestId: args.tokenPayload.requestId,
|
||||||
|
createdByClientId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ensuredRooms.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
sessionId: args.tokenPayload.sessionId,
|
||||||
|
requestId: args.tokenPayload.requestId,
|
||||||
|
isDefault: true,
|
||||||
|
sortOrder: 0,
|
||||||
|
title: '공유 채팅방',
|
||||||
|
requestBadgeLabel: null,
|
||||||
|
chatTypeId: null,
|
||||||
|
lastChatTypeId: null,
|
||||||
|
contextLabel: null,
|
||||||
|
contextDescription: null,
|
||||||
|
notifyOffline: false,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
} satisfies ChatShareResolvedRoom,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ensuredRooms.map((room) => mapResolvedShareRoomItem(room));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveActiveManagedShareRoom(args: {
|
||||||
|
managedResource:
|
||||||
|
| Awaited<ReturnType<typeof resolveManagedChatShareContext>>['managedResource']
|
||||||
|
| null;
|
||||||
|
tokenPayload: ChatShareTokenPayload;
|
||||||
|
requestedSessionId?: string | null;
|
||||||
|
}) {
|
||||||
|
const rooms = await resolveManagedShareRooms({
|
||||||
|
managedResource: args.managedResource,
|
||||||
|
tokenPayload: args.tokenPayload,
|
||||||
|
});
|
||||||
|
const requestedSessionId = args.requestedSessionId?.trim() || '';
|
||||||
|
const requestedRoom = requestedSessionId ? rooms.find((room) => room.sessionId === requestedSessionId) ?? null : null;
|
||||||
|
const defaultRoom = rooms.find((room) => room.isDefault) ?? rooms[0] ?? null;
|
||||||
|
const activeRoom = requestedRoom ?? defaultRoom;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rooms,
|
||||||
|
activeRoom,
|
||||||
|
requestedRoomMatched: !requestedSessionId || Boolean(requestedRoom),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createManagedChatShareTokenId() {
|
function createManagedChatShareTokenId() {
|
||||||
return `chat_share_${randomUUID().replace(/-/g, '').slice(0, 20)}`;
|
return `chat_share_${randomUUID().replace(/-/g, '').slice(0, 20)}`;
|
||||||
}
|
}
|
||||||
@@ -991,8 +1113,14 @@ function buildManagedSharePlaceholderRequest(tokenPayload: ChatShareTokenPayload
|
|||||||
} satisfies ListedChatConversationRequest;
|
} satisfies ListedChatConversationRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildChatShareSnapshot(tokenPayload: ChatShareTokenPayload) {
|
async function buildChatShareSnapshot(
|
||||||
const normalizedSessionId = tokenPayload.sessionId.trim();
|
tokenPayload: ChatShareTokenPayload,
|
||||||
|
options?: {
|
||||||
|
sessionId?: string | null;
|
||||||
|
requestId?: string | null;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const normalizedSessionId = options?.sessionId?.trim() || tokenPayload.sessionId.trim();
|
||||||
const conversation = await getChatConversation(normalizedSessionId, null);
|
const conversation = await getChatConversation(normalizedSessionId, null);
|
||||||
|
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
@@ -1004,7 +1132,7 @@ async function buildChatShareSnapshot(tokenPayload: ChatShareTokenPayload) {
|
|||||||
listChatConversationMessages(normalizedSessionId, { limit: 1000 }),
|
listChatConversationMessages(normalizedSessionId, { limit: 1000 }),
|
||||||
]);
|
]);
|
||||||
const requestMap = new Map(requests.map((request) => [request.requestId.trim(), request] as const));
|
const requestMap = new Map(requests.map((request) => [request.requestId.trim(), request] as const));
|
||||||
const targetRequestId = tokenPayload.requestId.trim();
|
const targetRequestId = options?.requestId?.trim() || tokenPayload.requestId.trim();
|
||||||
const targetRequestFromStore = requestMap.get(targetRequestId) ?? null;
|
const targetRequestFromStore = requestMap.get(targetRequestId) ?? null;
|
||||||
const placeholderTargetRequest = targetRequestFromStore ? null : buildManagedSharePlaceholderRequest(tokenPayload, conversation);
|
const placeholderTargetRequest = targetRequestFromStore ? null : buildManagedSharePlaceholderRequest(tokenPayload, conversation);
|
||||||
const targetRequest = targetRequestFromStore ?? placeholderTargetRequest;
|
const targetRequest = targetRequestFromStore ?? placeholderTargetRequest;
|
||||||
@@ -1185,6 +1313,28 @@ async function resolveManagedChatShareContext(token: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildManagedShareRoomRuntimeSnapshot(
|
||||||
|
snapshot: ReturnType<typeof chatRuntimeService.getSnapshot>,
|
||||||
|
sessionId: string,
|
||||||
|
) {
|
||||||
|
const normalizedSessionId = sessionId.trim();
|
||||||
|
const running = snapshot.running.filter((item) => item.sessionId === normalizedSessionId);
|
||||||
|
const queued = snapshot.queued.filter((item) => item.sessionId === normalizedSessionId);
|
||||||
|
const sessions = snapshot.sessions.filter((item) => item.sessionId === normalizedSessionId);
|
||||||
|
const recent = snapshot.recent.filter((item) => item.sessionId === normalizedSessionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedAt: snapshot.generatedAt,
|
||||||
|
runningCount: running.length,
|
||||||
|
queuedCount: queued.length,
|
||||||
|
sessionCount: sessions.length,
|
||||||
|
running,
|
||||||
|
queued,
|
||||||
|
sessions,
|
||||||
|
recent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function resolveChatSharePayloadFromManagedResource(
|
function resolveChatSharePayloadFromManagedResource(
|
||||||
managedResource:
|
managedResource:
|
||||||
| Awaited<ReturnType<typeof getSharedResourceTokenDetailBySharePath>>
|
| Awaited<ReturnType<typeof getSharedResourceTokenDetailBySharePath>>
|
||||||
@@ -1402,8 +1552,13 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestedRelativePath = normalizeChatResourceWildcard(wildcard);
|
const requestedRelativePath = normalizeChatResourceWildcard(wildcard);
|
||||||
const allowedSessionPrefix = path.posix.join('.codex_chat', tokenPayload.sessionId, 'resource') + '/';
|
const mappedSessionIds = managedContext.managedResource?.token.id
|
||||||
const isAllowedSessionResource = requestedRelativePath.startsWith(allowedSessionPrefix);
|
? await resolveChatShareTokenRoomSessionIds(managedContext.managedResource.token.id)
|
||||||
|
: [];
|
||||||
|
const allowedSessionIds = Array.from(new Set([tokenPayload.sessionId, ...mappedSessionIds].filter(Boolean)));
|
||||||
|
const isAllowedSessionResource = allowedSessionIds.some((sessionId) =>
|
||||||
|
requestedRelativePath.startsWith(path.posix.join('.codex_chat', sessionId, 'resource') + '/'),
|
||||||
|
);
|
||||||
const isAllowedManagedResource = requestedRelativePath.startsWith(RESOURCE_MANAGER_ROOT_PREFIX);
|
const isAllowedManagedResource = requestedRelativePath.startsWith(RESOURCE_MANAGER_ROOT_PREFIX);
|
||||||
|
|
||||||
if (!requestedRelativePath || (!isAllowedSessionResource && !isAllowedManagedResource)) {
|
if (!requestedRelativePath || (!isAllowedSessionResource && !isAllowedManagedResource)) {
|
||||||
@@ -1556,6 +1711,13 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await assignSharedResourceTokenToRequests(sessionId, [requestId], managedResourceTokenId);
|
await assignSharedResourceTokenToRequests(sessionId, [requestId], managedResourceTokenId);
|
||||||
|
await upsertChatShareTokenRoomMap({
|
||||||
|
tokenId: managedResourceTokenId,
|
||||||
|
sessionId,
|
||||||
|
rootRequestId: requestId,
|
||||||
|
isDefault: true,
|
||||||
|
createdByClientId: clientId || null,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -1582,11 +1744,144 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/rooms`, async (request, reply) => {
|
||||||
|
const params = z.object({
|
||||||
|
token: z.string().trim().min(1).max(16000),
|
||||||
|
}).parse(request.params ?? {});
|
||||||
|
const payload = z.object({
|
||||||
|
chatTypeId: z.string().trim().min(1).max(120),
|
||||||
|
chatTypeLabel: z.string().trim().min(1).max(200),
|
||||||
|
title: z.string().trim().min(1).max(200),
|
||||||
|
requestBadgeLabel: z.string().trim().max(120).optional().nullable(),
|
||||||
|
seedMessage: z.string().trim().min(1).max(20000),
|
||||||
|
}).parse(request.body ?? {});
|
||||||
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
|
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||||
|
|
||||||
|
if (!tokenPayload) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '공유 링크가 유효하지 않습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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: '이 공유 링크에는 새 채팅방을 추가할 권한이 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentToken = managedContext.managedResource.token;
|
||||||
|
const clientId = getRequestClientId(request);
|
||||||
|
const sessionId = createManagedChatShareSessionId();
|
||||||
|
const requestId = createManagedChatShareRequestId();
|
||||||
|
const { userMessageId } = createManagedChatShareMessageIds();
|
||||||
|
const createdAt = new Date().toISOString();
|
||||||
|
|
||||||
|
await createChatConversation({
|
||||||
|
sessionId,
|
||||||
|
clientId: clientId || null,
|
||||||
|
title: payload.title,
|
||||||
|
requestBadgeLabel: payload.requestBadgeLabel ?? null,
|
||||||
|
chatTypeId: payload.chatTypeId,
|
||||||
|
lastChatTypeId: payload.chatTypeId,
|
||||||
|
contextLabel: payload.chatTypeLabel,
|
||||||
|
contextDescription: null,
|
||||||
|
notifyOffline: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await appendChatConversationMessage(
|
||||||
|
{
|
||||||
|
sessionId,
|
||||||
|
clientId: clientId || null,
|
||||||
|
title: payload.title,
|
||||||
|
requestBadgeLabel: payload.requestBadgeLabel ?? null,
|
||||||
|
chatTypeId: payload.chatTypeId,
|
||||||
|
lastChatTypeId: payload.chatTypeId,
|
||||||
|
contextLabel: payload.chatTypeLabel,
|
||||||
|
contextDescription: null,
|
||||||
|
notifyOffline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sessionId,
|
||||||
|
messageId: userMessageId,
|
||||||
|
author: 'user',
|
||||||
|
text: payload.seedMessage,
|
||||||
|
timestamp: createdAt,
|
||||||
|
clientRequestId: requestId,
|
||||||
|
parts: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await upsertChatConversationRequest(sessionId, {
|
||||||
|
requestId,
|
||||||
|
requesterClientId: clientId || null,
|
||||||
|
chatTypeId: payload.chatTypeId,
|
||||||
|
chatTypeLabel: payload.chatTypeLabel,
|
||||||
|
requestOrigin: 'composer',
|
||||||
|
sharedResourceTokenId: currentToken.id,
|
||||||
|
status: 'completed',
|
||||||
|
statusMessage: '공유 채팅방 시작 요청을 준비했습니다.',
|
||||||
|
userMessageId,
|
||||||
|
userText: payload.seedMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
await assignSharedResourceTokenToRequests(sessionId, [requestId], currentToken.id);
|
||||||
|
await upsertChatShareTokenRoomMap({
|
||||||
|
tokenId: currentToken.id,
|
||||||
|
sessionId,
|
||||||
|
rootRequestId: requestId,
|
||||||
|
isDefault: false,
|
||||||
|
createdByClientId: clientId || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const room = await getChatShareTokenRoomMap(currentToken.id, sessionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
room: room
|
||||||
|
? mapResolvedShareRoomItem(room)
|
||||||
|
: {
|
||||||
|
sessionId,
|
||||||
|
requestId,
|
||||||
|
isDefault: false,
|
||||||
|
sortOrder: 0,
|
||||||
|
title: payload.title,
|
||||||
|
requestBadgeLabel: payload.requestBadgeLabel ?? null,
|
||||||
|
chatTypeId: payload.chatTypeId,
|
||||||
|
lastChatTypeId: payload.chatTypeId,
|
||||||
|
contextLabel: payload.chatTypeLabel,
|
||||||
|
contextDescription: null,
|
||||||
|
notifyOffline: true,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/room-settings`, async (request, reply) => {
|
app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/room-settings`, async (request, reply) => {
|
||||||
const params = z.object({
|
const params = z.object({
|
||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
}).parse(request.params ?? {});
|
}).parse(request.params ?? {});
|
||||||
const payload = z.object({
|
const payload = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
accessPin: z.string().regex(/^\d{4}$/u).optional().nullable(),
|
accessPin: z.string().regex(/^\d{4}$/u).optional().nullable(),
|
||||||
accessPinPromptTtlMinutes: z.number().int().min(0).max(7 * 24 * 60).optional().nullable(),
|
accessPinPromptTtlMinutes: z.number().int().min(0).max(7 * 24 * 60).optional().nullable(),
|
||||||
chatTypeId: z.string().trim().min(1).max(120).optional().nullable(),
|
chatTypeId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
@@ -1620,6 +1915,18 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const currentToken = managedContext.managedResource.token;
|
const currentToken = managedContext.managedResource.token;
|
||||||
|
|
||||||
if (!hasManagedShareAllowedApp(managedContext.managedResource, 'chat-room-settings')) {
|
if (!hasManagedShareAllowedApp(managedContext.managedResource, 'chat-room-settings')) {
|
||||||
@@ -1639,6 +1946,8 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
resourceType: currentToken.resourceType,
|
resourceType: currentToken.resourceType,
|
||||||
shareToken: currentToken.shareToken,
|
shareToken: currentToken.shareToken,
|
||||||
sharePath: currentToken.sharePath,
|
sharePath: currentToken.sharePath,
|
||||||
|
resourceAllowedAppIds: currentToken.allowedAppIds,
|
||||||
|
resourceAllowedAppIdsOverrideEnabled: currentToken.resourceAllowedAppIdsOverrideEnabled,
|
||||||
permissions: currentToken.permissions,
|
permissions: currentToken.permissions,
|
||||||
enabled: currentToken.enabled,
|
enabled: currentToken.enabled,
|
||||||
expiresAt: currentToken.expiresAt,
|
expiresAt: currentToken.expiresAt,
|
||||||
@@ -1654,10 +1963,10 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedConversation = await getChatConversation(tokenPayload.sessionId, getRequestClientId(request));
|
let updatedConversation = await getChatConversation(resolvedRoomContext.activeRoom.sessionId, getRequestClientId(request));
|
||||||
|
|
||||||
if (payload.chatTypeId || payload.title || payload.notifyOffline != null) {
|
if (payload.chatTypeId || payload.title || payload.notifyOffline != null) {
|
||||||
updatedConversation = await updateChatConversationContext(tokenPayload.sessionId, {
|
updatedConversation = await updateChatConversationContext(resolvedRoomContext.activeRoom.sessionId, {
|
||||||
clientId: getRequestClientId(request),
|
clientId: getRequestClientId(request),
|
||||||
chatTypeId: payload.chatTypeId?.trim() || undefined,
|
chatTypeId: payload.chatTypeId?.trim() || undefined,
|
||||||
lastChatTypeId: payload.chatTypeId?.trim() || undefined,
|
lastChatTypeId: payload.chatTypeId?.trim() || undefined,
|
||||||
@@ -1687,6 +1996,162 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get(`${CHAT_SHARE_ROUTE_PREFIX}/:token/runtime`, async (request, reply) => {
|
||||||
|
const params = z.object({
|
||||||
|
token: z.string().trim().min(1).max(16000),
|
||||||
|
}).parse(request.params ?? {});
|
||||||
|
const query = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
|
}).parse(request.query ?? {});
|
||||||
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
|
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||||
|
|
||||||
|
if (!tokenPayload) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '공유 링크가 유효하지 않습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const unavailableMessage = resolveManagedShareUnavailableMessage(managedContext.managedResource);
|
||||||
|
|
||||||
|
if (unavailableMessage) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: unavailableMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasManagedShareAllowedApp(managedContext.managedResource, 'chat-room-settings')) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에는 채팅방 설정 앱 권한이 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: query.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
item: buildManagedShareRoomRuntimeSnapshot(chatRuntimeService.getSnapshot(), resolvedRoomContext.activeRoom.sessionId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/runtime-requests/:requestId/cancel`, async (request, reply) => {
|
||||||
|
const params = z.object({
|
||||||
|
token: z.string().trim().min(1).max(16000),
|
||||||
|
requestId: z.string().trim().min(1).max(120),
|
||||||
|
}).parse(request.params ?? {});
|
||||||
|
const payload = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
|
}).parse(request.body ?? {});
|
||||||
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
|
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||||
|
|
||||||
|
if (!tokenPayload) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '공유 링크가 유효하지 않습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.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: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = getChatRuntimeController();
|
||||||
|
|
||||||
|
if (!controller) {
|
||||||
|
return reply.code(503).send({
|
||||||
|
message: '채팅 런타임 컨트롤러가 준비되지 않았습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const detail = controller.getJobDetail(params.requestId);
|
||||||
|
|
||||||
|
if (!detail.item || detail.item.sessionId !== resolvedRoomContext.activeRoom.sessionId) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '이 채팅방에서 취소할 처리중 세션을 찾지 못했습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.availableActions.remove) {
|
||||||
|
const removed = await controller.removeQueuedJob(params.requestId);
|
||||||
|
|
||||||
|
if (!removed) {
|
||||||
|
return reply.code(409).send({
|
||||||
|
message: '대기 요청 취소 처리에 실패했습니다. 상태를 새로고침한 뒤 다시 시도해 주세요.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
action: 'removed',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.availableActions.cancel) {
|
||||||
|
const cancelled = await controller.cancelJob(params.requestId);
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
return reply.code(409).send({
|
||||||
|
message: '실행 중 요청 취소 처리에 실패했습니다. 상태를 새로고침한 뒤 다시 시도해 주세요.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
action: 'cancelled',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.code(409).send({
|
||||||
|
message: '이미 종료된 요청이라 지금은 취소할 수 없습니다.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.post(`${CHAT_SHARE_ROUTE_PREFIX}`, async (request, reply) => {
|
app.post(`${CHAT_SHARE_ROUTE_PREFIX}`, async (request, reply) => {
|
||||||
const parsedPayload = z.object({
|
const parsedPayload = z.object({
|
||||||
kind: z.enum(['request-bundle', 'inquiry-message', 'prompt']),
|
kind: z.enum(['request-bundle', 'inquiry-message', 'prompt']),
|
||||||
@@ -1853,6 +2318,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
const params = z.object({
|
const params = z.object({
|
||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
}).parse(request.params ?? {});
|
}).parse(request.params ?? {});
|
||||||
|
const query = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional(),
|
||||||
|
}).parse(request.query ?? {});
|
||||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||||
|
|
||||||
@@ -1862,14 +2330,6 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareSnapshot = await buildChatShareSnapshot(tokenPayload);
|
|
||||||
|
|
||||||
if (!shareSnapshot) {
|
|
||||||
return reply.code(404).send({
|
|
||||||
message: '공유 대상을 찾을 수 없습니다.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const unavailableMessage = resolveManagedShareUnavailableMessage(managedContext.managedResource);
|
const unavailableMessage = resolveManagedShareUnavailableMessage(managedContext.managedResource);
|
||||||
|
|
||||||
if (unavailableMessage) {
|
if (unavailableMessage) {
|
||||||
@@ -1882,6 +2342,31 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: query.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeRoom = resolvedRoomContext.activeRoom;
|
||||||
|
|
||||||
|
if (!activeRoom) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '공유 채팅방 목록을 찾을 수 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareSnapshot = await buildChatShareSnapshot(tokenPayload, {
|
||||||
|
sessionId: activeRoom.sessionId,
|
||||||
|
requestId: activeRoom.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shareSnapshot) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '공유 대상을 찾을 수 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const accessPinStatus = await validateSharedResourceAccessPinBySharePath(managedContext.sharePath, getRequestChatSharePin(request), {
|
const accessPinStatus = await validateSharedResourceAccessPinBySharePath(managedContext.sharePath, getRequestChatSharePin(request), {
|
||||||
clientId: getRequestClientId(request),
|
clientId: getRequestClientId(request),
|
||||||
});
|
});
|
||||||
@@ -1904,8 +2389,8 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
ok: true,
|
ok: true,
|
||||||
share: {
|
share: {
|
||||||
kind: tokenPayload.kind,
|
kind: tokenPayload.kind,
|
||||||
sessionId: tokenPayload.sessionId,
|
sessionId: activeRoom.sessionId,
|
||||||
requestId: tokenPayload.requestId,
|
requestId: activeRoom.requestId,
|
||||||
sharePath: resolveChatSharePath(params.token),
|
sharePath: resolveChatSharePath(params.token),
|
||||||
createdAt: managedContext.managedResource?.token.createdAt ?? null,
|
createdAt: managedContext.managedResource?.token.createdAt ?? null,
|
||||||
expiresAt: effectiveExpiresAt,
|
expiresAt: effectiveExpiresAt,
|
||||||
@@ -1928,8 +2413,8 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
blockedReason,
|
blockedReason,
|
||||||
},
|
},
|
||||||
conversation: {
|
conversation: {
|
||||||
sessionId: shareSnapshot.conversation?.sessionId ?? tokenPayload.sessionId,
|
sessionId: shareSnapshot.conversation?.sessionId ?? activeRoom.sessionId,
|
||||||
title: shareSnapshot.conversation?.title ?? '공유 채팅',
|
title: shareSnapshot.conversation?.title ?? activeRoom.title ?? '공유 채팅',
|
||||||
requestBadgeLabel: shareSnapshot.conversation?.requestBadgeLabel ?? null,
|
requestBadgeLabel: shareSnapshot.conversation?.requestBadgeLabel ?? null,
|
||||||
chatTypeId: shareSnapshot.conversation?.chatTypeId ?? null,
|
chatTypeId: shareSnapshot.conversation?.chatTypeId ?? null,
|
||||||
lastChatTypeId: shareSnapshot.conversation?.lastChatTypeId ?? null,
|
lastChatTypeId: shareSnapshot.conversation?.lastChatTypeId ?? null,
|
||||||
@@ -1943,6 +2428,8 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
messages: shareSnapshot.messages,
|
messages: shareSnapshot.messages,
|
||||||
activityLogs: shareSnapshot.activityLogs,
|
activityLogs: shareSnapshot.activityLogs,
|
||||||
roomRequestCounts: shareSnapshot.roomRequestCounts,
|
roomRequestCounts: shareSnapshot.roomRequestCounts,
|
||||||
|
rooms: resolvedRoomContext.rooms,
|
||||||
|
activeSessionId: activeRoom.sessionId,
|
||||||
promptTarget: shareSnapshot.promptTarget,
|
promptTarget: shareSnapshot.promptTarget,
|
||||||
refreshedAt: new Date().toISOString(),
|
refreshedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -1952,6 +2439,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
const params = z.object({
|
const params = z.object({
|
||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
}).parse(request.params ?? {});
|
}).parse(request.params ?? {});
|
||||||
|
const payload = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
|
}).parse(request.body ?? {});
|
||||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||||
|
|
||||||
@@ -1967,7 +2457,22 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareSnapshot = await buildChatShareSnapshot(tokenPayload);
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareSnapshot = await buildChatShareSnapshot(tokenPayload, {
|
||||||
|
sessionId: resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
requestId: resolvedRoomContext.activeRoom.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!shareSnapshot) {
|
if (!shareSnapshot) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -1995,9 +2500,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveChatService()?.resetSessionData(tokenPayload.sessionId);
|
getActiveChatService()?.resetSessionData(resolvedRoomContext.activeRoom.sessionId);
|
||||||
chatRuntimeService.clearSession(tokenPayload.sessionId);
|
chatRuntimeService.clearSession(resolvedRoomContext.activeRoom.sessionId);
|
||||||
const item = await clearChatConversationData(tokenPayload.sessionId, null);
|
const item = await clearChatConversationData(resolvedRoomContext.activeRoom.sessionId, null);
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -2009,7 +2514,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
|
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
|
||||||
actorLabel: 'share-viewer',
|
actorLabel: 'share-viewer',
|
||||||
summary: '공유 채팅에서 채팅방 기록을 초기화했습니다.',
|
summary: '공유 채팅에서 채팅방 기록을 초기화했습니다.',
|
||||||
detail: tokenPayload.sessionId,
|
detail: resolvedRoomContext.activeRoom.sessionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2027,6 +2532,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
text: z.string().trim().min(1).max(20000),
|
text: z.string().trim().min(1).max(20000),
|
||||||
mode: z.enum(['queue', 'direct']).optional(),
|
mode: z.enum(['queue', 'direct']).optional(),
|
||||||
parentRequestId: z.string().trim().min(1).max(120).optional().nullable(),
|
parentRequestId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
}).parse(request.body ?? {});
|
}).parse(request.body ?? {});
|
||||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||||
@@ -2043,7 +2549,22 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareSnapshot = await buildChatShareSnapshot(tokenPayload);
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareSnapshot = await buildChatShareSnapshot(tokenPayload, {
|
||||||
|
sessionId: resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
requestId: resolvedRoomContext.activeRoom.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!shareSnapshot) {
|
if (!shareSnapshot) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -2100,13 +2621,17 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const queuedRequestId = await getActiveChatService()?.submitExternalMessage(tokenPayload.sessionId, payload.text, {
|
const queuedRequestId = await getActiveChatService()?.submitExternalMessage(
|
||||||
|
resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
payload.text,
|
||||||
|
{
|
||||||
mode: payload.mode === 'direct' ? 'direct' : 'queue',
|
mode: payload.mode === 'direct' ? 'direct' : 'queue',
|
||||||
requestOrigin: 'composer',
|
requestOrigin: 'composer',
|
||||||
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
||||||
parentRequestId: resolvedParentRequestId,
|
parentRequestId: resolvedParentRequestId,
|
||||||
clientId: shareSnapshot.targetRequest.requesterClientId ?? shareSnapshot.conversation?.clientId ?? null,
|
clientId: shareSnapshot.targetRequest.requesterClientId ?? shareSnapshot.conversation?.clientId ?? null,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!queuedRequestId) {
|
if (!queuedRequestId) {
|
||||||
return reply.code(503).send({
|
return reply.code(503).send({
|
||||||
@@ -2133,6 +2658,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
}).parse(request.params ?? {});
|
}).parse(request.params ?? {});
|
||||||
const payload = z.object({
|
const payload = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
fileName: z.string().trim().max(255).optional(),
|
fileName: z.string().trim().max(255).optional(),
|
||||||
mimeType: z.string().trim().max(200).optional(),
|
mimeType: z.string().trim().max(200).optional(),
|
||||||
contentBase64: z.string().trim().min(1).max(CHAT_ATTACHMENT_ROUTE_BODY_LIMIT * 2),
|
contentBase64: z.string().trim().min(1).max(CHAT_ATTACHMENT_ROUTE_BODY_LIMIT * 2),
|
||||||
@@ -2146,7 +2672,22 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareSnapshot = await buildChatShareSnapshot(tokenPayload);
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareSnapshot = await buildChatShareSnapshot(tokenPayload, {
|
||||||
|
sessionId: resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
requestId: resolvedRoomContext.activeRoom.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!shareSnapshot) {
|
if (!shareSnapshot) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -2186,7 +2727,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saved = await saveChatAttachmentFile({
|
const saved = await saveChatAttachmentFile({
|
||||||
sessionId: tokenPayload.sessionId,
|
sessionId: resolvedRoomContext.activeRoom.sessionId,
|
||||||
fileName: payload.fileName,
|
fileName: payload.fileName,
|
||||||
mimeType: payload.mimeType,
|
mimeType: payload.mimeType,
|
||||||
contentBase64: payload.contentBase64,
|
contentBase64: payload.contentBase64,
|
||||||
@@ -2217,6 +2758,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
}).parse(request.params ?? {});
|
}).parse(request.params ?? {});
|
||||||
const payload = z.object({
|
const payload = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
parentRequestId: z.string().trim().min(1).max(120),
|
parentRequestId: z.string().trim().min(1).max(120),
|
||||||
promptIndex: z.number().int().min(0).max(99),
|
promptIndex: z.number().int().min(0).max(99),
|
||||||
promptTitle: z.string().trim().min(1).max(500),
|
promptTitle: z.string().trim().min(1).max(500),
|
||||||
@@ -2248,7 +2790,22 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareSnapshot = await buildChatShareSnapshot(tokenPayload);
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareSnapshot = await buildChatShareSnapshot(tokenPayload, {
|
||||||
|
sessionId: resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
requestId: resolvedRoomContext.activeRoom.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!shareSnapshot) {
|
if (!shareSnapshot) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -2313,19 +2870,23 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingPromptRequest = await findExistingActivePromptFollowupRequest(
|
const existingPromptRequest = await findExistingActivePromptFollowupRequest(
|
||||||
tokenPayload.sessionId,
|
resolvedRoomContext.activeRoom.sessionId,
|
||||||
normalizedParentRequestId,
|
normalizedParentRequestId,
|
||||||
payload.followupText,
|
payload.followupText,
|
||||||
);
|
);
|
||||||
|
|
||||||
const queuedRequestId = existingPromptRequest?.requestId ?? await getActiveChatService()?.submitExternalMessage(tokenPayload.sessionId, payload.followupText, {
|
const queuedRequestId = existingPromptRequest?.requestId ?? await getActiveChatService()?.submitExternalMessage(
|
||||||
|
resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
payload.followupText,
|
||||||
|
{
|
||||||
mode: resolvePromptFollowupMode(payload.mode),
|
mode: resolvePromptFollowupMode(payload.mode),
|
||||||
requestOrigin: 'prompt',
|
requestOrigin: 'prompt',
|
||||||
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
||||||
parentRequestId: normalizedParentRequestId,
|
parentRequestId: normalizedParentRequestId,
|
||||||
promptContextRef: payload.contextRef ?? null,
|
promptContextRef: payload.contextRef ?? null,
|
||||||
clientId: shareSnapshot.targetRequest.requesterClientId ?? shareSnapshot.conversation?.clientId ?? null,
|
clientId: shareSnapshot.targetRequest.requesterClientId ?? shareSnapshot.conversation?.clientId ?? null,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!queuedRequestId) {
|
if (!queuedRequestId) {
|
||||||
return reply.code(503).send({
|
return reply.code(503).send({
|
||||||
@@ -2333,7 +2894,11 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const persisted = await persistChatConversationPromptSelection(tokenPayload.sessionId, normalizedParentRequestId, payload);
|
const persisted = await persistChatConversationPromptSelection(
|
||||||
|
resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
normalizedParentRequestId,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
if (!persisted) {
|
if (!persisted) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -2341,8 +2906,8 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveChatService()?.broadcastRequestUpdate(tokenPayload.sessionId, persisted.request);
|
getActiveChatService()?.broadcastRequestUpdate(resolvedRoomContext.activeRoom.sessionId, persisted.request);
|
||||||
getActiveChatService()?.broadcastMessageUpdate(tokenPayload.sessionId, persisted.message);
|
getActiveChatService()?.broadcastMessageUpdate(resolvedRoomContext.activeRoom.sessionId, persisted.message);
|
||||||
|
|
||||||
if (managedContext.managedResource) {
|
if (managedContext.managedResource) {
|
||||||
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
|
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
|
||||||
@@ -2365,6 +2930,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
}).parse(request.params ?? {});
|
}).parse(request.params ?? {});
|
||||||
const payload = z.object({
|
const payload = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
parentRequestId: z.string().trim().min(1).max(120),
|
parentRequestId: z.string().trim().min(1).max(120),
|
||||||
type: z.enum(['prompt', 'verification']),
|
type: z.enum(['prompt', 'verification']),
|
||||||
}).parse(request.body ?? {});
|
}).parse(request.body ?? {});
|
||||||
@@ -2377,7 +2943,22 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareSnapshot = await buildChatShareSnapshot(tokenPayload);
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareSnapshot = await buildChatShareSnapshot(tokenPayload, {
|
||||||
|
sessionId: resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
requestId: resolvedRoomContext.activeRoom.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!shareSnapshot) {
|
if (!shareSnapshot) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -2425,7 +3006,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
item = await markChatConversationRequestManualCompletion(
|
item = await markChatConversationRequestManualCompletion(
|
||||||
tokenPayload.sessionId,
|
resolvedRoomContext.activeRoom.sessionId,
|
||||||
normalizedParentRequestId,
|
normalizedParentRequestId,
|
||||||
payload.type,
|
payload.type,
|
||||||
);
|
);
|
||||||
@@ -2445,7 +3026,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveChatService()?.broadcastRequestUpdate(tokenPayload.sessionId, item);
|
getActiveChatService()?.broadcastRequestUpdate(resolvedRoomContext.activeRoom.sessionId, item);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -2458,6 +3039,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
}).parse(request.params ?? {});
|
}).parse(request.params ?? {});
|
||||||
const payload = z.object({
|
const payload = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
parentRequestId: z.string().trim().min(1).max(120),
|
parentRequestId: z.string().trim().min(1).max(120),
|
||||||
}).parse(request.body ?? {});
|
}).parse(request.body ?? {});
|
||||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
@@ -2469,7 +3051,22 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareSnapshot = await buildChatShareSnapshot(tokenPayload);
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareSnapshot = await buildChatShareSnapshot(tokenPayload, {
|
||||||
|
sessionId: resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
requestId: resolvedRoomContext.activeRoom.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!shareSnapshot) {
|
if (!shareSnapshot) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -2527,7 +3124,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await cancelUnansweredChatConversationRequest(
|
const result = await cancelUnansweredChatConversationRequest(
|
||||||
tokenPayload.sessionId,
|
resolvedRoomContext.activeRoom.sessionId,
|
||||||
normalizedParentRequestId,
|
normalizedParentRequestId,
|
||||||
'사용자 요청으로 중단된 요청을 취소 처리했습니다.',
|
'사용자 요청으로 중단된 요청을 취소 처리했습니다.',
|
||||||
);
|
);
|
||||||
@@ -2556,7 +3153,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveChatService()?.broadcastRequestUpdate(tokenPayload.sessionId, result.item);
|
getActiveChatService()?.broadcastRequestUpdate(resolvedRoomContext.activeRoom.sessionId, result.item);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -2569,6 +3166,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
}).parse(request.params ?? {});
|
}).parse(request.params ?? {});
|
||||||
const payload = z.object({
|
const payload = z.object({
|
||||||
|
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||||
parentRequestId: z.string().trim().min(1).max(120),
|
parentRequestId: z.string().trim().min(1).max(120),
|
||||||
}).parse(request.body ?? {});
|
}).parse(request.body ?? {});
|
||||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
@@ -2580,7 +3178,22 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareSnapshot = await buildChatShareSnapshot(tokenPayload);
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: payload.sessionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareSnapshot = await buildChatShareSnapshot(tokenPayload, {
|
||||||
|
sessionId: resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
requestId: resolvedRoomContext.activeRoom.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!shareSnapshot) {
|
if (!shareSnapshot) {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
@@ -2656,14 +3269,18 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const queuedRequestId = await getActiveChatService()?.submitExternalMessage(tokenPayload.sessionId, normalizedUserText, {
|
const queuedRequestId = await getActiveChatService()?.submitExternalMessage(
|
||||||
|
resolvedRoomContext.activeRoom.sessionId,
|
||||||
|
normalizedUserText,
|
||||||
|
{
|
||||||
requestId: normalizedParentRequestId,
|
requestId: normalizedParentRequestId,
|
||||||
mode: 'direct',
|
mode: 'direct',
|
||||||
requestOrigin: targetRequest.requestOrigin === 'prompt' ? 'prompt' : 'composer',
|
requestOrigin: targetRequest.requestOrigin === 'prompt' ? 'prompt' : 'composer',
|
||||||
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
||||||
parentRequestId: targetRequest.requestOrigin === 'prompt' ? targetRequest.parentRequestId?.trim() || null : null,
|
parentRequestId: targetRequest.requestOrigin === 'prompt' ? targetRequest.parentRequestId?.trim() || null : null,
|
||||||
clientId: targetRequest.requesterClientId ?? shareSnapshot.conversation?.clientId ?? null,
|
clientId: targetRequest.requesterClientId ?? shareSnapshot.conversation?.clientId ?? null,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!queuedRequestId) {
|
if (!queuedRequestId) {
|
||||||
return reply.code(503).send({
|
return reply.code(503).send({
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { db } from '../db/client.js';
|
||||||
|
import {
|
||||||
|
CHAT_CONVERSATION_TABLE,
|
||||||
|
ensureChatConversationTables,
|
||||||
|
} from './chat-room-service.js';
|
||||||
|
|
||||||
|
const CHAT_SHARE_TOKEN_ROOM_MAP_TABLE = 'chat_share_token_room_maps';
|
||||||
|
|
||||||
|
export type ChatShareTokenRoomMapItem = {
|
||||||
|
tokenId: string;
|
||||||
|
sessionId: string;
|
||||||
|
rootRequestId: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
createdByClientId: string | null;
|
||||||
|
title: string;
|
||||||
|
requestBadgeLabel: string | null;
|
||||||
|
chatTypeId: string | null;
|
||||||
|
lastChatTypeId: string | null;
|
||||||
|
contextLabel: string | null;
|
||||||
|
contextDescription: string | null;
|
||||||
|
notifyOffline: boolean;
|
||||||
|
createdAt: string | null;
|
||||||
|
updatedAt: string | null;
|
||||||
|
conversationUpdatedAt: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeOptionalText(value: unknown) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value.trim();
|
||||||
|
return normalized || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRequiredText(value: unknown) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBoolean(value: unknown) {
|
||||||
|
return value === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeInteger(value: unknown, fallback = 0) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.trunc(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateTime(value: unknown) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim();
|
||||||
|
return normalized || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapChatShareTokenRoomRow(row: Record<string, unknown>): ChatShareTokenRoomMapItem {
|
||||||
|
return {
|
||||||
|
tokenId: normalizeRequiredText(row.shared_resource_token_id),
|
||||||
|
sessionId: normalizeRequiredText(row.session_id),
|
||||||
|
rootRequestId: normalizeRequiredText(row.root_request_id),
|
||||||
|
isDefault: normalizeBoolean(row.is_default),
|
||||||
|
sortOrder: normalizeInteger(row.sort_order),
|
||||||
|
createdByClientId: normalizeOptionalText(row.created_by_client_id),
|
||||||
|
title: normalizeRequiredText(row.title) || '공유 채팅방',
|
||||||
|
requestBadgeLabel: normalizeOptionalText(row.request_badge_label),
|
||||||
|
chatTypeId: normalizeOptionalText(row.chat_type_id),
|
||||||
|
lastChatTypeId: normalizeOptionalText(row.last_chat_type_id),
|
||||||
|
contextLabel: normalizeOptionalText(row.context_label),
|
||||||
|
contextDescription: normalizeOptionalText(row.context_description),
|
||||||
|
notifyOffline: normalizeBoolean(row.notify_offline),
|
||||||
|
createdAt: normalizeDateTime(row.created_at),
|
||||||
|
updatedAt: normalizeDateTime(row.updated_at),
|
||||||
|
conversationUpdatedAt: normalizeDateTime(row.conversation_updated_at),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureChatShareTokenRoomMapTable() {
|
||||||
|
await ensureChatConversationTables();
|
||||||
|
|
||||||
|
const hasTable = await db.schema.hasTable(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE);
|
||||||
|
|
||||||
|
if (!hasTable) {
|
||||||
|
await db.schema.createTable(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE, (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('shared_resource_token_id', 120).notNullable().index();
|
||||||
|
table.string('session_id', 120).notNullable().index();
|
||||||
|
table.string('root_request_id', 120).notNullable();
|
||||||
|
table.boolean('is_default').notNullable().defaultTo(false);
|
||||||
|
table.integer('sort_order').notNullable().defaultTo(0);
|
||||||
|
table.string('created_by_client_id', 120).nullable();
|
||||||
|
table.timestamp('archived_at', { useTz: true }).nullable().index();
|
||||||
|
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
|
||||||
|
table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
|
||||||
|
table.unique(['shared_resource_token_id', 'session_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredColumns: Array<[string, (table: any) => void]> = [
|
||||||
|
['shared_resource_token_id', (table) => table.string('shared_resource_token_id', 120).notNullable().index()],
|
||||||
|
['session_id', (table) => table.string('session_id', 120).notNullable().index()],
|
||||||
|
['root_request_id', (table) => table.string('root_request_id', 120).notNullable().defaultTo('')],
|
||||||
|
['is_default', (table) => table.boolean('is_default').notNullable().defaultTo(false)],
|
||||||
|
['sort_order', (table) => table.integer('sort_order').notNullable().defaultTo(0)],
|
||||||
|
['created_by_client_id', (table) => table.string('created_by_client_id', 120).nullable()],
|
||||||
|
['archived_at', (table) => table.timestamp('archived_at', { useTz: true }).nullable().index()],
|
||||||
|
['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
|
||||||
|
['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [columnName, createColumn] of requiredColumns) {
|
||||||
|
const hasColumn = await db.schema.hasColumn(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE, columnName);
|
||||||
|
|
||||||
|
if (!hasColumn) {
|
||||||
|
await db.schema.alterTable(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE, (table) => {
|
||||||
|
createColumn(table);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listChatShareTokenRoomMaps(tokenId: string) {
|
||||||
|
const normalizedTokenId = tokenId.trim();
|
||||||
|
|
||||||
|
if (!normalizedTokenId) {
|
||||||
|
return [] as ChatShareTokenRoomMapItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureChatShareTokenRoomMapTable();
|
||||||
|
|
||||||
|
const rows = await db(`${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.is_default', 'desc')
|
||||||
|
.orderBy('room_map.sort_order', 'asc')
|
||||||
|
.orderBy('room_map.created_at', 'asc');
|
||||||
|
|
||||||
|
return rows.map((row) => mapChatShareTokenRoomRow(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChatShareTokenRoomMap(tokenId: string, sessionId: string) {
|
||||||
|
const normalizedTokenId = tokenId.trim();
|
||||||
|
const normalizedSessionId = sessionId.trim();
|
||||||
|
|
||||||
|
if (!normalizedTokenId || !normalizedSessionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rooms = await listChatShareTokenRoomMaps(normalizedTokenId);
|
||||||
|
return rooms.find((item) => item.sessionId === normalizedSessionId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertChatShareTokenRoomMap(args: {
|
||||||
|
tokenId: string;
|
||||||
|
sessionId: string;
|
||||||
|
rootRequestId: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
sortOrder?: number | null;
|
||||||
|
createdByClientId?: string | null;
|
||||||
|
}) {
|
||||||
|
const normalizedTokenId = args.tokenId.trim();
|
||||||
|
const normalizedSessionId = args.sessionId.trim();
|
||||||
|
const normalizedRootRequestId = args.rootRequestId.trim();
|
||||||
|
|
||||||
|
if (!normalizedTokenId || !normalizedSessionId || !normalizedRootRequestId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureChatShareTokenRoomMapTable();
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
const current = await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
|
||||||
|
.where({
|
||||||
|
shared_resource_token_id: normalizedTokenId,
|
||||||
|
session_id: normalizedSessionId,
|
||||||
|
})
|
||||||
|
.whereNull('archived_at')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const maxSortOrderRow = await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
|
||||||
|
.where({ shared_resource_token_id: normalizedTokenId })
|
||||||
|
.whereNull('archived_at')
|
||||||
|
.max<{ max_sort_order?: number | string | null }>('sort_order as max_sort_order')
|
||||||
|
.first();
|
||||||
|
const nextSortOrder = args.sortOrder != null
|
||||||
|
? Math.max(0, Math.trunc(Number(args.sortOrder) || 0))
|
||||||
|
: Math.max(0, normalizeInteger(maxSortOrderRow?.max_sort_order) + (current ? 0 : 1));
|
||||||
|
|
||||||
|
if (args.isDefault === true) {
|
||||||
|
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
|
||||||
|
.where({ shared_resource_token_id: normalizedTokenId })
|
||||||
|
.whereNull('archived_at')
|
||||||
|
.update({
|
||||||
|
is_default: false,
|
||||||
|
updated_at: db.fn.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
shared_resource_token_id: normalizedTokenId,
|
||||||
|
session_id: normalizedSessionId,
|
||||||
|
root_request_id: normalizedRootRequestId,
|
||||||
|
is_default: args.isDefault === true,
|
||||||
|
sort_order: nextSortOrder,
|
||||||
|
created_by_client_id: normalizeOptionalText(args.createdByClientId),
|
||||||
|
archived_at: null,
|
||||||
|
updated_at: db.fn.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
|
||||||
|
.where({
|
||||||
|
shared_resource_token_id: normalizedTokenId,
|
||||||
|
session_id: normalizedSessionId,
|
||||||
|
})
|
||||||
|
.update(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE).insert({
|
||||||
|
...payload,
|
||||||
|
created_at: db.fn.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return getChatShareTokenRoomMap(normalizedTokenId, normalizedSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureDefaultChatShareTokenRoomMap(args: {
|
||||||
|
tokenId: string;
|
||||||
|
sessionId: string;
|
||||||
|
rootRequestId: string;
|
||||||
|
createdByClientId?: string | null;
|
||||||
|
}) {
|
||||||
|
const normalizedTokenId = args.tokenId.trim();
|
||||||
|
const normalizedSessionId = args.sessionId.trim();
|
||||||
|
const normalizedRootRequestId = args.rootRequestId.trim();
|
||||||
|
|
||||||
|
if (!normalizedTokenId || !normalizedSessionId || !normalizedRootRequestId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await getChatShareTokenRoomMap(normalizedTokenId, normalizedSessionId);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
await upsertChatShareTokenRoomMap({
|
||||||
|
tokenId: normalizedTokenId,
|
||||||
|
sessionId: normalizedSessionId,
|
||||||
|
rootRequestId: normalizedRootRequestId,
|
||||||
|
isDefault: true,
|
||||||
|
createdByClientId: args.createdByClientId ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rooms = await listChatShareTokenRoomMaps(normalizedTokenId);
|
||||||
|
|
||||||
|
if (rooms.some((item) => item.isDefault)) {
|
||||||
|
return rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
await upsertChatShareTokenRoomMap({
|
||||||
|
tokenId: normalizedTokenId,
|
||||||
|
sessionId: normalizedSessionId,
|
||||||
|
rootRequestId: normalizedRootRequestId,
|
||||||
|
isDefault: true,
|
||||||
|
createdByClientId: args.createdByClientId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return listChatShareTokenRoomMaps(normalizedTokenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveChatShareTokenRoomSessionIds(tokenId: string) {
|
||||||
|
const rooms = await listChatShareTokenRoomMaps(tokenId);
|
||||||
|
return rooms.map((item) => item.sessionId).filter(Boolean);
|
||||||
|
}
|
||||||
@@ -1901,6 +1901,54 @@ export async function fetchChatRuntimeSnapshot() {
|
|||||||
return response.item;
|
return response.item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchChatShareRuntimeSnapshot(
|
||||||
|
token: string,
|
||||||
|
options?: {
|
||||||
|
sessionId?: string | null;
|
||||||
|
sharePin?: string | null;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
const normalizedSessionId = options?.sessionId?.trim() || '';
|
||||||
|
|
||||||
|
if (normalizedSessionId) {
|
||||||
|
query.set('sessionId', normalizedSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await requestChatApi<{ ok: boolean; item: ChatRuntimeSnapshot }>(
|
||||||
|
`/shares/${encodeURIComponent(token)}/runtime${query.size > 0 ? `?${query.toString()}` : ''}`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
allowUnauthenticated: true,
|
||||||
|
sharePin: options?.sharePin,
|
||||||
|
timeoutMs: 20000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelChatShareRuntimeRequest(
|
||||||
|
token: string,
|
||||||
|
payload: {
|
||||||
|
requestId: string;
|
||||||
|
sessionId?: string | null;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const response = await requestChatApi<{ ok: boolean; action: 'cancelled' | 'removed' }>(
|
||||||
|
`/shares/${encodeURIComponent(token)}/runtime-requests/${encodeURIComponent(payload.requestId)}/cancel`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionId: payload.sessionId?.trim() || undefined,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
allowUnauthenticated: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.action;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchChatSourceChanges(limit = 300) {
|
export async function fetchChatSourceChanges(limit = 300) {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
query.set('limit', String(Math.max(1, Math.min(500, Math.round(limit)))));
|
query.set('limit', String(Math.max(1, Math.min(500, Math.round(limit)))));
|
||||||
@@ -2268,11 +2316,14 @@ export async function clearChatConversationRoom(sessionId: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearChatShareConversationRoom(token: string) {
|
export async function clearChatShareConversationRoom(token: string, sessionId?: string | null) {
|
||||||
const response = await requestChatApi<{ ok: boolean; item: ChatConversationSummary }>(
|
const response = await requestChatApi<{ ok: boolean; item: ChatConversationSummary }>(
|
||||||
`/shares/${encodeURIComponent(token)}/clear`,
|
`/shares/${encodeURIComponent(token)}/clear`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionId: sessionId?.trim() || undefined,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
allowUnauthenticated: true,
|
allowUnauthenticated: true,
|
||||||
@@ -2282,6 +2333,44 @@ export async function clearChatShareConversationRoom(token: string) {
|
|||||||
return response.item;
|
return response.item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createChatShareRoom(
|
||||||
|
token: string,
|
||||||
|
payload: {
|
||||||
|
chatTypeId: string;
|
||||||
|
chatTypeLabel: string;
|
||||||
|
title: string;
|
||||||
|
requestBadgeLabel?: string | null;
|
||||||
|
seedMessage: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const response = await requestChatApi<{ ok: boolean; room: ChatShareRoomSummary }>(
|
||||||
|
`/shares/${encodeURIComponent(token)}/rooms`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
allowUnauthenticated: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: normalizeRequiredText(response.room.sessionId),
|
||||||
|
requestId: normalizeRequiredText(response.room.requestId),
|
||||||
|
isDefault: response.room.isDefault === true,
|
||||||
|
sortOrder: Number.isFinite(response.room.sortOrder) ? Number(response.room.sortOrder) : 0,
|
||||||
|
title: normalizeRequiredText(response.room.title) || '공유 채팅방',
|
||||||
|
requestBadgeLabel: normalizeOptionalText(response.room.requestBadgeLabel),
|
||||||
|
chatTypeId: normalizeOptionalText(response.room.chatTypeId),
|
||||||
|
lastChatTypeId: normalizeOptionalText(response.room.lastChatTypeId),
|
||||||
|
contextLabel: normalizeOptionalText(response.room.contextLabel),
|
||||||
|
contextDescription: normalizeOptionalText(response.room.contextDescription),
|
||||||
|
notifyOffline: response.room.notifyOffline === true,
|
||||||
|
createdAt: normalizeOptionalText(response.room.createdAt),
|
||||||
|
updatedAt: normalizeOptionalText(response.room.updatedAt),
|
||||||
|
} satisfies ChatShareRoomSummary;
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteChatConversationRequest(sessionId: string, requestId: string) {
|
export async function deleteChatConversationRequest(sessionId: string, requestId: string) {
|
||||||
const response = await requestChatApi<{ ok: boolean; deleted: boolean; sessionId: string; requestId: string }>(
|
const response = await requestChatApi<{ ok: boolean; deleted: boolean; sessionId: string; requestId: string }>(
|
||||||
`/conversations/${encodeURIComponent(sessionId)}/requests/${encodeURIComponent(requestId)}`,
|
`/conversations/${encodeURIComponent(sessionId)}/requests/${encodeURIComponent(requestId)}`,
|
||||||
@@ -2313,6 +2402,7 @@ export async function persistChatPromptSelection(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
payload: {
|
payload: {
|
||||||
parentRequestId: string;
|
parentRequestId: string;
|
||||||
|
sessionId?: string | null;
|
||||||
promptIndex: number;
|
promptIndex: number;
|
||||||
promptTitle: string;
|
promptTitle: string;
|
||||||
promptSignature: string;
|
promptSignature: string;
|
||||||
@@ -2391,6 +2481,22 @@ export async function submitChatPromptSelection(
|
|||||||
|
|
||||||
export type ChatShareKind = 'request-bundle' | 'inquiry-message' | 'prompt';
|
export type ChatShareKind = 'request-bundle' | 'inquiry-message' | 'prompt';
|
||||||
|
|
||||||
|
export type ChatShareRoomSummary = {
|
||||||
|
sessionId: string;
|
||||||
|
requestId: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
title: string;
|
||||||
|
requestBadgeLabel?: string | null;
|
||||||
|
chatTypeId?: string | null;
|
||||||
|
lastChatTypeId?: string | null;
|
||||||
|
contextLabel?: string | null;
|
||||||
|
contextDescription?: string | null;
|
||||||
|
notifyOffline?: boolean;
|
||||||
|
createdAt?: string | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ChatShareSnapshot = {
|
export type ChatShareSnapshot = {
|
||||||
share: {
|
share: {
|
||||||
kind: ChatShareKind;
|
kind: ChatShareKind;
|
||||||
@@ -2432,6 +2538,8 @@ export type ChatShareSnapshot = {
|
|||||||
requests: ChatConversationRequest[];
|
requests: ChatConversationRequest[];
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
activityLogs: ChatConversationActivityLog[];
|
activityLogs: ChatConversationActivityLog[];
|
||||||
|
rooms: ChatShareRoomSummary[];
|
||||||
|
activeSessionId?: string | null;
|
||||||
roomRequestCounts?: {
|
roomRequestCounts?: {
|
||||||
processingCount: number;
|
processingCount: number;
|
||||||
unansweredCount: number;
|
unansweredCount: number;
|
||||||
@@ -2565,6 +2673,7 @@ export async function createManagedChatShareRoom(payload: ManagedChatShareRoomDr
|
|||||||
export async function saveChatShareRoomSettings(
|
export async function saveChatShareRoomSettings(
|
||||||
token: string,
|
token: string,
|
||||||
input: {
|
input: {
|
||||||
|
sessionId?: string | null;
|
||||||
accessPin?: string | null;
|
accessPin?: string | null;
|
||||||
accessPinPromptTtlMinutes?: number | null;
|
accessPinPromptTtlMinutes?: number | null;
|
||||||
chatTypeId?: string | null;
|
chatTypeId?: string | null;
|
||||||
@@ -2592,6 +2701,7 @@ export async function saveChatShareRoomSettings(
|
|||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
sessionId: input.sessionId,
|
||||||
accessPin: input.accessPin,
|
accessPin: input.accessPin,
|
||||||
accessPinPromptTtlMinutes: input.accessPinPromptTtlMinutes,
|
accessPinPromptTtlMinutes: input.accessPinPromptTtlMinutes,
|
||||||
chatTypeId: input.chatTypeId,
|
chatTypeId: input.chatTypeId,
|
||||||
@@ -2626,7 +2736,7 @@ export async function saveChatShareRoomSettings(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchChatShareSnapshot(token: string, options?: { sharePin?: string | null }) {
|
export async function fetchChatShareSnapshot(token: string, options?: { sharePin?: string | null; sessionId?: string | null }) {
|
||||||
const response = await requestChatApi<{
|
const response = await requestChatApi<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
share: ChatShareSnapshot['share'];
|
share: ChatShareSnapshot['share'];
|
||||||
@@ -2636,11 +2746,13 @@ export async function fetchChatShareSnapshot(token: string, options?: { sharePin
|
|||||||
requests: ChatConversationRequest[];
|
requests: ChatConversationRequest[];
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
activityLogs: ChatConversationActivityLog[];
|
activityLogs: ChatConversationActivityLog[];
|
||||||
|
rooms?: ChatShareRoomSummary[];
|
||||||
|
activeSessionId?: string | null;
|
||||||
roomRequestCounts?: ChatShareSnapshot['roomRequestCounts'];
|
roomRequestCounts?: ChatShareSnapshot['roomRequestCounts'];
|
||||||
promptTarget?: ChatShareSnapshot['promptTarget'];
|
promptTarget?: ChatShareSnapshot['promptTarget'];
|
||||||
refreshedAt: string;
|
refreshedAt: string;
|
||||||
}>(
|
}>(
|
||||||
`/shares/${encodeURIComponent(token)}`,
|
`/shares/${encodeURIComponent(token)}${options?.sessionId?.trim() ? `?sessionId=${encodeURIComponent(options.sessionId.trim())}` : ''}`,
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
allowUnauthenticated: true,
|
allowUnauthenticated: true,
|
||||||
@@ -2707,6 +2819,24 @@ export async function fetchChatShareSnapshot(token: string, options?: { sharePin
|
|||||||
? response.messages.map((message, index) => normalizeChatMessage(message, index))
|
? response.messages.map((message, index) => normalizeChatMessage(message, index))
|
||||||
: [],
|
: [],
|
||||||
activityLogs: Array.isArray(response.activityLogs) ? response.activityLogs : [],
|
activityLogs: Array.isArray(response.activityLogs) ? response.activityLogs : [],
|
||||||
|
rooms: Array.isArray(response.rooms)
|
||||||
|
? response.rooms.map((item) => ({
|
||||||
|
sessionId: normalizeRequiredText(item.sessionId),
|
||||||
|
requestId: normalizeRequiredText(item.requestId),
|
||||||
|
isDefault: item.isDefault === true,
|
||||||
|
sortOrder: Number.isFinite(item.sortOrder) ? Number(item.sortOrder) : 0,
|
||||||
|
title: normalizeRequiredText(item.title) || '공유 채팅방',
|
||||||
|
requestBadgeLabel: normalizeOptionalText(item.requestBadgeLabel),
|
||||||
|
chatTypeId: normalizeOptionalText(item.chatTypeId),
|
||||||
|
lastChatTypeId: normalizeOptionalText(item.lastChatTypeId),
|
||||||
|
contextLabel: normalizeOptionalText(item.contextLabel),
|
||||||
|
contextDescription: normalizeOptionalText(item.contextDescription),
|
||||||
|
notifyOffline: item.notifyOffline === true,
|
||||||
|
createdAt: normalizeOptionalText(item.createdAt),
|
||||||
|
updatedAt: normalizeOptionalText(item.updatedAt),
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
activeSessionId: normalizeOptionalText(response.activeSessionId),
|
||||||
roomRequestCounts: response.roomRequestCounts
|
roomRequestCounts: response.roomRequestCounts
|
||||||
? {
|
? {
|
||||||
processingCount: Number.isFinite(response.roomRequestCounts.processingCount) ? response.roomRequestCounts.processingCount : 0,
|
processingCount: Number.isFinite(response.roomRequestCounts.processingCount) ? response.roomRequestCounts.processingCount : 0,
|
||||||
@@ -2722,6 +2852,7 @@ export async function submitChatShareMessage(
|
|||||||
token: string,
|
token: string,
|
||||||
text: string,
|
text: string,
|
||||||
options?: {
|
options?: {
|
||||||
|
sessionId?: string | null;
|
||||||
mode?: 'queue' | 'direct';
|
mode?: 'queue' | 'direct';
|
||||||
parentRequestId?: string | null;
|
parentRequestId?: string | null;
|
||||||
},
|
},
|
||||||
@@ -2732,6 +2863,7 @@ export async function submitChatShareMessage(
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text,
|
text,
|
||||||
|
sessionId: options?.sessionId?.trim() || undefined,
|
||||||
mode: options?.mode === 'direct' ? 'direct' : 'queue',
|
mode: options?.mode === 'direct' ? 'direct' : 'queue',
|
||||||
parentRequestId: options?.parentRequestId?.trim() || undefined,
|
parentRequestId: options?.parentRequestId?.trim() || undefined,
|
||||||
}),
|
}),
|
||||||
@@ -2790,6 +2922,7 @@ export async function completeChatShareManualBadge(
|
|||||||
token: string,
|
token: string,
|
||||||
payload: {
|
payload: {
|
||||||
parentRequestId: string;
|
parentRequestId: string;
|
||||||
|
sessionId?: string | null;
|
||||||
type: 'prompt' | 'verification';
|
type: 'prompt' | 'verification';
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -2811,6 +2944,7 @@ export async function cancelChatShareRequest(
|
|||||||
token: string,
|
token: string,
|
||||||
payload: {
|
payload: {
|
||||||
parentRequestId: string;
|
parentRequestId: string;
|
||||||
|
sessionId?: string | null;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const response = await requestChatApi<{ ok: boolean; item: ChatConversationRequest }>(
|
const response = await requestChatApi<{ ok: boolean; item: ChatConversationRequest }>(
|
||||||
@@ -2831,6 +2965,7 @@ export async function retryChatShareRequest(
|
|||||||
token: string,
|
token: string,
|
||||||
payload: {
|
payload: {
|
||||||
parentRequestId: string;
|
parentRequestId: string;
|
||||||
|
sessionId?: string | null;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return requestChatApi<{ ok: boolean; queuedRequestId: string }>(
|
return requestChatApi<{ ok: boolean; queuedRequestId: string }>(
|
||||||
|
|||||||
@@ -210,6 +210,71 @@
|
|||||||
box-shadow: inset 0 0 0 1px rgba(219, 226, 236, 0.82);
|
box-shadow: inset 0 0 0 1px rgba(219, 226, 236, 0.82);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-list-panel {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(248, 250, 252, 0.94);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(219, 226, 236, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #f8fbff 0%, #eef4fb 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(191, 204, 220, 0.82),
|
||||||
|
0 6px 18px rgba(148, 163, 184, 0.08);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-card--active {
|
||||||
|
background: linear-gradient(180deg, #dbeafe 0%, #d7ecff 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(59, 130, 246, 0.38),
|
||||||
|
0 10px 24px rgba(59, 130, 246, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-card-title {
|
||||||
|
min-width: 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-card-meta {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__create-room-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__create-room-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__message-list {
|
.chat-share-page__message-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@@ -222,6 +287,26 @@
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__conversation-loading-block {
|
||||||
|
display: grid;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-content: center;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 240px;
|
||||||
|
padding: 24px 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__composer-loading-block {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 132px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__search-modal {
|
.chat-share-page__search-modal {
|
||||||
top: 16px;
|
top: 16px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
@@ -947,24 +1032,50 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
min-height: calc(100vh - 48px);
|
min-height: calc(100vh - 48px);
|
||||||
|
min-width: 0;
|
||||||
padding: 14px 16px 18px;
|
padding: 14px 16px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-settings-tabs {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__room-settings-tabs .ant-tabs-nav {
|
.chat-share-page__room-settings-tabs .ant-tabs-nav {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-settings-tabs .ant-tabs-nav-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-settings-tabs .ant-tabs-nav-list {
|
||||||
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__room-settings-tabs .ant-tabs-tab {
|
.chat-share-page__room-settings-tabs .ant-tabs-tab {
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__room-settings-tabs .ant-tabs-content-holder {
|
.chat-share-page__room-settings-tabs .ant-tabs-content-holder {
|
||||||
|
min-width: 0;
|
||||||
min-height: calc(100vh - 140px);
|
min-height: calc(100vh - 140px);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-settings-tabs .ant-tabs-content,
|
||||||
|
.chat-share-page__room-settings-tabs .ant-tabs-tabpane {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__room-settings-panel {
|
.chat-share-page__room-settings-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__room-settings-panel-head {
|
.chat-share-page__room-settings-panel-head {
|
||||||
@@ -999,6 +1110,29 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-settings-runtime-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-settings-runtime-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid rgba(191, 219, 254, 0.9);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(248, 250, 252, 0.96);
|
||||||
|
box-shadow: 0 10px 24px rgba(148, 163, 184, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-settings-runtime-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__room-settings-checkbox-group {
|
.chat-share-page__room-settings-checkbox-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -1586,19 +1720,49 @@
|
|||||||
max-width: min(82%, 920px);
|
max-width: min(82%, 920px);
|
||||||
border: 1px solid #dbe2ec;
|
border: 1px solid #dbe2ec;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 10px 12px 9px;
|
padding: 12px 12px 9px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__message-tone-label {
|
.chat-share-page__message-tone-head {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: -9px;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__message-tone-meta {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__message-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__message-tone-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 20px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__message-tone-time {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__message-tone--question {
|
.chat-share-page__message-tone--question {
|
||||||
@@ -1610,7 +1774,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__message-tone--question .chat-share-page__message-tone-label {
|
.chat-share-page__message-tone--question .chat-share-page__message-tone-label {
|
||||||
right: 12px;
|
margin-left: auto;
|
||||||
background: #e7f1ff;
|
background: #e7f1ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1622,10 +1786,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__message-tone--answer .chat-share-page__message-tone-label {
|
.chat-share-page__message-tone--answer .chat-share-page__message-tone-label {
|
||||||
left: 12px;
|
|
||||||
background: #e7f7ee;
|
background: #e7f7ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__message-action-button.ant-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__message-action-button.ant-btn.ant-btn-dangerous {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.chat-share-page__response-block {
|
.chat-share-page__response-block {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -2034,21 +2212,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__composer-send-mode {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-share-page__composer-send-mode-text.ant-typography {
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-share-page__reply-reference {
|
.chat-share-page__reply-reference {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2222,16 +2385,6 @@
|
|||||||
padding: 0 6px 6px;
|
padding: 0 6px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__request-block > .chat-share-page__message-time {
|
|
||||||
justify-self: end;
|
|
||||||
padding: 0 0 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-share-page__response-block > .chat-share-page__message-time {
|
|
||||||
justify-self: start;
|
|
||||||
padding: 0 6px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-share-page__section-head--compact {
|
.chat-share-page__section-head--compact {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user