Files
ai-code-app/etc/servers/work-server/src/services/managed-schedule-service.ts

169 lines
5.6 KiB
TypeScript

import path from 'node:path';
import { access, mkdir, rm } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import { getEnv } from '../config/env.js';
import { sendManagedStockAlertWebPush } from './stock-alert-service.js';
export type ManagedScheduleServiceResult = {
ok: boolean;
skipped: boolean;
title: string;
body: string;
itemCount: number;
lines: string[];
ios: {
ok: boolean;
skipped: boolean;
reason?: string;
sentCount: number;
failedCount: number;
};
web: {
ok: boolean;
skipped: boolean;
reason?: string;
sentCount: number;
failedCount: number;
};
};
export type ManagedScheduleServiceMetadata = {
scheduleId: number;
serviceKey: string;
packageName: string;
relativeDirectory: string;
manifestPath: string;
readmePath: string;
sourcePath: string;
runtimePath: string;
};
function sanitizeManagedServiceToken(value: string) {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 60);
}
function appendScheduleSuffixOnce(value: string, suffix: string) {
const normalizedValue = value.trim();
const normalizedSuffix = suffix.trim().toLowerCase();
if (!normalizedValue || !normalizedSuffix) {
return normalizedValue;
}
return normalizedValue.toLowerCase().endsWith(`-${normalizedSuffix}`)
? normalizedValue
: `${normalizedValue}-${suffix.trim()}`;
}
function getScheduleRepoRoot() {
const env = getEnv();
return env.SERVER_COMMAND_MAIN_PROJECT_ROOT || env.PLAN_MAIN_PROJECT_REPO_PATH || process.cwd();
}
export function buildManagedScheduleServiceMetadata(scheduleId: number, workId: string): ManagedScheduleServiceMetadata {
const workToken = sanitizeManagedServiceToken(appendScheduleSuffixOnce(workId, 'service')) || 'task-service';
const serviceKey = `schedule-${scheduleId}-${workToken}`;
const relativeDirectory = `.auto_codex/schedule/${scheduleId}`;
const packageName = workToken || `schedule-${scheduleId}-service`;
return {
scheduleId,
serviceKey,
packageName,
relativeDirectory,
manifestPath: `${relativeDirectory}/service-manifest.json`,
readmePath: `${relativeDirectory}/README.md`,
sourcePath: `${relativeDirectory}/service.ts`,
runtimePath: `${relativeDirectory}/service.mjs`,
};
}
export async function prepareManagedScheduleServiceDirectory(scheduleId: number) {
const repoRoot = getScheduleRepoRoot();
const absoluteDirectory = path.join(repoRoot, '.auto_codex', 'schedule', String(scheduleId));
await mkdir(absoluteDirectory, { recursive: true });
await rm(path.join(absoluteDirectory, 'managed-service'), { recursive: true, force: true });
}
export async function removeManagedScheduleServiceArtifacts(scheduleId: number) {
const repoRoot = getScheduleRepoRoot();
const scheduleDirectory = path.join(repoRoot, '.auto_codex', 'schedule', String(scheduleId));
await rm(path.join(scheduleDirectory, 'managed-service'), { recursive: true, force: true });
await rm(path.join(scheduleDirectory, 'README.md'), { force: true });
await rm(path.join(scheduleDirectory, 'service-manifest.json'), { force: true });
await rm(path.join(scheduleDirectory, 'service.ts'), { force: true });
await rm(path.join(scheduleDirectory, 'service.mjs'), { force: true });
}
export async function hasManagedScheduleServicePackage(relativeDirectory: string | null | undefined) {
const trimmedDirectory = String(relativeDirectory ?? '').trim();
if (!trimmedDirectory) {
return false;
}
const repoRoot = getScheduleRepoRoot();
const runtimePath = path.join(repoRoot, trimmedDirectory, 'service.mjs');
const manifestPath = path.join(repoRoot, trimmedDirectory, 'service-manifest.json');
try {
await Promise.all([access(runtimePath), access(manifestPath)]);
return true;
} catch {
return false;
}
}
export async function runManagedScheduleService(relativeDirectory: string) {
const repoRoot = getScheduleRepoRoot();
const runtimePath = path.join(repoRoot, relativeDirectory, 'service.mjs');
const importedModule = await import(`${pathToFileURL(runtimePath).href}?t=${Date.now()}`);
const run =
typeof importedModule.run === 'function'
? importedModule.run
: typeof importedModule.default?.run === 'function'
? importedModule.default.run
: null;
if (!run) {
throw new Error(`스케줄 서비스 실행 함수를 찾을 수 없습니다: ${relativeDirectory}/service.mjs`);
}
return run({
scheduleRoot: path.join(repoRoot, trimmedRelativeDirectory(relativeDirectory)),
scheduleDirectory: trimmedRelativeDirectory(relativeDirectory),
repoRoot,
now: new Date().toISOString(),
runCurrentPriceStockAlertService(definition: { scheduleId: number; serviceKey: string; title: string }) {
return sendManagedStockAlertWebPush({
scheduleId: definition.scheduleId,
serviceKey: definition.serviceKey,
title: definition.title,
mode: 'price',
});
},
runChangeRateThresholdStockAlertService(
definition: { scheduleId: number; serviceKey: string; title: string; thresholdPercent: number },
) {
return sendManagedStockAlertWebPush({
scheduleId: definition.scheduleId,
serviceKey: definition.serviceKey,
title: definition.title,
mode: 'change-threshold',
thresholdPercent: definition.thresholdPercent,
});
},
}) as Promise<ManagedScheduleServiceResult>;
}
function trimmedRelativeDirectory(relativeDirectory: string) {
return String(relativeDirectory ?? '').trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
}