152 lines
4.3 KiB
TypeScript
152 lines
4.3 KiB
TypeScript
import { WATCH_CI_PROMPT_PREFIX } from 'ejclaw-runners-shared';
|
|
|
|
import { TIMEZONE } from './config.js';
|
|
import type { ScheduledTask } from './types.js';
|
|
import { formatElapsedKorean } from './utils.js';
|
|
|
|
export type WatcherStatusPhase =
|
|
| 'checking'
|
|
| 'waiting'
|
|
| 'retrying'
|
|
| 'completed';
|
|
|
|
export const WATCH_CI_PREFIX = WATCH_CI_PROMPT_PREFIX;
|
|
export const TASK_STATUS_MESSAGE_PREFIX = '\u2063\u2063\u2063';
|
|
export const DEFAULT_WATCH_CI_MAX_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
let watchTimeFormatter: Intl.DateTimeFormat | null = null;
|
|
|
|
function getWatchTimeFormatter(): Intl.DateTimeFormat {
|
|
watchTimeFormatter ??= new Intl.DateTimeFormat('ko-KR', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false,
|
|
timeZone: TIMEZONE,
|
|
});
|
|
return watchTimeFormatter;
|
|
}
|
|
|
|
export function isWatchCiTask(task: Pick<ScheduledTask, 'prompt'>): boolean {
|
|
return task.prompt.startsWith(WATCH_CI_PREFIX);
|
|
}
|
|
|
|
export function isGitHubCiTask(
|
|
task: Pick<ScheduledTask, 'ci_provider'>,
|
|
): boolean {
|
|
return task.ci_provider === 'github';
|
|
}
|
|
|
|
export function isTaskStatusControlMessage(content: string): boolean {
|
|
return content.startsWith(TASK_STATUS_MESSAGE_PREFIX);
|
|
}
|
|
|
|
export function extractWatchCiTarget(prompt: string): string | null {
|
|
const match = prompt.match(
|
|
/Watch target:\n([\s\S]*?)\n\nCheck instructions:/,
|
|
);
|
|
return match?.[1]?.trim() || null;
|
|
}
|
|
|
|
function formatTimeLabel(timestampIso: string): string {
|
|
return getWatchTimeFormatter()
|
|
.format(new Date(timestampIso))
|
|
.replace(/:/g, '시 ')
|
|
.replace(/시 (\d{2})$/, '분 $1초');
|
|
}
|
|
|
|
function formatWatchIntervalLabel(
|
|
task: Pick<ScheduledTask, 'schedule_type' | 'schedule_value'>,
|
|
): string | null {
|
|
if (task.schedule_type !== 'interval') return null;
|
|
const ms = parseInt(task.schedule_value, 10);
|
|
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
|
|
const totalSeconds = Math.floor(ms / 1000);
|
|
if (totalSeconds < 60) return `${totalSeconds}초`;
|
|
|
|
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
if (totalMinutes < 60) {
|
|
const seconds = totalSeconds % 60;
|
|
return seconds > 0 ? `${totalMinutes}분 ${seconds}초` : `${totalMinutes}분`;
|
|
}
|
|
|
|
const hours = Math.floor(totalMinutes / 60);
|
|
const minutes = totalMinutes % 60;
|
|
return minutes > 0 ? `${hours}시간 ${minutes}분` : `${hours}시간`;
|
|
}
|
|
|
|
function formatElapsedLabel(
|
|
startedAtIso: string,
|
|
checkedAtIso: string,
|
|
): string {
|
|
const elapsedMs = Math.max(
|
|
0,
|
|
new Date(checkedAtIso).getTime() - new Date(startedAtIso).getTime(),
|
|
);
|
|
return formatElapsedKorean(elapsedMs);
|
|
}
|
|
|
|
export function renderWatchCiStatusMessage(args: {
|
|
task: Pick<ScheduledTask, 'prompt' | 'schedule_type' | 'schedule_value'>;
|
|
phase: WatcherStatusPhase;
|
|
checkedAt: string;
|
|
statusStartedAt?: string | null;
|
|
nextRun?: string | null;
|
|
}): string {
|
|
const target = extractWatchCiTarget(args.task.prompt) || 'CI watcher';
|
|
const title =
|
|
args.phase === 'completed'
|
|
? `CI 감시 종료: ${target}`
|
|
: `CI 감시 중: ${target}`;
|
|
const statusLabel =
|
|
args.phase === 'checking'
|
|
? '확인 중'
|
|
: args.phase === 'retrying'
|
|
? '재시도 대기'
|
|
: args.phase === 'completed'
|
|
? '완료'
|
|
: '대기 중';
|
|
|
|
const lines = [
|
|
title,
|
|
`- 상태: ${statusLabel}`,
|
|
`- 마지막 확인: ${formatTimeLabel(args.checkedAt)}`,
|
|
];
|
|
if (args.statusStartedAt) {
|
|
lines.push(
|
|
`- 경과 시간: ${formatElapsedLabel(args.statusStartedAt, args.checkedAt)}`,
|
|
);
|
|
}
|
|
const intervalLabel = formatWatchIntervalLabel(args.task);
|
|
if (intervalLabel) {
|
|
lines.push(`- 확인 간격: ${intervalLabel}`);
|
|
}
|
|
|
|
if (args.nextRun) {
|
|
lines.push(`- 다음 확인: ${formatTimeLabel(args.nextRun)}`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
export function getTaskQueueJid(
|
|
task: Pick<ScheduledTask, 'chat_jid' | 'context_mode' | 'id' | 'prompt'>,
|
|
): string {
|
|
return task.context_mode === 'isolated' || isWatchCiTask(task)
|
|
? `${task.chat_jid}::task:${task.id}`
|
|
: task.chat_jid;
|
|
}
|
|
|
|
export function getTaskRuntimeTaskId(
|
|
task: Pick<ScheduledTask, 'context_mode' | 'id' | 'prompt'>,
|
|
): string | undefined {
|
|
return task.context_mode === 'isolated' || isWatchCiTask(task)
|
|
? task.id
|
|
: undefined;
|
|
}
|
|
|
|
export function shouldUseTaskScopedSession(
|
|
task: Pick<ScheduledTask, 'context_mode'>,
|
|
): boolean {
|
|
return task.context_mode === 'isolated';
|
|
}
|