187 lines
6.3 KiB
TypeScript
187 lines
6.3 KiB
TypeScript
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<ComposerDraftStorage> | 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<string, string> = {};
|
|
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(),
|
|
};
|
|
});
|
|
}
|