chore: sync local workspace changes
This commit is contained in:
@@ -2,9 +2,11 @@ import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
getAppConfig,
|
||||
getChatContextSettingsConfig,
|
||||
getChatTypesConfig,
|
||||
normalizeAppConfigSnapshot,
|
||||
upsertAppConfig,
|
||||
upsertChatContextSettingsConfig,
|
||||
upsertChatTypesConfig,
|
||||
} from '../services/app-config-service.js';
|
||||
import {
|
||||
@@ -13,9 +15,44 @@ import {
|
||||
} from '../services/automation-context-config-service.js';
|
||||
import { getAutomationTypesConfig, upsertAutomationTypesConfig } from '../services/automation-type-config-service.js';
|
||||
|
||||
function getRequestAppOrigin(request: { headers: Record<string, string | string[] | undefined> }) {
|
||||
const rawAppOrigin = request.headers['x-app-origin'];
|
||||
const appOrigin = Array.isArray(rawAppOrigin) ? rawAppOrigin[0] : rawAppOrigin;
|
||||
|
||||
if (appOrigin?.trim()) {
|
||||
return appOrigin.trim();
|
||||
}
|
||||
|
||||
const rawOrigin = request.headers.origin;
|
||||
const origin = Array.isArray(rawOrigin) ? rawOrigin[0] : rawOrigin;
|
||||
return origin?.trim() ?? '';
|
||||
}
|
||||
|
||||
function getRequestAppDomain(request: { headers: Record<string, string | string[] | undefined> }) {
|
||||
const rawAppDomain = request.headers['x-app-domain'];
|
||||
const appDomain = Array.isArray(rawAppDomain) ? rawAppDomain[0] : rawAppDomain;
|
||||
|
||||
if (appDomain?.trim()) {
|
||||
return appDomain.trim();
|
||||
}
|
||||
|
||||
const appOrigin = getRequestAppOrigin(request);
|
||||
|
||||
if (!appOrigin) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(appOrigin).hostname;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
app.get('/api/app-config', async () => {
|
||||
const config = await getAppConfig();
|
||||
app.get('/api/app-config', async (request) => {
|
||||
const appOrigin = getRequestAppOrigin(request);
|
||||
const config = await getAppConfig(appOrigin);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -23,8 +60,8 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/chat-types', async () => {
|
||||
const chatTypes = await getChatTypesConfig();
|
||||
app.get('/api/chat-types', async (request) => {
|
||||
const chatTypes = await getChatTypesConfig(getRequestAppOrigin(request));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -32,6 +69,15 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/chat-context-settings', async (request) => {
|
||||
const settings = await getChatContextSettingsConfig(getRequestAppOrigin(request));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
settings,
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/automation-types', async () => {
|
||||
const automationTypes = await getAutomationTypesConfig();
|
||||
|
||||
@@ -66,7 +112,9 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
chatTypes: z.array(z.unknown()),
|
||||
}).parse(payload ?? {});
|
||||
|
||||
const savedChatTypes = await upsertChatTypesConfig(parsed.chatTypes);
|
||||
const appOrigin = getRequestAppOrigin(request);
|
||||
const appDomain = getRequestAppDomain(request);
|
||||
const savedChatTypes = await upsertChatTypesConfig(parsed.chatTypes, appOrigin, appDomain);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -79,6 +127,37 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/chat-context-settings', async (request, reply) => {
|
||||
try {
|
||||
let payload: unknown = request.body ?? {};
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = z.object({
|
||||
settings: z.unknown(),
|
||||
}).parse(payload ?? {});
|
||||
|
||||
const appOrigin = getRequestAppOrigin(request);
|
||||
const appDomain = getRequestAppDomain(request);
|
||||
const savedSettings = await upsertChatContextSettingsConfig(parsed.settings, appOrigin, appDomain);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
settings: savedSettings,
|
||||
};
|
||||
} catch (error) {
|
||||
return reply.code(409).send({
|
||||
message: error instanceof Error ? error.message : '채팅 Context 설정 저장에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/automation-types', async (request, reply) => {
|
||||
try {
|
||||
let payload: unknown = request.body ?? {};
|
||||
@@ -159,7 +238,9 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
throw new Error('설정 값 형식이 올바르지 않습니다.');
|
||||
}
|
||||
|
||||
const savedConfig = await upsertAppConfig(config as Record<string, unknown>);
|
||||
const appOrigin = getRequestAppOrigin(request);
|
||||
const appDomain = getRequestAppDomain(request);
|
||||
const savedConfig = await upsertAppConfig(config as Record<string, unknown>, appOrigin, appDomain);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
|
||||
@@ -109,7 +109,7 @@ export async function registerBoardRoutes(app: FastifyInstance) {
|
||||
return {
|
||||
ok: true,
|
||||
item: result.item,
|
||||
planItemId: result.planItemId,
|
||||
planItemIds: result.planItemIds,
|
||||
alreadyReceived: result.alreadyReceived,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,9 +6,10 @@ 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 { getActiveChatService, getChatRuntimeController } from '../services/chat-service.js';
|
||||
import { ensureChatSessionResourceDirectories, getActiveChatService, getChatRuntimeController } from '../services/chat-service.js';
|
||||
import { rollbackChatRuntimeRequest } from '../services/chat-runtime-rollback-service.js';
|
||||
import {
|
||||
CHAT_CONTEXT_DESCRIPTION_MAX_LENGTH,
|
||||
createChatConversation,
|
||||
deleteUnansweredChatConversationRequest,
|
||||
deleteChatConversation,
|
||||
@@ -40,10 +41,12 @@ function resolveStaticContentType(filePath: string) {
|
||||
case '.json':
|
||||
case '.css':
|
||||
case '.html':
|
||||
case '.md':
|
||||
case '.txt':
|
||||
case '.diff':
|
||||
return 'text/plain; charset=utf-8';
|
||||
case '.md':
|
||||
case '.markdown':
|
||||
return 'text/markdown; charset=utf-8';
|
||||
case '.svg':
|
||||
return 'image/svg+xml';
|
||||
case '.png':
|
||||
@@ -190,7 +193,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
|
||||
const viewerClientId = getClientIdHeader(request);
|
||||
const clientId = canViewAllConversations(request) ? null : viewerClientId;
|
||||
const items = await listChatConversations(clientId, query.limit ?? 50, viewerClientId || null);
|
||||
const items = await listChatConversations(clientId, query.limit ?? 200, viewerClientId || null);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -239,7 +242,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
);
|
||||
const absolutePath = path.join(resolveChatAttachmentRepoPath(), ...relativePath.split('/'));
|
||||
|
||||
await mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await ensureChatSessionResourceDirectories(resolveChatAttachmentRepoPath(), payload.sessionId);
|
||||
await writeFile(absolutePath, buffer);
|
||||
|
||||
return {
|
||||
@@ -375,8 +378,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
title: z.string().trim().max(200).optional(),
|
||||
chatTypeId: z.string().trim().max(120).nullable().optional(),
|
||||
lastChatTypeId: z.string().trim().max(120).nullable().optional(),
|
||||
generalSectionName: z.string().trim().max(120).optional().nullable(),
|
||||
contextLabel: z.string().trim().max(200).optional(),
|
||||
contextDescription: z.string().trim().max(2000).optional(),
|
||||
contextDescription: z.string().trim().max(CHAT_CONTEXT_DESCRIPTION_MAX_LENGTH).optional(),
|
||||
notifyOffline: z.boolean().optional(),
|
||||
}).parse(request.body ?? {});
|
||||
|
||||
@@ -387,6 +391,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
title: payload.title ?? '새 대화',
|
||||
chatTypeId: payload.chatTypeId ?? null,
|
||||
lastChatTypeId: payload.lastChatTypeId ?? payload.chatTypeId ?? null,
|
||||
generalSectionName: payload.generalSectionName ?? null,
|
||||
contextLabel: payload.contextLabel ?? null,
|
||||
contextDescription: payload.contextDescription ?? null,
|
||||
notifyOffline: payload.notifyOffline ?? true,
|
||||
@@ -502,8 +507,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
title: z.string().trim().min(1).max(200).optional(),
|
||||
chatTypeId: z.string().trim().max(120).optional().nullable(),
|
||||
lastChatTypeId: z.string().trim().max(120).optional().nullable(),
|
||||
generalSectionName: z.string().trim().max(120).optional().nullable(),
|
||||
contextLabel: z.string().trim().max(200).optional().nullable(),
|
||||
contextDescription: z.string().trim().max(2000).optional().nullable(),
|
||||
contextDescription: z.string().trim().max(CHAT_CONTEXT_DESCRIPTION_MAX_LENGTH).optional().nullable(),
|
||||
notifyOffline: z.boolean().optional(),
|
||||
}).parse(request.body ?? {});
|
||||
|
||||
@@ -521,6 +527,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
clientId: current.clientId,
|
||||
chatTypeId: payload.chatTypeId ?? current.chatTypeId,
|
||||
lastChatTypeId: payload.lastChatTypeId ?? current.lastChatTypeId ?? current.chatTypeId,
|
||||
generalSectionName: payload.generalSectionName ?? current.generalSectionName,
|
||||
contextLabel: payload.contextLabel ?? current.contextLabel,
|
||||
contextDescription: payload.contextDescription ?? current.contextDescription,
|
||||
notifyOffline: payload.notifyOffline ?? current.notifyOffline,
|
||||
|
||||
1256
etc/servers/work-server/src/routes/plan.js
Normal file
1256
etc/servers/work-server/src/routes/plan.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -64,10 +64,12 @@ import {
|
||||
listPlanScheduledTasks,
|
||||
mapPlanScheduledTaskRow,
|
||||
registerPlanScheduledTaskNow,
|
||||
syncManagedServiceGenerationCompletion,
|
||||
updatePlanScheduledTask,
|
||||
updatePlanScheduledTaskSchema,
|
||||
} from '../services/plan-schedule-service.js';
|
||||
import { getVisitorClientByClientId } from '../services/visitor-history-service.js';
|
||||
import { progressBoardPostAutomationByPlanResult } from '../services/board-service.js';
|
||||
|
||||
const completeActionSchema = z.object({
|
||||
note: z.string().trim().min(1).optional(),
|
||||
@@ -164,7 +166,10 @@ export async function registerPlanRoutes(app: FastifyInstance) {
|
||||
|
||||
const payload = createPlanScheduledTaskSchema.parse(request.body ?? {});
|
||||
const row = await createPlanScheduledTask(payload);
|
||||
const ignoreScheduleDueForImmediateRegistration = !payload.repeatWindowStartTime;
|
||||
const ignoreScheduleDueForImmediateRegistration =
|
||||
payload.repeatWindows.length === 0
|
||||
&& payload.scheduleWeekdays.length === 0
|
||||
&& payload.scheduleDateRanges.length === 0;
|
||||
const immediateRegistration =
|
||||
payload.executionMode === 'managed-service' && payload.recreateManagedServiceOnNextSave
|
||||
? await registerPlanScheduledTaskNow(Number(row.id), new Date(), {
|
||||
@@ -231,16 +236,26 @@ export async function registerPlanRoutes(app: FastifyInstance) {
|
||||
&& Boolean(row.immediate_run_enabled ?? true)
|
||||
&& payload.enabled !== false
|
||||
);
|
||||
const effectiveRepeatWindowStartTime =
|
||||
payload.repeatWindowStartTime !== undefined
|
||||
? payload.repeatWindowStartTime
|
||||
: (typeof row?.repeat_window_start_time === 'string' ? row.repeat_window_start_time : null);
|
||||
const effectiveRepeatWindows =
|
||||
payload.repeatWindows !== undefined
|
||||
? payload.repeatWindows
|
||||
: (row ? mapPlanScheduledTaskRow(row).repeatWindows : []);
|
||||
const effectiveScheduleDateRanges =
|
||||
payload.scheduleDateRanges !== undefined
|
||||
? payload.scheduleDateRanges
|
||||
: (row ? mapPlanScheduledTaskRow(row).scheduleDateRanges : []);
|
||||
const effectiveScheduleWeekdays =
|
||||
payload.scheduleWeekdays !== undefined
|
||||
? payload.scheduleWeekdays
|
||||
: (row ? mapPlanScheduledTaskRow(row).scheduleWeekdays : []);
|
||||
const immediateRegistration = shouldTriggerImmediateRegistration
|
||||
? await registerPlanScheduledTaskNow(id, new Date(), {
|
||||
? await registerPlanScheduledTaskNow(id, new Date(), {
|
||||
ignoreScheduleDue:
|
||||
payload.recreateManagedServiceOnNextSave === true
|
||||
? false
|
||||
: !effectiveRepeatWindowStartTime,
|
||||
: effectiveRepeatWindows.length === 0
|
||||
&& effectiveScheduleWeekdays.length === 0
|
||||
&& effectiveScheduleDateRanges.length === 0,
|
||||
forceManagedServiceGeneration: payload.recreateManagedServiceOnNextSave === true,
|
||||
})
|
||||
: null;
|
||||
@@ -576,6 +591,8 @@ export async function registerPlanRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
await syncManagedServiceGenerationCompletion(id);
|
||||
|
||||
const planLabel = formatPlanNotificationLabel(String(row.work_id), id);
|
||||
await notifyPlanEvent(
|
||||
id,
|
||||
@@ -583,6 +600,7 @@ export async function registerPlanRoutes(app: FastifyInstance) {
|
||||
'수동 작업완료로 release 반영 대기 상태가 되었습니다.',
|
||||
'development-completed',
|
||||
);
|
||||
await progressBoardPostAutomationByPlanResult(id, 'completed');
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -605,6 +623,8 @@ export async function registerPlanRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
await syncManagedServiceGenerationCompletion(id);
|
||||
|
||||
const planLabel = formatPlanNotificationLabel(String(row.work_id), id);
|
||||
await notifyPlanEvent(
|
||||
id,
|
||||
@@ -612,6 +632,7 @@ export async function registerPlanRoutes(app: FastifyInstance) {
|
||||
payload.note ?? '작업이 완료 처리되었습니다.',
|
||||
'plan-completed',
|
||||
);
|
||||
await progressBoardPostAutomationByPlanResult(id, 'completed');
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -748,8 +769,12 @@ export async function registerPlanRoutes(app: FastifyInstance) {
|
||||
try {
|
||||
const env = getEnv();
|
||||
const isReleaseMergeFailure = item.status === '작업완료' && item.workerStatus === 'release반영실패';
|
||||
const sourceWorkCount = Math.max(0, Number(item.usageSnapshot?.sourceWorkCount ?? 0) || 0);
|
||||
const requiresRollbackBeforeCancel =
|
||||
sourceWorkCount > 0 &&
|
||||
(item.status === '릴리즈완료' || item.workerStatus === 'main반영실패');
|
||||
|
||||
if (!isReleaseMergeFailure) {
|
||||
if (!isReleaseMergeFailure && requiresRollbackBeforeCancel) {
|
||||
await recreateReleaseBranchFromMain(
|
||||
{
|
||||
repoPath: env.PLAN_GIT_REPO_PATH,
|
||||
|
||||
239
etc/servers/work-server/src/routes/resource-manager.ts
Normal file
239
etc/servers/work-server/src/routes/resource-manager.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { env } from '../config/env.js';
|
||||
import {
|
||||
copyResourceManagerItem,
|
||||
createResourceManagerDirectory,
|
||||
createResourceManagerFile,
|
||||
deleteResourceManagerItem,
|
||||
ensureResourceManagerRoot,
|
||||
getResourceManagerTree,
|
||||
listResourceManagerDirectory,
|
||||
moveResourceManagerItem,
|
||||
openResourceManagerPreviewStream,
|
||||
readResourceManagerFile,
|
||||
saveResourceManagerFile,
|
||||
uploadResourceManagerFile,
|
||||
} from '../services/resource-manager-service.js';
|
||||
|
||||
const queryPathSchema = z.object({
|
||||
path: z.string().trim().optional().default(''),
|
||||
});
|
||||
|
||||
const createDirectoryBodySchema = z.object({
|
||||
parentPath: z.string().trim().optional().default(''),
|
||||
name: z.string().trim().min(1).max(255),
|
||||
});
|
||||
|
||||
const createFileBodySchema = z.object({
|
||||
parentPath: z.string().trim().optional().default(''),
|
||||
name: z.string().trim().min(1).max(255),
|
||||
content: z.string().optional().default(''),
|
||||
});
|
||||
|
||||
const saveFileBodySchema = z.object({
|
||||
path: z.string().trim().min(1),
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
const uploadFileBodySchema = z.object({
|
||||
parentPath: z.string().trim().optional().default(''),
|
||||
fileName: z.string().trim().min(1).max(255),
|
||||
contentBase64: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
const copyMoveBodySchema = z.object({
|
||||
path: z.string().trim().min(1),
|
||||
targetDirectoryPath: z.string().trim().optional().default(''),
|
||||
nextName: z.string().trim().max(255).optional().nullable(),
|
||||
});
|
||||
|
||||
function getRequestAccessToken(request: FastifyRequest) {
|
||||
const tokenHeader = request.headers['x-access-token'];
|
||||
return Array.isArray(tokenHeader) ? tokenHeader[0]?.trim() ?? '' : String(tokenHeader ?? '').trim();
|
||||
}
|
||||
|
||||
function ensureAuthorized(request: FastifyRequest, reply: FastifyReply, tokenOverride?: string | null) {
|
||||
if ((tokenOverride ?? getRequestAccessToken(request)) === env.SERVER_COMMAND_ACCESS_TOKEN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
reply.status(403);
|
||||
void reply.send({
|
||||
message: '권한 토큰이 필요합니다.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveRepoRootPath() {
|
||||
return env.SERVER_COMMAND_MAIN_PROJECT_ROOT || env.PLAN_MAIN_PROJECT_REPO_PATH || env.PLAN_GIT_REPO_PATH;
|
||||
}
|
||||
|
||||
export async function registerResourceManagerRoutes(app: FastifyInstance) {
|
||||
app.get('/api/resource-manager/tree', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repoRootPath = resolveRepoRootPath();
|
||||
await ensureResourceManagerRoot(repoRootPath);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await getResourceManagerTree(repoRootPath),
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/resource-manager/directory', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = queryPathSchema.parse(request.query ?? {});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await listResourceManagerDirectory(resolveRepoRootPath(), query.path),
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/resource-manager/file', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = queryPathSchema.extend({
|
||||
path: z.string().trim().min(1),
|
||||
}).parse(request.query ?? {});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await readResourceManagerFile(resolveRepoRootPath(), query.path),
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/resource-manager/preview/*', async (request, reply) => {
|
||||
const query = z.object({
|
||||
token: z.string().trim().optional(),
|
||||
}).parse(request.query ?? {});
|
||||
|
||||
if (!ensureAuthorized(request, reply, query.token ?? null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wildcard = String((request.params as { '*': string | undefined })['*'] ?? '').trim();
|
||||
const preview = await openResourceManagerPreviewStream(resolveRepoRootPath(), decodeURIComponent(wildcard));
|
||||
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
reply.type(preview.contentType);
|
||||
return reply.send(preview.stream);
|
||||
});
|
||||
|
||||
app.post('/api/resource-manager/directories', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = createDirectoryBodySchema.parse(request.body ?? {});
|
||||
await createResourceManagerDirectory(resolveRepoRootPath(), payload.parentPath, payload.name);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/resource-manager/files', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = createFileBodySchema.parse(request.body ?? {});
|
||||
await createResourceManagerFile(resolveRepoRootPath(), payload.parentPath, payload.name, payload.content);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
});
|
||||
|
||||
app.put('/api/resource-manager/files/content', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = saveFileBodySchema.parse(request.body ?? {});
|
||||
await saveResourceManagerFile(resolveRepoRootPath(), payload.path, payload.content);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/resource-manager/files/upload', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = uploadFileBodySchema.parse(request.body ?? {});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await uploadResourceManagerFile(
|
||||
resolveRepoRootPath(),
|
||||
payload.parentPath,
|
||||
payload.fileName,
|
||||
payload.contentBase64,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/resource-manager/items/copy', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = copyMoveBodySchema.parse(request.body ?? {});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await copyResourceManagerItem(
|
||||
resolveRepoRootPath(),
|
||||
payload.path,
|
||||
payload.targetDirectoryPath,
|
||||
payload.nextName,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/resource-manager/items/move', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = copyMoveBodySchema.parse(request.body ?? {});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await moveResourceManagerItem(
|
||||
resolveRepoRootPath(),
|
||||
payload.path,
|
||||
payload.targetDirectoryPath,
|
||||
payload.nextName,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
app.delete('/api/resource-manager/items', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = queryPathSchema.extend({
|
||||
path: z.string().trim().min(1),
|
||||
}).parse(request.query ?? {});
|
||||
await deleteResourceManagerItem(resolveRepoRootPath(), query.path);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2,16 +2,45 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { env } from '../config/env.js';
|
||||
import { listServerCommands, restartServerCommand, serverCommandKeys } from '../services/server-command-service.js';
|
||||
import {
|
||||
cancelServerRestartReservation,
|
||||
confirmServerRestartReservation,
|
||||
getRestartReservationWorkloadSummary,
|
||||
getServerRestartReservation,
|
||||
scheduleServerRestartReservation,
|
||||
} from '../services/server-restart-reservation-service.js';
|
||||
|
||||
const serverCommandParamSchema = z.object({
|
||||
key: z.enum(serverCommandKeys),
|
||||
});
|
||||
|
||||
const restartReservationBodySchema = z.object({
|
||||
autoExecuteDelaySeconds: z.number().int().min(1).max(300).optional(),
|
||||
});
|
||||
|
||||
function getRequestAccessToken(request: FastifyRequest) {
|
||||
const tokenHeader = request.headers['x-access-token'];
|
||||
return Array.isArray(tokenHeader) ? tokenHeader[0]?.trim() ?? '' : String(tokenHeader ?? '').trim();
|
||||
}
|
||||
|
||||
function getRequestClientId(request: FastifyRequest) {
|
||||
const clientIdHeader = request.headers['x-client-id'];
|
||||
return Array.isArray(clientIdHeader) ? clientIdHeader[0]?.trim() ?? '' : String(clientIdHeader ?? '').trim();
|
||||
}
|
||||
|
||||
function getRequestAppOrigin(request: FastifyRequest) {
|
||||
const appOriginHeader = request.headers['x-app-origin'];
|
||||
const appOrigin = Array.isArray(appOriginHeader) ? appOriginHeader[0] : appOriginHeader;
|
||||
|
||||
if (appOrigin?.trim()) {
|
||||
return appOrigin.trim();
|
||||
}
|
||||
|
||||
const originHeader = request.headers.origin;
|
||||
const origin = Array.isArray(originHeader) ? originHeader[0] : originHeader;
|
||||
return origin?.trim() ?? '';
|
||||
}
|
||||
|
||||
function ensureAuthorized(request: FastifyRequest, reply: FastifyReply) {
|
||||
if (getRequestAccessToken(request) === env.SERVER_COMMAND_ACCESS_TOKEN) {
|
||||
return true;
|
||||
@@ -42,6 +71,25 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const { key } = serverCommandParamSchema.parse(request.params);
|
||||
|
||||
if (key === 'test' || key === 'work-server') {
|
||||
const workloadSummary = await getRestartReservationWorkloadSummary();
|
||||
const pendingCount =
|
||||
workloadSummary.codexRunningCount
|
||||
+ workloadSummary.codexQueuedCount
|
||||
+ workloadSummary.automationRunningCount
|
||||
+ workloadSummary.automationQueuedCount;
|
||||
|
||||
if (pendingCount > 0) {
|
||||
reply.status(409);
|
||||
return {
|
||||
ok: false,
|
||||
message: `진행 중인 Codex Live/자동화 작업 ${pendingCount}건이 있어 즉시 재기동할 수 없습니다. 재기동 예약을 사용해 주세요.`,
|
||||
workloadSummary,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = await restartServerCommand(key);
|
||||
|
||||
return {
|
||||
@@ -51,4 +99,64 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
restartState: result.restartState,
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/server-commands/restart-reservation', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await getServerRestartReservation(),
|
||||
};
|
||||
});
|
||||
|
||||
app.put('/api/server-commands/restart-reservation', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let payload: unknown = request.body ?? {};
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = restartReservationBodySchema.parse(payload ?? {});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await scheduleServerRestartReservation({
|
||||
clientId: getRequestClientId(request),
|
||||
appOrigin: getRequestAppOrigin(request),
|
||||
autoExecuteDelaySeconds: parsed.autoExecuteDelaySeconds,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/server-commands/restart-reservation/confirm', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await confirmServerRestartReservation(app.log),
|
||||
};
|
||||
});
|
||||
|
||||
app.delete('/api/server-commands/restart-reservation', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await cancelServerRestartReservation(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user