feat: refine codex live chat context flows

This commit is contained in:
2026-05-08 21:15:51 +09:00
parent 82c0d8a197
commit 442879313f
92 changed files with 14815 additions and 7314 deletions

View File

@@ -1,8 +1,8 @@
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import {
getAppConfig,
getChatContextSettingsConfig,
getAppConfigSnapshot,
getChatTypesConfig,
normalizeAppConfigSnapshot,
upsertAppConfig,
@@ -52,20 +52,20 @@ function getRequestAppDomain(request: { headers: Record<string, string | string[
export async function registerAppConfigRoutes(app: FastifyInstance) {
app.get('/api/app-config', async (request) => {
const appOrigin = getRequestAppOrigin(request);
const config = await getAppConfig(appOrigin);
const config = await getAppConfigSnapshot(appOrigin);
return {
ok: true,
config: normalizeAppConfigSnapshot(config),
config,
};
});
app.get('/api/chat-types', async (request) => {
const chatTypes = await getChatTypesConfig(getRequestAppOrigin(request));
const chatTypeConfig = await getChatTypesConfig(getRequestAppOrigin(request));
return {
ok: true,
chatTypes,
...chatTypeConfig,
};
});
@@ -108,17 +108,21 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
}
}
const parsed = z.object({
chatTypes: z.array(z.unknown()),
}).parse(payload ?? {});
const parsed = z
.object({
chatTypes: z.array(z.unknown()).optional(),
customChatTypes: z.array(z.unknown()).optional(),
})
.parse(payload ?? {});
const appOrigin = getRequestAppOrigin(request);
const appDomain = getRequestAppDomain(request);
const savedChatTypes = await upsertChatTypesConfig(parsed.chatTypes, appOrigin, appDomain);
const targetChatTypes = parsed.customChatTypes ?? parsed.chatTypes ?? [];
const savedChatTypeConfig = await upsertChatTypesConfig(targetChatTypes, appOrigin, appDomain);
return {
ok: true,
chatTypes: savedChatTypes,
...savedChatTypeConfig,
};
} catch (error) {
return reply.code(409).send({

View File

@@ -0,0 +1,13 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveStaticContentType } from './chat.js';
test('resolveStaticContentType returns html content type for chat resource html files', () => {
assert.equal(resolveStaticContentType('/tmp/sample.html'), 'text/html; charset=utf-8');
assert.equal(resolveStaticContentType('/tmp/sample.htm'), 'text/html; charset=utf-8');
});
test('resolveStaticContentType keeps plain text content type for code resources', () => {
assert.equal(resolveStaticContentType('/tmp/sample.ts'), 'text/plain; charset=utf-8');
assert.equal(resolveStaticContentType('/tmp/sample.diff'), 'text/plain; charset=utf-8');
});

View File

@@ -10,6 +10,7 @@ import { ensureChatSessionResourceDirectories, getActiveChatService, getChatRunt
import { rollbackChatRuntimeRequest } from '../services/chat-runtime-rollback-service.js';
import {
CHAT_CONTEXT_DESCRIPTION_MAX_LENGTH,
clearChatConversationData,
createChatConversation,
deleteUnansweredChatConversationRequest,
deleteChatConversation,
@@ -22,13 +23,14 @@ import {
updateChatConversationContext,
} from '../services/chat-room-service.js';
import { chatRuntimeService } from '../services/chat-runtime-service.js';
import { resolveMainProjectRoot } from '../services/main-project-root-service.js';
const CHAT_ATTACHMENT_ROUTE_BODY_LIMIT = 20 * 1024 * 1024;
const CHAT_ATTACHMENT_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
const CHAT_PUBLIC_ROUTE_PREFIX = '/.codex_chat/';
const CHAT_API_RESOURCE_ROUTE_PREFIX = '/api/chat/resources';
function resolveStaticContentType(filePath: string) {
export function resolveStaticContentType(filePath: string) {
const extension = path.extname(filePath).toLowerCase();
switch (extension) {
@@ -40,10 +42,12 @@ function resolveStaticContentType(filePath: string) {
case '.cjs':
case '.json':
case '.css':
case '.html':
case '.txt':
case '.diff':
return 'text/plain; charset=utf-8';
case '.html':
case '.htm':
return 'text/html; charset=utf-8';
case '.md':
case '.markdown':
return 'text/markdown; charset=utf-8';
@@ -139,7 +143,7 @@ function sanitizeChatAttachmentFileName(fileName: string) {
}
function resolveChatAttachmentRepoPath() {
return path.resolve(env.SERVER_COMMAND_MAIN_PROJECT_ROOT || env.PLAN_MAIN_PROJECT_REPO_PATH || env.PLAN_GIT_REPO_PATH);
return resolveMainProjectRoot();
}
function getClientIdHeader(request: { headers: Record<string, unknown> }) {
@@ -421,7 +425,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
const messageLimit = query.limit ?? 6;
const messageLimit = query.limit ?? 8;
const detailPage = await listChatConversationDetailPage(params.sessionId, {
limit: messageLimit,
beforeMessageId: query.beforeMessageId ?? null,
@@ -562,4 +566,34 @@ export async function registerChatRoutes(app: FastifyInstance) {
sessionId: params.sessionId,
};
});
app.post('/api/chat/conversations/:sessionId/clear', async (request, reply) => {
const params = z.object({
sessionId: z.string().trim().min(1).max(120),
}).parse(request.params ?? {});
const clientId = canViewAllConversations(request) ? null : getClientIdHeader(request);
const current = await getChatConversation(params.sessionId, clientId || null);
if (!current) {
return reply.code(404).send({
message: '초기화할 채팅방을 찾을 수 없습니다.',
});
}
getActiveChatService()?.resetSessionData(params.sessionId);
chatRuntimeService.clearSession(params.sessionId);
const item = await clearChatConversationData(params.sessionId, clientId || null);
if (!item) {
return reply.code(404).send({
message: '채팅방 데이터 초기화 후 다시 불러오지 못했습니다.',
});
}
return {
ok: true,
item,
};
});
}

View File

@@ -6,6 +6,7 @@ import {
cancelServerRestartReservation,
confirmServerRestartReservation,
getRestartReservationWorkloadSummary,
requestImmediateRestartRecovery,
getServerRestartReservation,
scheduleServerRestartReservation,
} from '../services/server-restart-reservation-service.js';
@@ -90,14 +91,40 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
}
}
const result = await restartServerCommand(key);
try {
const result = await restartServerCommand(key);
return {
ok: true,
item: result.server,
commandOutput: result.commandOutput,
restartState: result.restartState,
};
return {
ok: true,
item: result.server,
commandOutput: result.commandOutput,
restartState: result.restartState,
};
} catch (error) {
const message = error instanceof Error ? error.message : '서버 재기동에 실패했습니다.';
if (key !== 'test' && key !== 'work-server') {
throw error;
}
if (!/(build|vite|tsc|typescript|npm run build|pnpm build|rollup|esbuild|compilation|compile|exit:\d+)/i.test(message)) {
throw error;
}
await requestImmediateRestartRecovery(app.log, key, message);
const server = (await listServerCommands()).find((item) => item.key === key);
if (!server) {
throw new Error(`${key} 서버 상태를 다시 읽지 못했습니다.`);
}
return {
ok: true,
item: server,
commandOutput: `${message}\n\n빌드 실패를 감지해 Codex 자동 개선과 재기동 재시도를 시작했습니다.`,
restartState: 'accepted' as const,
};
}
});
app.get('/api/server-commands/restart-reservation', async (request, reply) => {