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) => `
${integrationGlyph(integration.app_name)}
${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();