const APP_VERSION = "0.1.0"; const STORAGE_KEYS = { servers: "remoteconn_servers_v4", global: "remoteconn_global_v3", actionLogs: "remoteconn_action_logs_v1", sessionLogs: "remoteconn_session_logs_v1", knownHosts: "remoteconn_known_hosts_v1", credentials: "remoteconn_credentials_v1", pluginPackages: "remoteconn_plugin_packages_v1", pluginRecords: "remoteconn_plugin_records_v1", }; const PERMISSION_WHITELIST = new Set([ "commands.register", "session.read", "session.write", "ui.notice", "ui.statusbar", "storage.read", "storage.write", "logs.read", ]); const SESSION_STATES = [ "idle", "connecting", "auth_pending", "connected", "reconnecting", "disconnected", "error", ]; const SESSION_TRANSITIONS = { idle: new Set(["connecting", "disconnected"]), connecting: new Set(["auth_pending", "error", "disconnected"]), auth_pending: new Set(["connected", "error", "disconnected"]), connected: new Set(["reconnecting", "disconnected", "error"]), reconnecting: new Set(["connected", "error", "disconnected"]), disconnected: new Set(["connecting", "idle"]), error: new Set(["connecting", "disconnected"]), }; const themePresets = { tide: { label: "潮汐蓝", accentColor: "#5bd2ff", bgColor: "#192b4d", textColor: "#e6f0ff", liquidAlpha: 0.64, blurRadius: 22, motionDuration: 14, }, mint: { label: "薄荷流", accentColor: "#53ffcb", bgColor: "#102b34", textColor: "#ecfff7", liquidAlpha: 0.7, blurRadius: 24, motionDuration: 16, }, sunrise: { label: "晨曦橙", accentColor: "#ffb86c", bgColor: "#2e223b", textColor: "#fff4df", liquidAlpha: 0.58, blurRadius: 20, motionDuration: 12, }, }; const defaultServers = [ { id: "srv-1", name: "生产主机", username: "ops", host: "172.16.10.8", port: 22, tags: ["prod", "beijing"], authType: "privateKey", timeout: 20, heartbeat: 15, transportMode: "web", projectPath: "~/workspace/remoteconn", projectCandidates: "~/workspace/remoteconn,~/workspace/common", autoStartCodex: false, credentialRefId: "cred-1", lastConnectedAt: "2026-02-19 08:31:00", }, { id: "srv-2", name: "预发环境", username: "devops", host: "10.20.1.45", port: 22, tags: ["staging", "shanghai"], authType: "password", timeout: 25, heartbeat: 20, transportMode: "web", projectPath: "~/workspace/staging", projectCandidates: "~/workspace/staging", autoStartCodex: false, credentialRefId: "cred-2", lastConnectedAt: "2026-02-18 23:10:00", }, { id: "srv-3", name: "个人实验机", username: "gavin", host: "192.168.31.12", port: 2222, tags: ["lab"], authType: "certificate", timeout: 18, heartbeat: 12, transportMode: "ios", projectPath: "~/projects/lab", projectCandidates: "~/projects/lab,~/projects/demo", autoStartCodex: false, credentialRefId: "cred-3", lastConnectedAt: "2026-02-17 14:02:00", }, ]; const defaultCredentials = { "cred-1": { id: "cred-1", type: "privateKey", password: "", privateKey: "~/.ssh/id_ed25519", passphrase: "", certPath: "", createdAt: "2026-02-19 08:20:00", updatedAt: "2026-02-19 08:31:00", }, "cred-2": { id: "cred-2", type: "password", password: "", privateKey: "", passphrase: "", certPath: "", createdAt: "2026-02-18 23:00:00", updatedAt: "2026-02-18 23:10:00", }, "cred-3": { id: "cred-3", type: "certificate", password: "", privateKey: "~/.ssh/lab_key", passphrase: "", certPath: "~/.ssh/lab_key-cert.pub", createdAt: "2026-02-17 13:50:00", updatedAt: "2026-02-17 14:02:00", }, }; const defaultGlobalConfig = { fontFamily: "JetBrains Mono", fontSize: 15, lineHeight: 1.4, cursorStyle: "block", unicode11: true, autoReconnect: true, reconnectLimit: 3, themePreset: "tide", accentColor: "#5bd2ff", bgColor: "#192b4d", textColor: "#e6f0ff", liquidAlpha: 0.64, blurRadius: 22, motionDuration: 14, hostKeyPolicy: "strict", logRetentionDays: 30, maskSecrets: true, credentialMemoryPolicy: "remember", }; const samplePluginPackages = [ { manifest: { id: "codex-shortcuts", name: "Codex Shortcuts", version: "0.1.0", minAppVersion: "0.1.0", description: "提供常用 Codex 快捷命令", entry: "main.js", style: "styles.css", permissions: ["commands.register", "session.read", "session.write", "ui.notice", "storage.read", "storage.write"], author: "RemoteConn", }, mainJs: ` module.exports = { async onload(ctx) { const count = Number((await ctx.storage.get("boot_count")) || 0) + 1; await ctx.storage.set("boot_count", count); ctx.logger.info("插件启动次数", String(count)); ctx.commands.register({ id: "git-status", title: "Git 状态", when: "connected", async handler() { await ctx.session.send("git status -sb"); }, }); ctx.commands.register({ id: "codex-doctor", title: "Codex Doctor", when: "connected", async handler() { await ctx.session.send("codex --doctor"); }, }); ctx.session.on("connected", ({ serverName }) => { ctx.ui.showNotice("插件已接入会话:" + serverName, "info"); }); }, async onunload() { return true; }, }; `, stylesCss: ` .command-chip[data-plugin-id="codex-shortcuts"] { border-color: rgba(95, 228, 255, 0.65); } `, }, { manifest: { id: "latency-watcher", name: "Latency Watcher", version: "0.1.0", minAppVersion: "0.1.0", description: "当延迟过高时给出提示", entry: "main.js", style: "styles.css", permissions: ["session.read", "ui.notice", "logs.read"], author: "RemoteConn", }, mainJs: ` module.exports = { onload(ctx) { ctx.session.on("latency", ({ latency }) => { if (latency > 180) { ctx.ui.showNotice("当前延迟较高:" + latency + "ms", "warn"); } }); }, }; `, stylesCss: ` .state-pill.state-connected { text-shadow: 0 0 8px rgba(121, 243, 189, 0.4); } `, }, ]; const dom = { screens: document.querySelectorAll(".screen"), serverList: document.getElementById("serverList"), serverSearch: document.getElementById("serverSearch"), serverSort: document.getElementById("serverSort"), serverForm: document.getElementById("serverForm"), authTypeSelect: document.getElementById("authTypeSelect"), authFields: document.getElementById("authFields"), consoleServerName: document.getElementById("consoleServerName"), globalForm: document.getElementById("globalForm"), themePresetSelect: document.getElementById("themePresetSelect"), contrastHint: document.getElementById("contrastHint"), terminalOutput: document.getElementById("terminalOutput"), terminalInput: document.getElementById("terminalInput"), sessionStateBadge: document.getElementById("sessionStateBadge"), latencyValue: document.getElementById("latencyValue"), pluginCommandBar: document.getElementById("pluginCommandBar"), saveServerBtn: document.getElementById("saveServerBtn"), saveGlobalBtn: document.getElementById("saveGlobalBtn"), codexDialog: document.getElementById("codexDialog"), globalDialog: document.getElementById("globalDialog"), hostKeyDialog: document.getElementById("hostKeyDialog"), globalSnapshot: document.getElementById("globalSnapshot"), hostKeySummary: document.getElementById("hostKeySummary"), hostKeyFingerprint: document.getElementById("hostKeyFingerprint"), hostKeyRemember: document.getElementById("hostKeyRemember"), sessionLogList: document.getElementById("sessionLogList"), sessionLogDetail: document.getElementById("sessionLogDetail"), logServerFilter: document.getElementById("logServerFilter"), logStatusFilter: document.getElementById("logStatusFilter"), logDateFrom: document.getElementById("logDateFrom"), logDateTo: document.getElementById("logDateTo"), pluginPackageInput: document.getElementById("pluginPackageInput"), pluginList: document.getElementById("pluginList"), pluginRuntimeLog: document.getElementById("pluginRuntimeLog"), toast: document.getElementById("toast"), }; const appState = { currentView: "servermanager", viewHistory: ["servermanager"], servers: [], credentials: {}, knownHosts: {}, globalConfig: { ...defaultGlobalConfig }, selectedServerId: null, selectedServerIds: new Set(), search: "", sortBy: "recent", actionLogs: [], sessionLogs: [], selectedSessionLogId: null, terminalLines: [], sessionState: "idle", currentTransport: null, reconnectAttempts: 0, reconnectTimer: null, reconnectReason: "", currentSessionLogId: null, sessionMeta: null, pendingHostKeyDecision: null, pluginRuntimeLogs: [], }; class TinyEmitter { constructor() { this.listeners = new Map(); } on(eventName, listener) { if (!this.listeners.has(eventName)) { this.listeners.set(eventName, new Set()); } this.listeners.get(eventName).add(listener); return () => this.off(eventName, listener); } off(eventName, listener) { const bucket = this.listeners.get(eventName); if (!bucket) return; bucket.delete(listener); if (bucket.size === 0) { this.listeners.delete(eventName); } } emit(eventName, payload) { const bucket = this.listeners.get(eventName); if (!bucket) return; bucket.forEach((listener) => { try { listener(payload); } catch (error) { console.error("事件处理异常", error); } }); } } /** * 传输层抽象:这里用 MockTransport 模拟 iOS Native / Web 网关 / 小程序网关三种模式。 * 真实接入时只需要保持 connect/exec/disconnect/on 这四类能力一致即可替换。 */ class MockTransport { constructor(mode = "web") { this.mode = mode; this.connected = false; this.currentPath = "~"; this.emitter = new TinyEmitter(); this.latencyTimer = null; this.heartbeatTimer = null; this.server = null; this.codexInstalled = true; } on(eventName, listener) { return this.emitter.on(eventName, listener); } async connect({ server }) { this.server = server; this.codexInstalled = !server.host.includes("10.20.1.45"); await sleep(220 + Math.floor(Math.random() * 160)); this.connected = true; this.currentPath = "~"; this.latencyTimer = window.setInterval(() => { if (!this.connected) return; const base = this.mode === "ios" ? 34 : this.mode === "miniapp" ? 78 : 52; const jitter = Math.floor(Math.random() * 130); this.emitter.emit("latency", { latency: base + jitter }); }, 2000); this.heartbeatTimer = window.setInterval(() => { if (!this.connected) return; this.emitter.emit("stdout", { text: `[心跳] ${formatDateTime(new Date(), true)} channel alive` }); }, Math.max(6000, (server.heartbeat || 15) * 1000)); return { welcome: [ `连接模式: ${this.mode.toUpperCase()}`, `登录到 ${server.username}@${server.host}:${server.port}`, "Last login: Thu Feb 19 08:35:11 2026", ], }; } async exec(command) { if (!this.connected) { const error = new Error("连接已断开,无法执行命令"); this.emitter.emit("error", { error }); throw error; } const cleaned = String(command || "").trim(); if (!cleaned) { return { code: 0, stdout: "", stderr: "" }; } this.emitter.emit("stdout", { text: `$ ${cleaned}` }); await sleep(80 + Math.floor(Math.random() * 120)); if (cleaned.startsWith("cd ")) { const rawPath = cleaned.slice(3).trim(); const path = rawPath.replace(/^['"]|['"]$/g, ""); if (path.includes("missing") || path.includes("不存在")) { const stderr = "cd: no such file or directory"; this.emitter.emit("stderr", { text: stderr }); return { code: 1, stdout: "", stderr }; } this.currentPath = path; const stdout = `切换目录成功: ${path}`; this.emitter.emit("stdout", { text: stdout }); return { code: 0, stdout, stderr: "" }; } if (cleaned === "command -v codex") { if (!this.codexInstalled) { const stderr = "codex: command not found"; this.emitter.emit("stderr", { text: stderr }); return { code: 1, stdout: "", stderr }; } const stdout = "/usr/local/bin/codex"; this.emitter.emit("stdout", { text: stdout }); return { code: 0, stdout, stderr: "" }; } if (cleaned.startsWith("codex")) { if (!this.codexInstalled) { const stderr = "codex: command not found"; this.emitter.emit("stderr", { text: stderr }); return { code: 127, stdout: "", stderr }; } const stdout = "Codex 已启动,等待任务输入..."; this.emitter.emit("stdout", { text: stdout }); return { code: 0, stdout, stderr: "" }; } if (cleaned === "clear") { return { code: 0, stdout: "", stderr: "" }; } if (cleaned === "git status -sb") { const stdout = "## main...origin/main\n M prototype/liquid-console.js\n M prototype/liquid-console.html"; this.emitter.emit("stdout", { text: stdout }); return { code: 0, stdout, stderr: "" }; } if (cleaned === "codex --doctor") { const stdout = "[doctor] environment ok\n[doctor] plugin runtime ready\n[doctor] transport healthy"; this.emitter.emit("stdout", { text: stdout }); return { code: 0, stdout, stderr: "" }; } const stdout = `执行完成: ${cleaned}`; this.emitter.emit("stdout", { text: stdout }); return { code: 0, stdout, stderr: "" }; } disconnect(reason = "manual") { if (!this.connected) return; this.connected = false; if (this.latencyTimer) { window.clearInterval(this.latencyTimer); this.latencyTimer = null; } if (this.heartbeatTimer) { window.clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } this.emitter.emit("disconnect", { reason }); } } /** * Web 平台无法直接访问 ~/.remoteconn,这里使用 PluginFsAdapter 抽象一层“虚拟插件目录”。 * 这样未来替换为 iOS 本地文件系统或桌面文件系统时,不需要重写插件管理逻辑。 */ class PluginFsAdapter { constructor() { this.cache = new Map(); } loadPackages() { const saved = loadJson(STORAGE_KEYS.pluginPackages, []); if (Array.isArray(saved)) { saved.forEach((pkg) => { if (pkg?.manifest?.id) { this.cache.set(pkg.manifest.id, pkg); } }); } samplePluginPackages.forEach((pkg) => { if (!this.cache.has(pkg.manifest.id)) { this.cache.set(pkg.manifest.id, pkg); } }); this.persist(); return this.listPackages(); } listPackages() { return Array.from(this.cache.values()).sort((a, b) => String(a.manifest?.name || "").localeCompare(String(b.manifest?.name || ""), "zh-CN") ); } getPackage(pluginId) { return this.cache.get(pluginId) || null; } upsertPackage(pluginPackage) { this.cache.set(pluginPackage.manifest.id, pluginPackage); this.persist(); } removePackage(pluginId) { this.cache.delete(pluginId); this.persist(); } exportPackages() { return this.listPackages(); } persist() { saveJson(STORAGE_KEYS.pluginPackages, Array.from(this.cache.values())); } } /** * 插件运行时管理器,负责加载、权限校验、生命周期调用、命令注册和错误隔离。 * 关键原则:单插件崩溃不影响 SSH 主链路。 */ class PluginManager { constructor(fsAdapter) { this.fsAdapter = fsAdapter; this.records = new Map(); this.runtime = new Map(); this.commands = new Map(); this.sessionHandlers = new Map(); } bootstrap() { this.fsAdapter.loadPackages(); this.loadRecords(); this.renderPluginList(); this.renderPluginRuntimeLog(); // 自动恢复已启用插件 const records = Array.from(this.records.values()).filter((item) => item.enabled); records.forEach((record) => { this.enable(record.id).catch((error) => { this.logRuntime("error", record.id, `自动加载失败: ${error.message}`); }); }); } loadRecords() { const stored = loadJson(STORAGE_KEYS.pluginRecords, []); if (Array.isArray(stored)) { stored.forEach((record) => { if (record?.id) { this.records.set(record.id, { ...record }); } }); } this.fsAdapter.listPackages().forEach((pkg) => { if (!this.records.has(pkg.manifest.id)) { this.records.set(pkg.manifest.id, { id: pkg.manifest.id, enabled: false, status: "discovered", errorCount: 0, lastError: "", installedAt: formatDateTime(new Date()), updatedAt: formatDateTime(new Date()), lastLoadedAt: "", }); } }); this.persistRecords(); } persistRecords() { saveJson(STORAGE_KEYS.pluginRecords, Array.from(this.records.values())); } refresh() { this.fsAdapter.loadPackages(); this.loadRecords(); this.renderPluginList(); } validatePackage(pluginPackage) { if (!pluginPackage || typeof pluginPackage !== "object") { throw new Error("插件包必须是对象"); } const { manifest, mainJs, stylesCss } = pluginPackage; if (!manifest || typeof manifest !== "object") { throw new Error("缺少 manifest"); } const required = ["id", "name", "version", "minAppVersion", "description", "entry", "style", "permissions"]; required.forEach((field) => { if (!(field in manifest)) { throw new Error(`manifest 缺少字段: ${field}`); } }); if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(manifest.id)) { throw new Error("插件 id 不符合规范"); } if (!/^\d+\.\d+\.\d+$/.test(manifest.version)) { throw new Error("version 必须是 SemVer 格式,例如 0.1.0"); } if (!/^\d+\.\d+\.\d+$/.test(manifest.minAppVersion)) { throw new Error("minAppVersion 必须是 SemVer 格式"); } if (manifest.entry !== "main.js" || manifest.style !== "styles.css") { throw new Error("entry/style 目前仅支持 main.js 与 styles.css"); } if (!Array.isArray(manifest.permissions)) { throw new Error("permissions 必须是数组"); } manifest.permissions.forEach((permission) => { if (!PERMISSION_WHITELIST.has(permission)) { throw new Error(`未知权限: ${permission}`); } }); if (typeof mainJs !== "string" || !mainJs.trim()) { throw new Error("mainJs 不能为空"); } if (typeof stylesCss !== "string") { throw new Error("stylesCss 必须是字符串"); } if (/^\s*\*/m.test(stylesCss) || /^\s*(body|html)\s*[{,]/m.test(stylesCss)) { throw new Error("styles.css 禁止使用全局选择器(* / body / html)"); } return { manifest: { ...manifest }, mainJs, stylesCss, }; } installPackage(pluginPackage) { const valid = this.validatePackage(pluginPackage); this.fsAdapter.upsertPackage(valid); const record = this.records.get(valid.manifest.id) || { id: valid.manifest.id, enabled: false, status: "discovered", errorCount: 0, lastError: "", installedAt: formatDateTime(new Date()), updatedAt: formatDateTime(new Date()), lastLoadedAt: "", }; record.updatedAt = formatDateTime(new Date()); record.status = "validated"; record.lastError = ""; this.records.set(valid.manifest.id, record); this.persistRecords(); this.renderPluginList(); this.logRuntime("info", valid.manifest.id, "插件包校验通过并写入虚拟目录"); } async enable(pluginId) { const pluginPackage = this.fsAdapter.getPackage(pluginId); if (!pluginPackage) { throw new Error("插件不存在"); } const validPackage = this.validatePackage(pluginPackage); const record = this.ensureRecord(pluginId); if (record.errorCount >= 3) { throw new Error("插件已触发熔断,请先重载后再启用"); } if (this.runtime.has(pluginId)) { await this.disable(pluginId, false); } record.status = "loading"; record.lastError = ""; this.persistRecords(); this.renderPluginList(); const runtime = { pluginId, manifest: validPackage.manifest, cleanupFns: [], api: null, styleNode: null, }; try { runtime.styleNode = this.mountStyle(validPackage.manifest, validPackage.stylesCss); const ctx = this.createPluginContext(validPackage.manifest, runtime); runtime.api = this.loadPluginApi(validPackage.mainJs, ctx); if (runtime.api && typeof runtime.api.onload === "function") { await runWithTimeout(Promise.resolve(runtime.api.onload(ctx)), 3000, "onload 超时"); } this.runtime.set(pluginId, runtime); record.enabled = true; record.status = "active"; record.lastLoadedAt = formatDateTime(new Date()); record.lastError = ""; this.persistRecords(); this.renderPluginList(); this.renderCommandBar(); this.logRuntime("info", pluginId, "插件已启用"); } catch (error) { if (runtime.styleNode) { runtime.styleNode.remove(); } record.enabled = false; record.status = "failed"; record.errorCount += 1; record.lastError = error.message; this.persistRecords(); this.renderPluginList(); this.renderCommandBar(); this.logRuntime("error", pluginId, `加载失败: ${error.message}`); throw error; } } async disable(pluginId, updateUi = true) { const runtime = this.runtime.get(pluginId); const record = this.ensureRecord(pluginId); if (runtime) { if (runtime.api && typeof runtime.api.onunload === "function") { try { await runWithTimeout(Promise.resolve(runtime.api.onunload()), 3000, "onunload 超时"); } catch (error) { this.logRuntime("warn", pluginId, `onunload 执行异常: ${error.message}`); } } runtime.cleanupFns.forEach((cleanup) => { try { cleanup(); } catch (error) { console.error("插件清理失败", error); } }); if (runtime.styleNode) { runtime.styleNode.remove(); } } this.runtime.delete(pluginId); Array.from(this.commands.keys()) .filter((commandId) => commandId.startsWith(`${pluginId}:`)) .forEach((commandId) => { this.commands.delete(commandId); }); this.sessionHandlers.delete(pluginId); record.enabled = false; if (record.status !== "failed") { record.status = "stopped"; } this.persistRecords(); if (updateUi) { this.renderPluginList(); this.renderCommandBar(); } this.logRuntime("info", pluginId, "插件已禁用"); } async reload(pluginId) { const record = this.ensureRecord(pluginId); record.errorCount = 0; record.lastError = ""; this.persistRecords(); await this.disable(pluginId, false); await this.enable(pluginId); this.logRuntime("info", pluginId, "插件已重载"); } async remove(pluginId) { await this.disable(pluginId, false); this.fsAdapter.removePackage(pluginId); this.records.delete(pluginId); this.persistRecords(); this.renderPluginList(); this.renderCommandBar(); this.logRuntime("info", pluginId, "插件包已移除"); } createPluginContext(manifest, runtime) { const assertPermission = (permission) => { if (!manifest.permissions.includes(permission)) { throw new Error(`权限不足: ${permission}`); } }; const pluginId = manifest.id; return { app: { version: APP_VERSION, platform: this.resolvePlatformTag(), }, commands: { register: (command) => { assertPermission("commands.register"); if (!command || typeof command !== "object") { throw new Error("command 必须是对象"); } if (!command.id || !command.title || typeof command.handler !== "function") { throw new Error("command 需要 id/title/handler"); } const commandId = `${pluginId}:${command.id}`; this.commands.set(commandId, { id: commandId, pluginId, title: command.title, when: command.when || "always", handler: command.handler, }); const cleanup = () => { this.commands.delete(commandId); this.renderCommandBar(); }; runtime.cleanupFns.push(cleanup); this.renderCommandBar(); }, }, session: { on: (eventName, handler) => { assertPermission("session.read"); if (typeof handler !== "function") { throw new Error("session.on handler 必须是函数"); } if (!this.sessionHandlers.has(pluginId)) { this.sessionHandlers.set(pluginId, []); } const wrapped = (payload) => { try { handler(payload); } catch (error) { this.logRuntime("error", pluginId, `会话事件异常: ${error.message}`); } }; this.sessionHandlers.get(pluginId).push({ eventName, handler: wrapped }); const cleanup = () => { const handlers = this.sessionHandlers.get(pluginId) || []; this.sessionHandlers.set( pluginId, handlers.filter((item) => item.handler !== wrapped) ); }; runtime.cleanupFns.push(cleanup); }, send: async (input) => { assertPermission("session.write"); await runRemoteCommand(String(input || ""), { source: `plugin:${pluginId}`, }); }, }, storage: { get: async (key) => { assertPermission("storage.read"); const namespace = loadJson(`remoteconn_plugin_data_${pluginId}`, {}); return namespace?.[key]; }, set: async (key, value) => { assertPermission("storage.write"); const namespace = loadJson(`remoteconn_plugin_data_${pluginId}`, {}); namespace[key] = value; saveJson(`remoteconn_plugin_data_${pluginId}`, namespace); }, }, ui: { showNotice: (message, level = "info") => { assertPermission("ui.notice"); const prefix = level === "warn" ? "[警告]" : level === "error" ? "[错误]" : "[提示]"; showToast(`${prefix} ${message}`); }, }, logger: { info: (...args) => this.logRuntime("info", pluginId, args.join(" ")), warn: (...args) => this.logRuntime("warn", pluginId, args.join(" ")), error: (...args) => this.logRuntime("error", pluginId, args.join(" ")), }, }; } loadPluginApi(mainJs, ctx) { const module = { exports: {} }; const exportsRef = module.exports; const factory = new Function( "ctx", "module", "exports", `"use strict"; const window = undefined; const document = undefined; const localStorage = undefined; const globalThis = undefined; ${mainJs} return module.exports;` ); const output = factory(ctx, module, exportsRef); return output || module.exports; } mountStyle(manifest, stylesCss) { const styleEl = document.createElement("style"); styleEl.dataset.pluginId = manifest.id; styleEl.textContent = stylesCss; document.head.append(styleEl); return styleEl; } emitSessionEvent(eventName, payload) { this.sessionHandlers.forEach((items, pluginId) => { items .filter((item) => item.eventName === eventName) .forEach((item) => { try { item.handler(payload); } catch (error) { this.logRuntime("error", pluginId, `事件 ${eventName} 处理失败: ${error.message}`); } }); }); } async runCommand(commandId) { const command = this.commands.get(commandId); if (!command) { throw new Error("命令不存在"); } try { await Promise.resolve(command.handler()); } catch (error) { this.logRuntime("error", command.pluginId, `命令执行失败: ${error.message}`); throw error; } } listCommands() { return Array.from(this.commands.values()); } listPluginItems() { return this.fsAdapter.listPackages().map((pkg) => { const record = this.ensureRecord(pkg.manifest.id); return { package: pkg, record, }; }); } ensureRecord(pluginId) { if (!this.records.has(pluginId)) { this.records.set(pluginId, { id: pluginId, enabled: false, status: "discovered", errorCount: 0, lastError: "", installedAt: formatDateTime(new Date()), updatedAt: formatDateTime(new Date()), lastLoadedAt: "", }); } return this.records.get(pluginId); } resolvePlatformTag() { const current = findCurrentServer(); return current?.transportMode || "web"; } logRuntime(level, pluginId, message) { const line = `[${formatDateTime(new Date(), true)}] [${level.toUpperCase()}] [${pluginId}] ${message}`; appState.pluginRuntimeLogs.unshift(line); while (appState.pluginRuntimeLogs.length > 200) { appState.pluginRuntimeLogs.pop(); } this.renderPluginRuntimeLog(); } renderPluginRuntimeLog() { if (!dom.pluginRuntimeLog) return; dom.pluginRuntimeLog.innerHTML = appState.pluginRuntimeLogs .map((line) => `

${escapeHtml(line)}

`) .join(""); } renderCommandBar() { if (!dom.pluginCommandBar) return; const commands = this.listCommands().filter((command) => { if (command.when === "connected") { return appState.sessionState === "connected"; } return true; }); if (commands.length === 0) { dom.pluginCommandBar.innerHTML = `暂无插件命令`; return; } dom.pluginCommandBar.innerHTML = commands .map( (command) => `` ) .join(""); } renderPluginList() { if (!dom.pluginList) return; const rows = this.listPluginItems(); if (rows.length === 0) { dom.pluginList.innerHTML = `

尚未安装插件

`; return; } dom.pluginList.innerHTML = rows .map(({ package: pkg, record }) => { const permissions = (pkg.manifest.permissions || []).join(", "); const enableBtn = record.enabled ? `` : ``; return `
${escapeHtml(pkg.manifest.name)}
${escapeHtml(pkg.manifest.id)} · v${escapeHtml(pkg.manifest.version)}
${escapeHtml(record.status || "unknown")}
权限:${escapeHtml(permissions || "无")}
最后错误:${escapeHtml(record.lastError || "- ")}
${enableBtn}
`; }) .join(""); } } const pluginFsAdapter = new PluginFsAdapter(); const pluginManager = new PluginManager(pluginFsAdapter); init(); function init() { bootstrapData(); bindEvents(); if (appState.servers.length === 0) { appState.servers = structuredClone(defaultServers); } appState.selectedServerId = appState.servers[0]?.id || null; hydrateServerForm(findCurrentServer()); hydrateGlobalForm(appState.globalConfig); applyTheme(appState.globalConfig, true); renderServerList(); renderTerminal(); renderSessionState(); renderLogFilters(); renderSessionLogs(); pluginManager.bootstrap(); appendActionLog("系统", "servermanager 已作为默认首页载入"); appendTerminal("系统", "RemoteConn 终端已就绪,点击“连接”建立 SSH 会话"); } function bootstrapData() { appState.servers = loadJson(STORAGE_KEYS.servers, structuredClone(defaultServers)); appState.globalConfig = { ...defaultGlobalConfig, ...loadJson(STORAGE_KEYS.global, {}), }; appState.actionLogs = loadJson(STORAGE_KEYS.actionLogs, []); appState.sessionLogs = loadJson(STORAGE_KEYS.sessionLogs, []); appState.knownHosts = loadJson(STORAGE_KEYS.knownHosts, {}); if (appState.globalConfig.credentialMemoryPolicy === "remember") { appState.credentials = loadJson(STORAGE_KEYS.credentials, structuredClone(defaultCredentials)); } else { appState.credentials = structuredClone(defaultCredentials); } // 数据兜底,避免历史脏数据导致 UI 报错。 appState.servers = Array.isArray(appState.servers) ? appState.servers : structuredClone(defaultServers); appState.actionLogs = Array.isArray(appState.actionLogs) ? appState.actionLogs : []; appState.sessionLogs = Array.isArray(appState.sessionLogs) ? appState.sessionLogs : []; enforceLogRetention(); } function bindEvents() { document.addEventListener("click", handleActionClick); dom.serverSearch.addEventListener("input", (event) => { appState.search = event.target.value.trim().toLowerCase(); renderServerList(); }); dom.serverSort.addEventListener("change", (event) => { appState.sortBy = event.target.value; renderServerList(); }); dom.authTypeSelect.addEventListener("change", (event) => { renderAuthFields(event.target.value, collectServerFormData({ persistCredential: false })); }); dom.themePresetSelect.addEventListener("change", (event) => { applyThemePreset(event.target.value); }); dom.saveServerBtn.addEventListener("click", () => saveCurrentServer()); dom.saveGlobalBtn.addEventListener("click", () => saveGlobalConfig()); dom.globalForm.addEventListener("input", (event) => { const watched = [ "accentColor", "bgColor", "textColor", "liquidAlpha", "blurRadius", "motionDuration", "fontFamily", "fontSize", "lineHeight", ]; if (watched.includes(event.target.name)) { const preview = collectGlobalFormData(); applyTheme(preview, false); } }); dom.terminalInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); executeTerminalInput(); } }); } function handleActionClick(event) { const trigger = event.target.closest("[data-action]"); if (!trigger) { const tabTrigger = event.target.closest("[data-tab-target]"); if (tabTrigger) { switchTab(tabTrigger); } return; } const action = trigger.dataset.action; if (action === "server-activate") { saveCurrentServer(false); appState.selectedServerId = trigger.dataset.id; hydrateServerForm(findCurrentServer()); renderServerList(); appendActionLog("服务器", `切换到 ${findCurrentServer()?.name || "未命名"}`); return; } if (action === "server-toggle-select") { const id = trigger.dataset.id; if (!id) return; if (trigger.checked) { appState.selectedServerIds.add(id); } else { appState.selectedServerIds.delete(id); } return; } if (action === "close-modal") { const dialog = trigger.closest("dialog"); dialog?.close(); return; } const actionMap = { "server-create": createServer, "server-delete": deleteServers, "server-select-all": selectAllServers, "server-test": testConnection, "navigate-config": () => navigateTo("config"), "navigate-manager": () => navigateTo("servermanager"), "open-console": openConsole, "go-back": goBack, "open-log": openLogWindow, "open-global-dialog": openGlobalDialog, "open-codex-dialog": openCodexDialog, "run-codex": () => runCodex(trigger.dataset.sandbox), "clear-console": clearConsole, "terminal-send": executeTerminalInput, "session-reconnect": reconnectSessionManually, "session-disconnect": () => disconnectCurrentSession("用户主动断开", true), "logs-apply-filter": renderSessionLogs, "logs-export": exportSessionLogs, "hostkey-trust": () => settleHostKeyDecision(true), "hostkey-cancel": () => settleHostKeyDecision(false), "theme-auto-contrast": autoOptimizeThemeBackground, "theme-reset": resetThemePreset, "restore-global-defaults": restoreGlobalDefaults, "clear-known-hosts": clearKnownHosts, "clear-credentials": clearCredentials, "plugins-refresh": () => pluginManager.refresh(), "plugins-install-sample": installSamplePlugins, "plugins-import-json": importPluginFromInput, "plugins-export": exportPluginPackages, "plugin-enable": () => togglePlugin(trigger.dataset.pluginId, true), "plugin-disable": () => togglePlugin(trigger.dataset.pluginId, false), "plugin-reload": () => reloadPlugin(trigger.dataset.pluginId), "plugin-remove": () => removePlugin(trigger.dataset.pluginId), "plugin-command-run": () => runPluginCommand(trigger.dataset.commandId), }; const handler = actionMap[action]; if (handler) { Promise.resolve(handler()).catch((error) => { console.error(error); showToast(`操作失败: ${error.message}`); }); } } function navigateTo(viewName, pushHistory = true) { if (!viewName || viewName === appState.currentView) { return; } if (pushHistory) { appState.viewHistory.push(viewName); } appState.currentView = viewName; dom.screens.forEach((screen) => { screen.classList.toggle("is-active", screen.dataset.screen === viewName); }); if (viewName === "console") { updateServerLabels(); renderTerminal(); pluginManager.renderCommandBar(); } if (viewName === "log") { renderLogFilters(); renderSessionLogs(); } if (viewName === "config") { pluginManager.renderPluginList(); pluginManager.renderPluginRuntimeLog(); } } function goBack() { if (appState.viewHistory.length <= 1) { showToast("当前已经是首页"); return; } appState.viewHistory.pop(); const previous = appState.viewHistory[appState.viewHistory.length - 1] || "servermanager"; navigateTo(previous, false); } function createServer() { saveCurrentServer(false); const now = formatDateTime(new Date()); const credentialRefId = upsertCredential("password", { password: "", privateKey: "", passphrase: "", certPath: "", }); const draft = { ...structuredClone(defaultServers[0]), id: `srv-${Date.now()}`, name: "新服务器", host: "", username: "", tags: ["new"], authType: "password", transportMode: "web", projectPath: "~/workspace/remoteconn", projectCandidates: "~/workspace/remoteconn", autoStartCodex: false, credentialRefId, lastConnectedAt: now, }; appState.servers.unshift(draft); appState.selectedServerId = draft.id; persistServers(); renderServerList(); hydrateServerForm(draft); appendActionLog("服务器", "已创建新服务器草稿"); showToast("已新建服务器"); } function deleteServers() { const toDelete = new Set(appState.selectedServerIds); if (toDelete.size === 0 && appState.selectedServerId) { toDelete.add(appState.selectedServerId); } if (toDelete.size === 0) { showToast("没有可删除的服务器"); return; } appState.servers = appState.servers.filter((server) => !toDelete.has(server.id)); appState.selectedServerIds.clear(); if (appState.servers.length === 0) { const fallback = structuredClone(defaultServers[0]); fallback.id = `srv-${Date.now()}`; appState.servers = [fallback]; } appState.selectedServerId = appState.servers[0].id; persistServers(); renderServerList(); hydrateServerForm(findCurrentServer()); appendActionLog("服务器", `已删除 ${toDelete.size} 台服务器`); showToast("删除完成"); } function selectAllServers() { appState.selectedServerIds = new Set(appState.servers.map((server) => server.id)); renderServerList(); appendActionLog("服务器", "已全选服务器"); showToast("已全选"); } async function testConnection() { const server = collectServerFormData({ persistCredential: false }); if (!server.host || !server.username) { showToast("请先填写主机地址和用户名"); return; } showToast("测试连接中..."); await sleep(300); const fingerprint = computeHostFingerprint(server.host, server.port); appendActionLog("连接测试", `${server.username}@${server.host}:${server.port} 指纹 ${fingerprint.slice(0, 22)}...`); showToast("连接测试通过"); } function renderServerList() { if (!dom.serverList) return; const visibleServers = getSortedServers().filter((server) => { if (!appState.search) return true; const scope = `${server.name} ${server.host} ${server.tags.join(" ")}`.toLowerCase(); return scope.includes(appState.search); }); if (visibleServers.length === 0) { dom.serverList.innerHTML = `

没有匹配的服务器

`; return; } dom.serverList.innerHTML = visibleServers .map((server) => { const checked = appState.selectedServerIds.has(server.id) ? "checked" : ""; return `
${escapeHtml(server.name)} ${escapeHtml(server.authType)}
${escapeHtml(server.username)}@${escapeHtml(server.host)}:${server.port}
${escapeHtml(server.tags.join(", ") || "- ")} · ${escapeHtml(server.transportMode)}
最近连接:${escapeHtml(server.lastConnectedAt || "未连接")}
`; }) .join(""); } function getSortedServers() { const copy = [...appState.servers]; if (appState.sortBy === "name") { copy.sort((a, b) => String(a.name).localeCompare(String(b.name), "zh-CN")); return copy; } if (appState.sortBy === "host") { copy.sort((a, b) => String(a.host).localeCompare(String(b.host), "zh-CN")); return copy; } copy.sort((a, b) => { const ta = new Date(a.lastConnectedAt || 0).getTime() || 0; const tb = new Date(b.lastConnectedAt || 0).getTime() || 0; return tb - ta; }); return copy; } function saveCurrentServer(showSavedToast = true) { const server = collectServerFormData({ persistCredential: true }); const idx = appState.servers.findIndex((item) => item.id === server.id); if (idx === -1) { appState.servers.unshift(server); appState.selectedServerId = server.id; } else { appState.servers[idx] = server; } persistServers(); renderServerList(); updateServerLabels(); appendActionLog("服务器", `已保存 ${server.name}`); if (showSavedToast) { showToast("服务器配置已保存"); } } function collectServerFormData({ persistCredential = true } = {}) { const current = findCurrentServer() || defaultServers[0]; const form = dom.serverForm; const authType = form.elements.namedItem("authType")?.value || current.authType; const credentialPayload = { password: form.elements.namedItem("password")?.value ?? "", privateKey: form.elements.namedItem("privateKey")?.value ?? "", passphrase: form.elements.namedItem("passphrase")?.value ?? "", certPath: form.elements.namedItem("certPath")?.value ?? "", }; const credentialRefId = persistCredential ? upsertCredential(authType, credentialPayload, current.credentialRefId) : current.credentialRefId || ""; const normalizedTags = String(form.elements.namedItem("tags")?.value || "") .split(",") .map((tag) => tag.trim()) .filter(Boolean); return { id: appState.selectedServerId || current.id || `srv-${Date.now()}`, name: form.elements.namedItem("name").value.trim() || "未命名服务器", username: form.elements.namedItem("username").value.trim() || "root", host: form.elements.namedItem("host").value.trim() || "127.0.0.1", port: Number(form.elements.namedItem("port").value || 22), tags: normalizedTags, authType, timeout: Number(form.elements.namedItem("timeout").value || 20), heartbeat: Number(form.elements.namedItem("heartbeat").value || 15), transportMode: form.elements.namedItem("transportMode").value || "web", projectPath: form.elements.namedItem("projectPath").value.trim() || "~/workspace/remoteconn", projectCandidates: form.elements.namedItem("projectCandidates").value.trim(), autoStartCodex: Boolean(form.elements.namedItem("autoStartCodex")?.checked), credentialRefId, lastConnectedAt: current.lastConnectedAt || "未连接", }; } function hydrateServerForm(server) { if (!server) return; const form = dom.serverForm; form.elements.namedItem("name").value = server.name || ""; form.elements.namedItem("username").value = server.username || ""; form.elements.namedItem("host").value = server.host || ""; form.elements.namedItem("port").value = String(server.port || 22); form.elements.namedItem("tags").value = Array.isArray(server.tags) ? server.tags.join(",") : ""; form.elements.namedItem("authType").value = server.authType || "password"; form.elements.namedItem("timeout").value = String(server.timeout || 20); form.elements.namedItem("heartbeat").value = String(server.heartbeat || 15); form.elements.namedItem("transportMode").value = server.transportMode || "web"; form.elements.namedItem("projectPath").value = server.projectPath || "~/workspace/remoteconn"; form.elements.namedItem("projectCandidates").value = server.projectCandidates || ""; form.elements.namedItem("autoStartCodex").checked = Boolean(server.autoStartCodex); const credential = getCredentialByRef(server.credentialRefId); renderAuthFields(server.authType, { password: credential.password || "", privateKey: credential.privateKey || "", passphrase: credential.passphrase || "", certPath: credential.certPath || "", }); updateServerLabels(); } /** * 认证字段随认证类型动态切换,避免用户看到不相关字段导致误填。 */ function renderAuthFields(authType, sourceCredential) { const credential = sourceCredential || { password: "", privateKey: "", passphrase: "", certPath: "" }; const templates = { password: ` `, privateKey: ` `, certificate: ` `, }; dom.authFields.innerHTML = templates[authType] || templates.password; } function upsertCredential(type, payload, existingId = "") { const now = formatDateTime(new Date()); const credentialId = existingId || `cred-${Date.now()}`; const next = { id: credentialId, type, password: payload.password || "", privateKey: payload.privateKey || "", passphrase: payload.passphrase || "", certPath: payload.certPath || "", createdAt: appState.credentials[credentialId]?.createdAt || now, updatedAt: now, }; appState.credentials[credentialId] = next; persistCredentials(); return credentialId; } function getCredentialByRef(credentialRefId) { if (!credentialRefId) return {}; return appState.credentials[credentialRefId] || {}; } function persistCredentials() { if (appState.globalConfig.credentialMemoryPolicy === "remember") { saveJson(STORAGE_KEYS.credentials, appState.credentials); } } function persistServers() { saveJson(STORAGE_KEYS.servers, appState.servers); } function saveGlobalConfig() { appState.globalConfig = collectGlobalFormData(); saveJson(STORAGE_KEYS.global, appState.globalConfig); applyTheme(appState.globalConfig, true); enforceLogRetention(); appendActionLog("全局", "全局配置已保存"); showToast("全局配置已保存"); if (appState.globalConfig.credentialMemoryPolicy === "session") { localStorage.removeItem(STORAGE_KEYS.credentials); } else { persistCredentials(); } } function collectGlobalFormData() { const form = dom.globalForm; return { fontFamily: form.elements.namedItem("fontFamily").value, fontSize: Number(form.elements.namedItem("fontSize").value || 15), lineHeight: Number(form.elements.namedItem("lineHeight").value || 1.4), cursorStyle: form.elements.namedItem("cursorStyle").value, unicode11: Boolean(form.elements.namedItem("unicode11").checked), autoReconnect: Boolean(form.elements.namedItem("autoReconnect").checked), reconnectLimit: Number(form.elements.namedItem("reconnectLimit").value || 3), themePreset: form.elements.namedItem("themePreset").value, accentColor: form.elements.namedItem("accentColor").value, bgColor: form.elements.namedItem("bgColor").value, textColor: form.elements.namedItem("textColor").value, liquidAlpha: Number(form.elements.namedItem("liquidAlpha").value || 0.64), blurRadius: Number(form.elements.namedItem("blurRadius").value || 22), motionDuration: Number(form.elements.namedItem("motionDuration").value || 14), hostKeyPolicy: form.elements.namedItem("hostKeyPolicy").value, logRetentionDays: Number(form.elements.namedItem("logRetentionDays").value || 30), maskSecrets: Boolean(form.elements.namedItem("maskSecrets").checked), credentialMemoryPolicy: form.elements.namedItem("credentialMemoryPolicy").value, }; } function hydrateGlobalForm(config) { const form = dom.globalForm; form.elements.namedItem("fontFamily").value = config.fontFamily; form.elements.namedItem("fontSize").value = String(config.fontSize); form.elements.namedItem("lineHeight").value = String(config.lineHeight); form.elements.namedItem("cursorStyle").value = config.cursorStyle; form.elements.namedItem("unicode11").checked = Boolean(config.unicode11); form.elements.namedItem("autoReconnect").checked = Boolean(config.autoReconnect); form.elements.namedItem("reconnectLimit").value = String(config.reconnectLimit || 3); form.elements.namedItem("themePreset").value = config.themePreset; form.elements.namedItem("accentColor").value = config.accentColor; form.elements.namedItem("bgColor").value = config.bgColor; form.elements.namedItem("textColor").value = config.textColor; form.elements.namedItem("liquidAlpha").value = String(config.liquidAlpha); form.elements.namedItem("blurRadius").value = String(config.blurRadius || 22); form.elements.namedItem("motionDuration").value = String(config.motionDuration || 14); form.elements.namedItem("hostKeyPolicy").value = config.hostKeyPolicy; form.elements.namedItem("logRetentionDays").value = String(config.logRetentionDays); form.elements.namedItem("maskSecrets").checked = Boolean(config.maskSecrets); form.elements.namedItem("credentialMemoryPolicy").value = config.credentialMemoryPolicy || "remember"; } function restoreGlobalDefaults() { appState.globalConfig = structuredClone(defaultGlobalConfig); hydrateGlobalForm(appState.globalConfig); applyTheme(appState.globalConfig, true); saveJson(STORAGE_KEYS.global, appState.globalConfig); appendActionLog("全局", "已恢复默认配置"); showToast("已恢复默认设置"); } function applyThemePreset(presetKey) { const preset = themePresets[presetKey]; if (!preset) return; const form = dom.globalForm; form.elements.namedItem("accentColor").value = preset.accentColor; form.elements.namedItem("bgColor").value = preset.bgColor; form.elements.namedItem("textColor").value = preset.textColor; form.elements.namedItem("liquidAlpha").value = String(preset.liquidAlpha); form.elements.namedItem("blurRadius").value = String(preset.blurRadius); form.elements.namedItem("motionDuration").value = String(preset.motionDuration); const preview = collectGlobalFormData(); applyTheme(preview, false); appendActionLog("主题", `已切换到 ${preset.label}`); showToast(`主题:${preset.label}`); } /** * 主题应用引擎:负责把全局配置映射为 CSS 变量,并给出 WCAG 对比度提示。 */ function applyTheme(config, updateHint = false) { const root = document.documentElement; const accent = normalizeHex(config.accentColor, "#5bd2ff"); const bg = normalizeHex(config.bgColor, "#192b4d"); const text = normalizeHex(config.textColor, "#e6f0ff"); const alpha = clamp(Number(config.liquidAlpha || 0.64), 0.35, 0.95); const border = hexToRgba(accent, 0.24); const surface = hexToRgba(blendHex(bg, "#0a1222", 0.52), alpha); const bottom = hexToRgba(blendHex(bg, "#0a1222", 0.26), 0.96); root.style.setProperty("--bg", bg); root.style.setProperty("--accent", accent); root.style.setProperty("--text", text); root.style.setProperty("--surface", surface); root.style.setProperty("--surface-border", border); root.style.setProperty("--bottom-bar", bottom); root.style.setProperty("--blur-radius", `${clamp(Number(config.blurRadius || 22), 0, 40)}px`); root.style.setProperty("--motion-duration", `${clamp(Number(config.motionDuration || 14), 6, 30)}s`); const contrast = calcContrastRatio(text, bg); if (updateHint && dom.contrastHint) { const status = contrast >= 4.5 ? "对比度良好" : contrast >= 3 ? "对比度一般" : "对比度偏低"; dom.contrastHint.textContent = `${status}(WCAG 对比度 ${contrast.toFixed(2)})`; } dom.terminalOutput.style.fontFamily = `${config.fontFamily}, monospace`; dom.terminalOutput.style.fontSize = `${clamp(Number(config.fontSize || 15), 12, 22)}px`; dom.terminalOutput.style.lineHeight = String(clamp(Number(config.lineHeight || 1.4), 1, 2)); } function autoOptimizeThemeBackground() { const formData = collectGlobalFormData(); const candidate = pickBestBackground(formData.textColor, formData.accentColor); dom.globalForm.elements.namedItem("bgColor").value = candidate; applyTheme(collectGlobalFormData(), true); appendActionLog("主题", `已自动优化背景为 ${candidate}`); showToast("已按对比度自动优化背景"); } function resetThemePreset() { const preset = themePresets[appState.globalConfig.themePreset] || themePresets.tide; dom.globalForm.elements.namedItem("accentColor").value = preset.accentColor; dom.globalForm.elements.namedItem("bgColor").value = preset.bgColor; dom.globalForm.elements.namedItem("textColor").value = preset.textColor; dom.globalForm.elements.namedItem("liquidAlpha").value = String(preset.liquidAlpha); dom.globalForm.elements.namedItem("blurRadius").value = String(preset.blurRadius); dom.globalForm.elements.namedItem("motionDuration").value = String(preset.motionDuration); applyTheme(collectGlobalFormData(), true); showToast("主题预设已恢复"); } function clearKnownHosts() { appState.knownHosts = {}; saveJson(STORAGE_KEYS.knownHosts, appState.knownHosts); appendActionLog("安全", "已清空 known_hosts"); showToast("已清除主机指纹"); } function clearCredentials() { appState.credentials = {}; localStorage.removeItem(STORAGE_KEYS.credentials); appendActionLog("安全", "已清除本地凭据"); showToast("已清除本地凭据"); } function openConsole() { saveCurrentServer(false); navigateTo("console"); connectSelectedServer().catch((error) => { handleSessionError(error); }); } async function connectSelectedServer({ isReconnect = false } = {}) { const server = findCurrentServer(); if (!server) { throw new Error("当前没有可连接服务器"); } if (appState.currentTransport) { disconnectCurrentSession("切换会话", true); } appState.reconnectReason = ""; transitionSessionState(isReconnect ? "reconnecting" : "connecting", "开始建立连接"); const sessionLog = createSessionLog(server); appState.currentSessionLogId = sessionLog.id; const fingerprint = computeHostFingerprint(server.host, server.port); const trusted = await verifyHostKey(server, fingerprint); if (!trusted) { throw new Error("用户取消主机指纹确认"); } transitionSessionState("auth_pending", "认证中"); await sleep(150); const credential = getCredentialByRef(server.credentialRefId); if (!credential || !credential.id) { throw new Error("未找到凭据,请先保存认证信息"); } if (server.authType === "password" && !credential.password) { throw new Error("密码认证缺少密码"); } if ((server.authType === "privateKey" || server.authType === "certificate") && !credential.privateKey) { throw new Error("私钥认证缺少私钥路径"); } const transport = new MockTransport(server.transportMode || "web"); const offFns = []; offFns.push( transport.on("stdout", ({ text }) => { appendTerminal("SSH", text); pluginManager.emitSessionEvent("stdout", { text, serverName: server.name }); }) ); offFns.push( transport.on("stderr", ({ text }) => { appendTerminal("ERR", text); pluginManager.emitSessionEvent("stderr", { text, serverName: server.name }); }) ); offFns.push( transport.on("latency", ({ latency }) => { dom.latencyValue.textContent = `${latency} ms`; updateSessionLatency(latency); pluginManager.emitSessionEvent("latency", { latency, serverName: server.name }); }) ); offFns.push( transport.on("disconnect", ({ reason }) => { appState.currentTransport = null; appState.sessionMeta = null; dom.latencyValue.textContent = "-- ms"; finalizeSessionLog(reason === "manual" ? "disconnected" : "error", reason); if (reason === "manual") { transitionSessionState("disconnected", "会话已断开"); appendTerminal("系统", "连接已断开"); pluginManager.emitSessionEvent("disconnected", { reason, serverName: server.name }); return; } appendTerminal("系统", `连接中断: ${reason}`); pluginManager.emitSessionEvent("disconnected", { reason, serverName: server.name }); if (appState.globalConfig.autoReconnect && appState.reconnectAttempts < appState.globalConfig.reconnectLimit) { scheduleReconnect(reason); } else { transitionSessionState("error", "连接中断"); } }) ); const result = await transport.connect({ server, credential }); appState.currentTransport = transport; appState.sessionMeta = { serverId: server.id, serverName: server.name, offFns, }; transitionSessionState("connected", "连接成功"); appendActionLog("连接", `连接到 ${server.name}`); appendTerminal("系统", result.welcome.join("\n")); pluginManager.emitSessionEvent("connected", { serverName: server.name, serverId: server.id }); updateSessionLogStatus("connected"); server.lastConnectedAt = formatDateTime(new Date()); persistServers(); renderServerList(); appState.reconnectAttempts = 0; pluginManager.renderCommandBar(); if (server.autoStartCodex) { await runCodex("workspace-write"); } } function scheduleReconnect(reason) { appState.reconnectAttempts += 1; const delayMs = Math.min(4000, 800 * appState.reconnectAttempts); appState.reconnectReason = reason; transitionSessionState("reconnecting", `自动重连(${appState.reconnectAttempts}/${appState.globalConfig.reconnectLimit})`); showToast(`连接中断,${Math.round(delayMs / 1000)} 秒后重连`); if (appState.reconnectTimer) { window.clearTimeout(appState.reconnectTimer); } appState.reconnectTimer = window.setTimeout(() => { connectSelectedServer({ isReconnect: true }).catch((error) => { handleSessionError(error); }); }, delayMs); } async function reconnectSessionManually() { if (!findCurrentServer()) { showToast("当前没有服务器"); return; } disconnectCurrentSession("manual reconnect", true); await connectSelectedServer({ isReconnect: true }); } function disconnectCurrentSession(reason = "manual", manual = true) { if (appState.reconnectTimer) { window.clearTimeout(appState.reconnectTimer); appState.reconnectTimer = null; } if (!appState.currentTransport) { if (manual) { transitionSessionState("disconnected", "未建立连接"); } return; } appState.currentTransport.disconnect(manual ? "manual" : reason); } function transitionSessionState(nextState, reason = "") { if (!SESSION_STATES.includes(nextState)) { return; } const current = appState.sessionState; const allowed = SESSION_TRANSITIONS[current] || new Set(); if (current !== nextState && !allowed.has(nextState)) { console.warn(`非法状态跳转: ${current} -> ${nextState}`); } appState.sessionState = nextState; renderSessionState(reason); } function renderSessionState(reason = "") { const state = appState.sessionState; dom.sessionStateBadge.className = `state-pill state-${state}`; dom.sessionStateBadge.textContent = reason ? `${state} · ${reason}` : state; } function handleSessionError(error) { const message = error instanceof Error ? error.message : String(error); transitionSessionState("error", message); appendTerminal("错误", message); appendActionLog("连接", `失败: ${message}`); finalizeSessionLog("error", message); showToast(`连接失败: ${message}`); } async function verifyHostKey(server, fingerprint) { const key = `${server.host}:${server.port}`; const stored = appState.knownHosts[key]; const policy = appState.globalConfig.hostKeyPolicy; if (stored && stored !== fingerprint) { throw new Error("主机指纹变更,已拒绝连接"); } if (policy === "trustFirstUse") { if (!stored) { appState.knownHosts[key] = fingerprint; saveJson(STORAGE_KEYS.knownHosts, appState.knownHosts); appendActionLog("安全", `首次信任 ${key}`); } return true; } if (policy === "strict") { if (!stored) { const accepted = await requestHostKeyDecision(server, fingerprint, true); if (accepted) { appState.knownHosts[key] = fingerprint; saveJson(STORAGE_KEYS.knownHosts, appState.knownHosts); } return accepted; } return true; } const accepted = await requestHostKeyDecision(server, fingerprint, false); if (accepted && dom.hostKeyRemember.checked) { appState.knownHosts[key] = fingerprint; saveJson(STORAGE_KEYS.knownHosts, appState.knownHosts); } return accepted; } function requestHostKeyDecision(server, fingerprint, strictHint) { dom.hostKeySummary.textContent = `${server.username}@${server.host}:${server.port} · ${strictHint ? "严格模式:首次连接需确认" : "手动确认模式"}`; dom.hostKeyFingerprint.textContent = `SHA256:${fingerprint}`; dom.hostKeyRemember.checked = true; dom.hostKeyDialog.showModal(); return new Promise((resolve) => { appState.pendingHostKeyDecision = { resolve, }; }); } function settleHostKeyDecision(accepted) { const pending = appState.pendingHostKeyDecision; appState.pendingHostKeyDecision = null; dom.hostKeyDialog.close(); if (pending?.resolve) { pending.resolve(Boolean(accepted)); } } function openCodexDialog() { if (appState.sessionState !== "connected") { showToast("请先建立连接再启动 Codex"); return; } dom.codexDialog.showModal(); } /** * Codex 模式编排: * 1) cd 到项目目录;2) 检测 codex;3) 启动 codex。 */ async function runCodex(sandbox) { if (!sandbox) return; dom.codexDialog.close(); if (appState.sessionState !== "connected") { showToast("连接未建立,无法启动 Codex"); return; } const server = findCurrentServer(); if (!server) { showToast("未选择服务器"); return; } const targetPath = server.projectPath || "~"; appendActionLog("Codex", `启动流程(${sandbox})`); const cdResult = await runRemoteCommand(`cd ${shellQuote(targetPath)}`, { source: "codex", markerType: "cd", }); if (cdResult.code !== 0) { appendTerminal("Codex", "目录切换失败:请检查项目路径是否存在且有权限"); showToast("Codex 启动失败:目录不可用"); return; } const checkResult = await runRemoteCommand("command -v codex", { source: "codex", markerType: "check", }); if (checkResult.code !== 0 || !checkResult.stdout.trim()) { appendTerminal("Codex", "检测到远端未安装 codex,请先安装后再试"); appendTerminal("Codex", "安装指引:参考内部文档《Codex CLI 部署指南》"); showToast("Codex 未安装"); return; } const runResult = await runRemoteCommand(`codex --sandbox ${sandbox}`, { source: "codex", markerType: "run", }); if (runResult.code === 0) { appendActionLog("Codex", `已进入 Codex 模式 (${sandbox})`); showToast(`Codex 已启动: ${sandbox}`); } else { appendActionLog("Codex", `启动失败(${sandbox})`); showToast("Codex 启动失败"); } } function clearConsole() { appState.terminalLines = []; renderTerminal(); appendActionLog("终端", "已清屏"); showToast("终端已清屏"); } function executeTerminalInput() { const command = String(dom.terminalInput.value || "").trim(); if (!command) return; dom.terminalInput.value = ""; runRemoteCommand(command, { source: "manual", markerType: "manual", }).catch((error) => { appendTerminal("错误", error.message); }); } async function runRemoteCommand(command, { source = "manual", markerType = "manual" } = {}) { if (!appState.currentTransport || appState.sessionState !== "connected") { throw new Error("当前未连接,无法执行命令"); } const startedAt = Date.now(); const result = await appState.currentTransport.exec(command); const elapsed = Date.now() - startedAt; pushSessionCommandMarker({ command, source, markerType, code: result.code, elapsed, }); if (result.code !== 0 && result.stderr) { appendTerminal("错误", `${command} -> ${result.stderr}`); } return result; } function appendTerminal(scope, message) { const text = String(message || ""); const lines = text.split(/\n/); lines.forEach((line) => { appState.terminalLines.push(`[${formatDateTime(new Date(), true)}] [${scope}] ${line}`); }); while (appState.terminalLines.length > 600) { appState.terminalLines.shift(); } renderTerminal(); } function renderTerminal() { dom.terminalOutput.textContent = appState.terminalLines.join("\n"); dom.terminalOutput.scrollTop = dom.terminalOutput.scrollHeight; } function openGlobalDialog() { const snapshot = collectGlobalFormData(); dom.globalSnapshot.textContent = JSON.stringify(snapshot, null, 2); dom.globalDialog.showModal(); } function openLogWindow() { navigateTo("log"); } function appendActionLog(scope, message) { const line = `[${formatDateTime(new Date(), true)}] [${scope}] ${message}`; appState.actionLogs.unshift(line); while (appState.actionLogs.length > 300) { appState.actionLogs.pop(); } saveJson(STORAGE_KEYS.actionLogs, appState.actionLogs); } function createSessionLog(server) { const now = formatDateTime(new Date()); const log = { id: `sess-${Date.now()}`, sessionId: `session-${Date.now()}`, serverId: server.id, serverName: server.name, host: `${server.username}@${server.host}:${server.port}`, transportMode: server.transportMode, startAt: now, endAt: "", status: "connecting", error: "", commandMarkers: [], latency: [], }; appState.sessionLogs.unshift(log); while (appState.sessionLogs.length > 200) { appState.sessionLogs.pop(); } saveJson(STORAGE_KEYS.sessionLogs, appState.sessionLogs); appState.selectedSessionLogId = log.id; return log; } function updateSessionLogStatus(status, error = "") { if (!appState.currentSessionLogId) return; const log = appState.sessionLogs.find((item) => item.id === appState.currentSessionLogId); if (!log) return; log.status = status; if (error) { log.error = error; } saveJson(STORAGE_KEYS.sessionLogs, appState.sessionLogs); } function finalizeSessionLog(status, error = "") { if (!appState.currentSessionLogId) return; const log = appState.sessionLogs.find((item) => item.id === appState.currentSessionLogId); if (!log) return; log.status = status; log.error = error || ""; if (!log.endAt) { log.endAt = formatDateTime(new Date()); } saveJson(STORAGE_KEYS.sessionLogs, appState.sessionLogs); appState.currentSessionLogId = null; if (appState.currentView === "log") { renderSessionLogs(); } } function updateSessionLatency(latency) { if (!appState.currentSessionLogId) return; const log = appState.sessionLogs.find((item) => item.id === appState.currentSessionLogId); if (!log) return; log.latency.push({ at: formatDateTime(new Date(), true), value: latency }); if (log.latency.length > 40) { log.latency.shift(); } saveJson(STORAGE_KEYS.sessionLogs, appState.sessionLogs); } function pushSessionCommandMarker(marker) { if (!appState.currentSessionLogId) return; const log = appState.sessionLogs.find((item) => item.id === appState.currentSessionLogId); if (!log) return; log.commandMarkers.push({ at: formatDateTime(new Date(), true), ...marker, }); if (log.commandMarkers.length > 120) { log.commandMarkers.shift(); } saveJson(STORAGE_KEYS.sessionLogs, appState.sessionLogs); if (appState.currentView === "log") { renderSessionLogs(); } } function enforceLogRetention() { const now = Date.now(); const maxDays = clamp(Number(appState.globalConfig.logRetentionDays || 30), 1, 365); appState.sessionLogs = appState.sessionLogs.filter((log) => { const timestamp = new Date(log.startAt).getTime(); if (!timestamp) return true; const diffDays = (now - timestamp) / (24 * 60 * 60 * 1000); return diffDays <= maxDays; }); saveJson(STORAGE_KEYS.sessionLogs, appState.sessionLogs); } function renderLogFilters() { const current = dom.logServerFilter.value; const options = [ ``, ...appState.servers.map( (server) => `` ), ]; dom.logServerFilter.innerHTML = options.join(""); if (current) { dom.logServerFilter.value = current; } } function getFilteredSessionLogs() { const serverId = dom.logServerFilter.value || "all"; const status = dom.logStatusFilter.value || "all"; const dateFrom = dom.logDateFrom.value ? new Date(dom.logDateFrom.value).getTime() : 0; const dateTo = dom.logDateTo.value ? new Date(dom.logDateTo.value).getTime() + 24 * 60 * 60 * 1000 : Infinity; return appState.sessionLogs.filter((log) => { if (serverId !== "all" && log.serverId !== serverId) return false; if (status !== "all" && log.status !== status) return false; const time = new Date(log.startAt).getTime(); if (dateFrom && time < dateFrom) return false; if (isFinite(dateTo) && time > dateTo) return false; return true; }); } function renderSessionLogs() { const filtered = getFilteredSessionLogs(); if (filtered.length === 0) { dom.sessionLogList.innerHTML = `

当前筛选条件下无日志

`; dom.sessionLogDetail.textContent = ""; return; } if (!filtered.some((item) => item.id === appState.selectedSessionLogId)) { appState.selectedSessionLogId = filtered[0].id; } dom.sessionLogList.innerHTML = filtered .slice(0, 50) .map((log) => { const active = log.id === appState.selectedSessionLogId ? "is-active" : ""; const duration = log.endAt ? formatDuration(new Date(log.endAt).getTime() - new Date(log.startAt).getTime()) : "进行中"; return `
${escapeHtml(log.serverName)} ${escapeHtml(log.status)}
${escapeHtml(log.startAt)} -> ${escapeHtml(log.endAt || "--")}
耗时:${escapeHtml(duration)} · 命令:${log.commandMarkers.length}
错误:${escapeHtml(log.error || "- ")}
`; }) .join(""); // 列表项点击委托:沿用 data-action 机制 const detailLog = appState.sessionLogs.find((item) => item.id === appState.selectedSessionLogId) || filtered[0]; if (detailLog) { renderSessionLogDetail(detailLog); } } document.addEventListener("click", (event) => { const selectTrigger = event.target.closest("[data-action='session-log-select']"); if (!selectTrigger) return; const logId = selectTrigger.dataset.id; if (!logId) return; appState.selectedSessionLogId = logId; renderSessionLogs(); }); function renderSessionLogDetail(log) { const lines = []; lines.push(`会话ID: ${log.sessionId}`); lines.push(`服务器: ${log.serverName} (${log.host})`); lines.push(`模式: ${log.transportMode}`); lines.push(`开始: ${log.startAt}`); lines.push(`结束: ${log.endAt || "进行中"}`); lines.push(`状态: ${log.status}`); lines.push(`错误: ${log.error || "-"}`); lines.push(""); lines.push("关键命令:"); if (!log.commandMarkers.length) { lines.push(" - 无"); } else { log.commandMarkers.forEach((marker) => { lines.push( ` - [${marker.at}] (${marker.markerType}) ${maskSensitive(marker.command)} -> code=${marker.code}, ${marker.elapsed}ms` ); }); } if (log.latency?.length) { lines.push(""); lines.push("延迟采样:"); log.latency.slice(-10).forEach((item) => { lines.push(` - [${item.at}] ${item.value}ms`); }); } dom.sessionLogDetail.textContent = lines.join("\n"); } function exportSessionLogs() { const logs = getFilteredSessionLogs(); if (logs.length === 0) { showToast("没有可导出的日志"); return; } const masked = appState.globalConfig.maskSecrets; const chunks = []; chunks.push(`# RemoteConn Session Export (${formatDateTime(new Date())})`); chunks.push(""); logs.forEach((log, index) => { chunks.push(`## ${index + 1}. ${log.serverName} [${log.status}]`); chunks.push(`- host: ${masked ? maskHost(log.host) : log.host}`); chunks.push(`- transport: ${log.transportMode}`); chunks.push(`- start: ${log.startAt}`); chunks.push(`- end: ${log.endAt || "--"}`); chunks.push(`- error: ${masked ? maskSensitive(log.error || "") : log.error || ""}`); chunks.push(`- commands:`); log.commandMarkers.forEach((marker) => { const cmd = masked ? maskSensitive(marker.command) : marker.command; chunks.push(` - [${marker.at}] ${cmd} => code:${marker.code}`); }); chunks.push(""); }); const file = new Blob([chunks.join("\n")], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(file); const a = document.createElement("a"); a.href = url; a.download = `remoteconn-session-${formatDateStamp(new Date())}.txt`; document.body.append(a); a.click(); a.remove(); URL.revokeObjectURL(url); appendActionLog("日志", `导出 ${logs.length} 条会话日志`); showToast("日志已导出"); } async function installSamplePlugins() { samplePluginPackages.forEach((pkg) => { try { pluginManager.installPackage(pkg); } catch (error) { pluginManager.logRuntime("error", pkg.manifest.id, `安装示例失败: ${error.message}`); } }); pluginManager.refresh(); showToast("示例插件已写入"); } function importPluginFromInput() { const raw = String(dom.pluginPackageInput.value || "").trim(); if (!raw) { showToast("请先输入插件包 JSON"); return; } let payload; try { payload = JSON.parse(raw); } catch (error) { throw new Error("JSON 解析失败"); } const packages = Array.isArray(payload) ? payload : [payload]; packages.forEach((item) => { pluginManager.installPackage(item); }); dom.pluginPackageInput.value = ""; pluginManager.refresh(); showToast(`已导入 ${packages.length} 个插件包`); } function exportPluginPackages() { const packages = pluginFsAdapter.exportPackages(); const file = new Blob([JSON.stringify(packages, null, 2)], { type: "application/json;charset=utf-8", }); const url = URL.createObjectURL(file); const a = document.createElement("a"); a.href = url; a.download = `remoteconn-plugins-${formatDateStamp(new Date())}.json`; document.body.append(a); a.click(); a.remove(); URL.revokeObjectURL(url); appendActionLog("插件", `导出插件包 ${packages.length} 个`); showToast("插件包已导出"); } async function togglePlugin(pluginId, enabled) { if (!pluginId) return; if (enabled) { await pluginManager.enable(pluginId); } else { await pluginManager.disable(pluginId); } pluginManager.renderPluginList(); pluginManager.renderCommandBar(); } async function reloadPlugin(pluginId) { if (!pluginId) return; await pluginManager.reload(pluginId); pluginManager.renderPluginList(); pluginManager.renderCommandBar(); } async function removePlugin(pluginId) { if (!pluginId) return; await pluginManager.remove(pluginId); pluginManager.renderPluginList(); pluginManager.renderCommandBar(); } async function runPluginCommand(commandId) { if (!commandId) return; await pluginManager.runCommand(commandId); } function switchTab(button) { const tabsRoot = button.closest("[data-tabs-root]"); if (!tabsRoot) return; const target = button.dataset.tabTarget; const scope = tabsRoot.parentElement; if (!target || !scope) return; tabsRoot.querySelectorAll(".tab-btn").forEach((tab) => { tab.classList.toggle("is-active", tab === button); }); scope.querySelectorAll(".tab-pane").forEach((pane) => { pane.classList.toggle("is-active", pane.dataset.pane === target); }); } function updateServerLabels() { const server = findCurrentServer(); if (!server) return; if (dom.consoleServerName) { dom.consoleServerName.textContent = server.name; } } function findCurrentServer() { return appState.servers.find((server) => server.id === appState.selectedServerId); } function showToast(message) { if (!dom.toast) return; dom.toast.textContent = String(message || ""); dom.toast.classList.add("show"); window.clearTimeout(showToast.timer); showToast.timer = window.setTimeout(() => { dom.toast.classList.remove("show"); }, 1700); } function computeHostFingerprint(host, port) { const source = `${host}:${port}`; let hash = 2166136261; for (let i = 0; i < source.length; i += 1) { hash ^= source.charCodeAt(i); hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); } const hex = Math.abs(hash >>> 0).toString(16).padStart(8, "0"); return `${hex}${hex.slice(0, 6)}${hex.slice(2, 8)}`; } function loadJson(key, fallback) { try { const raw = localStorage.getItem(key); if (!raw) return fallback; return JSON.parse(raw); } catch (error) { console.error(`读取 ${key} 失败`, error); return fallback; } } function saveJson(key, value) { localStorage.setItem(key, JSON.stringify(value)); } function formatDateTime(date, short = false) { const target = date instanceof Date ? date : new Date(date); const year = target.getFullYear(); const month = String(target.getMonth() + 1).padStart(2, "0"); const day = String(target.getDate()).padStart(2, "0"); const hh = String(target.getHours()).padStart(2, "0"); const mm = String(target.getMinutes()).padStart(2, "0"); const ss = String(target.getSeconds()).padStart(2, "0"); return short ? `${hh}:${mm}:${ss}` : `${year}-${month}-${day} ${hh}:${mm}:${ss}`; } function formatDateStamp(date) { const target = date instanceof Date ? date : new Date(date); const year = target.getFullYear(); const month = String(target.getMonth() + 1).padStart(2, "0"); const day = String(target.getDate()).padStart(2, "0"); return `${year}${month}${day}`; } function formatDuration(ms) { if (!Number.isFinite(ms) || ms <= 0) return "0s"; const sec = Math.round(ms / 1000); const m = Math.floor(sec / 60); const s = sec % 60; if (m === 0) return `${s}s`; return `${m}m ${s}s`; } function shellQuote(path) { return `'${String(path || "").replace(/'/g, "'\\''")}'`; } function clamp(value, min, max) { if (!Number.isFinite(value)) return min; return Math.min(max, Math.max(min, value)); } function normalizeHex(input, fallback) { const value = String(input || "").trim(); if (/^#[0-9a-fA-F]{6}$/.test(value)) return value.toLowerCase(); return fallback; } function hexToRgb(hex) { const value = normalizeHex(hex, "#000000"); return { r: Number.parseInt(value.slice(1, 3), 16), g: Number.parseInt(value.slice(3, 5), 16), b: Number.parseInt(value.slice(5, 7), 16), }; } function rgbToHex({ r, g, b }) { const cr = clamp(Math.round(r), 0, 255).toString(16).padStart(2, "0"); const cg = clamp(Math.round(g), 0, 255).toString(16).padStart(2, "0"); const cb = clamp(Math.round(b), 0, 255).toString(16).padStart(2, "0"); return `#${cr}${cg}${cb}`; } function hexToRgba(hex, alpha) { const { r, g, b } = hexToRgb(hex); return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`; } function blendHex(baseHex, overlayHex, ratio) { const base = hexToRgb(baseHex); const overlay = hexToRgb(overlayHex); const t = clamp(ratio, 0, 1); return rgbToHex({ r: base.r * (1 - t) + overlay.r * t, g: base.g * (1 - t) + overlay.g * t, b: base.b * (1 - t) + overlay.b * t, }); } function srgbToLinear(value) { const normalized = value / 255; if (normalized <= 0.03928) { return normalized / 12.92; } return ((normalized + 0.055) / 1.055) ** 2.4; } function calcLuminance(hex) { const { r, g, b } = hexToRgb(hex); return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b); } function calcContrastRatio(hexA, hexB) { const la = calcLuminance(hexA); const lb = calcLuminance(hexB); const lighter = Math.max(la, lb); const darker = Math.min(la, lb); return (lighter + 0.05) / (darker + 0.05); } function pickBestBackground(textColor, accentColor) { const text = normalizeHex(textColor, "#e6f0ff"); const accent = normalizeHex(accentColor, "#5bd2ff"); const candidates = [ "#0a1325", "#132747", "#102b34", "#2e223b", blendHex(accent, "#0a1222", 0.75), blendHex(accent, "#0a1222", 0.88), ]; let best = candidates[0]; let bestScore = 0; candidates.forEach((candidate) => { const score = calcContrastRatio(text, candidate); if (score > bestScore) { bestScore = score; best = candidate; } }); return best; } function maskSensitive(value) { if (!value) return ""; return String(value) .replace(/([0-9]{1,3}\.){3}[0-9]{1,3}/g, "***.***.***.***") .replace(/(token|password|passphrase|secret)\s*[=:]\s*[^\s]+/gi, "$1=***") .replace(/~\/.+?(?=\s|$)/g, "~/***"); } function maskHost(host) { return String(host || "") .replace(/([a-zA-Z0-9._%+-]+)@/, "***@") .replace(/([0-9]{1,3}\.){3}[0-9]{1,3}/, "***.***.***.***"); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function escapeAttr(value) { return escapeHtml(value).replaceAll("`", "`"); } function sleep(ms) { return new Promise((resolve) => { window.setTimeout(resolve, ms); }); } function runWithTimeout(promise, timeoutMs, timeoutMessage) { let timer = null; return new Promise((resolve, reject) => { timer = window.setTimeout(() => { reject(new Error(timeoutMessage)); }, timeoutMs); Promise.resolve(promise) .then((value) => { window.clearTimeout(timer); resolve(value); }) .catch((error) => { window.clearTimeout(timer); reject(error); }); }); }