feat: update codex live chat workflow
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { getAppConfig, upsertAppConfig } from '../services/app-config-service.js';
|
||||
import { z } from 'zod';
|
||||
import { getAppConfig, getChatTypesConfig, upsertAppConfig, upsertChatTypesConfig } from '../services/app-config-service.js';
|
||||
|
||||
export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
app.get('/api/app-config', async () => {
|
||||
@@ -11,6 +12,44 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/chat-types', async () => {
|
||||
const chatTypes = await getChatTypesConfig();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
chatTypes,
|
||||
};
|
||||
});
|
||||
|
||||
app.put('/api/chat-types', async (request, reply) => {
|
||||
try {
|
||||
let payload: unknown = request.body ?? {};
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = z.object({
|
||||
chatTypes: z.array(z.unknown()),
|
||||
}).parse(payload ?? {});
|
||||
|
||||
const savedChatTypes = await upsertChatTypesConfig(parsed.chatTypes);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
chatTypes: savedChatTypes,
|
||||
};
|
||||
} catch (error) {
|
||||
return reply.code(409).send({
|
||||
message: error instanceof Error ? error.message : '채팅유형 저장에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/app-config', async (request, reply) => {
|
||||
try {
|
||||
let payload: unknown = request.body ?? {};
|
||||
|
||||
@@ -6,16 +6,14 @@ import type { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { env } from '../config/env.js';
|
||||
import { hasErrorLogViewAccessToken } from '../services/error-log-service.js';
|
||||
import { getChatRuntimeController } from '../services/chat-service.js';
|
||||
import { getActiveChatService, getChatRuntimeController } from '../services/chat-service.js';
|
||||
import {
|
||||
createChatConversation,
|
||||
deleteUnansweredChatConversationRequest,
|
||||
deleteChatConversation,
|
||||
ensureChatConversationTables,
|
||||
getChatConversation,
|
||||
listChatConversationActivityLogs,
|
||||
listChatConversationMessages,
|
||||
listChatConversationRequests,
|
||||
listChatConversationDetailPage,
|
||||
listChatConversations,
|
||||
markChatConversationResponsesRead,
|
||||
updateChatConversationContext,
|
||||
@@ -136,7 +134,7 @@ function sanitizeChatAttachmentFileName(fileName: string) {
|
||||
}
|
||||
|
||||
function resolveChatAttachmentRepoPath() {
|
||||
return path.resolve(env.PLAN_MAIN_PROJECT_REPO_PATH ?? env.PLAN_GIT_REPO_PATH);
|
||||
return path.resolve(env.SERVER_COMMAND_MAIN_PROJECT_ROOT || env.PLAN_MAIN_PROJECT_REPO_PATH || env.PLAN_GIT_REPO_PATH);
|
||||
}
|
||||
|
||||
function getClientIdHeader(request: { headers: Record<string, unknown> }) {
|
||||
@@ -314,6 +312,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
const payload = z.object({
|
||||
sessionId: z.string().trim().min(1).max(120),
|
||||
title: z.string().trim().max(200).optional(),
|
||||
chatTypeId: z.string().trim().max(120).nullable().optional(),
|
||||
contextLabel: z.string().trim().max(200).optional(),
|
||||
contextDescription: z.string().trim().max(2000).optional(),
|
||||
notifyOffline: z.boolean().optional(),
|
||||
@@ -324,6 +323,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
sessionId: payload.sessionId,
|
||||
clientId: clientId || null,
|
||||
title: payload.title ?? '새 대화',
|
||||
chatTypeId: payload.chatTypeId ?? null,
|
||||
contextLabel: payload.contextLabel ?? null,
|
||||
contextDescription: payload.contextDescription ?? null,
|
||||
notifyOffline: payload.notifyOffline ?? true,
|
||||
@@ -353,30 +353,20 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const messageLimit = query.limit ?? 500;
|
||||
const messages = await listChatConversationMessages(params.sessionId, {
|
||||
const messageLimit = query.limit ?? 6;
|
||||
const detailPage = await listChatConversationDetailPage(params.sessionId, {
|
||||
limit: messageLimit,
|
||||
beforeMessageId: query.beforeMessageId ?? null,
|
||||
});
|
||||
const requests = await listChatConversationRequests(params.sessionId, 500);
|
||||
const activityLogs = await listChatConversationActivityLogs(params.sessionId, 500);
|
||||
const oldestLoadedMessageId = messages[0]?.id ?? null;
|
||||
const hasOlderMessages =
|
||||
oldestLoadedMessageId != null
|
||||
? (await listChatConversationMessages(params.sessionId, {
|
||||
limit: 1,
|
||||
beforeMessageId: oldestLoadedMessageId,
|
||||
})).length > 0
|
||||
: false;
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item,
|
||||
messages,
|
||||
requests,
|
||||
activityLogs,
|
||||
oldestLoadedMessageId,
|
||||
hasOlderMessages,
|
||||
messages: detailPage.messages,
|
||||
requests: detailPage.requests,
|
||||
activityLogs: detailPage.activityLogs,
|
||||
oldestLoadedMessageId: detailPage.oldestLoadedMessageId,
|
||||
hasOlderMessages: detailPage.hasOlderMessages,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -447,6 +437,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
}).parse(request.params ?? {});
|
||||
const payload = z.object({
|
||||
title: z.string().trim().min(1).max(200).optional(),
|
||||
chatTypeId: z.string().trim().max(120).optional().nullable(),
|
||||
contextLabel: z.string().trim().max(200).optional().nullable(),
|
||||
contextDescription: z.string().trim().max(2000).optional().nullable(),
|
||||
notifyOffline: z.boolean().optional(),
|
||||
@@ -464,6 +455,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
const item = await updateChatConversationContext(params.sessionId, {
|
||||
title: payload.title ?? current.title,
|
||||
clientId: current.clientId,
|
||||
chatTypeId: payload.chatTypeId ?? current.chatTypeId,
|
||||
contextLabel: payload.contextLabel ?? current.contextLabel,
|
||||
contextDescription: payload.contextDescription ?? current.contextDescription,
|
||||
notifyOffline: payload.notifyOffline ?? current.notifyOffline,
|
||||
@@ -489,6 +481,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
await getActiveChatService()?.forgetSession(params.sessionId);
|
||||
const deleted = await deleteChatConversation(params.sessionId);
|
||||
|
||||
return {
|
||||
|
||||
55
etc/servers/work-server/src/routes/crud.test.ts
Normal file
55
etc/servers/work-server/src/routes/crud.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { maskCrudRowSensitiveFields } from './crud.js';
|
||||
|
||||
test('maskCrudRowSensitiveFields masks password and related login identifier fields in credential-like rows', () => {
|
||||
const rows = [
|
||||
{
|
||||
service_name: 'legacy-admin',
|
||||
login_id: 'admin-master',
|
||||
password: 'super-secret-password',
|
||||
note: 'keep this as-is',
|
||||
},
|
||||
];
|
||||
|
||||
const masked = maskCrudRowSensitiveFields(rows);
|
||||
|
||||
assert.deepEqual(masked, [
|
||||
{
|
||||
service_name: 'legacy-admin',
|
||||
login_id: 'ad********er',
|
||||
password: 'su*****************rd',
|
||||
note: 'keep this as-is',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('maskCrudRowSensitiveFields keeps generic ids unchanged when no credential secret field exists', () => {
|
||||
const rows = [
|
||||
{
|
||||
id: 12,
|
||||
user_id: 'owner-01',
|
||||
title: 'normal business row',
|
||||
},
|
||||
];
|
||||
|
||||
const masked = maskCrudRowSensitiveFields(rows);
|
||||
|
||||
assert.deepEqual(masked, rows);
|
||||
});
|
||||
|
||||
test('maskCrudRowSensitiveFields masks nested secret values recursively', () => {
|
||||
const payload = {
|
||||
profile: {
|
||||
username: 'how2ice',
|
||||
access_token: 'tok_1234567890',
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(maskCrudRowSensitiveFields(payload), {
|
||||
profile: {
|
||||
username: 'ho***ce',
|
||||
access_token: 'to**********90',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,61 @@ const deleteSchema = z.object({
|
||||
});
|
||||
|
||||
const protectedBoardPostAutomationFields = new Set(['automation_plan_item_id', 'automation_received_at']);
|
||||
const secretFieldPattern = /(?:^|_|-)(?:password|passwd|pwd|passcode|secret|token|api[_-]?key|access[_-]?key|private[_-]?key)(?:$|_|-)/i;
|
||||
const loginFieldPattern =
|
||||
/(?:^|_|-)(?:login[_-]?id|login[_-]?name|username|user[_-]?name|user[_-]?id|account[_-]?id|account[_-]?name|admin[_-]?id|admin[_-]?name|member[_-]?id|member[_-]?name|email)(?:$|_|-)/i;
|
||||
|
||||
function maskCredentialValue(value: string) {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (trimmed.length <= 2) {
|
||||
return '*'.repeat(trimmed.length);
|
||||
}
|
||||
|
||||
if (trimmed.length <= 4) {
|
||||
return `${trimmed[0]}${'*'.repeat(trimmed.length - 2)}${trimmed.at(-1) ?? ''}`;
|
||||
}
|
||||
|
||||
return `${trimmed.slice(0, 2)}${'*'.repeat(Math.max(2, trimmed.length - 4))}${trimmed.slice(-2)}`;
|
||||
}
|
||||
|
||||
function shouldMaskLoginField(fieldName: string, row: Record<string, unknown>) {
|
||||
if (!loginFieldPattern.test(fieldName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.keys(row).some((candidateField) => secretFieldPattern.test(candidateField));
|
||||
}
|
||||
|
||||
export function maskCrudRowSensitiveFields<T>(value: T): T {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => maskCrudRowSensitiveFields(item)) as T;
|
||||
}
|
||||
|
||||
if (!value || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
const row = value as Record<string, unknown>;
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
Object.entries(row).forEach(([fieldName, fieldValue]) => {
|
||||
if (typeof fieldValue === 'string') {
|
||||
if (secretFieldPattern.test(fieldName) || shouldMaskLoginField(fieldName, row)) {
|
||||
result[fieldName] = maskCredentialValue(fieldValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
result[fieldName] = maskCrudRowSensitiveFields(fieldValue);
|
||||
});
|
||||
|
||||
return result as T;
|
||||
}
|
||||
|
||||
function applyFilters(query: Knex.QueryBuilder, filters: z.infer<typeof filterSchema>[] = []) {
|
||||
filters.forEach((filter) => {
|
||||
@@ -138,7 +193,7 @@ export async function registerCrudRoutes(app: FastifyInstance) {
|
||||
ok: true,
|
||||
table,
|
||||
count: rows.length,
|
||||
rows,
|
||||
rows: maskCrudRowSensitiveFields(rows),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -150,7 +205,7 @@ export async function registerCrudRoutes(app: FastifyInstance) {
|
||||
return {
|
||||
ok: true,
|
||||
table,
|
||||
rows: inserted,
|
||||
rows: maskCrudRowSensitiveFields(inserted),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -197,7 +252,7 @@ export async function registerCrudRoutes(app: FastifyInstance) {
|
||||
ok: true,
|
||||
table,
|
||||
count: rows.length,
|
||||
rows,
|
||||
rows: maskCrudRowSensitiveFields(rows),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -214,7 +269,7 @@ export async function registerCrudRoutes(app: FastifyInstance) {
|
||||
ok: true,
|
||||
table,
|
||||
count: rows.length,
|
||||
rows,
|
||||
rows: maskCrudRowSensitiveFields(rows),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user