const invoke = window.__TAURI_INTERNALS__?.invoke; const loginView = document.getElementById("login-view"); const appShell = document.getElementById("app-shell"); const loginButton = document.getElementById("login-button"); const loginError = document.getElementById("login-error"); const vaultModal = document.getElementById("vault-modal"); const vaultModalTitle = document.getElementById("vault-modal-title"); const vaultModalCopy = document.getElementById("vault-modal-copy"); const vaultPasswordInput = document.getElementById("vault-password-input"); const vaultModalError = document.getElementById("vault-modal-error"); const vaultModalSave = document.getElementById("vault-modal-save"); const logoutButton = document.getElementById("logout-button"); const userTrigger = document.getElementById("user-trigger"); const userMenu = document.getElementById("user-menu"); const manageDevicesButton = document.getElementById("manage-devices"); const openMcpModalButton = document.getElementById("open-mcp-modal"); const deviceModal = document.getElementById("device-modal"); const deviceList = document.getElementById("device-list"); const closeDeviceModal = document.getElementById("close-device-modal"); const mcpModal = document.getElementById("mcp-modal"); const closeMcpModal = document.getElementById("close-mcp-modal"); const mcpIntegrationList = document.getElementById("mcp-integration-list"); const mcpConfig = document.getElementById("mcp-config"); const copyMcpConfigButton = document.getElementById("copy-mcp-config"); const copyMcpConfigButtonLabel = copyMcpConfigButton.querySelector(".button-label"); const userName = document.getElementById("user-name"); const userEmail = document.getElementById("user-email"); const folderList = document.getElementById("folder-list"); const entryList = document.getElementById("entry-list"); const detailFolderLabel = document.getElementById("detail-folder-label"); const entryTitle = document.getElementById("entry-title"); const metadataList = document.getElementById("metadata-list"); const secretList = document.getElementById("secret-list"); const searchInput = document.getElementById("search-input"); const typeFilter = document.getElementById("type-filter"); const detailBadge = document.getElementById("detail-badge"); const editEntryButton = document.getElementById("edit-entry-button"); const deleteEntryButton = document.getElementById("delete-entry-button"); const restoreEntryButton = document.getElementById("restore-entry-button"); const saveEntryButton = document.getElementById("save-entry-button"); const cancelEditButton = document.getElementById("cancel-edit-button"); const addSecretButton = document.getElementById("add-secret-button"); const newEntryButton = document.getElementById("new-entry-button"); const nameSection = document.getElementById("name-section"); const nameView = document.getElementById("name-view"); const nameInput = document.getElementById("name-input"); const metadataEditor = document.getElementById("metadata-editor"); const addMetadataButton = document.getElementById("add-metadata-button"); const entryModal = document.getElementById("entry-modal"); const closeEntryModal = document.getElementById("close-entry-modal"); const entryModalCancel = document.getElementById("entry-modal-cancel"); const entryModalSave = document.getElementById("entry-modal-save"); const entryModalFolder = document.getElementById("entry-modal-folder"); const entryModalTitle = document.getElementById("entry-modal-title"); const entryModalType = document.getElementById("entry-modal-type"); const secretModal = document.getElementById("secret-modal"); const closeSecretModal = document.getElementById("close-secret-modal"); const secretModalCancel = document.getElementById("secret-modal-cancel"); const secretModalSave = document.getElementById("secret-modal-save"); const secretModalTitle = document.getElementById("secret-modal-title"); const secretNameInput = document.getElementById("secret-name-input"); const secretTypeInput = document.getElementById("secret-type-input"); const secretValueInput = document.getElementById("secret-value-input"); const historyModal = document.getElementById("history-modal"); const closeHistoryModal = document.getElementById("close-history-modal"); const historyModalCopy = document.getElementById("history-modal-copy"); const historyList = document.getElementById("history-list"); const appState = { shell: null, activeFolder: "所有项目", activeType: "", query: "", mcpJson: "", editing: false, draftMetadata: [], revealedSecrets: {}, secretModalMode: "create", editingSecretId: null, historySecretId: null, }; function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function displaySecretName(name) { const normalized = String(name ?? "") .trim() .toLowerCase() .replaceAll("_", " "); if (normalized === "api token" || normalized === "token") return "访问令牌"; return String(name ?? ""); } function integrationGlyph(appName) { if (appName === "Cursor") return "⌘"; if (appName === "Claude Code") return ">_"; return "⌁"; } function selectedEntry() { return appState.shell?.selected_entry || null; } function resetSecretCache() { appState.revealedSecrets = {}; } function setSelectedDetail(detail) { if (!appState.shell) return; appState.shell.selected_entry_id = detail?.id || null; appState.shell.selected_entry = detail; appState.editing = false; appState.draftMetadata = detail?.metadata ? detail.metadata.map((item) => ({ ...item })) : []; resetSecretCache(); } function showLogin() { loginView.classList.remove("hidden"); appShell.classList.add("hidden"); } function showShell() { loginView.classList.add("hidden"); appShell.classList.remove("hidden"); } function setLoginError(message) { loginError.textContent = message || ""; loginError.classList.toggle("hidden", !message); } function setVaultModalError(message) { vaultModalError.textContent = message || ""; vaultModalError.classList.toggle("hidden", !message); } function openVaultModal({ setup = false } = {}) { vaultModalTitle.textContent = setup ? "设置本地 Vault 主密码" : "解锁本地 Vault"; vaultModalCopy.textContent = setup ? "首次使用,请先设置本地 vault 主密码。" : "请输入本地 vault 主密码以解锁。"; vaultPasswordInput.value = ""; setVaultModalError(""); vaultModal.classList.remove("hidden"); setTimeout(() => vaultPasswordInput.focus(), 0); } function closeVaultModal() { vaultModal.classList.add("hidden"); vaultPasswordInput.value = ""; setVaultModalError(""); } function setAvatar() { const avatar = document.querySelector(".avatar"); const initial = appState.shell?.user?.name?.trim()?.[0] || "S"; avatar.textContent = initial.toUpperCase(); } function renderFolders() { if (!appState.shell) return; userName.textContent = appState.shell.user.name; userEmail.textContent = appState.shell.user.email; setAvatar(); const activeFolder = appState.activeFolder; const folders = [ { label: "所有项目", count: appState.shell.folders .filter((folder) => folder.kind === "folder") .reduce((sum, folder) => sum + folder.count, 0), }, ...appState.shell.folders, ]; folderList.innerHTML = folders .map( (folder) => ` ` ) .join(""); } function renderTypeFilter() { if (!appState.shell) return; typeFilter.innerHTML = [ ``, ...appState.shell.entry_types.map( (entryType) => `` ), ].join(""); } function renderEntries() { if (!appState.shell) return; const entries = appState.shell.entries; const selectedId = appState.shell.selected_entry_id; if (!entries.length) { entryList.innerHTML = `
没有匹配的条目
调整搜索词、类型筛选或文件夹后再试。
`; return; } entryList.innerHTML = entries .map( (entry) => ` ` ) .join(""); } function renderMetadataEditor() { metadataEditor.innerHTML = appState.draftMetadata .map( (field, index) => `
` ) .join(""); } function syncEditMode() { const editing = appState.editing; const hasSelection = Boolean(selectedEntry()); const deleted = Boolean(selectedEntry()?.deleted); editEntryButton.classList.toggle("hidden", editing || !hasSelection || deleted); saveEntryButton.classList.toggle("hidden", !editing); cancelEditButton.classList.toggle("hidden", !editing); nameSection.classList.toggle("hidden", !editing); nameView.classList.add("hidden"); nameInput.classList.toggle("hidden", !editing); metadataList.classList.toggle("hidden", editing); metadataEditor.classList.toggle("hidden", !editing); addMetadataButton.classList.toggle("hidden", !editing); addSecretButton.classList.toggle("hidden", editing || !hasSelection || deleted); } function renderSecretCard(secret) { const revealed = appState.revealedSecrets[secret.id]; const isVisible = Boolean(revealed?.visible); const value = isVisible ? revealed.value : secret.masked_value; return `
${escapeHtml(displaySecretName(secret.name))}
${escapeHtml(value)}
`; } function renderDetail() { const detail = selectedEntry(); if (!detail) { detailFolderLabel.textContent = "-"; entryTitle.textContent = "未选择条目"; nameView.textContent = "-"; nameInput.value = ""; metadataList.innerHTML = `
从左侧列表选择一个条目后,这里会显示结构化元数据。
`; metadataEditor.innerHTML = ""; secretList.innerHTML = `
选中条目后会在这里显示受保护的密钥字段。
`; detailBadge.classList.add("hidden"); deleteEntryButton.classList.add("hidden"); restoreEntryButton.classList.add("hidden"); syncEditMode(); return; } detailFolderLabel.textContent = detail.folder; entryTitle.textContent = detail.title; nameView.textContent = detail.title; nameInput.value = detail.title; detailBadge.classList.toggle("hidden", !detail.deleted); deleteEntryButton.classList.toggle("hidden", detail.deleted); restoreEntryButton.classList.toggle("hidden", !detail.deleted); metadataList.innerHTML = detail.metadata.length ? detail.metadata .map( (field) => `
${escapeHtml(field.label)}
${escapeHtml(field.value)}
` ) .join("") : `
当前条目还没有元数据。
`; renderMetadataEditor(); secretList.innerHTML = detail.secrets.length ? detail.secrets.map(renderSecretCard).join("") : `
当前条目还没有密钥字段。
`; syncEditMode(); } function renderShell(shell) { appState.shell = shell; appState.activeFolder = "所有项目"; appState.activeType = ""; appState.query = ""; appState.editing = false; appState.draftMetadata = shell.selected_entry?.metadata ? shell.selected_entry.metadata.map((item) => ({ ...item })) : []; resetSecretCache(); searchInput.value = ""; renderFolders(); renderTypeFilter(); renderEntries(); renderDetail(); } async function ensureUnlockedShell(data) { if (!data?.logged_in) return data; if (data?.shell) return data; const vault = data?.vault; if (!vault) return data; return new Promise((resolve, reject) => { const setup = !vault.has_master_password; openVaultModal({ setup }); const submit = async () => { const password = vaultPasswordInput.value.trim(); if (!password) { setVaultModalError(setup ? "请先设置本地 vault 主密码" : "请输入本地 vault 主密码"); return; } vaultModalSave.disabled = true; try { if (setup) { await invoke("setup_master_password", { password }); } else { await invoke("unlock_vault", { password }); } cleanup(); closeVaultModal(); const bootstrapped = await invoke("app_bootstrap"); resolve(bootstrapped); } catch (error) { setVaultModalError(String(error)); } finally { vaultModalSave.disabled = false; } }; const onClick = () => { void submit(); }; const onKeydown = (event) => { if (event.key !== "Enter") return; event.preventDefault(); void submit(); }; const cleanup = () => { vaultModalSave.removeEventListener("click", onClick); vaultPasswordInput.removeEventListener("keydown", onKeydown); }; vaultModalSave.addEventListener("click", onClick); vaultPasswordInput.addEventListener("keydown", onKeydown); }); } function renderMcpDialog(data) { appState.mcpJson = data.mcp_json; mcpIntegrationList.innerHTML = data.integrations .map( (integration) => `
${escapeHtml(integration.app_name)}
` ) .join(""); mcpConfig.textContent = data.mcp_json; } function closeEntryModalPanel() { entryModal.classList.add("hidden"); entryModalFolder.value = ""; entryModalTitle.value = ""; entryModalType.value = ""; } function openEntryModalPanel() { const detail = selectedEntry(); entryModalFolder.value = detail?.folder || "Refining"; entryModalType.value = detail?.entry_type || "service"; entryModalTitle.value = ""; entryModal.classList.remove("hidden"); } function closeSecretModalPanel() { secretModal.classList.add("hidden"); appState.secretModalMode = "create"; appState.editingSecretId = null; secretNameInput.value = ""; secretTypeInput.value = "text"; secretValueInput.value = ""; } function openSecretModalPanel(mode, options = {}) { appState.secretModalMode = mode; appState.editingSecretId = options.secretId || null; secretModalTitle.textContent = mode === "edit" ? "编辑密钥" : "新增密钥"; secretNameInput.value = options.name || ""; secretTypeInput.value = options.secretType || "text"; secretValueInput.value = options.value || ""; secretModal.classList.remove("hidden"); } function closeHistoryModalPanel() { historyModal.classList.add("hidden"); appState.historySecretId = null; historyList.innerHTML = ""; } function setTransientLabel(labelNode, nextText, originalText) { if (!labelNode) return; labelNode.textContent = nextText; setTimeout(() => { labelNode.textContent = originalText; }, 1200); } async function bootstrap() { if (!invoke) { showLogin(); return; } try { let data = await invoke("app_bootstrap"); if (!data.logged_in) { showLogin(); return; } data = await ensureUnlockedShell(data); renderShell(data.shell); showShell(); } catch (error) { setLoginError(String(error)); showLogin(); } } async function doDemoLogin() { if (!invoke) return; setLoginError(""); loginButton.disabled = true; loginButton.textContent = "正在打开浏览器..."; try { let data = await invoke("continue_demo_login"); data = await ensureUnlockedShell(data); renderShell(data.shell); showShell(); } catch (error) { setLoginError(String(error)); } finally { loginButton.textContent = "前往浏览器登录"; loginButton.disabled = false; } } async function doLogout() { if (!invoke) return; await invoke("logout"); userMenu.classList.add("hidden"); appState.shell = null; showLogin(); } async function loadEntryDetail(entryId) { const detail = await invoke("entry_detail", { entryId }); setSelectedDetail(detail); renderEntries(); renderDetail(); } async function refreshEntries() { if (!appState.shell) return; const deletedOnly = appState.activeFolder === "最近删除"; const folder = appState.activeFolder === "所有项目" || deletedOnly ? null : appState.activeFolder; const query = appState.query.trim() || null; const entries = await invoke("list_entries", { query: { folder, entryType: appState.activeType || null, query, deletedOnly, }, }); appState.shell.entries = entries; renderEntries(); const nextSelected = entries.find((entry) => entry.id === appState.shell.selected_entry_id) || entries[0]; if (!nextSelected) { setSelectedDetail(null); renderDetail(); return; } if (!selectedEntry() || nextSelected.id !== appState.shell.selected_entry_id) { await loadEntryDetail(nextSelected.id); return; } renderDetail(); } async function refreshShell() { const data = await invoke("app_bootstrap"); if (!data.logged_in || !data.shell) return; const previousFolder = appState.activeFolder; const previousType = appState.activeType; const previousQuery = appState.query; const previousSelectedId = appState.shell?.selected_entry_id; renderShell(data.shell); appState.activeFolder = previousFolder; appState.activeType = previousType; appState.query = previousQuery; renderFolders(); renderTypeFilter(); await refreshEntries(); if (previousSelectedId && appState.shell.entries.some((entry) => entry.id === previousSelectedId)) { await loadEntryDetail(previousSelectedId); } } function applyUpdatedDetail(detail) { if (!appState.shell) return; const current = appState.shell.entries.find((entry) => entry.id === detail.id); if (current) { current.title = detail.title; current.subtitle = detail.entry_type; current.folder = detail.folder; } setSelectedDetail(detail); renderEntries(); renderDetail(); } async function openDevices() { if (!invoke) return; const devices = await invoke("device_list"); deviceList.innerHTML = devices.length ? devices .map( (device) => `
${escapeHtml(device.name)}
${escapeHtml(device.platform)} · ${escapeHtml(device.client_version)}
${escapeHtml(device.last_seen)}
${ device.ip ? `
IP · ${escapeHtml(device.ip)}
` : "" }
` ) .join("") : `
当前没有在线设备。
`; deviceModal.classList.remove("hidden"); userMenu.classList.add("hidden"); } async function openMcpDialog() { if (!invoke) return; const data = await invoke("mcp_dialog_data"); renderMcpDialog(data); mcpModal.classList.remove("hidden"); } async function ensureSecretValue(secretId, { visible = false } = {}) { const existing = appState.revealedSecrets[secretId]; if (existing) { if (visible) existing.visible = true; return existing; } const revealed = await invoke("reveal_secret_value", { secretId }); appState.revealedSecrets[secretId] = { ...revealed, visible }; return appState.revealedSecrets[secretId]; } async function toggleSecretVisibility(secretId) { const existing = appState.revealedSecrets[secretId]; if (existing?.visible) { existing.visible = false; } else { await ensureSecretValue(secretId, { visible: true }); } renderDetail(); } async function copySecretValue(secretId, button) { const revealed = await ensureSecretValue(secretId); await navigator.clipboard.writeText(revealed.value); setTransientLabel(button?.querySelector(".button-label"), "已复制", "复制"); } async function beginEditSecret(secretId) { const detail = selectedEntry(); const secret = detail?.secrets.find((item) => item.id === secretId); if (!secret) return; const revealed = await ensureSecretValue(secretId); openSecretModalPanel("edit", { secretId, name: secret.name, secretType: secret.secret_type, value: revealed.value, }); } async function openSecretHistory(secretId) { const detail = selectedEntry(); const secret = detail?.secrets.find((item) => item.id === secretId); if (!secret) return; const history = await invoke("secret_history", { secretId }); appState.historySecretId = secretId; historyModalCopy.textContent = `查看 ${secret.name} 的历史版本并选择回滚目标。`; historyList.innerHTML = history.length ? history .map( (item) => `
v${item.version} · ${escapeHtml(item.action)}
${escapeHtml(item.created_at)}
名称:${escapeHtml(item.name)} 类型:${escapeHtml(item.secret_type)} history_id:${item.history_id}
${escapeHtml(item.value)}
` ) .join("") : `
当前密钥还没有历史记录。
`; historyModal.classList.remove("hidden"); } async function saveSecretModalState() { const detail = selectedEntry(); if (!detail) return; const payload = { name: secretNameInput.value.trim(), secretType: secretTypeInput.value || "text", value: secretValueInput.value, }; if (!payload.name || !payload.value) { window.alert("请填写完整的密钥名称和内容。"); return; } if (appState.secretModalMode === "edit" && appState.editingSecretId) { const updated = await invoke("update_secret", { secret: { id: appState.editingSecretId, name: payload.name, secretType: payload.secretType, value: payload.value, }, }); closeSecretModalPanel(); applyUpdatedDetail(updated); return; } const created = await invoke("create_secret", { entryId: detail.id, secret: payload, }); closeSecretModalPanel(); applyUpdatedDetail(created); } async function createNewEntry() { const folder = entryModalFolder.value.trim(); const title = entryModalTitle.value.trim(); const entryType = entryModalType.value.trim() || "service"; if (!folder || !title) { window.alert("请填写项目和名称。"); return; } const detail = await invoke("create_entry", { entry: { folder, title, entryType, metadata: [], secrets: [], }, }); closeEntryModalPanel(); appState.activeFolder = folder; await refreshShell(); await loadEntryDetail(detail.id); } folderList.addEventListener("click", async (event) => { const button = event.target.closest("[data-folder]"); if (!button || !appState.shell) return; appState.activeFolder = button.dataset.folder; appState.editing = false; renderFolders(); await refreshEntries(); }); entryList.addEventListener("click", async (event) => { const button = event.target.closest("[data-entry-id]"); if (!button) return; const { entryId } = button.dataset; if (!entryId || entryId === appState.shell?.selected_entry_id) return; await loadEntryDetail(entryId); }); searchInput.addEventListener("input", async () => { appState.query = searchInput.value; await refreshEntries(); }); typeFilter.addEventListener("change", async () => { appState.activeType = typeFilter.value; await refreshEntries(); }); editEntryButton.addEventListener("click", () => { const detail = selectedEntry(); if (!detail) return; appState.editing = true; appState.draftMetadata = detail.metadata.map((item) => ({ ...item })); renderDetail(); }); cancelEditButton.addEventListener("click", () => { appState.editing = false; appState.draftMetadata = selectedEntry()?.metadata.map((item) => ({ ...item })) || []; renderDetail(); }); metadataEditor.addEventListener("input", (event) => { const target = event.target; const keyIndex = target.dataset.metadataKey; const valueIndex = target.dataset.metadataValue; if (keyIndex !== undefined) { appState.draftMetadata[Number(keyIndex)].label = target.value; } if (valueIndex !== undefined) { appState.draftMetadata[Number(valueIndex)].value = target.value; } }); metadataEditor.addEventListener("click", (event) => { const button = event.target.closest("[data-remove-metadata]"); if (!button) return; const index = Number(button.dataset.removeMetadata); appState.draftMetadata.splice(index, 1); renderMetadataEditor(); }); addMetadataButton.addEventListener("click", () => { appState.draftMetadata.push({ label: "", value: "" }); renderMetadataEditor(); }); saveEntryButton.addEventListener("click", async () => { const detail = selectedEntry(); if (!detail) return; try { const updated = await invoke("update_entry_detail", { entry: { ...detail, title: nameInput.value.trim(), metadata: appState.draftMetadata.filter((item) => item.label.trim()), }, }); applyUpdatedDetail(updated); } catch (error) { window.alert(String(error)); } }); deleteEntryButton.addEventListener("click", async () => { if (!appState.shell?.selected_entry_id) return; if (!window.confirm("确定要将当前条目移到最近删除吗?")) return; try { await invoke("delete_entry", { entryId: appState.shell.selected_entry_id }); appState.editing = false; await refreshShell(); await refreshEntries(); } catch (error) { window.alert(String(error)); } }); restoreEntryButton.addEventListener("click", async () => { if (!appState.shell?.selected_entry_id) return; try { await invoke("restore_deleted_entry", { entryId: appState.shell.selected_entry_id }); appState.activeFolder = "所有项目"; appState.editing = false; await refreshShell(); await refreshEntries(); } catch (error) { window.alert(String(error)); } }); addSecretButton.addEventListener("click", () => { if (!selectedEntry()) return; openSecretModalPanel("create"); }); newEntryButton.addEventListener("click", () => { openEntryModalPanel(); }); loginButton.addEventListener("click", doDemoLogin); logoutButton.addEventListener("click", doLogout); manageDevicesButton.addEventListener("click", openDevices); openMcpModalButton.addEventListener("click", openMcpDialog); userTrigger.addEventListener("click", () => userMenu.classList.toggle("hidden")); closeDeviceModal.addEventListener("click", () => deviceModal.classList.add("hidden")); closeMcpModal.addEventListener("click", () => mcpModal.classList.add("hidden")); closeEntryModal.addEventListener("click", closeEntryModalPanel); entryModalCancel.addEventListener("click", closeEntryModalPanel); entryModalSave.addEventListener("click", async () => { try { await createNewEntry(); } catch (error) { window.alert(String(error)); } }); closeSecretModal.addEventListener("click", closeSecretModalPanel); secretModalCancel.addEventListener("click", closeSecretModalPanel); secretModalSave.addEventListener("click", async () => { try { await saveSecretModalState(); } catch (error) { window.alert(String(error)); } }); closeHistoryModal.addEventListener("click", closeHistoryModalPanel); deviceModal.addEventListener("click", (event) => { if (event.target === deviceModal) { deviceModal.classList.add("hidden"); } }); mcpModal.addEventListener("click", (event) => { if (event.target === mcpModal) { mcpModal.classList.add("hidden"); } }); entryModal.addEventListener("click", (event) => { if (event.target === entryModal) { closeEntryModalPanel(); } }); secretModal.addEventListener("click", (event) => { if (event.target === secretModal) { closeSecretModalPanel(); } }); historyModal.addEventListener("click", (event) => { if (event.target === historyModal) { closeHistoryModalPanel(); } }); mcpIntegrationList.addEventListener("click", async (event) => { const button = event.target.closest("[data-apply-target]"); if (!button || !invoke) return; button.disabled = true; button.classList.add("is-loading"); try { const result = await invoke("apply_mcp_config", { target: button.dataset.applyTarget, }); renderMcpDialog(result.data); } finally { button.disabled = false; button.classList.remove("is-loading"); } }); secretList.addEventListener("click", async (event) => { const button = event.target.closest("[data-secret-action]"); if (!button) return; const secretId = button.dataset.secretId; const action = button.dataset.secretAction; if (!secretId) return; try { if (action === "toggle") { await toggleSecretVisibility(secretId); return; } if (action === "copy") { await copySecretValue(secretId, button); return; } if (action === "edit") { await beginEditSecret(secretId); return; } if (action === "history") { await openSecretHistory(secretId); return; } if (action === "delete") { if (!window.confirm("确定要删除这个密钥吗?")) return; await invoke("delete_secret", { secretId }); await loadEntryDetail(selectedEntry().id); } } catch (error) { window.alert(String(error)); } }); historyList.addEventListener("click", async (event) => { const button = event.target.closest("[data-history-version]"); if (!button || !appState.historySecretId) return; if (!window.confirm(`确定回滚到 v${button.dataset.historyVersion} 吗?`)) return; try { const detail = await invoke("rollback_secret", { secretId: appState.historySecretId, version: Number(button.dataset.historyVersion), historyId: Number(button.dataset.historyId), }); closeHistoryModalPanel(); applyUpdatedDetail(detail); } catch (error) { window.alert(String(error)); } }); copyMcpConfigButton.addEventListener("click", async () => { if (!appState.mcpJson) return; await navigator.clipboard.writeText(appState.mcpJson); copyMcpConfigButtonLabel.textContent = "已复制"; setTimeout(() => { copyMcpConfigButtonLabel.textContent = "复制"; }, 1200); }); document.addEventListener("click", (event) => { if (!userTrigger.contains(event.target) && !userMenu.contains(event.target)) { userMenu.classList.add("hidden"); } }); bootstrap();