import { normalizeAttachmentPaths } from '../../shared/workspace/utils'; import { normalizeDashboardAttachmentPath } from '../../shared/workspace/workspaceMarkdown'; import type { CronJob, MCPConfigResponse, MCPServerDraft, } from './types'; const COMPOSER_DRAFT_STORAGE_PREFIX = 'nanobot-dashboard-composer-draft:v1:'; export function stateLabel(s?: string) { return (s || 'IDLE').toUpperCase(); } export function normalizeRuntimeState(s?: string) { const raw = stateLabel(s); if (raw.includes('ERROR') || raw.includes('FAIL')) return 'ERROR'; if (raw.includes('TOOL') || raw.includes('EXEC') || raw.includes('ACTION')) return 'TOOL_CALL'; if (raw.includes('THINK') || raw.includes('PLAN') || raw.includes('REASON') || raw === 'RUNNING') return 'THINKING'; if (raw.includes('SUCCESS') || raw.includes('DONE') || raw.includes('COMPLETE')) return 'SUCCESS'; if (raw.includes('IDLE') || raw.includes('STOP')) return 'IDLE'; return raw; } export interface ComposerDraftStorage { command: string; attachments: string[]; updated_at_ms: number; } export function getComposerDraftStorageKey(botId: string): string { return `${COMPOSER_DRAFT_STORAGE_PREFIX}${String(botId || '').trim()}`; } export function loadComposerDraft(botId: string): ComposerDraftStorage | null { const id = String(botId || '').trim(); if (!id || typeof window === 'undefined') return null; try { const raw = window.localStorage.getItem(getComposerDraftStorageKey(id)); if (!raw) return null; const parsed = JSON.parse(raw) as Partial | null; const command = String(parsed?.command || ''); const attachments = normalizeAttachmentPaths(parsed?.attachments) .map(normalizeDashboardAttachmentPath) .filter(Boolean); return { command, attachments, updated_at_ms: Number(parsed?.updated_at_ms || Date.now()), }; } catch { return null; } } export function persistComposerDraft(botId: string, commandRaw: string, attachmentsRaw: string[]): void { const id = String(botId || '').trim(); if (!id || typeof window === 'undefined') return; const command = String(commandRaw || ''); const attachments = normalizeAttachmentPaths(attachmentsRaw) .map(normalizeDashboardAttachmentPath) .filter(Boolean); const key = getComposerDraftStorageKey(id); try { if (!command.trim() && attachments.length === 0) { window.localStorage.removeItem(key); return; } const payload: ComposerDraftStorage = { command, attachments, updated_at_ms: Date.now(), }; window.localStorage.setItem(key, JSON.stringify(payload)); } catch { // ignore localStorage write failures } } export function clampTemperature(value: number) { if (Number.isNaN(value)) return 0.2; return Math.min(1, Math.max(0, value)); } export function clampMaxTokens(value: number) { if (Number.isNaN(value)) return 8192; return Math.min(32768, Math.max(256, Math.round(value))); } export function clampCpuCores(value: number) { if (Number.isNaN(value)) return 1; if (value === 0) return 0; return Math.min(16, Math.max(0.1, Math.round(value * 10) / 10)); } export function clampMemoryMb(value: number) { if (Number.isNaN(value)) return 1024; if (value === 0) return 0; return Math.min(65536, Math.max(256, Math.round(value))); } export function clampStorageGb(value: number) { if (Number.isNaN(value)) return 10; if (value === 0) return 0; return Math.min(1024, Math.max(1, Math.round(value))); } export function formatBytes(bytes: number): string { const value = Number(bytes || 0); if (!Number.isFinite(value) || value <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const idx = Math.min(units.length - 1, Math.floor(Math.log(value) / Math.log(1024))); const scaled = value / Math.pow(1024, idx); return `${scaled >= 10 ? scaled.toFixed(1) : scaled.toFixed(2)} ${units[idx]}`; } export function formatPercent(value: number): string { const n = Number(value || 0); if (!Number.isFinite(n)) return '0.00%'; return `${Math.max(0, n).toFixed(2)}%`; } export function formatWorkspaceTime(raw: string | undefined, isZh: boolean): string { const text = String(raw || '').trim(); if (!text) return '-'; const dt = new Date(text); if (Number.isNaN(dt.getTime())) return '-'; try { return dt.toLocaleString(isZh ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit', hour12: false, }); } catch { return dt.toLocaleString(); } } export function formatCronSchedule(job: CronJob, isZh: boolean) { const s = job.schedule || {}; if (s.kind === 'every' && Number(s.everyMs) > 0) { const sec = Math.round(Number(s.everyMs) / 1000); return isZh ? `每 ${sec}s` : `every ${sec}s`; } if (s.kind === 'cron') { if (s.tz) return `${s.expr || '-'} (${s.tz})`; return s.expr || '-'; } if (s.kind === 'at' && Number(s.atMs) > 0) { return new Date(Number(s.atMs)).toLocaleString(); } return '-'; } export function mapMcpResponseToDrafts(payload?: MCPConfigResponse | null): MCPServerDraft[] { const rows = payload?.mcp_servers && typeof payload.mcp_servers === 'object' ? payload.mcp_servers : {}; return Object.entries(rows).map(([name, cfg]) => { const rawHeaders = cfg?.headers && typeof cfg.headers === 'object' ? cfg.headers : {}; const headers: Record = {}; Object.entries(rawHeaders).forEach(([k, v]) => { const key = String(k || '').trim(); if (!key) return; headers[key] = String(v ?? '').trim(); }); const headerEntries = Object.entries(headers); const botIdHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-id'); const botSecretHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-secret'); const type: MCPServerDraft['type'] = String(cfg?.type || 'streamableHttp') === 'sse' ? 'sse' : 'streamableHttp'; return { name: String(name || '').trim(), type, url: String(cfg?.url || '').trim(), botId: String(botIdHeader?.[1] || '').trim(), botSecret: String(botSecretHeader?.[1] || '').trim(), toolTimeout: String(Number(cfg?.toolTimeout || 60) || 60), headers, locked: Boolean(cfg?.locked), originName: String(name || '').trim(), }; }); }