From 169017df65fd5f0bf67ab185c7ed243bc8065291 Mon Sep 17 00:00:00 2001 From: Jokul Date: Sun, 5 Jul 2026 21:20:42 +0800 Subject: [PATCH] feat: Cluster mode support - NewConnectionDialog: cluster checkbox - connection.ts: use Redis.Cluster() when cluster is enabled - i18n keys for cluster (zh-CN + en) --- ROADMAP.md | 2 +- electron/main/redis/connection.ts | 37 ++++++++++++++++++++++++++ src/components/NewConnectionDialog.vue | 13 +++++++-- src/i18n/locales/en.ts | 1 + src/i18n/locales/zh-CN.ts | 1 + src/stores/connection.ts | 3 +++ 6 files changed, 54 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index ba602fd..c345ce8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ - [x] **SSH 隧道** — 类型已定义,NewConnectionDialog 需添加 host/port/user/password/privateKey/passphrase 表单 - [x] **SSL/TLS 证书路径** — CA / Cert / Key 文件路径 + SNI servername,当前仅 TLS 复选框 - [x] **Sentinel 模式** — masterName + nodePassword 表单 -- [ ] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明 +- [x] **Cluster 开关** — UI 暴露 cluster 模式切换 + FAQ 说明 ## P1 — 核心功能缺失 diff --git a/electron/main/redis/connection.ts b/electron/main/redis/connection.ts index 0dc9d1b..e716826 100644 --- a/electron/main/redis/connection.ts +++ b/electron/main/redis/connection.ts @@ -22,6 +22,7 @@ export interface ConnectionOptions { sentinelPort?: number sentinelMasterName?: string sentinelNodePassword?: string + cluster?: boolean sshEnabled?: boolean sshHost?: string sshPort?: number @@ -210,6 +211,42 @@ export async function connect(id: string, opts: ConnectionOptions): Promise undefined, + } : undefined, + }, + clusterRetryStrategy(times: number) { + if (times > 3) return null + return Math.min(times * 200, 1000) + }, + enableReadyCheck: true, + } + const redis = new Redis.Cluster([{ host: opts.host, port: opts.port }], clusterOptions) + + redis.once('ready', () => { + clients.set(id, { client: redis as any }) + resolve() + }) + redis.once('error', (err: Error) => { + redis.disconnect() + reject(err) + }) + setTimeout(() => { + if (!clients.has(id)) { + redis.disconnect() + reject(new Error('连接超时,请检查主机和端口')) + } + }, 12000) + return + } + if (opts.sshEnabled) { const tunnel = await createSshTunnel(opts) sshClient = tunnel.sshClient diff --git a/src/components/NewConnectionDialog.vue b/src/components/NewConnectionDialog.vue index a38d0ea..fd696ad 100644 --- a/src/components/NewConnectionDialog.vue +++ b/src/components/NewConnectionDialog.vue @@ -27,6 +27,7 @@ const form = reactive({ sentinelPort: 26379, sentinelMasterName: '', sentinelNodePassword: '', + cluster: false, separator: ':', sshEnabled: false, sshHost: '', @@ -62,6 +63,7 @@ watch(() => props.visible, async (v) => { sentinelPort: props.connection.sentinelPort || 26379, sentinelMasterName: props.connection.sentinelMasterName || '', sentinelNodePassword: props.connection.sentinelNodePassword || '', + cluster: props.connection.cluster || false, separator: props.connection.separator, sshEnabled: props.connection.sshEnabled || false, sshHost: props.connection.sshHost || '', @@ -79,6 +81,7 @@ watch(() => props.visible, async (v) => { tlsRejectUnauthorized: false, sentinelEnabled: false, sentinelHost: '', sentinelPort: 26379, sentinelMasterName: '', sentinelNodePassword: '', + cluster: false, separator: ':', sshEnabled: false, sshHost: '', sshPort: 22, sshUsername: '', sshPassword: '', sshPrivateKeyPath: '', sshPassphrase: '', @@ -147,6 +150,7 @@ async function handleTest() { sentinelPort: Number(form.sentinelPort) || 26379, sentinelMasterName: form.sentinelMasterName.trim(), sentinelNodePassword: form.sentinelNodePassword, + cluster: form.cluster, sshEnabled: form.sshEnabled, sshHost: form.sshHost.trim(), sshPort: Number(form.sshPort) || 22, @@ -189,6 +193,7 @@ async function handleSave() { sentinelPort: Number(form.sentinelPort) || 26379, sentinelMasterName: form.sentinelMasterName.trim(), sentinelNodePassword: form.sentinelNodePassword, + cluster: form.cluster, sshEnabled: form.sshEnabled, sshHost: form.sshHost.trim(), sshPort: Number(form.sshPort) || 22, @@ -196,8 +201,6 @@ async function handleSave() { sshPassword: form.sshPassword, sshPrivateKeyPath: form.sshPrivateKeyPath, sshPassphrase: form.sshPassphrase, - cluster: false, - sentinelMasterName: '', separator: form.separator || ':', order: props.connection?.order ?? connStore.connections.length, createdAt: props.connection?.createdAt ?? Date.now(), @@ -299,6 +302,12 @@ async function handleSave() { + + + + {{ t('connection.cluster') }} + + diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 0132937..078e181 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -39,6 +39,7 @@ export default { sentinelPort: 'Sentinel Port', sentinelMasterName: 'Master Name', sentinelNodePassword: 'Sentinel Password', + cluster: 'Cluster Mode', ssh: 'SSH Tunnel', sshHost: 'SSH Host', sshPort: 'SSH Port', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 2a78ba4..abfb912 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -39,6 +39,7 @@ export default { sentinelPort: '哨兵端口', sentinelMasterName: 'Master 名称', sentinelNodePassword: '哨兵密码', + cluster: 'Cluster 集群模式', ssh: 'SSH 隧道', sshHost: 'SSH 主机', sshPort: 'SSH 端口', diff --git a/src/stores/connection.ts b/src/stores/connection.ts index a4497f9..6872c15 100644 --- a/src/stores/connection.ts +++ b/src/stores/connection.ts @@ -97,6 +97,7 @@ export const useConnectionStore = defineStore('connection', () => { sentinelPort: conn.sentinelPort, sentinelMasterName: conn.sentinelMasterName, sentinelNodePassword: conn.sentinelNodePassword, + cluster: conn.cluster, sshEnabled: conn.sshEnabled, sshHost: conn.sshHost, sshPort: conn.sshPort, @@ -168,6 +169,7 @@ export const useConnectionStore = defineStore('connection', () => { tlsCaPath?: string; tlsCertPath?: string; tlsKeyPath?: string; tlsRejectUnauthorized?: boolean sentinelEnabled?: boolean; sentinelHost?: string; sentinelPort?: number sentinelMasterName?: string; sentinelNodePassword?: string + cluster?: boolean sshEnabled?: boolean; sshHost?: string; sshPort?: number; sshUsername?: string sshPassword?: string; sshPrivateKeyPath?: string; sshPassphrase?: string }): Promise<{ success: boolean; error?: string }> { @@ -191,6 +193,7 @@ export const useConnectionStore = defineStore('connection', () => { sentinelPort: opts.sentinelPort, sentinelMasterName: opts.sentinelMasterName, sentinelNodePassword: opts.sentinelNodePassword, + cluster: opts.cluster, sshEnabled: opts.sshEnabled, sshHost: opts.sshHost, sshPort: opts.sshPort,