oclaw/interfaces/admin/static/js/pages/attachments.js
oliver f6f129931f Ship Cursor-only MCP admin and modular Admin/Chat UI.
Plugins/Skills install and edit via mcpServers JSON, with clearer row actions, soft reloads, and instant loading placeholders; also drop MCP market and harden related scheduler/logging/exec guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 19:38:26 +08:00

229 lines
12 KiB
JavaScript

import { t, el, apiGet, apiPost, renderPageShell, renderSectionCard } from "../core.js";
async function renderAttachments() {
const status = el("div", { class: "muted", text: "" });
const hint = el("div", { class: "muted", style: "line-height:1.45;", text: t("attachments.excelPolicyHint") });
const loadBtn = el("button", { class: "btn", type: "button", text: t("action.refresh") });
const saveBtn = el("button", { class: "btn btn--primary", type: "button", text: t("attachments.save") });
const resetBtn = el("button", { class: "btn", type: "button", text: t("attachments.resetDefaults") });
const rowInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const colInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const cellInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const maxSheetsInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const largePreviewRowsInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const toolEnabledInput = el("input", { type: "checkbox" });
const toolMinRowsInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const toolMaxBytesInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const sqlTimeoutInput = el("input", { class: "input", type: "number", min: "100", max: "120000", step: "1" });
const imageReplayCapInput = el("input", { class: "input", type: "number", min: "600", max: "30000", step: "1" });
const videoReplayCapInput = el("input", { class: "input", type: "number", min: "600", max: "30000", step: "1" });
const videoTranscriptChunkSizeInput = el("input", { class: "input", type: "number", min: "1", max: "8000", step: "1" });
const videoTranscriptChunkOverlapInput = el("input", { class: "input", type: "number", min: "1", max: "4000", step: "1" });
const archiveMaxDepthInput = el("input", { class: "input", type: "number", min: "1", max: "10", step: "1" });
const archiveMaxFileCountInput = el("input", { class: "input", type: "number", min: "1", max: "20000", step: "1" });
const archiveMaxEntryBytesInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const archiveMaxTotalBytesInput = el("input", { class: "input", type: "number", min: "1", step: "1" });
const clampTimeout = (raw, fallback = 8000) => {
const n = parseInt(String(raw ?? "").trim(), 10);
if (!Number.isFinite(n)) return fallback;
return Math.max(100, Math.min(120000, n));
};
const parsePositiveInt = (v) => {
const n = parseInt(String(v ?? "").trim(), 10);
if (!Number.isFinite(n) || n < 1) return null;
return n;
};
const readUiLimits = () => {
const rows = parsePositiveInt(rowInput.value);
const cols = parsePositiveInt(colInput.value);
const chars = parsePositiveInt(cellInput.value);
const maxSheets = parsePositiveInt(maxSheetsInput.value);
const previewRows = parsePositiveInt(largePreviewRowsInput.value);
const minRows = parsePositiveInt(toolMinRowsInput.value);
const maxBytes = parsePositiveInt(toolMaxBytesInput.value);
const sqlTimeoutMs = parsePositiveInt(sqlTimeoutInput.value);
const imageReplayCapChars = parsePositiveInt(imageReplayCapInput.value);
const videoReplayCapChars = parsePositiveInt(videoReplayCapInput.value);
const videoTranscriptChunkSize = parsePositiveInt(videoTranscriptChunkSizeInput.value);
const videoTranscriptChunkOverlap = parsePositiveInt(videoTranscriptChunkOverlapInput.value);
const archiveMaxDepth = parsePositiveInt(archiveMaxDepthInput.value);
const archiveMaxFileCount = parsePositiveInt(archiveMaxFileCountInput.value);
const archiveMaxEntryBytes = parsePositiveInt(archiveMaxEntryBytesInput.value);
const archiveMaxTotalBytes = parsePositiveInt(archiveMaxTotalBytesInput.value);
if (
rows === null ||
cols === null ||
chars === null ||
maxSheets === null ||
previewRows === null ||
minRows === null ||
maxBytes === null ||
sqlTimeoutMs === null ||
imageReplayCapChars === null ||
videoReplayCapChars === null ||
videoTranscriptChunkSize === null ||
videoTranscriptChunkOverlap === null ||
archiveMaxDepth === null ||
archiveMaxFileCount === null ||
archiveMaxEntryBytes === null ||
archiveMaxTotalBytes === null
) {
throw new Error(t("attachments.invalidNumber"));
}
if (previewRows > 200 && !window.confirm(t("attachments.highPreviewWarn"))) {
throw new Error("cancelled");
}
return {
max_rows_read: rows,
max_columns: cols,
max_cell_chars: chars,
max_excel_sheets: maxSheets,
large_table_preview_rows: previewRows,
tool_mode_enabled: !!toolEnabledInput.checked,
tool_mode_min_rows: minRows,
tool_mode_max_bytes: maxBytes,
sql_timeout_ms: clampTimeout(sqlTimeoutMs, 8000),
image_result_replay_cap_chars: Math.max(600, Math.min(30000, imageReplayCapChars)),
video_result_replay_cap_chars: Math.max(600, Math.min(30000, videoReplayCapChars)),
video_transcript_chunk_size: Math.max(1, Math.min(8000, videoTranscriptChunkSize)),
video_transcript_chunk_overlap: Math.max(1, Math.min(4000, videoTranscriptChunkOverlap)),
archive_max_depth: Math.max(1, Math.min(10, archiveMaxDepth)),
archive_max_file_count: Math.max(1, Math.min(20000, archiveMaxFileCount)),
archive_max_entry_bytes: Math.max(1, archiveMaxEntryBytes),
archive_max_total_uncompressed_bytes: Math.max(1, archiveMaxTotalBytes),
};
};
const applyUiLimits = (limits) => {
const l = limits && typeof limits === "object" ? limits : {};
rowInput.value = String(l.max_rows_read || 5000);
colInput.value = String(l.max_columns || 200);
cellInput.value = String(l.max_cell_chars || 500);
maxSheetsInput.value = String(l.max_excel_sheets || 50);
largePreviewRowsInput.value = String(l.large_table_preview_rows || 20);
toolEnabledInput.checked = !!l.tool_mode_enabled;
toolMinRowsInput.value = String(l.tool_mode_min_rows || 5000);
toolMaxBytesInput.value = String(l.tool_mode_max_bytes || 31457280);
sqlTimeoutInput.value = String(clampTimeout(l.sql_timeout_ms, 8000));
imageReplayCapInput.value = String(Math.max(600, Math.min(30000, parsePositiveInt(l.image_result_replay_cap_chars) || 4000)));
videoReplayCapInput.value = String(Math.max(600, Math.min(30000, parsePositiveInt(l.video_result_replay_cap_chars) || 4000)));
videoTranscriptChunkSizeInput.value = String(Math.max(1, Math.min(8000, parsePositiveInt(l.video_transcript_chunk_size) || 1600)));
videoTranscriptChunkOverlapInput.value = String(Math.max(1, Math.min(4000, parsePositiveInt(l.video_transcript_chunk_overlap) || 200)));
archiveMaxDepthInput.value = String(Math.max(1, Math.min(10, parsePositiveInt(l.archive_max_depth) || 2)));
archiveMaxFileCountInput.value = String(Math.max(1, Math.min(20000, parsePositiveInt(l.archive_max_file_count) || 200)));
archiveMaxEntryBytesInput.value = String(Math.max(1, parsePositiveInt(l.archive_max_entry_bytes) || 10485760));
archiveMaxTotalBytesInput.value = String(Math.max(1, parsePositiveInt(l.archive_max_total_uncompressed_bytes) || 52428800));
};
const load = async () => {
status.textContent = t("chat.loading");
try {
const r = await apiGet("/admin/api/chat/settings/attachment-limits");
const limits = r && typeof r.limits === "object" ? r.limits : {};
applyUiLimits(limits);
status.textContent = "";
} catch (_) {
status.textContent = t("attachments.loadError");
}
};
const save = async () => {
status.textContent = t("chat.sending");
try {
const next = readUiLimits();
await apiPost("/admin/api/chat/settings/attachment-limits", { limits: next });
status.textContent = t("attachments.saved");
setTimeout(() => {
if (status.textContent === t("attachments.saved")) status.textContent = "";
}, 1500);
} catch (e) {
if (String(e && e.message ? e.message : e) === "cancelled") {
status.textContent = "";
return;
}
status.textContent = `${t("common.error")}: ${String(e)}`;
}
};
const reset = async () => {
status.textContent = t("chat.sending");
try {
const resp = await apiPost("/admin/api/chat/settings/attachment-limits", { limits: null });
const limits = resp && typeof resp.limits === "object" ? resp.limits : {};
applyUiLimits(limits);
status.textContent = t("attachments.saved");
setTimeout(() => {
if (status.textContent === t("attachments.saved")) status.textContent = "";
}, 1500);
} catch (e) {
status.textContent = `${t("common.error")}: ${String(e)}`;
}
};
loadBtn.addEventListener("click", load);
saveBtn.addEventListener("click", save);
resetBtn.addEventListener("click", reset);
await load();
const inputRow = (labelText, inputEl) =>
el("div", { style: "display:grid;gap:6px;margin-top:8px;" }, [
el("div", { class: "muted", text: labelText }),
inputEl,
]);
return renderPageShell({
title: t("attachments.title"),
subtitle: t("attachments.subtitle"),
actions: [loadBtn, resetBtn, saveBtn],
sections: [
{ id: "attach-overview", label: t("attachments.toc.overview") },
{ id: "attach-table", label: t("attachments.toc.table") },
{ id: "attach-media", label: t("attachments.toc.media") },
{ id: "attach-archive", label: t("attachments.toc.archive") },
],
}, [
renderSectionCard(t("attachments.card.policy"), t("attachments.excelPolicy"), [hint], { id: "attach-overview" }),
el("div", { class: "page-grid page-grid--two" }, [
renderSectionCard(t("attachments.card.tableMode"), "", [
inputRow(t("attachments.maxRowsRead"), rowInput),
inputRow(t("attachments.maxColumns"), colInput),
inputRow(t("attachments.maxCellChars"), cellInput),
inputRow(t("attachments.maxExcelSheets"), maxSheetsInput),
inputRow(t("attachments.largePreviewRows"), largePreviewRowsInput),
el("label", { class: "muted", style: "display:flex;gap:8px;align-items:center;margin-top:10px;cursor:pointer;" }, [
toolEnabledInput,
el("span", { text: t("attachments.toolModeEnabled") }),
]),
inputRow(t("attachments.toolModeMinRows"), toolMinRowsInput),
inputRow(t("attachments.toolModeMaxBytes"), toolMaxBytesInput),
inputRow(t("attachments.sqlTimeoutMs"), sqlTimeoutInput),
el("div", { class: "muted u-muted-block", text: t("attachments.sqlTimeoutHint") }),
], { id: "attach-table" }),
renderSectionCard(t("attachments.card.media"), "", [
inputRow(t("attachments.imageReplayCapChars"), imageReplayCapInput),
el("div", { class: "muted u-muted-block", text: t("attachments.imageReplayCapHint") }),
inputRow(t("attachments.videoReplayCapChars"), videoReplayCapInput),
el("div", { class: "muted u-muted-block", text: t("attachments.videoReplayCapHint") }),
inputRow(t("attachments.videoTranscriptChunkSize"), videoTranscriptChunkSizeInput),
inputRow(t("attachments.videoTranscriptChunkOverlap"), videoTranscriptChunkOverlapInput),
el("div", { class: "muted u-muted-block", text: t("attachments.videoTranscriptChunkHint") }),
], { id: "attach-media" }),
]),
renderSectionCard(t("attachments.card.archive"), "", [
inputRow(t("attachments.archiveMaxDepth"), archiveMaxDepthInput),
inputRow(t("attachments.archiveMaxFileCount"), archiveMaxFileCountInput),
inputRow(t("attachments.archiveMaxEntryBytes"), archiveMaxEntryBytesInput),
inputRow(t("attachments.archiveMaxTotalBytes"), archiveMaxTotalBytesInput),
el("div", { class: "muted u-muted-block", text: t("attachments.archivePolicyHint") }),
el("div", { class: "u-h-8" }),
status,
], { id: "attach-archive" }),
]);
}
export { renderAttachments };