From f2b3d83b3c491b30c893e4e7cc32d5d9fe776640 Mon Sep 17 00:00:00 2001 From: hepengju Date: Sat, 4 Jul 2026 15:59:00 +0800 Subject: [PATCH 01/82] =?UTF-8?q?docs:=20=E5=AE=98=E7=BD=91=E6=9A=96?= =?UTF-8?q?=E8=89=B2=E4=B8=BB=E9=A2=98=E4=B8=8E=E9=A6=96=E9=A1=B5=20Hero/?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E8=A7=86=E8=A7=89=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/.vitepress/theme/styles.css | 177 ++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 5 deletions(-) diff --git a/docs/.vitepress/theme/styles.css b/docs/.vitepress/theme/styles.css index 89badb18..1b36dd56 100644 --- a/docs/.vitepress/theme/styles.css +++ b/docs/.vitepress/theme/styles.css @@ -1,25 +1,192 @@ +/* CC Switch 风格暖色主题 — 覆盖 VitePress 默认变量 */ :root { + /* 品牌色:陶土橙 */ + --vp-c-brand-1: #c75b3a; + --vp-c-brand-2: #d97706; + --vp-c-brand-3: #ea580c; + --vp-c-brand-soft: rgba(199, 91, 58, 0.14); + + /* 浅色:米色背景 */ + --vp-c-bg: #ede8df; + --vp-c-bg-alt: #e5dfd4; + --vp-c-bg-soft: #e0d9cc; + --vp-c-bg-elv: #f2ede6; + + --vp-c-text-1: rgba(45, 35, 25, 0.92); + --vp-c-text-2: rgba(45, 35, 25, 0.72); + --vp-c-text-3: rgba(45, 35, 25, 0.48); + + --vp-c-divider: rgba(45, 35, 25, 0.12); + --vp-c-border: rgba(45, 35, 25, 0.16); + --vp-c-gutter: rgba(45, 35, 25, 0.08); + + /* Hero:文字渐变保持原版;光晕用更浅的暖色 */ --vp-home-hero-name-color: transparent; --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #c7ba4e 30%, #bd34fe); - --vp-home-hero-image-background-image: linear-gradient(-45deg, #bd34fe 50%, #47caff 50%); - --vp-home-hero-image-filter: blur(44px); + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + rgba(240, 236, 207, 0.85) 50%, + rgba(253, 248, 242, 0.65) 50% + ); + --vp-home-hero-image-filter: blur(64px); + + /* 首页 Hero 环境光 / 网格(对齐 cc-switch-website) */ + --vp-home-top-light: linear-gradient(to bottom, rgba(199, 91, 58, 0.06) 0%, transparent 42%); + --vp-home-grid-opacity: 0.18; + --vp-home-grid-color: rgba(45, 35, 25, 0.14); - /* 字体采用Rspress的默认字体,似乎更好看一些 */ --vp-font-family-base: 'Inter var experimental', 'Inter var', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; --vp-font-family-mono: Menlo, Monaco, Consolas, 'Courier New', monospace; } +.dark { + /* 深色背景对齐 cc-switch-website/src/index.css .dark */ + --vp-c-bg: #211f1b; /* hsl(40 15% 12%) background */ + --vp-c-bg-alt: #1c1a17; /* hsl(40 12% 10%) sidebar */ + --vp-c-bg-soft: #302c28; /* hsl(38 12% 18%) card */ + --vp-c-bg-elv: #393632; /* hsl(38 10% 22%) muted */ + + --vp-c-brand-1: #f97316; + --vp-c-brand-2: #ea580c; + --vp-c-brand-3: #c75b3a; + --vp-c-brand-soft: rgba(249, 115, 22, 0.16); + + --vp-c-text-1: rgba(232, 226, 214, 0.92); /* hsl(35 20% 88%) foreground */ + --vp-c-text-2: rgba(154, 146, 133, 0.88); /* hsl(35 15% 55%) muted-foreground */ + --vp-c-text-3: rgba(154, 146, 133, 0.58); + + --vp-c-divider: rgba(66, 64, 60, 0.55); /* hsl(38 10% 25%) border */ + --vp-c-border: #42403c; + --vp-c-gutter: rgba(66, 64, 60, 0.35); + + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + rgba(249, 115, 22, 0.14) 50%, + rgba(189, 52, 254, 0.08) 50% + ); + + --vp-home-top-light: linear-gradient(to bottom, rgba(249, 115, 22, 0.1) 0%, transparent 45%); + --vp-home-grid-opacity: 0.1; + --vp-home-grid-color: rgba(232, 226, 214, 0.1); +} + @media (min-width: 640px) { :root { - --vp-home-hero-image-filter: blur(56px); + --vp-home-hero-image-filter: blur(72px); } } @media (min-width: 960px) { :root { - --vp-home-hero-image-filter: blur(68px); + --vp-home-hero-image-filter: blur(84px); + } +} + +/* ── 首页 Hero:顶光 + 网格纹理(参考 cc-switch HeroSection) ── */ +.VPHero { + position: relative; + overflow: hidden; +} + +.VPHero::before { + content: ''; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background: var(--vp-home-top-light); +} + +.VPHero::after { + content: ''; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + opacity: var(--vp-home-grid-opacity); + background-image: + linear-gradient(to right, var(--vp-home-grid-color) 1px, transparent 1px), + linear-gradient(to bottom, var(--vp-home-grid-color) 1px, transparent 1px); + background-size: 60px 60px; +} + +.VPHero .container { + position: relative; + z-index: 1; +} + +/* Logo 光晕:更大、更散、更淡 */ +.VPHero .image-bg { + opacity: 0.5; + transform: translate(-50%, -50%) scale(1.35); +} + +@media (min-width: 960px) { + .VPHero .image-bg { + transform: translate(-50%, -50%) scale(1.5); + } +} + +/* 截图预览区环境光 */ +.preview-wrapper { + position: relative; + z-index: 1; +} + +.preview-wrapper::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + z-index: -1; + width: min(920px, 92vw); + height: 70%; + pointer-events: none; + border-radius: 24px; + background: linear-gradient(to bottom right, rgba(199, 91, 58, 0.12), rgba(189, 52, 254, 0.06)); + filter: blur(48px); + transform: translateX(-50%); +} + +.dark .preview-wrapper::before { + background: linear-gradient(to bottom right, rgba(249, 115, 22, 0.16), rgba(189, 52, 254, 0.1)); + filter: blur(56px); +} + +/* 顶栏:首页顶部透明无分割线,滚动后毛玻璃 + 分割线(对齐 cc-switch SiteNavbar) */ +.VPNavBar.home.top { + background-color: transparent !important; + backdrop-filter: none; +} + +.VPNavBar.home.top .divider-line { + background-color: transparent !important; +} + +.VPNavBar.home.top .content-body { + background-color: transparent !important; +} + +.VPNavBar:not(.home.top), +.VPNavBar.screen-open { + background-color: color-mix(in srgb, var(--vp-c-bg) 82%, transparent) !important; + backdrop-filter: blur(14px); +} + +.VPNavBar:not(.home.top) .divider-line, +.VPNavBar.screen-open .divider-line { + background-color: var(--vp-c-divider) !important; +} + +@media (min-width: 960px) { + .VPNavBar.home.top .content-body { + background-color: transparent !important; + } + + .VPNavBar:not(.home.top) .content-body { + background-color: color-mix(in srgb, var(--vp-c-bg) 82%, transparent) !important; } } From a09f41ab5eac20f9f48fb57f77c95c413492cc03 Mon Sep 17 00:00:00 2001 From: hepengju Date: Sat, 4 Jul 2026 16:11:27 +0800 Subject: [PATCH 02/82] =?UTF-8?q?docs:=20=E6=A0=B9=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=8C=89=E6=B5=8F=E8=A7=88=E5=99=A8=E8=AF=AD=E8=A8=80=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=E4=B8=AD=E6=96=87=E9=A6=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/.vitepress/config.ts | 4 ++++ docs/.vitepress/theme/index.ts | 12 +++++++++++- docs/.vitepress/theme/lang-redirect.ts | 23 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 docs/.vitepress/theme/lang-redirect.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1622f9b1..a976a63d 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,5 +1,7 @@ import { defineConfig } from 'vitepress' +import { createLangRedirectScript } from './theme/lang-redirect' + /** 子路径部署时改为 `/仓库名/`(建议以 / 结尾),与 Vite `base` 一致 */ const siteBase: string = '/' @@ -20,6 +22,8 @@ export default defineConfig({ head: [ ['link', { rel: 'icon', type: 'image/svg+xml', href: siteAsset('images/logo.svg') }], ['link', { rel: 'icon', type: 'image/png', href: siteAsset('images/logo.png') }], + // 根路径:中文浏览器或已选中文 → /zh/(见 theme/lang-redirect.ts) + ['script', { type: 'text/javascript' }, createLangRedirectScript(viteBase)], ], themeConfig: { logo: siteAsset('images/logo.svg'), diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index abbba87f..c4d0bc9b 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,8 +1,10 @@ +import { inBrowser, useData } from 'vitepress' import DefaultTheme from 'vitepress/theme' -import { h } from 'vue' +import { h, watchEffect } from 'vue' import AppPreview from './components/AppPreview.vue' import HeroActions from './components/HeroActions.vue' +import { LANG_STORAGE_KEY } from './lang-redirect' import './styles.css' @@ -14,4 +16,12 @@ export default { 'home-hero-after': () => h(AppPreview), }) }, + setup() { + const { lang } = useData() + // 导航栏切换语言后记住偏好,下次访问根路径不再按浏览器猜测 + watchEffect(() => { + if (!inBrowser) return + localStorage.setItem(LANG_STORAGE_KEY, lang.value.startsWith('zh') ? 'zh' : 'en') + }) + }, } diff --git a/docs/.vitepress/theme/lang-redirect.ts b/docs/.vitepress/theme/lang-redirect.ts new file mode 100644 index 00000000..13e0605a --- /dev/null +++ b/docs/.vitepress/theme/lang-redirect.ts @@ -0,0 +1,23 @@ +/** localStorage 键:用户手动切换语言或自动跳转后写入,优先于浏览器语言 */ +export const LANG_STORAGE_KEY = 'redisme-docs-lang' + +/** 根路径访问时按偏好 / 浏览器语言跳转中文首页(inline script,尽早执行) */ +export function createLangRedirectScript(base: string): string { + const normalizedBase = base === '/' ? '/' : base.endsWith('/') ? base : `${base}/` + const zhHome = normalizedBase === '/' ? '/zh/' : `${normalizedBase}zh/` + + return `(function(){try{ +var k=${JSON.stringify(LANG_STORAGE_KEY)}; +var base=${JSON.stringify(normalizedBase)}; +var zh=${JSON.stringify(zhHome)}; +var p=location.pathname; +if(p.endsWith('/index.html'))p=p.slice(0,-11)||'/'; +var root=base==='/'?'/':base.replace(/\\/$/,''); +if(p!==root&&p!==root+'/')return; +var s=localStorage.getItem(k); +if(s==='zh'){location.replace(zh);return} +if(s==='en')return; +var l=(navigator.language||navigator.userLanguage||'').toLowerCase(); +if(l.startsWith('zh'))location.replace(zh) +}catch(e){}})();` +} From 64340fad8682b31bc6bfc9ec8ea3e243c78dd368 Mon Sep 17 00:00:00 2001 From: hepengju Date: Sat, 4 Jul 2026 16:16:45 +0800 Subject: [PATCH 03/82] =?UTF-8?q?docs:=20=E6=96=87=E6=A1=A3=E9=A1=B5?= =?UTF-8?q?=E9=A1=B6=E9=83=A8=E4=B9=9F=E9=9A=90=E8=97=8F=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E5=88=86=E5=89=B2=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/.vitepress/theme/styles.css | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/.vitepress/theme/styles.css b/docs/.vitepress/theme/styles.css index 1b36dd56..d254e065 100644 --- a/docs/.vitepress/theme/styles.css +++ b/docs/.vitepress/theme/styles.css @@ -155,37 +155,37 @@ filter: blur(56px); } -/* 顶栏:首页顶部透明无分割线,滚动后毛玻璃 + 分割线(对齐 cc-switch SiteNavbar) */ -.VPNavBar.home.top { +/* 顶栏:任意页面顶部透明无分割线,滚动后毛玻璃 + 分割线(VPNavBar.top = y===0) */ +.VPNavBar.top { background-color: transparent !important; backdrop-filter: none; } -.VPNavBar.home.top .divider-line { +.VPNavBar.top .divider-line { background-color: transparent !important; } -.VPNavBar.home.top .content-body { +.VPNavBar.top .content-body { background-color: transparent !important; } -.VPNavBar:not(.home.top), +.VPNavBar:not(.top), .VPNavBar.screen-open { background-color: color-mix(in srgb, var(--vp-c-bg) 82%, transparent) !important; backdrop-filter: blur(14px); } -.VPNavBar:not(.home.top) .divider-line, +.VPNavBar:not(.top) .divider-line, .VPNavBar.screen-open .divider-line { background-color: var(--vp-c-divider) !important; } @media (min-width: 960px) { - .VPNavBar.home.top .content-body { + .VPNavBar.top .content-body { background-color: transparent !important; } - .VPNavBar:not(.home.top) .content-body { + .VPNavBar:not(.top) .content-body { background-color: color-mix(in srgb, var(--vp-c-bg) 82%, transparent) !important; } } From 836d793917d0162ff063b93c888d198284008d59 Mon Sep 17 00:00:00 2001 From: hepengju Date: Sat, 4 Jul 2026 16:47:42 +0800 Subject: [PATCH 04/82] =?UTF-8?q?docs:=20=E4=BF=AE=E5=A4=8D=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E4=B8=8B=E8=BD=BD=E4=B8=8B=E6=8B=89=E9=81=AE=E6=8C=A1?= =?UTF-8?q?=E5=B9=B6=E8=B0=83=E6=95=B4=E5=8C=BA=E5=9D=97=E9=97=B4=E8=B7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/.vitepress/theme/components/AppPreview.vue | 6 +++--- docs/.vitepress/theme/components/HeroActions.vue | 5 ++++- docs/.vitepress/theme/styles.css | 13 ++++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/.vitepress/theme/components/AppPreview.vue b/docs/.vitepress/theme/components/AppPreview.vue index d6163167..7870f4ae 100644 --- a/docs/.vitepress/theme/components/AppPreview.vue +++ b/docs/.vitepress/theme/components/AppPreview.vue @@ -25,7 +25,7 @@ const imgSrc = computed(() => { diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index 5f934cdf..ac2f4bc2 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -759,6 +759,11 @@ export default { readonlyHint: 'Executing commands is temporarily not supported in read-only mode', readonlyWriteHint: 'Write or non-readonly commands are not allowed in read-only mode', autoCopyHint: 'Auto Copy Command Result', + outputModeHint: 'Output format: redis-cli TTY / --raw / --json / --csv', + outputStandard: 'TTY', + outputRaw: 'Raw', + outputJson: 'JSON', + outputCsv: 'CSV', commandHint: 'View Command List', commandTitle: 'Commands', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index c3d8bcc8..66a99c75 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -748,6 +748,11 @@ export default { readonlyHint: '只读模式下暂不支持执行命令', readonlyWriteHint: '只读模式下不允许执行写入或非只读命令', autoCopyHint: '自动复制命令结果', + outputModeHint: '输出格式,对齐 redis-cli 的 TTY / --raw / --json / --csv', + outputStandard: 'TTY', + outputRaw: 'Raw', + outputJson: 'JSON', + outputCsv: 'CSV', commandHint: '查看命令列表', commandTitle: '命令列表', keywordHint: '命令或概述模糊筛选', diff --git a/src/types/tauri-specta.ts b/src/types/tauri-specta.ts index 3b99350c..c18165f2 100644 --- a/src/types/tauri-specta.ts +++ b/src/types/tauri-specta.ts @@ -121,6 +121,9 @@ export type AppSettings = { /** 前后端 IPC 字节格式:utf8 文本或 base64 原始字节(hex/binary/msgpack 等视图格式在前端处理) */ export type BytesFormat = "utf8" | "base64"; +/** 终端输出格式,对应 redis-cli `--raw` / `--csv` / `--json`;默认 TTY */ +export type CliOutputMode = "standard" | "raw" | "json" | "csv"; + export type CommandLogEntry = { id: number, timestamp: string, @@ -265,6 +268,8 @@ export type RedisCommand = { command: string, node: string | null, autoBroadcast: boolean | null, + /** 终端输出格式;`None` 等同 `standard`(TTY) */ + outputMode: CliOutputMode | null, }; export type RedisCopyParam = RedisCopyParam_Serialize | RedisCopyParam_Deserialize; diff --git a/src/views/tab/RedisClient.vue b/src/views/tab/RedisClient.vue index c0743333..f2316bf6 100644 --- a/src/views/tab/RedisClient.vue +++ b/src/views/tab/RedisClient.vue @@ -63,6 +63,7 @@ async function killClient(row: RedisClientListRow) { command: `client kill ${row.addr}`, node: node.value, autoBroadcast: null, + outputMode: null, } await meCommands.executeCommand(share.conn!.id, param) meOk(t('redisClient.killClientOk')) diff --git a/src/views/tab/RedisTauri.vue b/src/views/tab/RedisTauri.vue index fc149b74..251a32df 100644 --- a/src/views/tab/RedisTauri.vue +++ b/src/views/tab/RedisTauri.vue @@ -149,7 +149,12 @@ const minimalFieldAsCommand: RedisFieldAsCommand_Deserialize = { valFmt: null, } -const minimalRedisCmd: RedisCommand = { command: 'PING', node: null, autoBroadcast: null } +const minimalRedisCmd: RedisCommand = { + command: 'PING', + node: null, + autoBroadcast: null, + outputMode: null, +} const minimalMemoryParam: RedisMemoryParam = { match: null, diff --git a/src/views/tab/RedisTerminal.vue b/src/views/tab/RedisTerminal.vue index 8e11ef8d..10912037 100644 --- a/src/views/tab/RedisTerminal.vue +++ b/src/views/tab/RedisTerminal.vue @@ -6,6 +6,7 @@ import MeIcon from '@/components/MeIcon.vue' import MeShortcut from '@/components/MeShortcut.vue' import { commandHelp, isReadonlyCommand } from '@/locales/cmd' import { shareProvideKey } from '@/types/me-interface' +import type { CliOutputMode } from '@/types/tauri-specta' import { getTerminalShortcuts } from '@/utils/shortcut' import { meCopy, meCommands, isZh } from '@/utils/util' @@ -18,6 +19,15 @@ const canEdit = computed(() => !share.readonly) /** 只读列表头:英文 Read-only 较宽,中文只读可窄一些 */ const readonlyColWidth = computed(() => (isZh.value ? 88 : 120)) +/** 终端输出格式(对齐 redis-cli --raw / --json / --csv;每次进入默认 TTY) */ +const outputModeOptions: { value: CliOutputMode; labelKey: string }[] = [ + { value: 'standard', labelKey: 'redisTerminal.outputStandard' }, + { value: 'raw', labelKey: 'redisTerminal.outputRaw' }, + { value: 'json', labelKey: 'redisTerminal.outputJson' }, + { value: 'csv', labelKey: 'redisTerminal.outputCsv' }, +] +const outputMode = ref('standard') + // 待颜色的文本 function colorText(color: string, text: string, bold = false): string { return bold @@ -32,6 +42,15 @@ const welcome = computed(() => t('redisTerminal.welcome', { RedisME: colorText(share.color, 'RedisME', true) }), ) +// 成功输出统一绿色;raw 空结果保留空行占位 +function formatCommandResult(data: string): string { + if (outputMode.value === 'raw' && data === '') { + return colorText('var(--el-color-success)', '
') + } + const html = data.split(/\r?\n/).join('
') + return colorText('var(--el-color-success)', html) +} + // 定制化执行命令 async function execCommand(command: string): Promise { if (!canEdit.value && !isReadonlyCommand(command)) { @@ -39,11 +58,15 @@ async function execCommand(command: string): Promise { } try { - const param = { command, node: node.value, autoBroadcast: autoBroadcast.value } + const param = { + command, + node: node.value, + autoBroadcast: autoBroadcast.value, + outputMode: outputMode.value, + } const data = await meCommands.executeCommand(share.conn!.id, param, false) autoCopyIfNeed(data) - const html = data.split(/\r?\n/).join('
') - return colorText('var(--el-color-success)', html) + return formatCommandResult(data) } catch (e: unknown) { autoCopyIfNeed(e) return colorText('var(--el-color-error)', `(error) ${String(e)}`) @@ -144,8 +167,17 @@ const keyShortcuts = computed(() => getTerminalShortcuts(t))
+ + + + + - + getTerminalShortcuts(t)) right: 10px; bottom: 0; z-index: 10; + align-items: center; } } From f5e1ade326e8751b564a3f4ad5d725104ff34251 Mon Sep 17 00:00:00 2001 From: hepengju Date: Wed, 8 Jul 2026 22:10:39 +0800 Subject: [PATCH 26/82] =?UTF-8?q?fix:=20=E5=A4=A7=20STRING=20=E5=80=BC?= =?UTF-8?q?=E8=B6=85=201MB=20=E9=A2=84=E8=A7=88=E5=8A=A0=E8=BD=BD=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20bitmap=20=E5=A4=A7=20offset=20=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E5=B4=A9=E6=BA=83=20#136?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src-tauri/src/client/client_trait.rs | 38 +++++++++++-- src-tauri/src/client/mod.rs | 3 + src-tauri/src/utils/model.rs | 6 ++ src/locales/lang/en.ts | 5 ++ src/locales/lang/zh-cn.ts | 5 ++ src/types/tauri-specta.ts | 10 ++++ src/views/tab/RedisTauri.vue | 3 + src/views/tab/RedisValue.vue | 83 +++++++++++++++++++++++++++- 8 files changed, 146 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/client/client_trait.rs b/src-tauri/src/client/client_trait.rs index 58ef6ea4..63673543 100644 --- a/src-tauri/src/client/client_trait.rs +++ b/src-tauri/src/client/client_trait.rs @@ -205,7 +205,8 @@ pub fn field_scan0( let bytes_format = param.bytes_format.as_ref().cloned().unwrap_or_default(); // String, Json, List, Hash(WithKey), Stream(WithKey), Stream 直接获取得到值 - let (mut value, key_type, mut cc, length) = field_scan_0_get(&mut conn, ¶m, &bytes_format)?; + let (mut value, key_type, mut cc, length, value_truncated) = + field_scan_0_get(&mut conn, ¶m, &bytes_format)?; // Hash, Set, Zset 进行扫描(hscan, sscan, zscan) let key = param.key; if value.is_none() { @@ -261,14 +262,37 @@ pub fn field_scan0( cc, length, with_field_key, + value_truncated, ) } +/// STRING 按阈值决定 GET 全量或 GETRANGE 预览;返回 (bytes, strlen, truncated) +fn load_string_bytes( + conn: &mut MutexGuard, + key: &RedisKey, + param: &FieldScanParam, +) -> AnyResult<(Vec, usize, bool)> { + let strlen: usize = conn.strlen(key)?; + let force_full = param.force_full_value.unwrap_or(false); + if !force_full { + if let Some(limit) = param.value_byte_limit { + if strlen > limit as usize { + let preview = param.value_preview_bytes.unwrap_or(1000) as usize; + let end = preview.saturating_sub(1) as isize; + let value: Vec = conn.getrange(key, 0, end)?; + return Ok((value, strlen, true)); + } + } + } + let value: Vec = conn.get(key)?; + Ok((value, strlen, false)) +} + pub fn field_scan_0_get( mut conn: &mut MutexGuard, param: &FieldScanParam, bytes_format: &BytesFormat, -) -> AnyResult<(Option, ValueType, ScanCursor, usize)> { +) -> AnyResult<(Option, ValueType, ScanCursor, usize, bool)> { let key = ¶m.key; let hash_key = param.hash_key.clone(); @@ -277,6 +301,7 @@ pub fn field_scan_0_get( // String类型的bytes长度 let mut length = 0; + let mut value_truncated = false; let value: Option = match key_type { ValueType::None => { @@ -285,8 +310,9 @@ pub fn field_scan_0_get( }) } ValueType::String => { - let value: Vec = conn.get(key)?; - length = value.len(); + let (value, strlen, truncated) = load_string_bytes(&mut conn, key, param)?; + length = strlen; + value_truncated = truncated; let value: String = format_bytes(&value, bytes_format); cc.finished = true; Some(serde_json::to_value(value)?) @@ -408,7 +434,7 @@ pub fn field_scan_0_get( } _ => None, }; - Ok((value, key_type, cc, length)) + Ok((value, key_type, cc, length, value_truncated)) } pub fn field_scan_1_cmd( @@ -534,6 +560,7 @@ pub fn field_scan_4_return( cursor: ScanCursor, length: usize, with_field_key: bool, + value_truncated: bool, ) -> AnyResult { let ttl: i64 = conn.ttl(&key)?; let size: u64 = redis::cmd("memory") @@ -551,6 +578,7 @@ pub fn field_scan_4_return( value, cursor, length, + value_truncated, }) } diff --git a/src-tauri/src/client/mod.rs b/src-tauri/src/client/mod.rs index c8a8dc1a..464fa8de 100644 --- a/src-tauri/src/client/mod.rs +++ b/src-tauri/src/client/mod.rs @@ -215,6 +215,9 @@ mod tests { load_all: false, meta: None, bytes_format: Some(BytesFormat::Base64), + value_byte_limit: None, + value_preview_bytes: None, + force_full_value: None, } } diff --git a/src-tauri/src/utils/model.rs b/src-tauri/src/utils/model.rs index 48315c1b..6a608925 100644 --- a/src-tauri/src/utils/model.rs +++ b/src-tauri/src/utils/model.rs @@ -321,6 +321,10 @@ api_model!(FieldScanParam { load_all: bool, meta: Option, // 扩展参数 bytes_format: Option, // 扫描/展示用字节格式 + /// STRING 全量加载字节上限;超过且未 force 时仅 GETRANGE 预览前 value_preview_bytes + value_byte_limit: Option, + value_preview_bytes: Option, + force_full_value: Option, }); api_model!(FiledScanMeta { @@ -377,6 +381,8 @@ api_model!(FieldScanResult { value: serde_json::Value, cursor: ScanCursor, length: usize, // String/Hash字段:原始 bytes 长度;集合类型:元素总数(HLEN/LLEN/SCARD/ZCARD/XLEN) + /// STRING 因超过 value_byte_limit 仅返回预览片段时为 true + value_truncated: bool, }); // Redis键: 由于键是字节存储的,考虑转换为utf-8字符串显示后可能会丢失信息,因此封装为对象 diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index ac2f4bc2..429f5435 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -836,6 +836,11 @@ export default { textLength: 'Bytes Length: ', textEntries: 'Scanned: ', totalCount: 'Total: ', + valueTruncatedTitle: 'Value too large — showing preview', + valueTruncatedDesc: + 'This key is about {size}, exceeding the {limit} limit. Only the first {preview} bytes are shown to keep the UI responsive.', + valueTruncatedDismiss: 'Keep Preview', + valueTruncatedLoadAll: 'Load Full Value', viewCodec: 'Codec', keyShortHint: 'View KeyShort', keyShort: { diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index 66a99c75..d0d368f6 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -823,6 +823,11 @@ export default { textLength: '字节长度:', textEntries: '已扫描:', totalCount: '总数:', + valueTruncatedTitle: '值过大,已切换为预览模式', + valueTruncatedDesc: + '该键约 {size},超过 {limit} 安全阈值。为避免卡顿,当前仅展示前 {preview} 字节。', + valueTruncatedDismiss: '继续使用预览', + valueTruncatedLoadAll: '仍要加载全部', viewCodec: '数据编码', keyShortHint: '查看快捷键', keyShort: { diff --git a/src/types/tauri-specta.ts b/src/types/tauri-specta.ts index c18165f2..7a6fc71c 100644 --- a/src/types/tauri-specta.ts +++ b/src/types/tauri-specta.ts @@ -166,6 +166,10 @@ export type FieldScanParam_Deserialize = { loadAll: boolean, meta: FiledScanMeta | null, bytesFormat: BytesFormat | null, + /** STRING 全量加载字节上限;超过且未 force 时仅 GETRANGE 预览前 value_preview_bytes */ + valueByteLimit: number | null, + valuePreviewBytes: number | null, + forceFullValue: boolean | null, }; export type FieldScanParam_Serialize = { @@ -176,6 +180,10 @@ export type FieldScanParam_Serialize = { loadAll: boolean, meta: FiledScanMeta | null, bytesFormat: BytesFormat | null, + /** STRING 全量加载字节上限;超过且未 force 时仅 GETRANGE 预览前 value_preview_bytes */ + valueByteLimit: number | null, + valuePreviewBytes: number | null, + forceFullValue: boolean | null, }; export type FieldScanResult = { @@ -185,6 +193,8 @@ export type FieldScanResult = { value: any, cursor: ScanCursor, length: number, + /** STRING 因超过 value_byte_limit 仅返回预览片段时为 true */ + valueTruncated: boolean, }; export type FiledScanMeta = { diff --git a/src/views/tab/RedisTauri.vue b/src/views/tab/RedisTauri.vue index 251a32df..9ffef8c4 100644 --- a/src/views/tab/RedisTauri.vue +++ b/src/views/tab/RedisTauri.vue @@ -89,6 +89,9 @@ const minimalFieldScan: FieldScanParam_Deserialize = { loadAll: false, meta: null, bytesFormat: null, + valueByteLimit: null, + valuePreviewBytes: null, + forceFullValue: null, } const minimalSetParam: RedisSetParam_Deserialize = { diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index e37ddacd..e60c2945 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -99,7 +99,6 @@ const { t } = useI18n() const share = inject(shareProvideKey)! const connUi = inject(connUiProvideKey)! const canEdit = computed(() => !share.readonly) -const canSave = computed(() => canEdit.value && (stringType.value || jsonType.value)) // #endregion // #region 核心状态(fieldScan 结果 / 游标 / 编辑) @@ -116,6 +115,17 @@ const valueEditorRemountKey = ref(0) /** 手动控制「加载更多」按钮,避免 cursor 变化导致按钮闪现 */ const showMore = ref(false) +/** 临时:STRING 全量加载阈值与预览长度,后期迁至 settings */ +const VALUE_BYTE_LIMIT = 1024 * 1024 +const VALUE_PREVIEW_BYTES = 1000 +/** 用户确认「仍要加载全部」后为 true,fieldScan 走 GET 全量 */ +const forceFullValue = ref(false) +const valueTruncatedDismissed = ref(false) +const valueTruncated = computed(() => redisValue.value?.valueTruncated ?? false) +const showValueTruncatedAlert = computed( + () => stringType.value && valueTruncated.value && !valueTruncatedDismissed.value, +) + /** Stream 扫描范围(meta 传给 fieldScan) */ const meta = ref({ maxId: '', minId: '' }) // #endregion @@ -128,6 +138,12 @@ const streamType = computed(() => 'stream' === redisValue.value?.type) const stringTypeOrWithHashKey = computed( () => 'string' === redisValue.value?.type || withHashKey.value, ) +const canSave = computed( + () => + canEdit.value && + (stringType.value || jsonType.value) && + !(valueTruncated.value && !forceFullValue.value), +) // #endregion // #region 视图模式:JSON 编辑器 / 表格 @@ -379,9 +395,27 @@ function buildFieldScanParam(loadAll: boolean) { loadAll, meta: meta.value, bytesFormat: toWireFormat(bytesFormat.value), + valueByteLimit: VALUE_BYTE_LIMIT, + valuePreviewBytes: VALUE_PREVIEW_BYTES, + forceFullValue: forceFullValue.value, } } +function dismissValueTruncated() { + valueTruncatedDismissed.value = true +} + +/** 用户主动刷新键时重新展示大值预览提示(与切换键时的 reset 不同,保留 forceFullValue) */ +function prepareManualKeyRefresh() { + valueTruncatedDismissed.value = false +} + +async function loadFullValue() { + if (loading.value) return + forceFullValue.value = true + await refreshKey(false) +} + /** * 「加载更多」专用:把新一页行追加到已有 redisValue.value,避免整表重渲染。 * 仅 hash/list/set/zset/stream 的行数组可拼接;string/json 等走整包替换。 @@ -451,7 +485,11 @@ async function refreshKey( // 刷新过程中 me-code 的 modelValue 会变,避免误写入 newValue suppressCodeUpdate.value = true - if (reset) resetParam() + if (reset) { + resetParam() + forceFullValue.value = false + valueTruncatedDismissed.value = false + } // 非「加载更多」时从第一页重新 scan if (!useCursor) cursor.value = null @@ -560,6 +598,7 @@ async function copyAsCommand() { async function onFooterRefreshKey() { if (loading.value) return + prepareManualKeyRefresh() await refreshKey(false) meOk(t('redisValue.refreshKeyOk')) } @@ -622,6 +661,7 @@ function toggleFavorite() { function onKeyMoreCommand(command: string) { if (command === 'refreshKey') { + prepareManualKeyRefresh() void refreshKey(false) } else if (command === 'copyKey') { meCopy(showKey.value) @@ -1113,6 +1153,31 @@ onUnmounted(() => {
+ +

+ {{ + t('redisValue.valueTruncatedDesc', { + size: meHumanSize(redisValue?.length ?? 0), + limit: meHumanSize(VALUE_BYTE_LIMIT), + preview: VALUE_PREVIEW_BYTES, + }) + }} +

+
+ + {{ t('redisValue.valueTruncatedDismiss') }} + + + {{ t('redisValue.valueTruncatedLoadAll') }} + +
+
{ flex-grow: 1; overflow: hidden; + .value-truncated-alert { + margin-bottom: 8px; + + .value-truncated-desc { + margin: 0 0 8px; + line-height: 1.5; + } + + .value-truncated-actions { + display: flex; + gap: 8px; + } + } + .table-view { margin-top: 10px; flex-grow: 1; From 9f772f4e5fe0dc6ea88184c41ad1c0664d3a6584 Mon Sep 17 00:00:00 2001 From: hepengju Date: Thu, 9 Jul 2026 21:09:39 +0800 Subject: [PATCH 27/82] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=94=AE?= =?UTF-8?q?=E5=80=BC=E7=A9=BA=E4=B8=B2=E4=BF=9D=E5=AD=98=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20FieldAdd=20=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/zh/changelog/future.md | 3 +++ src/locales/lang/en.ts | 4 ---- src/locales/lang/zh-cn.ts | 4 ---- src/views/ext/FieldAdd.vue | 45 ++++++++---------------------------- src/views/ext/FieldSet.vue | 5 ++-- src/views/tab/RedisValue.vue | 34 +++++++++++++++++++-------- 6 files changed, 38 insertions(+), 57 deletions(-) diff --git a/docs/zh/changelog/future.md b/docs/zh/changelog/future.md index 3973501a..08160ee3 100644 --- a/docs/zh/changelog/future.md +++ b/docs/zh/changelog/future.md @@ -1,5 +1,7 @@ ## Future +- String类型阈值保护 +- Binary格式恢复支持(setbit等场景还是需要的) - Hash/Set/ZSet的扫描模式支持 - Hash仅查询键的支持 - List/Stream支持正序倒序查询 @@ -50,3 +52,4 @@ - 发布订阅的编码支持 ✅️ - 键输入框的提示 ✅️ - 集群模式下的多数据库(valkey9.0支持) ✅️ +- 空值保存(新增键、保存值、保存字段值等场景)✅️ diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index 429f5435..8c021704 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -393,10 +393,7 @@ export default { typeRequired: 'Type required', ttlRequired: 'TTL required', ttlValidator: '-1 (Forever) or positive integer', - valueRequired: 'Value required', jsonValidator: 'Value must be in a valid JSON format', - hashValidator: 'HashKey and hashValue required', - streamValidator: 'Field and value required', hashHint: '(HashKey: HashValue)', hashHintTtl: '(HashKey: HashValue: TTL Seconds)', zsetHint: '(Value: Score)', @@ -418,7 +415,6 @@ export default { }, fieldSet: { - fieldValueRequired: 'Value Required', fieldScoreRequired: 'Score Required', editField: 'Edit Field', viewField: 'View Field', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index d0d368f6..6d58f199 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -384,10 +384,7 @@ export default { typeRequired: '请选择类型', ttlRequired: '请输入TTL', ttlValidator: '只允许-1(永久) 或 正整数', - valueRequired: '值不允许为空', jsonValidator: '值必须为有效的JSON格式', - hashValidator: '哈希键和值不允许为空', - streamValidator: '字段和值不允许为空', hashHint: '(哈希键:值)', hashHintTtl: '(哈希键:值:过期秒)', zsetHint: '(值:分数)', @@ -409,7 +406,6 @@ export default { }, fieldSet: { - fieldValueRequired: '值不能为空', fieldScoreRequired: '请输入分数', editField: '编辑字段', viewField: '查看字段', diff --git a/src/views/ext/FieldAdd.vue b/src/views/ext/FieldAdd.vue index 5fc12138..51d2e777 100644 --- a/src/views/ext/FieldAdd.vue +++ b/src/views/ext/FieldAdd.vue @@ -73,54 +73,30 @@ const rules = computed(() => ({ ) => { if (!(form.value.ttl === -1 || form.value.ttl > 0)) { callback(new Error(t('fieldAdd.ttlValidator'))) + return } callback() }, }, ], value: [ - { required: true, message: t('fieldAdd.valueRequired') }, { validator: ( _rule: FormItemRule, value: unknown, callback: (error?: string | Error) => void, ) => { + // string 等类型允许空串;json 类型空串与非法 JSON 均不通过 if (form.value.type === 'json') { + if (value === '') { + callback(new Error(t('fieldAdd.jsonValidator'))) + return + } try { meJsonParse(String(value)) // json 输入支持 json5 格式,此处转换为正常 json 字符串 } catch { callback(new Error(t('fieldAdd.jsonValidator'))) - } - } - callback() - }, - }, - ], - fieldValueList: [ - { - validator: ( - _rule: FormItemRule, - _value: unknown, - callback: (error?: string | Error) => void, - ) => { - if (form.value.type === 'hash' || form.value.type === 'stream') { - const count = form.value.fieldValueList.filter( - d => d.fieldKey === '' || d.fieldValue === '', - ).length - if (count > 0) { - callback( - new Error( - form.value.type === 'hash' - ? t('fieldAdd.hashValidator') - : t('fieldAdd.streamValidator'), - ), - ) - } - } else { - const count = form.value.fieldValueList.filter(d => d.fieldValue === '').length - if (count > 0) { - callback(new Error(t('fieldAdd.valueRequired'))) + return } } callback() @@ -136,7 +112,7 @@ const rules = computed(() => ({ ) => { if (form.value.type === 'stream') { if (value) return callback() - callback(new Error(t('fieldAdd.streamIdRequired'))) + return callback(new Error(t('fieldAdd.streamIdRequired'))) } callback() }, @@ -305,10 +281,7 @@ function handleKeyTypeChange() { - +
{ }) const rules = computed(() => ({ - fieldValue: [{ required: true, message: t('fieldSet.fieldValueRequired') }], fieldScore: [{ required: true, message: t('fieldSet.fieldScoreRequired') }], })) @@ -209,7 +208,7 @@ function submit() { const fmt = fieldViewFmt.value let fieldValue = form.value.fieldValue if (needsJsonNormalize(fmt)) { - fieldValue = meJsonNormal(fieldValue) + fieldValue = fieldValue === '' ? '' : meJsonNormal(fieldValue) } if (isCustomView(fmt)) { fieldValue = await meViewToWireAsync(fieldValue, fmt) @@ -338,7 +337,7 @@ async function refreshField() { align="left" style="width: 100%" /> - + { + const rv = redisValue.value + if (!rv || rv.newValue === null) return false + return rv.newValue !== showValue.value +}) // #endregion // #region 表格行数据与筛选 @@ -442,7 +450,7 @@ async function finalizeAfterFieldScan(reset: boolean, replaceData?: FieldScanRes } // 清空未保存编辑;fieldScan 结果即当前权威内容 if (redisValue.value) { - redisValue.value.newValue = '' + redisValue.value.newValue = null } suppressCodeUpdate.value = false if (reset) applyDefaultViewType() @@ -680,15 +688,21 @@ function onKeyMoreCommand(command: string) { // #region 保存整键值(STRING / JSON) async function setValue() { const rv = redisValue.value - if (!rv) return + if (!rv || rv.newValue === null) return let value = rv.newValue try { - if ( - jsonType.value || - (stringType.value && (bytesFormat.value === 'msgpack' || bytesFormat.value === 'strjson')) - ) { + if (jsonType.value) { + if (value === '') { + meErr(t('fieldAdd.jsonValidator')) + return + } value = meJsonNormal(value) + } else if ( + stringType.value && + (bytesFormat.value === 'msgpack' || bytesFormat.value === 'strjson') + ) { + value = value === '' ? '' : meJsonNormal(value) } if (stringType.value && isCustomView(bytesFormat.value)) { value = await meViewToWireAsync(value, bytesFormat.value) @@ -699,7 +713,7 @@ async function setValue() { const msg = e instanceof Error ? e.message : String(e) if (stringType.value && isCustomView(bytesFormat.value)) { setCustomCodecError(msg) - rv.newValue = '' + rv.newValue = null valueEditorRemountKey.value++ return } @@ -1533,7 +1547,7 @@ onUnmounted(() => { Date: Thu, 9 Jul 2026 21:16:23 +0800 Subject: [PATCH 28/82] =?UTF-8?q?feat:=20=E6=81=A2=E5=A4=8D=20Binary=20?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E6=A0=BC=E5=BC=8F=EF=BC=88setbit=20=E7=AD=89?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/zh/changelog/future.md | 4 ++-- src/locales/lang/en.ts | 2 ++ src/locales/lang/zh-cn.ts | 2 ++ src/utils/format.ts | 45 ++++++++++++++++++++++++++++++++---- src/views/tab/RedisValue.vue | 2 +- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/docs/zh/changelog/future.md b/docs/zh/changelog/future.md index 08160ee3..c49c27c6 100644 --- a/docs/zh/changelog/future.md +++ b/docs/zh/changelog/future.md @@ -1,7 +1,7 @@ ## Future -- String类型阈值保护 -- Binary格式恢复支持(setbit等场景还是需要的) +- String类型阈值保护 ✅️ +- Binary格式恢复支持(setbit等场景还是需要的)✅️ - Hash/Set/ZSet的扫描模式支持 - Hash仅查询键的支持 - List/Stream支持正序倒序查询 diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index 8c021704..dd6e08d5 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -336,6 +336,8 @@ export default { checkUpdateErr: 'Check Update Fail', invalidHexString: 'Invalid hex string: odd number of characters', invalidHexCharacter: 'Invalid hex character', + invalidBinaryString: 'Invalid binary string: length not multiple of 8', + invalidBinaryCharacter: 'Invalid binary character', }, tabMain: { diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index 6d58f199..073514df 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -326,6 +326,8 @@ export default { checkUpdateErr: '检查更新失败', invalidHexString: '无效的十六进制字符串:长度为奇数', invalidHexCharacter: '无效的十六进制字符', + invalidBinaryString: '无效的二进制字符串:长度不是 8 的倍数', + invalidBinaryCharacter: '无效的二进制字符', }, tabMain: { diff --git a/src/utils/format.ts b/src/utils/format.ts index 1b2d70ac..045c6af5 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -244,10 +244,17 @@ export async function testCodec( // #region 视图格式与 wire 转换 /** 键重命名等基础字节视图;KeyRename、FieldAdd */ -export const BYTES_FORMAT = ['UTF8', 'Hex', 'Base64'] as const +export const BYTES_FORMAT = ['UTF8', 'Hex', 'Binary', 'Base64'] as const /** 前端值/键展示格式;RedisValue 数据编码下拉 */ -export type ViewBytesFormat = 'utf8' | 'hex' | 'base64' | 'msgpack' | 'strjson' | `custom:${string}` +export type ViewBytesFormat = + | 'utf8' + | 'hex' + | 'binary' + | 'base64' + | 'msgpack' + | 'strjson' + | `custom:${string}` export const CUSTOM_FORMAT_PREFIX = 'custom:' as const @@ -342,7 +349,7 @@ export function needsJsonNormalize(view: ViewBytesFormat): boolean { export function meFormatViewValue(wire: string, view: ViewBytesFormat): string { if (!wire || view === 'utf8') return wire if (view === 'base64') return wire - if (view === 'hex') return meFormatBytes(wire, view) + if (view === 'hex' || view === 'binary') return meFormatBytes(wire, view) if (view === 'msgpack') return meMsgpackBase64ToJson(wire) if (view === 'strjson') return meStrJsonWireToDisplay(wire) if (isCustomView(view)) { @@ -364,7 +371,7 @@ export async function meFormatViewValueAsync(wire: string, view: ViewBytesFormat export function meViewToWire(text: string, view: ViewBytesFormat): string { if (!text || view === 'utf8') return text if (view === 'base64') return text - if (view === 'hex') return meToBase64(text, view) + if (view === 'hex' || view === 'binary') return meToBase64(text, view) if (view === 'msgpack') return meJsonToMsgpackBase64(text) if (view === 'strjson') return meDisplayToStrJsonWire(text) if (isCustomView(view)) { @@ -386,6 +393,7 @@ export async function meViewToWireAsync(text: string, view: ViewBytesFormat): Pr export function meFormatBytes(base64: string, bytesFormat: string): string { if (bytesFormat === 'base64') return base64 if (bytesFormat === 'hex') return base64ToHex(base64) + if (bytesFormat === 'binary') return base64ToBinary(base64) return 'Unknown bytesFormat: ' + bytesFormat } @@ -393,6 +401,7 @@ export function meFormatBytes(base64: string, bytesFormat: string): string { export function meToBase64(bytes: string, encoding: string): string { if (encoding === 'base64') return bytes if (encoding === 'hex') return hexToBase64(bytes) + if (encoding === 'binary') return binaryToBase64(bytes) return 'Unknown encoding: ' + encoding } @@ -404,6 +413,14 @@ function base64ToHex(base64: string): string { .join('') } +function base64ToBinary(base64: string): string { + if (!base64) return '' + const binary = atob(base64) + return Array.from(binary) + .map(char => char.charCodeAt(0).toString(2).padStart(8, '0')) + .join('') +} + function hexToBase64(hex: string): string { if (!hex) return '' if (hex.length % 2 !== 0) { @@ -424,6 +441,26 @@ function hexToBase64(hex: string): string { return btoa(binary) } +function binaryToBase64(binary: string): string { + if (!binary) return '' + if (binary.length % 8 !== 0) { + throw new Error(t('util.invalidBinaryString')) + } + if (!/^[01]+$/.test(binary)) { + throw new Error(t('util.invalidBinaryCharacter')) + } + const bytes: number[] = [] + for (let i = 0; i < binary.length; i += 8) { + const byte = Number.parseInt(binary.slice(i, i + 8), 2) + if (Number.isNaN(byte)) { + throw new Error(t('util.invalidBinaryCharacter')) + } + bytes.push(byte) + } + const binaryStr = bytes.map(b => String.fromCharCode(b)).join('') + return btoa(binaryStr) +} + function base64ToUint8Array(base64: string): Uint8Array { const binary = atob(base64) const bytes = new Uint8Array(binary.length) diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index 2b9593df..edbdee2b 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -239,7 +239,7 @@ const formatOptions = computed(() => { const viewDecodeFailed = computed(() => { if (!stringType.value) return false const fmt = displayBytesFormat.value - if (fmt === 'utf8' || fmt === 'hex' || fmt === 'base64') return false + if (fmt === 'utf8' || fmt === 'hex' || fmt === 'binary' || fmt === 'base64') return false const wire = displayWire.value if (!wire) return false if (isCustomView(fmt)) return customCodecFailed.value From 302538b71ade1d497c1883d8880c61bedabb936b Mon Sep 17 00:00:00 2001 From: hepengju Date: Thu, 9 Jul 2026 21:34:55 +0800 Subject: [PATCH 29/82] =?UTF-8?q?feat:=20String=20=E9=94=AE=E5=9C=A8=20MEM?= =?UTF-8?q?ORY=20USAGE=20=E4=B8=8D=E5=8F=AF=E7=94=A8=E6=97=B6=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=BC=B0=E7=AE=97=E5=86=85=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/zh/changelog/future.md | 2 +- src/locales/lang/en.ts | 1 + src/locales/lang/zh-cn.ts | 1 + src/utils/util.ts | 7 +++++++ src/views/tab/RedisValue.vue | 16 ++++++++++++++-- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/zh/changelog/future.md b/docs/zh/changelog/future.md index c49c27c6..323f9331 100644 --- a/docs/zh/changelog/future.md +++ b/docs/zh/changelog/future.md @@ -10,7 +10,7 @@ - RedisSearch的支持 - 慢日志治理的微信公众号文章 - 解析剪贴板URL -- Memory Usage命令不支持时前端显示大小 +- Memory Usage命令不支持时前端显示大小 ✅️ - 内存分析参考键扫描改为实时显示 - 旧版SSH证书的支持 - RedisInsight和TinyRDM都有的自动数据解压缩是什么 diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index dd6e08d5..e9cc5029 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -831,6 +831,7 @@ export default { locateKeyHint: 'Locate current key in key list', textMemory: 'Memory Usage: ', + textMemoryEstimate: 'Memory Usage (est.): ', textLength: 'Bytes Length: ', textEntries: 'Scanned: ', totalCount: 'Total: ', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index 073514df..115aa5eb 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -818,6 +818,7 @@ export default { locateKeyHint: '在键列表中定位当前键', textMemory: '内存占用:', + textMemoryEstimate: '内存占用(估算):', textLength: '字节长度:', textEntries: '已扫描:', totalCount: '总数:', diff --git a/src/utils/util.ts b/src/utils/util.ts index 19abeabc..f6eb5e6e 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -359,6 +359,13 @@ const humanUnits = [ { threshold: 1024 ** 4, symbol: 'T' }, ] as const +/** MEMORY USAGE 不可用时的 String 键内存粗估:键名 + 值字节 + Redis 对象/SDS 固定开销 */ +export function estimateStringMemory(key: string, valueByteLen: number): number { + const keyBytes = new TextEncoder().encode(key).length + const OVERHEAD = 56 + return keyBytes + valueByteLen + OVERHEAD +} + export function meHumanSize(size: number, zeroShow = '0B', fractionDigits = 2): string { if (!size) return zeroShow || '' diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index edbdee2b..f77c593c 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -57,6 +57,7 @@ import { meDeleteKey, meErr, meHumanSeconds, + estimateStringMemory, meHumanSize, meFormatDisplayValue, meJsonNormal, @@ -1002,8 +1003,19 @@ async function showGroups() { // #region 底部信息栏(内存 / 条数 / 槽位) const textMemory = computed(() => { - const sz = redisValue.value?.size - return sz != null && sz > 0 ? t('redisValue.textMemory') + meHumanSize(sz) : '' + const rv = redisValue.value + if (!rv) return '' + let sz = rv.size + let estimated = false + // 兼容不支持 MEMORY USAGE 的 Redis 变体:String 按键名+值长度粗估 + if (sz <= 0 && stringType.value) { + const key = share.redisKey?.key ?? '' + sz = estimateStringMemory(key, rv.length) + estimated = true + } + if (sz <= 0) return '' + const label = estimated ? t('redisValue.textMemoryEstimate') : t('redisValue.textMemory') + return label + meHumanSize(sz) }) /** 与 textLength 同一位置:String/单字段为字节长度,集合类型为总数 */ From fd49473033d67db17038493b672a09349a6149f0 Mon Sep 17 00:00:00 2001 From: hepengju Date: Thu, 9 Jul 2026 21:43:27 +0800 Subject: [PATCH 30/82] =?UTF-8?q?chore:=20=E5=8D=87=E7=BA=A7=20npm=20?= =?UTF-8?q?=E4=B8=8E=20Rust=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- package.json | 18 +- pnpm-lock.yaml | 503 +++++++++++++++++++++---------------------- src-tauri/Cargo.lock | 153 +++++++------ 3 files changed, 324 insertions(+), 350 deletions(-) diff --git a/package.json b/package.json index 267218c8..bb2270a8 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "@codemirror/language": "^6.12.4", "@codemirror/legacy-modes": "^6.5.3", "@codemirror/search": "^6.7.1", - "@codemirror/state": "^6.7.0", - "@codemirror/view": "^6.43.4", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.6", "@element-plus/icons-vue": "^2.3.2", "@lezer/highlight": "^1.2.3", "@msgpack/msgpack": "^3.1.3", @@ -52,12 +52,12 @@ "minimatch": "^10.2.5", "mitt": "^3.0.1", "nanoid": "^5.1.16", - "oxc-minify": "^0.138.0", + "oxc-minify": "^0.139.0", "sortablejs": "^1.15.7", "tauri-plugin-system-fonts-api": "^2.0.2", "vue": "^3.5.39", - "vue-chartjs": "^5.3.3", - "vue-codemirror6": "^1.5.2", + "vue-chartjs": "^5.3.4", + "vue-codemirror6": "^1.6.1", "vue-i18n": "^11.4.6", "vue-web-terminal": "^3.4.2", "xlsx": "npm:@e965/xlsx@^0.20.3", @@ -71,11 +71,11 @@ "sass": "^1.101.0", "unplugin-auto-import": "^21.0.0", "unplugin-svg-component": "^0.12.6", - "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2", - "vite-plus": "0.2.2", + "vite": "npm:@voidzero-dev/vite-plus-core@0.2.4", + "vite-plus": "0.2.4", "vitepress": "2.0.0-alpha.17", "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.24", - "vue-tsc": "3.3.5" + "vue-tsc": "3.3.7" }, - "packageManager": "pnpm@11.9.0" + "packageManager": "pnpm@11.10.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c4e0963..c8f25169 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,8 +116,8 @@ importers: specifier: ^5.1.16 version: 5.1.16 oxc-minify: - specifier: ^0.138.0 - version: 0.138.0 + specifier: ^0.139.0 + version: 0.139.0 sortablejs: specifier: ^1.15.7 version: 1.15.7 @@ -128,11 +128,11 @@ importers: specifier: ^3.5.39 version: 3.5.39(typescript@5.8.3) vue-chartjs: - specifier: ^5.3.3 - version: 5.3.3(chart.js@4.5.1)(vue@3.5.39(typescript@5.8.3)) + specifier: ^5.3.4 + version: 5.3.4(chart.js@4.5.1)(vue@3.5.39(typescript@5.8.3)) vue-codemirror6: - specifier: ^1.5.2 - version: 1.5.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)(codemirror@6.0.2)(style-mod@4.1.3)(vue@3.5.39(typescript@5.8.3)) + specifier: ^1.6.1 + version: 1.6.1(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)(codemirror@6.0.2)(style-mod@4.1.3)(vue@3.5.39(typescript@5.8.3)) vue-i18n: specifier: ^11.4.6 version: 11.4.6(vue@3.5.39(typescript@5.8.3)) @@ -154,7 +154,7 @@ importers: version: 4.17.24 '@vitejs/plugin-vue': specifier: ^6.0.7 - version: 6.0.7(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(vue@3.5.39(typescript@5.8.3)) + version: 6.0.7(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(vue@3.5.39(typescript@5.8.3)) front-matter: specifier: ^4.0.2 version: 4.0.2 @@ -168,20 +168,20 @@ importers: specifier: ^0.12.6 version: 0.12.6 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.2.2 - version: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.4 + version: '@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' vite-plus: - specifier: 0.2.2 - version: 0.2.2(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0) + specifier: 0.2.4 + version: 0.2.4(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0) vitepress: specifier: 2.0.0-alpha.17 - version: 2.0.0-alpha.17(@types/node@25.9.3)(async-validator@4.2.5)(lightningcss@1.32.0)(oxc-minify@0.138.0)(postcss@8.5.15)(sass@1.101.0)(sortablejs@1.15.7)(typescript@5.8.3)(yaml@2.9.0) + version: 2.0.0-alpha.17(@types/node@25.9.3)(async-validator@4.2.5)(lightningcss@1.32.0)(oxc-minify@0.139.0)(postcss@8.5.15)(sass@1.101.0)(sortablejs@1.15.7)(typescript@5.8.3)(yaml@2.9.0) vitest: specifier: npm:@voidzero-dev/vite-plus-test@0.1.24 - version: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' vue-tsc: - specifier: 3.3.5 - version: 3.3.5(typescript@5.8.3) + specifier: 3.3.7 + version: 3.3.7(typescript@5.8.3) packages: @@ -531,129 +531,129 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-minify/binding-android-arm-eabi@0.138.0': - resolution: {integrity: sha512-bnltntiD7kyAmpU4YxmNBt+aE8B5hs8R5HUfkLVr/6ofI13hKnxkA3cEfpziUiWmvNKoG0kLDUpaid0kPDCaPw==} + '@oxc-minify/binding-android-arm-eabi@0.139.0': + resolution: {integrity: sha512-aXrJvvhQ09YMg/atuVixnCdK9IE3hvogna+/ZqRuzs69n6DRrq7MbuRJnrfw+tQvtym9m7vNwQa3fENTemibGQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-minify/binding-android-arm64@0.138.0': - resolution: {integrity: sha512-tdLSE1pM0KBSZiBqwMgn7GNEakyJUBvxjZNhHLPNMCtdc7Jezh7iq/Wzs5aMFN+DMs+hbnfYYDvUup1XsR2lBQ==} + '@oxc-minify/binding-android-arm64@0.139.0': + resolution: {integrity: sha512-2LX1Wpsw+LtvOw/dxWVeMT/zBpfw9wdW7FBhFoXwhO24o6F7LPcsCh2BjxygsM8+D4pyJhPwlPekeGGPT/KH2A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-minify/binding-darwin-arm64@0.138.0': - resolution: {integrity: sha512-PTFcY0+bK2jaUve9Twl2BzsWAUCIOVXaWrufSQWI3mX2T4kUeqm5tDh32bKfQKb+Y23CE0djJk2p851+bxEykg==} + '@oxc-minify/binding-darwin-arm64@0.139.0': + resolution: {integrity: sha512-5AWuNuJaZxZLcyX7eptAac2nBycrjRIIPaPjiXEq9ozBemp8cXXjr2jkAUIaVcoohiKEYg3EegoaqPFeDPvOGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-minify/binding-darwin-x64@0.138.0': - resolution: {integrity: sha512-xk7Bp8tjBttB0hW4UGZMlm0Nyih0A5PosSC1lS+04919cKJS2uUFXRAiYoSHmfLGUASUURCT2ZcDjiDVuJHDPA==} + '@oxc-minify/binding-darwin-x64@0.139.0': + resolution: {integrity: sha512-RnvVqNNiy0FIhn9y+EPoiC9me9QZxb6O/wuhr+uE7pZm6eG5kw37SvudwaYjYmH/7Xfo87bqEffdowEi7CGsOA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-minify/binding-freebsd-x64@0.138.0': - resolution: {integrity: sha512-TGS2CBDdDTlMBjHTVQ5vY5tvAx7M+XSOpHthTvR5whyuAln9oE4ojXBDmCZdwG42UZR+GI1gAndxsEVQUaAjjw==} + '@oxc-minify/binding-freebsd-x64@0.139.0': + resolution: {integrity: sha512-azE4rCmII0FYcX3qRCAqJXWeJZSC5YO/T+VKrZhk8270WRahRmhxJeZCIjmZPXmO7+BkORwUdlDtXYGe2GdIEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-minify/binding-linux-arm-gnueabihf@0.138.0': - resolution: {integrity: sha512-l2Z9a+2DRLKWFMQOu/s5ad/06FyJtdC3RSGlaJb9IwVBH7khIKV8zrf+lNYSy1v+iQJE54YFt9YorokOLQL+1w==} + '@oxc-minify/binding-linux-arm-gnueabihf@0.139.0': + resolution: {integrity: sha512-sfhONQwXir/KWbyrSf4TOkbtSW0n+JNeiXCsgSMTsSRqIdjRrxF6Z2XA2hHrlgWQnxdkdzTU3+aui3IXLzvcKw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-minify/binding-linux-arm-musleabihf@0.138.0': - resolution: {integrity: sha512-IG9/jFTYx9S/63rRP/I4/Xk6xvYRK4ErBAwDNerxi0QaMT7Z/c6UwoXvMq1k5pgVMhJNhSzP3aRA2d0XhuCJfw==} + '@oxc-minify/binding-linux-arm-musleabihf@0.139.0': + resolution: {integrity: sha512-6SZAlnCtNkLZVj8DgSSu59iVN/nELyRwTER1ZUB16fY9Isojtp/JY/Ho6AzanDSdq3sHXWcUZoPWP/2oRVhTAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-minify/binding-linux-arm64-gnu@0.138.0': - resolution: {integrity: sha512-9LFYnphlXv40KRp6jclqjegvnj15A483SnI+IlrBxZzuQo6DDI7bTWMCi7poEjbF1+ejKLvxS2+PKTxc28JYsw==} + '@oxc-minify/binding-linux-arm64-gnu@0.139.0': + resolution: {integrity: sha512-ikuSX44sYfuRwt9MA2kEyqQIqhZFBpxVxUaoym9bY8seTbLatBXPVX6UC+iXtZCm+zi5rfcBJkwGuE8X/YXEPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxc-minify/binding-linux-arm64-musl@0.138.0': - resolution: {integrity: sha512-S4vkxaA2PmK7WJFi9P22j5LVw5X6kaBbu03vbuvm/5vGMjvxA69uMI6Xxb1NaVDb3+3gNPr2QCuTJFraVBIj4A==} + '@oxc-minify/binding-linux-arm64-musl@0.139.0': + resolution: {integrity: sha512-ccPpGIncYRDtLWocKQ/l24mRGyf6O/0zQOSpjmqlSx3M1tWPQgVkoAfFcEmehC0V7rGTBFpTOgECbr2UO9grwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxc-minify/binding-linux-ppc64-gnu@0.138.0': - resolution: {integrity: sha512-EyDw91JDwS04LhNe4E4jDL03XTXzE+ildc+CgesGrOKJd480lVRckyeICXamN3u5blQ+IWkDwcUUHjr70ckEfA==} + '@oxc-minify/binding-linux-ppc64-gnu@0.139.0': + resolution: {integrity: sha512-qIfSeJdK65jNunUtjdYTbc1MuHnvYU8HV/ZtTJcj7bO3Xv7FQwl6OKxvx0ZiMm6vi5wNP+LsZy3QP0NI3O2Oiw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxc-minify/binding-linux-riscv64-gnu@0.138.0': - resolution: {integrity: sha512-A7Zmi0z27S4IwX6ZVgRZnlRgPmuyWk3DMr+0U9TviYA1SxvFwSOoD990OfVNe8gedUzJxd6ZgLWG5umRJV0diw==} + '@oxc-minify/binding-linux-riscv64-gnu@0.139.0': + resolution: {integrity: sha512-tpboSbuRf37MYwhRwO530ChIXLWi32gKDjgsGzKHoUA02P/+YNSvOOVW0vTHNM9kRds1NETvtMGKIXT/MIaD6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxc-minify/binding-linux-riscv64-musl@0.138.0': - resolution: {integrity: sha512-T96QBrkygS3HI4+LfBIBctOUhCfcKlO1B02e2IDckNvvvLKMjCgdxKXKShnK7ALBUvRgV1+Nz55zGLuEJvDYtg==} + '@oxc-minify/binding-linux-riscv64-musl@0.139.0': + resolution: {integrity: sha512-15aFEJ4MgsWN5hv7uWeY6D5xnsj8pbcet9BrxNMLx8+mwBa5LrYgd1+MPyuDAgZY8ibsC9Js40Rz3gWBk+ET5Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxc-minify/binding-linux-s390x-gnu@0.138.0': - resolution: {integrity: sha512-nCO8S7BKLJ8XvrJVzerG8lgsKRdnXXiZfVoLkR/Xf7aEpd51auONt2Fq/y++G8Lj8NJvTP6J/BQvEtjxv/xkjg==} + '@oxc-minify/binding-linux-s390x-gnu@0.139.0': + resolution: {integrity: sha512-k7VZzIYhiWhBhdMuS9IZFvsdisPVU30x/y7oH8WeQcxY3xL9HCtGaHPzIZWA2p47Ob+ye07wZpTIInfIrVnOjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxc-minify/binding-linux-x64-gnu@0.138.0': - resolution: {integrity: sha512-PB4wXHCwQ8pr8pXvF8WS8RxefLKmtdRYyNd+Y/823FbewndKXW98XdzOL00gkl2uCF/7JCKWhBsGc+O9q6KEvA==} + '@oxc-minify/binding-linux-x64-gnu@0.139.0': + resolution: {integrity: sha512-tIuF2sFUUQWTS0UIcSHXhYHGNb8Jug2T77wAX/Tf7C5hdEoUwUAy61H1U1wYtKkn/9nEEVzrsxPVRaJ3CSHGWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxc-minify/binding-linux-x64-musl@0.138.0': - resolution: {integrity: sha512-VImV8mlTZwCrfr9SjdBE9e9jlppC9l3cOFP9NTI+Lx6LBhoZKPs4DgMpE0x4yOeUsthaX58KyZf+ObET3vQoIA==} + '@oxc-minify/binding-linux-x64-musl@0.139.0': + resolution: {integrity: sha512-dKJKmbmQ0I69vGSbljah85X8/TS2nLNUh6ExVag3vLw0BFmbRJakmA2A1zpSRQ9W/rgPH6pZ4VpFnjP9L+EAJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxc-minify/binding-openharmony-arm64@0.138.0': - resolution: {integrity: sha512-msHKM5DM4wtJOluUM7+F9I0jbS6sha4oOhMis11TyeqmgSYIo2vaXfWC5rHwXN+zeU4FAS7yYPcC0YQwZfE8Dw==} + '@oxc-minify/binding-openharmony-arm64@0.139.0': + resolution: {integrity: sha512-Wu8CslB8A6Pmnp0O2Hmg1xdvGmoLoajpVZOAduK0vAxCk5DrKAueAwlsniAPmTlbJ5INhhGBlpG9V4kEuCNyOw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-minify/binding-wasm32-wasi@0.138.0': - resolution: {integrity: sha512-4ynGUn47p7cDhbzSHv8Ur74K1Tzfyzx/QbPmV3K6lUlktNRy4rC1+xpyJlUSyW/kzRRTnmp9Z0FTg7fgkF+STA==} + '@oxc-minify/binding-wasm32-wasi@0.139.0': + resolution: {integrity: sha512-z0RctlQnmafehWz8LwPrWr1r0xlk0o9KQZnF10yl8fJbFIgWRXTcj+8DDuUGKtTdEqb5ZGN93fQdOAdV5p2KxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@oxc-minify/binding-win32-arm64-msvc@0.138.0': - resolution: {integrity: sha512-ynqCyjrU3c0yGpt2JBoZINJEkL0k3OI/5tn9JSRFT1aDIXpto2K+j3vy4IMBf619+UNUf8K3gOcKJ9hDmRNaqQ==} + '@oxc-minify/binding-win32-arm64-msvc@0.139.0': + resolution: {integrity: sha512-s48mEefXP42a3YQhPmZbtSj8j1ZqtVQe0xxnWupGznfsneW+6kZk4DqwJtbwQVNQW6M3xyT4x7lW8NeKDWY9/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-minify/binding-win32-ia32-msvc@0.138.0': - resolution: {integrity: sha512-0atMEQDg4/gKBF2qjj63DSHIgYo/YGgkRrJnv/W26rWwzm72qOfBbwqxhbeJxvjADxKxyUFu9gzgvPhCssrcyA==} + '@oxc-minify/binding-win32-ia32-msvc@0.139.0': + resolution: {integrity: sha512-hD5AxmkJxYGX33uwlQKBUq+GGKNzH4faeB66c3JASXryDqZZuqa/ZmuGeiOWAcFdKr/PBVby7SslOvRk74epsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-minify/binding-win32-x64-msvc@0.138.0': - resolution: {integrity: sha512-592gz+7hvKyJc6xun0gF1fSXpp9XsXqlg53DfFI7BsElXouYvYdNk4mRmMzRg7arrz+WRhU9Ba1N1nitDnkG+w==} + '@oxc-minify/binding-win32-x64-msvc@0.139.0': + resolution: {integrity: sha512-y0G8o/M+1vIkSYl/1tY5wTK2U3XpyNseK8ca29hM287Gx46AfWFc7MRnMY1Dq8n/80clBL+3O6GNPVOxXRO7XA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1487,21 +1487,21 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/browser-preview@4.1.9': - resolution: {integrity: sha512-a4/OrkMDb/WUnE4OOB/4FJbK3rYVO7YykqtUgcTKG4p2a0R3XcjPVu7SLRHFBs2+NIYhv5yxp1Lz3dbdGBjIow==} + '@vitest/browser-preview@4.1.10': + resolution: {integrity: sha512-14MJrL59ZFkqXLjwfSk6RzTDy5Czf9UG4+8q8L6Gxjs2aPjEce/cVNYV14bXAc2BvMjUNu904+ZEZA1Xc1wtvQ==} peerDependencies: - vitest: 4.1.9 + vitest: 4.1.10 - '@vitest/browser@4.1.9': - resolution: {integrity: sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg==} + '@vitest/browser@4.1.10': + resolution: {integrity: sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==} peerDependencies: - vitest: 4.1.9 + vitest: 4.1.10 - '@vitest/expect@4.1.9': - resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.9': - resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1511,20 +1511,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.9': - resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.9': - resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.9': - resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.9': - resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.9': - resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} '@voidzero-dev/vite-plus-core@0.1.24': resolution: {integrity: sha512-iXPGBABnQnrDMx89H6MOCGcTZp+QW+3rY4YMVKdE6ydchSvPk2O3MI2vgaRVfOtWJ2IjnxSnf1n2yjP67ZBRFQ==} @@ -1589,8 +1589,8 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-core@0.2.2': - resolution: {integrity: sha512-yAbKexF3npOGjg1N5EtXxun+7vdM/0x6QE5jucO/dv0LFhCAIzSN3UvLVCeamJt/Bz3jt7DLqQHEgXXrjy8drA==} + '@voidzero-dev/vite-plus-core@0.2.4': + resolution: {integrity: sha512-AoAYGPwNO56o9TuCR+KaQGA5XpSnTpn2QYHK0DQ0f8j3wvaMmToeWOJF6STo+XMntMDfiaB7sOsSFNE+hPYyjg==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 @@ -1646,41 +1646,41 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@0.2.2': - resolution: {integrity: sha512-Wy0Shx3Waa2cQZGSrPm0cpO1Y5oNyKyC1jarv12bBcgV+4uoEBKX+ep2Nh7zwjfd8Ja4QMiePE7wciOSXxu8oQ==} + '@voidzero-dev/vite-plus-darwin-arm64@0.2.4': + resolution: {integrity: sha512-UQwoLpnBW3qLqj5H3airPQeMCX3kfffVDM2EYGe889Fn5dOS9VhikuWloJlcjwtKwqLbFqkrsGjfb6XRvuLozg==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@0.2.2': - resolution: {integrity: sha512-09xcW67OvsQItVPzmF8UckI+glM3DzyQO3A98deNQ4QtUF7Mt+4/cYKKcLKg2ExRWWXGNDnVG/j7/hiLjZzynw==} + '@voidzero-dev/vite-plus-darwin-x64@0.2.4': + resolution: {integrity: sha512-f4XtiA4cBc/Z260QvvXIlBqTb/dZMAioohQDoR5jLG9o9vIOaWE2Ujm/zcWDyNpyZ88RLRrcO8ZwiMrEsdzbJw==} engines: {node: '>=20.0.0'} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.2': - resolution: {integrity: sha512-bR6287UFNwulMiQRhbtXF8GYs9a8EjvefXf+Glm7AzbePUXnamW9cwYIj2j4Dgoje0yC4gA52UePEFjXZnJcjA==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.4': + resolution: {integrity: sha512-TUamG9wEehZI4SFG7M6nUKb6z/7x3nNOBbnPdWIpv4C8uWKHwNtsnaaVLeygHIUIJEhbByR3oEFDylYLLr4KRw==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.2': - resolution: {integrity: sha512-YGtvTHT7qP4c5pZmM4kLL78/d8hj2NS150R92cR2SVOW/l9Ilq5R5WrEiMA4k5Ea3B++IJWZT5MRFI0tW9qlcg==} + '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.4': + resolution: {integrity: sha512-CS9uQ22QFr+lZZp95Huccg3cip3QYVU9GWrhvATqMBXWXb8IRHOulzuXrFxzvhMBITyPaRS9m/eIHmnM4L4h4Q==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.2': - resolution: {integrity: sha512-FvaMI/vsy4PVM+Qd73K+KM8blfCAfaoZaGaGWNrrlMryhyPThXPnHoB1AQcrKbEAWb+z2fc4zLS4sH+8uI65fw==} + '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.4': + resolution: {integrity: sha512-X5XfvftFERjer78SG+QKaJCa9LgIKygx4w13sD1/RS/kYOtaf1+yifrJpOFel+t9TRe/YqGTZa5C8uFrDz3OHw==} engines: {node: '>=20.0.0'} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-musl@0.2.2': - resolution: {integrity: sha512-ZsMochHqXqxj2sGTJNJzz3vabppbe4BFgZAjJfsnVzkwR7jv6c5p1BM71LFWxP4qd5LL0TJT7lbeRhALlI44RQ==} + '@voidzero-dev/vite-plus-linux-x64-musl@0.2.4': + resolution: {integrity: sha512-2iIWW6JR0UOK4QIFKgUQHjaF/7hZxELePny8R1OIn2jzShRfQhS1cmxksSg8ce/5nhYrfQ9dkg5ZmdLbxhpS/A==} engines: {node: '>=20.0.0'} cpu: [x64] os: [linux] @@ -1717,14 +1717,14 @@ packages: jsdom: optional: true - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.2': - resolution: {integrity: sha512-noBNyJufux0cf18eDpQLOQUZ1Kybfx9zlr+yQ6gAnxMEsQXSvYqZgWymfRDesBE3G/0XB5bg+AUtWijp3TwVcw==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.4': + resolution: {integrity: sha512-3yY4FnpvKBxjmHtEcao6Wcs/VBstzC0GhXAPWetbiFEl/sgL66V4Uhws02esfOSWw8abqLrXyPE9vK+newtsuw==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.2': - resolution: {integrity: sha512-+VUui1OIaFX0tqdUAXjmoKVlujEtWVdcsFDw2Jff+D6b4LUTQaOMAaaic8nNdfZL6wEjweRREQLZi/icZAXtNQ==} + '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.4': + resolution: {integrity: sha512-ERnFrh1O0XZhmGSqND04jtHM7tyKaGAOeT1w/HpVFmL/cQK2vuLl2GKjEwOqkBLd2csNGCcnk3e7C2qDmrNR/Q==} engines: {node: '>=20.0.0'} cpu: [x64] os: [win32] @@ -1738,15 +1738,9 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.38': - resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==} - '@vue/compiler-core@3.5.39': resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==} - '@vue/compiler-dom@3.5.38': - resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==} - '@vue/compiler-dom@3.5.39': resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==} @@ -1768,8 +1762,8 @@ packages: '@vue/devtools-shared@8.1.3': resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==} - '@vue/language-core@3.3.5': - resolution: {integrity: sha512-UkKu5nhX89fg4VhlG/FOeI10G3cj/7radKT/cy9BT4Q9qJmJlSTAc/dP63Xqs29aypN4f39xUV6PsLNk/dcD6g==} + '@vue/language-core@3.3.7': + resolution: {integrity: sha512-LzmkKinXAMMoh8Jfi/jMUSDUjuPdv8mynH5WJGKfXyZtDw3hQ6GBaoI6Bcnl/Xqlu32q/0Z6i/trp4VXykzyLw==} '@vue/reactivity@3.5.39': resolution: {integrity: sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==} @@ -2540,8 +2534,8 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - oxc-minify@0.138.0: - resolution: {integrity: sha512-mWwRllP8EaIwAf9dSLUm111J/P6ZLnqHhmqZ+hVDaXHWjkW6Ms0L//JPRDCt6Fu3ENTu18n00VpPfzhseXvjeQ==} + oxc-minify@0.139.0: + resolution: {integrity: sha512-RC814aVzfb73cj/I5IVmN3MpGO/SKPvubT3nRYWtMD5WVeg87PWn/VX6/P3sWr0WI8a39WN9Cwckmp5nuZvY5w==} engines: {node: ^20.19.0 || >=22.12.0} oxfmt@0.57.0: @@ -2911,13 +2905,13 @@ packages: resolution: {integrity: sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==} engines: {node: '>= 0.10'} - vite-plus@0.2.2: - resolution: {integrity: sha512-bXO3O0F2/uxtvX9Ck0o67stTErH/Zh0GEcCMd9pAh22tTHABCNTDPrRMWVo733e7Ux3h0Y7HanJ7neOV/nid4g==} + vite-plus@0.2.4: + resolution: {integrity: sha512-gaBBjOXIq9lLRU44oAYdIr99p+JBLX1kxs+l/6LqGgSXwcVKAdDa1boSrOTELqYCkQQ0fpppXUGWi9o6JDT5zw==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} hasBin: true peerDependencies: - '@vitest/browser-playwright': 4.1.9 - '@vitest/browser-webdriverio': 4.1.9 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 peerDependenciesMeta: '@vitest/browser-playwright': optional: true @@ -2979,20 +2973,20 @@ packages: postcss: optional: true - vitest@4.1.9: - resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.9 - '@vitest/browser-preview': 4.1.9 - '@vitest/browser-webdriverio': 4.1.9 - '@vitest/coverage-istanbul': 4.1.9 - '@vitest/coverage-v8': 4.1.9 - '@vitest/ui': 4.1.9 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3023,15 +3017,15 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue-chartjs@5.3.3: - resolution: {integrity: sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==} + vue-chartjs@5.3.4: + resolution: {integrity: sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==} peerDependencies: chart.js: ^4.1.1 vue: ^3.0.0-0 || ^2.7.0 - vue-codemirror6@1.5.2: - resolution: {integrity: sha512-SJRW0r8776zdqIVSZZsHuTXErmql1PKATupN+RPs4IXSTAzzKl4Sl/804BazVb9OYrd2Ym5Yv97AmwPRVD4zeg==} - engines: {node: ^20.19.0 || >=22.12.0, pnpm: '>=10.3.0'} + vue-codemirror6@1.6.1: + resolution: {integrity: sha512-0Ue6nWy055UKP/6LkT0I7nW5SRSFJ+3mokwBdHePQsj11T02mrg3A0jtHyBqH23nRHQFxd3NKw63sZJ1ygo5dA==} + engines: {node: ^22.18.0 || >=24.12.0} peerDependencies: '@codemirror/autocomplete': ^6.0.0 '@codemirror/commands': ^6.0.0 @@ -3069,8 +3063,8 @@ packages: peerDependencies: vue: ^3.2.2 - vue-tsc@3.3.5: - resolution: {integrity: sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg==} + vue-tsc@3.3.7: + resolution: {integrity: sha512-+C+rgD49wAQ5bUTl2sp5a8Bzg4YoldMNXM+g7CFe604MYcQ8PrZPMQhIjJSzKXtPBCa+C5ayMipqjbA7splekQ==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -3443,68 +3437,68 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-minify/binding-android-arm-eabi@0.138.0': + '@oxc-minify/binding-android-arm-eabi@0.139.0': optional: true - '@oxc-minify/binding-android-arm64@0.138.0': + '@oxc-minify/binding-android-arm64@0.139.0': optional: true - '@oxc-minify/binding-darwin-arm64@0.138.0': + '@oxc-minify/binding-darwin-arm64@0.139.0': optional: true - '@oxc-minify/binding-darwin-x64@0.138.0': + '@oxc-minify/binding-darwin-x64@0.139.0': optional: true - '@oxc-minify/binding-freebsd-x64@0.138.0': + '@oxc-minify/binding-freebsd-x64@0.139.0': optional: true - '@oxc-minify/binding-linux-arm-gnueabihf@0.138.0': + '@oxc-minify/binding-linux-arm-gnueabihf@0.139.0': optional: true - '@oxc-minify/binding-linux-arm-musleabihf@0.138.0': + '@oxc-minify/binding-linux-arm-musleabihf@0.139.0': optional: true - '@oxc-minify/binding-linux-arm64-gnu@0.138.0': + '@oxc-minify/binding-linux-arm64-gnu@0.139.0': optional: true - '@oxc-minify/binding-linux-arm64-musl@0.138.0': + '@oxc-minify/binding-linux-arm64-musl@0.139.0': optional: true - '@oxc-minify/binding-linux-ppc64-gnu@0.138.0': + '@oxc-minify/binding-linux-ppc64-gnu@0.139.0': optional: true - '@oxc-minify/binding-linux-riscv64-gnu@0.138.0': + '@oxc-minify/binding-linux-riscv64-gnu@0.139.0': optional: true - '@oxc-minify/binding-linux-riscv64-musl@0.138.0': + '@oxc-minify/binding-linux-riscv64-musl@0.139.0': optional: true - '@oxc-minify/binding-linux-s390x-gnu@0.138.0': + '@oxc-minify/binding-linux-s390x-gnu@0.139.0': optional: true - '@oxc-minify/binding-linux-x64-gnu@0.138.0': + '@oxc-minify/binding-linux-x64-gnu@0.139.0': optional: true - '@oxc-minify/binding-linux-x64-musl@0.138.0': + '@oxc-minify/binding-linux-x64-musl@0.139.0': optional: true - '@oxc-minify/binding-openharmony-arm64@0.138.0': + '@oxc-minify/binding-openharmony-arm64@0.139.0': optional: true - '@oxc-minify/binding-wasm32-wasi@0.138.0': + '@oxc-minify/binding-wasm32-wasi@0.139.0': dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@oxc-minify/binding-win32-arm64-msvc@0.138.0': + '@oxc-minify/binding-win32-arm64-msvc@0.139.0': optional: true - '@oxc-minify/binding-win32-ia32-msvc@0.138.0': + '@oxc-minify/binding-win32-ia32-msvc@0.139.0': optional: true - '@oxc-minify/binding-win32-x64-msvc@0.138.0': + '@oxc-minify/binding-win32-x64-msvc@0.139.0': optional: true '@oxc-project/runtime@0.133.0': {} @@ -4058,10 +4052,10 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@vitejs/plugin-vue@6.0.7(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(vue@3.5.39(typescript@5.8.3))': + '@vitejs/plugin-vue@6.0.7(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(vue@3.5.39(typescript@5.8.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' vue: 3.5.39(typescript@5.8.3) '@vitejs/plugin-vue@6.0.7(vite@7.3.5(@types/node@25.9.3)(lightningcss@1.32.0)(sass@1.101.0)(yaml@2.9.0))(vue@3.5.39(typescript@5.8.3))': @@ -4070,28 +4064,28 @@ snapshots: vite: 7.3.5(@types/node@25.9.3)(lightningcss@1.32.0)(sass@1.101.0)(yaml@2.9.0) vue: 3.5.39(typescript@5.8.3) - '@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))': + '@vitest/browser-preview@4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' + '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))': + '@vitest/browser@4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) - '@vitest/utils': 4.1.9 + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' + vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -4099,16 +4093,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(vitest@4.1.9)': + '@vitest/browser@4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) - '@vitest/utils': 4.1.9 + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@25.9.3)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@25.9.3)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -4116,44 +4110,44 @@ snapshots: - utf-8-validate - vite - '@vitest/expect@4.1.9': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.9 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' - '@vitest/pretty-format@4.1.9': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.9': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.9 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.9': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.9': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.9': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.9 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -4171,7 +4165,7 @@ snapshots: typescript: 5.8.3 yaml: 2.9.0 - '@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)': dependencies: '@oxc-project/runtime': 0.138.0 '@oxc-project/types': 0.138.0 @@ -4185,25 +4179,25 @@ snapshots: typescript: 5.8.3 yaml: 2.9.0 - '@voidzero-dev/vite-plus-darwin-arm64@0.2.2': + '@voidzero-dev/vite-plus-darwin-arm64@0.2.4': optional: true - '@voidzero-dev/vite-plus-darwin-x64@0.2.2': + '@voidzero-dev/vite-plus-darwin-x64@0.2.4': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.2': + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.4': optional: true - '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.2': + '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.4': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.2': + '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.4': optional: true - '@voidzero-dev/vite-plus-linux-x64-musl@0.2.2': + '@voidzero-dev/vite-plus-linux-x64-musl@0.2.4': optional: true - '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 @@ -4217,7 +4211,7 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.2.4 tinyglobby: 0.2.17 - vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' ws: 8.21.0 optionalDependencies: '@types/node': 25.9.3 @@ -4243,10 +4237,10 @@ snapshots: - utf-8-validate - yaml - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.2': + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.4': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.2': + '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.4': optional: true '@volar/language-core@2.4.28': @@ -4261,14 +4255,6 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.38': - dependencies: - '@babel/parser': 7.29.7 - '@vue/shared': 3.5.38 - entities: 7.0.1 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - '@vue/compiler-core@3.5.39': dependencies: '@babel/parser': 7.29.7 @@ -4277,11 +4263,6 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.38': - dependencies: - '@vue/compiler-core': 3.5.38 - '@vue/shared': 3.5.38 - '@vue/compiler-dom@3.5.39': dependencies: '@vue/compiler-core': 3.5.39 @@ -4319,11 +4300,11 @@ snapshots: '@vue/devtools-shared@8.1.3': {} - '@vue/language-core@3.3.5': + '@vue/language-core@3.3.7': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.38 - '@vue/shared': 3.5.38 + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -5051,30 +5032,30 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - oxc-minify@0.138.0: + oxc-minify@0.139.0: optionalDependencies: - '@oxc-minify/binding-android-arm-eabi': 0.138.0 - '@oxc-minify/binding-android-arm64': 0.138.0 - '@oxc-minify/binding-darwin-arm64': 0.138.0 - '@oxc-minify/binding-darwin-x64': 0.138.0 - '@oxc-minify/binding-freebsd-x64': 0.138.0 - '@oxc-minify/binding-linux-arm-gnueabihf': 0.138.0 - '@oxc-minify/binding-linux-arm-musleabihf': 0.138.0 - '@oxc-minify/binding-linux-arm64-gnu': 0.138.0 - '@oxc-minify/binding-linux-arm64-musl': 0.138.0 - '@oxc-minify/binding-linux-ppc64-gnu': 0.138.0 - '@oxc-minify/binding-linux-riscv64-gnu': 0.138.0 - '@oxc-minify/binding-linux-riscv64-musl': 0.138.0 - '@oxc-minify/binding-linux-s390x-gnu': 0.138.0 - '@oxc-minify/binding-linux-x64-gnu': 0.138.0 - '@oxc-minify/binding-linux-x64-musl': 0.138.0 - '@oxc-minify/binding-openharmony-arm64': 0.138.0 - '@oxc-minify/binding-wasm32-wasi': 0.138.0 - '@oxc-minify/binding-win32-arm64-msvc': 0.138.0 - '@oxc-minify/binding-win32-ia32-msvc': 0.138.0 - '@oxc-minify/binding-win32-x64-msvc': 0.138.0 - - oxfmt@0.57.0(vite-plus@0.2.2(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)): + '@oxc-minify/binding-android-arm-eabi': 0.139.0 + '@oxc-minify/binding-android-arm64': 0.139.0 + '@oxc-minify/binding-darwin-arm64': 0.139.0 + '@oxc-minify/binding-darwin-x64': 0.139.0 + '@oxc-minify/binding-freebsd-x64': 0.139.0 + '@oxc-minify/binding-linux-arm-gnueabihf': 0.139.0 + '@oxc-minify/binding-linux-arm-musleabihf': 0.139.0 + '@oxc-minify/binding-linux-arm64-gnu': 0.139.0 + '@oxc-minify/binding-linux-arm64-musl': 0.139.0 + '@oxc-minify/binding-linux-ppc64-gnu': 0.139.0 + '@oxc-minify/binding-linux-riscv64-gnu': 0.139.0 + '@oxc-minify/binding-linux-riscv64-musl': 0.139.0 + '@oxc-minify/binding-linux-s390x-gnu': 0.139.0 + '@oxc-minify/binding-linux-x64-gnu': 0.139.0 + '@oxc-minify/binding-linux-x64-musl': 0.139.0 + '@oxc-minify/binding-openharmony-arm64': 0.139.0 + '@oxc-minify/binding-wasm32-wasi': 0.139.0 + '@oxc-minify/binding-win32-arm64-msvc': 0.139.0 + '@oxc-minify/binding-win32-ia32-msvc': 0.139.0 + '@oxc-minify/binding-win32-x64-msvc': 0.139.0 + + oxfmt@0.57.0(vite-plus@0.2.4(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)): dependencies: tinypool: 2.1.0 optionalDependencies: @@ -5097,7 +5078,7 @@ snapshots: '@oxfmt/binding-win32-arm64-msvc': 0.57.0 '@oxfmt/binding-win32-ia32-msvc': 0.57.0 '@oxfmt/binding-win32-x64-msvc': 0.57.0 - vite-plus: 0.2.2(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0) + vite-plus: 0.2.4(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0) oxlint-tsgolint@0.24.0: optionalDependencies: @@ -5108,7 +5089,7 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 0.24.0 '@oxlint-tsgolint/win32-x64': 0.24.0 - oxlint@1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.2(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)): + oxlint@1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.72.0 '@oxlint/binding-android-arm64': 1.72.0 @@ -5130,7 +5111,7 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.72.0 '@oxlint/binding-win32-x64-msvc': 1.72.0 oxlint-tsgolint: 0.24.0 - vite-plus: 0.2.2(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0) + vite-plus: 0.2.4(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0) path-browserify@1.0.1: {} @@ -5534,33 +5515,33 @@ snapshots: remove-trailing-separator: 1.1.0 replace-ext: 1.0.1 - vite-plus@0.2.2(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0): + vite-plus@0.2.4(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0): dependencies: '@oxc-project/types': 0.138.0 '@oxlint/plugins': 1.68.0 - '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(vitest@4.1.9) - '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) - '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.9 - '@vitest/runner': 4.1.9 - '@vitest/snapshot': 4.1.9 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 - '@voidzero-dev/vite-plus-core': 0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0) - oxfmt: 0.57.0(vite-plus@0.2.2(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) - oxlint: 1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.2(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + '@voidzero-dev/vite-plus-core': 0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0) + oxfmt: 0.57.0(vite-plus@0.2.4(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + oxlint: 1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) oxlint-tsgolint: 0.24.0 - vitest: 4.1.9(@types/node@25.9.3)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@25.9.3)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.2.2 - '@voidzero-dev/vite-plus-darwin-x64': 0.2.2 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.2.2 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.2.2 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.2.2 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.2.2 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.2.2 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.2.2 + '@voidzero-dev/vite-plus-darwin-arm64': 0.2.4 + '@voidzero-dev/vite-plus-darwin-x64': 0.2.4 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.2.4 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.2.4 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.2.4 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.2.4 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.2.4 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.2.4 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -5607,7 +5588,7 @@ snapshots: sass: 1.101.0 yaml: 2.9.0 - vitepress@2.0.0-alpha.17(@types/node@25.9.3)(async-validator@4.2.5)(lightningcss@1.32.0)(oxc-minify@0.138.0)(postcss@8.5.15)(sass@1.101.0)(sortablejs@1.15.7)(typescript@5.8.3)(yaml@2.9.0): + vitepress@2.0.0-alpha.17(@types/node@25.9.3)(async-validator@4.2.5)(lightningcss@1.32.0)(oxc-minify@0.139.0)(postcss@8.5.15)(sass@1.101.0)(sortablejs@1.15.7)(typescript@5.8.3)(yaml@2.9.0): dependencies: '@docsearch/css': 4.6.3 '@docsearch/js': 4.6.3 @@ -5629,7 +5610,7 @@ snapshots: vite: 7.3.5(@types/node@25.9.3)(lightningcss@1.32.0)(sass@1.101.0)(yaml@2.9.0) vue: 3.5.39(typescript@5.8.3) optionalDependencies: - oxc-minify: 0.138.0 + oxc-minify: 0.139.0 postcss: 8.5.15 transitivePeerDependencies: - '@types/node' @@ -5656,15 +5637,15 @@ snapshots: - universal-cookie - yaml - vitest@4.1.9(@types/node@25.9.3)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)): + vitest@4.1.10(@types/node@25.9.3)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.9 - '@vitest/runner': 4.1.9 - '@vitest/snapshot': 4.1.9 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 es-module-lexer: 2.2.0 expect-type: 1.4.0 magic-string: 0.30.21 @@ -5676,22 +5657,22 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)' why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.3 - '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) + '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.3)(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0))(esbuild@0.27.7)(sass@1.101.0)(typescript@5.8.3)(yaml@2.9.0)) transitivePeerDependencies: - msw vscode-uri@3.1.0: {} - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.39(typescript@5.8.3)): + vue-chartjs@5.3.4(chart.js@4.5.1)(vue@3.5.39(typescript@5.8.3)): dependencies: chart.js: 4.5.1 vue: 3.5.39(typescript@5.8.3) - vue-codemirror6@1.5.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)(codemirror@6.0.2)(style-mod@4.1.3)(vue@3.5.39(typescript@5.8.3)): + vue-codemirror6@1.6.1(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)(codemirror@6.0.2)(style-mod@4.1.3)(vue@3.5.39(typescript@5.8.3)): dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/commands': 6.10.4 @@ -5726,10 +5707,10 @@ snapshots: clipboard: 2.0.11 vue: 3.5.39(typescript@5.8.3) - vue-tsc@3.3.5(typescript@5.8.3): + vue-tsc@3.3.7(typescript@5.8.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.3.5 + '@vue/language-core': 3.3.7 typescript: 5.8.3 vue-web-terminal@3.4.2(typescript@5.8.3): diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1640ebdb..02a15aba 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -147,9 +147,9 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "async-broadcast" @@ -571,9 +571,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -656,9 +656,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "shlex", @@ -893,18 +893,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-bigint" @@ -1081,9 +1081,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "dbus" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "libc", "libdbus-sys", @@ -1103,9 +1103,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid", "pem-rfc7468", @@ -1383,9 +1383,9 @@ dependencies = [ [[package]] name = "embed-resource" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" dependencies = [ "cc", "memchr", @@ -2472,7 +2472,7 @@ checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8" dependencies = [ "num-integer", "num-traits", - "rand 0.10.1", + "rand 0.10.2", "rand_core 0.10.1", ] @@ -2731,9 +2731,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -2781,15 +2781,15 @@ dependencies = [ [[package]] name = "md5" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" +checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" @@ -2932,9 +2932,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -3335,7 +3335,7 @@ dependencies = [ "delegate", "futures", "log", - "rand 0.10.1", + "rand 0.10.2", "sha2 0.11.0", "thiserror 2.0.18", "tokio", @@ -3582,9 +3582,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", @@ -3635,9 +3635,9 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2e" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ "cpufeatures 0.3.0", "universal-hash", @@ -3792,9 +3792,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] @@ -3839,9 +3839,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -3892,7 +3892,7 @@ dependencies = [ "log", "num-bigint", "percent-encoding", - "rand 0.10.1", + "rand 0.10.2", "rustls", "rustls-native-certs", "ryu", @@ -3915,7 +3915,7 @@ dependencies = [ "fern", "log", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "redis", "russh", "rustls", @@ -3986,9 +3986,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", @@ -3998,9 +3998,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" dependencies = [ "aho-corasick", "memchr", @@ -4211,7 +4211,7 @@ dependencies = [ "pkcs5", "pkcs8", "polyval", - "rand 0.10.1", + "rand 0.10.2", "rand_core 0.10.1", "ring", "rsa", @@ -4278,9 +4278,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -4333,9 +4333,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "zeroize", ] @@ -4380,9 +4380,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -5267,9 +5267,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.3" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" dependencies = [ "anyhow", "bytes", @@ -5649,9 +5649,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.3" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" dependencies = [ "gtk", "http", @@ -5754,12 +5754,11 @@ dependencies = [ [[package]] name = "tendril" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" dependencies = [ "new_debug_unreachable", - "utf-8", ] [[package]] @@ -6260,17 +6259,11 @@ dependencies = [ "url", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8-width" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" +checksum = "159a7cadce548703edd50d24069bc294c5415ecab0a480e0cd1ca06d112dc94a" [[package]] name = "utf8_iter" @@ -7158,9 +7151,9 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +checksum = "4d93c89cdc2d3a63c3ec48ffe926931bdc069eafa8e4402fe6d8f790c9d1e576" [[package]] name = "yoke" @@ -7187,9 +7180,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.16.0" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" dependencies = [ "async-broadcast", "async-executor", @@ -7222,9 +7215,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.16.0" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", @@ -7237,9 +7230,9 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.2" +version = "4.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" dependencies = [ "serde", "winnow 1.0.3", @@ -7248,18 +7241,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", @@ -7346,9 +7339,9 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "5.12.0" +version = "5.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" dependencies = [ "endi", "enumflags2", @@ -7360,9 +7353,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.12.0" +version = "5.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", @@ -7373,9 +7366,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" dependencies = [ "proc-macro2", "quote", From bfdb238c7e5d032dd41c4e3aed535a0b94c22a91 Mon Sep 17 00:00:00 2001 From: hepengju Date: Thu, 9 Jul 2026 22:19:14 +0800 Subject: [PATCH 31/82] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=80=BC?= =?UTF-8?q?=E9=A1=B5=E4=B8=8E=E5=AD=97=E6=AE=B5=E7=BC=96=E8=BE=91=E5=BA=95?= =?UTF-8?q?=E9=83=A8=E5=A4=8D=E5=88=B6=E4=B8=BA=E5=91=BD=E4=BB=A4=E5=9B=BE?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/views/ext/FieldSet.vue | 51 ++---------------------------------- src/views/tab/RedisValue.vue | 12 --------- 2 files changed, 2 insertions(+), 61 deletions(-) diff --git a/src/views/ext/FieldSet.vue b/src/views/ext/FieldSet.vue index 2ba98f3e..279de028 100644 --- a/src/views/ext/FieldSet.vue +++ b/src/views/ext/FieldSet.vue @@ -6,7 +6,6 @@ import { useI18n } from 'vue-i18n' import { shareProvideKey } from '@/types/me-interface' import type { BytesFormat, - RedisFieldAsCommand_Deserialize, RedisFieldGet_Deserialize, RedisFieldSet_Deserialize, RedisFieldValue, @@ -26,15 +25,7 @@ import { viewFmtForField, type ViewBytesFormat, } from '@/utils/format' -import { - meCommands, - meCopy, - meErr, - meFormatDisplayValue, - meJsonNormal, - meOk, - meWarn, -} from '@/utils/util' +import { meCommands, meCopy, meErr, meFormatDisplayValue, meJsonNormal, meOk } from '@/utils/util' /** 含 UI 用 type / wireFieldKey,提交时剔除 */ type FieldSetForm = RedisFieldSet_Deserialize & { type: string; wireFieldKey?: string } @@ -44,7 +35,7 @@ type FieldSetOpen = Partial & { keyWireFmt?: BytesFormat /** 键级数据编码,用于默认字段 view */ keyViewFmt?: ViewBytesFormat - /** Stream 条目 ID(复制为命令等) */ + /** Stream 条目 ID */ streamId?: string /** 查看模式:表单只读,隐藏保存 */ readonly?: boolean @@ -82,7 +73,6 @@ const form = ref(cloneDeep(initForm)) /** fieldScan 原始 wire,切换字段编码时始终以此为源 */ const srcFieldWire = ref('') -const fieldStreamId = ref('') const keyWireFmt = ref('utf8') /** 键级 view 编码,field_get 与值页表格刷新一致 */ const keyViewFmt = ref('utf8') @@ -142,7 +132,6 @@ function open(data: FieldSetOpen) { Object.assign(form.value, cloneDeep(initForm)) Object.assign(form.value, data) srcFieldWire.value = String(data.srcFieldValue ?? '') - fieldStreamId.value = data.streamId ?? '' keyWireFmt.value = data.keyWireFmt ?? 'utf8' keyViewFmt.value = data.keyViewFmt ?? 'utf8' fieldViewFmt.value = defaultFieldViewFmt(data.keyViewFmt ?? 'utf8', keyWireFmt.value) @@ -232,35 +221,6 @@ function submit() { }) } -function buildFieldAsCommandParam(): RedisFieldAsCommand_Deserialize | null { - if (!share.conn || !form.value.key?.key) return null - const type = form.value.type - const fmt = fieldViewFmt.value - const param: RedisFieldAsCommand_Deserialize = { - key: form.value.key, - fieldKey: - type === 'hash' && form.value.wireFieldKey ? form.value.wireFieldKey : form.value.fieldKey, - fieldIndex: form.value.fieldIndex, - fieldValue: type === 'zset' || type === 'set' ? srcFieldWire.value : '', - streamId: fieldStreamId.value, - valFmt: toWireFormat(fmt), - } - if (type === 'stream') param.fieldValue = '' - return param -} - -async function copyFieldAsCommand() { - const conn = share.conn - const param = buildFieldAsCommandParam() - if (!conn || !param) return - const text = await meCommands.getFieldAsCommand(conn.id, param) - if (!text.trim()) { - meWarn(t('redisValue.copyCommandEmpty')) - return - } - meCopy(text, t('redisValue.copyCommandOk')) -} - function buildFieldGetParam(): RedisFieldGet_Deserialize | null { if (!form.value.key?.key) return null const type = form.value.type @@ -365,13 +325,6 @@ async function refreshField() { style="font-size: 18px; margin-left: 5px" icon="el-icon-document-copy" @click="meCopy(form.fieldValue)" /> - { @click="meCopy(showValue)" placement="top-start" /> - - Date: Thu, 9 Jul 2026 22:29:58 +0800 Subject: [PATCH 32/82] =?UTF-8?q?refactor:=20=E9=9B=86=E7=BE=A4=E6=A7=BD?= =?UTF-8?q?=E4=BD=8D=E4=B8=8E=E8=8A=82=E7=82=B9=E5=85=A5=E5=8F=A3=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=E5=80=BC=E9=A1=B5=E6=89=A9=E5=B1=95=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/views/tab/RedisValue.vue | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index f8fc3873..9870b6be 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -682,6 +682,10 @@ function onKeyMoreCommand(command: string) { renameKey() } else if (command === 'duplicateKey') { duplicateKey() + } else if (command === 'showSlot') { + void showSlot() + } else if (command === 'showLocation') { + void showLocation() } } // #endregion @@ -1171,6 +1175,12 @@ onUnmounted(() => { + + + + + + @@ -1456,25 +1466,6 @@ onUnmounted(() => { @click="openKeyShortDialog" style="margin-left: 5px; font-size: 20px" /> - - - - - From 3c1d4b5fc1bc231035ca6680410c7b20fdc7cffa Mon Sep 17 00:00:00 2001 From: hepengju Date: Fri, 10 Jul 2026 22:10:02 +0800 Subject: [PATCH 33/82] =?UTF-8?q?v4.3.0=20=E6=9B=B4=E6=96=B0=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/changelog/latest.md | 43 ++++++++++++++++++++++++++++--------- docs/zh/changelog/latest.md | 39 ++++++++++++++++++++++++++------- package.json | 2 +- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/docs/en/changelog/latest.md b/docs/en/changelog/latest.md index 742b1636..ec36e642 100644 --- a/docs/en/changelog/latest.md +++ b/docs/en/changelog/latest.md @@ -1,5 +1,28 @@ # 4.x Changelog +## [v4.3.0](https://mp.weixin.qq.com/s/k3KMigBxg3fJraezIMdP5Q) (2026-07-11) + +### ✨ New Features + +- Terminal: **Output aligned with redis-cli** TTY/Raw/JSON/CSV formats +- Empty values: Key creation, value editing, and field editing all support saving empty strings +- Value Tab + - Table supports **per-row refresh and per-row "Copy as Command"** + - Restored **Binary encoding format** (required for setbit and similar scenarios) + - Large STRING values load on preview when over threshold #136 + - Improved Stream value table ID display + - String keys show estimated memory when MEMORY USAGE is unavailable + - Cluster slot and node entries moved to the extended menu + - Shortcuts visible only in the editor +- Website + - Windows download: added Microsoft Store entry + - Warm and dark theme visual improvements; unified ambient light and grid background + - Auto-redirect to Chinese homepage when browser language is Chinese +- Details + - Read-only mode hides the append section for new keys + - New group folders collapsed by default when importing connections + - Export preview key name height adjusted to avoid scrollbars + ## 4.2.1 (2026-07-06) - Fixed charts not redrawing after data updates #132 @@ -8,20 +31,20 @@ ### ✨ New Features -- **Command parsing**: Aligned with redis-cli `sdssplitargs`; supports escapes inside quotes and binary arguments -- **Value Tab** - - Added **"Copy as Command"** - - Added **"Create Duplicate"** (standalone COPY / cluster DUMP+RESTORE) - - Added **locate button** next to key name; scrolls key list to current key on click -- **Import/Export**: Added **CMD command format** support -- **Key Area** +- Command parsing: Aligned with redis-cli `sdssplitargs`; supports escapes inside quotes and binary arguments +- Value Tab + - Added "Copy as Command" + - Added "Create Duplicate" (standalone COPY / cluster DUMP+RESTORE) + - Added locate button next to key name; scrolls key list to current key on click +- Import/Export: Added CMD command format support +- Key Area - F5 to refresh key list; connection color for refresh and search icons - Refresh button and footer text use connection color - Improved scan progress estimation for cluster scenarios -- **Connection** +- Connection - Connection editor: initial db input - - **Valkey 9+ cluster: specify initial db at connect time** -- **Pub/Sub**: **Message encoding selectable** for sending binary messages + - Valkey 9+ cluster: specify initial db at connect time +- Pub/Sub: Message encoding selectable for sending binary messages - Other: Added Redisee to website footer links ### 🐞 Bug Fixes diff --git a/docs/zh/changelog/latest.md b/docs/zh/changelog/latest.md index 0e24491e..0e78f223 100644 --- a/docs/zh/changelog/latest.md +++ b/docs/zh/changelog/latest.md @@ -1,6 +1,29 @@ # 4.x 更新日志 -## 4.2.1 (2026-07-06) +## [v4.3.0](https://mp.weixin.qq.com/s/k3KMigBxg3fJraezIMdP5Q) (2026-07-11) + +### ✨ 新增优化 + +- 终端: **输出对齐 redis-cli** TTY/Raw/JSON/CSV 格式 +- 空值: 新增键、值编辑、字段编辑均支持空字符串保存 +- 键值页 + - 表格支持**单行刷新和单行复制为命令** + - 恢复 **Binary 编码格式**(setbit 等场景需要) + - 大 STRING 值超阈值时预览加载 #136 + - 优化 Stream 值表格 ID 展示 + - String 键在 MEMORY USAGE 不可用时显示估算内存 + - 集群槽位与节点入口移至扩展菜单 + - 快捷键仅编辑器可见 +- 官网 + - Windows 下载增加微软应用商店入口 + - 暖色与深色主题视觉优化,统一环境光与网格背景 + - 浏览器语言为中文则自动跳转中文首页 +- 细节 + - 只读模式隐藏新增键所在append + - 导入连接时新分组文件夹默认折叠 + - 导出数据的预览键名高度调整,避免出现滚动条 + +## v4.2.1 (2026-07-06) - 修复图表数据更新后不自动重绘 #132 @@ -8,20 +31,20 @@ ### ✨ 新增优化 -- **命令解析**:对齐 redis-cli `sdssplitargs`,支持引号内转义与二进制参数 +- 命令解析: 对齐 redis-cli `sdssplitargs`,支持引号内转义与二进制参数 - 键值页 - - 新增 **「复制为命令」** - - 新增 **「创建副本」**(单机 COPY / 集群 DUMP+RESTORE) - - 键名旁新增**定位按钮**,点击后左侧键列表滚动至当前键 -- 导入导出: 新增支持 **CMD命令格式** + - 新增「复制为命令」 + - 新增「创建副本」(单机 COPY / 集群 DUMP+RESTORE) + - 键名旁新增定位按钮,点击后左侧键列表滚动至当前键 +- 导入导出: 新增支持 CMD 命令格式 - 键区 - 支持 F5 刷新键列表;沿用当前连接色,搜索图标同步更新 - 刷新按钮和底部文字颜色使用当前连接颜色 - 优化扫描进度预估,适配集群场景 - 连接 - 连接编辑新增初始 db 输入框 - - **支持 Valkey 9+ 集群连接时指定初始 db** -- 发布订阅:**消息内容支持选择编码**,以便发送二进制消息 + - 支持 Valkey 9+ 集群连接时指定初始 db +- 发布订阅: 消息内容支持选择编码,以便发送二进制消息 - 其他: 官网友情链接新增 Redisee ### 🐞 修复问题 diff --git a/package.json b/package.json index bb2270a8..7fc35114 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redis-me", - "version": "4.2.1", + "version": "4.3.0", "private": true, "type": "module", "scripts": { From f0cb783342a961240544d30fb7cf3e991019272a Mon Sep 17 00:00:00 2001 From: hepengju Date: Fri, 10 Jul 2026 22:45:04 +0800 Subject: [PATCH 34/82] v4.3.0 latest.json --- docs/en/changelog/latest.md | 2 +- docs/public/lastest/latest-gitee.json | 78 +++++++++++++------------- docs/public/lastest/latest-github.json | 78 +++++++++++++------------- docs/zh/changelog/latest.md | 2 +- 4 files changed, 80 insertions(+), 80 deletions(-) diff --git a/docs/en/changelog/latest.md b/docs/en/changelog/latest.md index ec36e642..f563336b 100644 --- a/docs/en/changelog/latest.md +++ b/docs/en/changelog/latest.md @@ -4,7 +4,7 @@ ### ✨ New Features -- Terminal: **Output aligned with redis-cli** TTY/Raw/JSON/CSV formats +- Terminal: **Output aligned with redis-cli** TTY/Raw/JSON/CSV formats #134 - Empty values: Key creation, value editing, and field editing all support saving empty strings - Value Tab - Table supports **per-row refresh and per-row "Copy as Command"** diff --git a/docs/public/lastest/latest-gitee.json b/docs/public/lastest/latest-gitee.json index 1ad2d657..7a8c564d 100644 --- a/docs/public/lastest/latest-gitee.json +++ b/docs/public/lastest/latest-gitee.json @@ -1,71 +1,71 @@ { - "version": "4.2.1", - "notes": "| OS | Download |\n| --- | --- |\n| Windows | [Installer (x64)](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_x64-setup.exe) · [Portable (x64)](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_portable_x64.zip) · [Installer (Arm64)](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64-setup.exe) · [Portable (Arm64)](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_portable_arm64.zip) |\n| Mac | [Apple Silicon](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_aarch64.dmg) · [Intel Mac](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_x64.dmg) |\n| Linux | [amd64 deb](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.deb) · [arm64 deb](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64.deb) · [x86_64 rpm](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME-4.2.1-1.x86_64.rpm) · [aarch64 rpm](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME-4.2.1-1.aarch64.rpm) · [amd64 AppImage](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.AppImage) · [aarch64 AppImage](https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_aarch64.AppImage) |", - "pub_date": "2026-07-06T13:36:33.646Z", + "version": "4.3.0", + "notes": "| OS | Download |\n| --- | --- |\n| Windows | [Installer (x64)](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_x64-setup.exe) · [Portable (x64)](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_portable_x64.zip) · [Installer (Arm64)](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64-setup.exe) · [Portable (Arm64)](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_portable_arm64.zip) |\n| Mac | [Apple Silicon](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_aarch64.dmg) · [Intel Mac](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_x64.dmg) |\n| Linux | [amd64 deb](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.deb) · [arm64 deb](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64.deb) · [x86_64 rpm](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME-4.3.0-1.x86_64.rpm) · [aarch64 rpm](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME-4.3.0-1.aarch64.rpm) · [amd64 AppImage](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.AppImage) · [aarch64 AppImage](https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_aarch64.AppImage) |", + "pub_date": "2026-07-10T14:21:33.364Z", "platforms": { - "darwin-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1dnUytBdDJXU1kzRS9YaFNVZUl1cVpVQ0t2MERqWlBIMzdSN1FxMTRDZHl5RlhoSnZNdEQ5blc3ajZQTllMWWRVU1IrRHg4a2ZmeVBJT2FDa05GN1FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NjAyCWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6CjEwaGd1dFY2akpQTXNJdXd2cEJmbmtQZzk2bEh1ek5MRDVVMTlaV2tRaFBtVVVMMzJ6NFNONUxMWEFhOVFjNXlOS3E0akkxOGxmQSsvdDhFUEJ1dkN3PT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_x64.app.tar.gz" - }, - "darwin-x86_64-app": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1dnUytBdDJXU1kzRS9YaFNVZUl1cVpVQ0t2MERqWlBIMzdSN1FxMTRDZHl5RlhoSnZNdEQ5blc3ajZQTllMWWRVU1IrRHg4a2ZmeVBJT2FDa05GN1FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NjAyCWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6CjEwaGd1dFY2akpQTXNJdXd2cEJmbmtQZzk2bEh1ek5MRDVVMTlaV2tRaFBtVVVMMzJ6NFNONUxMWEFhOVFjNXlOS3E0akkxOGxmQSsvdDhFUEJ1dkN3PT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_x64.app.tar.gz" - }, "darwin-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2ZXVmREREVWeUpiNjVLdWtvZ1ZOU1ZSYVJRUkZUYWJXb3FQaTkyZFNXZWowSWNqdjNKTjA1c0t0VGMvM3Z5eEVqK2djQVhZbkNCN05EbnU0YmRndmcwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NjU2CWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6Ckt6dTVSL3BwMU43ak1pMDBzMytYS0g2Z3ZFeGFZbkFoOStLdkljT3lSdkhBbTlZMUkxYzRDQVEyczBlT0NUM2ttNGxNUE9NSTdhL1U2R29NSmFVeUNRPT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_aarch64.app.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RmNXJ5T2ZyZlJkM3FoSDdsYTJkWFh5cDAzVk9Hc005YmQycWEzZ2hBU2c3WVZVS2FGb29KRCtNTW4xN3g3ZkcyYVQzaUo3RXA0a3BlQXh4RnhmQUFzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkyOTc0CWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6CmJMZzBScnlhYkl0eG5qZ1F3aHBHUWowemFvQWJ0NmVnUHJjSnNWN3BaVlNqODllclV6eTlydjk3cVVGMStDQmUyVUV6M0VDRnBNOWt0aEdGOXlZNER3PT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_aarch64.app.tar.gz" }, "darwin-aarch64-app": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2ZXVmREREVWeUpiNjVLdWtvZ1ZOU1ZSYVJRUkZUYWJXb3FQaTkyZFNXZWowSWNqdjNKTjA1c0t0VGMvM3Z5eEVqK2djQVhZbkNCN05EbnU0YmRndmcwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NjU2CWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6Ckt6dTVSL3BwMU43ak1pMDBzMytYS0g2Z3ZFeGFZbkFoOStLdkljT3lSdkhBbTlZMUkxYzRDQVEyczBlT0NUM2ttNGxNUE9NSTdhL1U2R29NSmFVeUNRPT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_aarch64.app.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RmNXJ5T2ZyZlJkM3FoSDdsYTJkWFh5cDAzVk9Hc005YmQycWEzZ2hBU2c3WVZVS2FGb29KRCtNTW4xN3g3ZkcyYVQzaUo3RXA0a3BlQXh4RnhmQUFzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkyOTc0CWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6CmJMZzBScnlhYkl0eG5qZ1F3aHBHUWowemFvQWJ0NmVnUHJjSnNWN3BaVlNqODllclV6eTlydjk3cVVGMStDQmUyVUV6M0VDRnBNOWt0aEdGOXlZNER3PT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_aarch64.app.tar.gz" + }, + "darwin-x86_64": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1d1VkU0WEptOWRjM1VLMjhEdFpTVnpHOU5tY2FLTTg1aHJQdUdHUXdwMStWUnY4WkRac0RzZmpER3V5Q1Bsbnhya2ErNUtMa3NmbXVtNXFhSlhCa0FnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMDMwCWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6Ci9DOVJ2aVFRS1BXejRTWjhZcjV0SWtISFU0OVlnTXU4ZlAxMFdCRmVLcWVpdDgzRnZGUEZWdGtGZ0RuUG1ZV0JGWnlRMHJia2ZRbEljeFpyQ3hJSUJ3PT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_x64.app.tar.gz" + }, + "darwin-x86_64-app": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1d1VkU0WEptOWRjM1VLMjhEdFpTVnpHOU5tY2FLTTg1aHJQdUdHUXdwMStWUnY4WkRac0RzZmpER3V5Q1Bsbnhya2ErNUtMa3NmbXVtNXFhSlhCa0FnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMDMwCWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6Ci9DOVJ2aVFRS1BXejRTWjhZcjV0SWtISFU0OVlnTXU4ZlAxMFdCRmVLcWVpdDgzRnZGUEZWdGtGZ0RuUG1ZV0JGWnlRMHJia2ZRbEljeFpyQ3hJSUJ3PT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_x64.app.tar.gz" }, "linux-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RBUnh4VUlWRE1UenNSamhTQkNGL2VIcC9IZElSUnR6WkNINUk4SXRjNmExdHVYcUlwN3ExdFpxTko4UHNmU0hhUStQVDZRT1M5NC9BdGI3clFOY0FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NzQ5CWZpbGU6UmVkaXNNRV80LjIuMV9hYXJjaDY0LkFwcEltYWdlCjVjTCtkSkdvUFc3SjRNcUdoeSsxaDg1Sk52VHMxOWdQNmg0cGk5TzhpUGtQbGU4aXJZZXAwdlREa2h5RjBIME1vVnNxSlNVU1lrRHFiQ1ZoSUZDUENRPT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_aarch64.AppImage" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2VMZnlYcGk0d0JRTE9zUGtjQU1MWUlzdjdua3pxU2FJV0gzZy9OZFphQ3FTZUR1L2xQMFowL3FiNHpSUmt6WWxacFlDRmd6VFg1VGtrb29kZ2JldlFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTM0CWZpbGU6UmVkaXNNRV80LjMuMF9hYXJjaDY0LkFwcEltYWdlCi9nL3NlTDJsaWlDSVlKYlVwcWZ6anFqVm1PU3Fmd2dxRjBVZysrRk94cDdidEJIQTNDblhlaHVCVEY2d055V3NjQ1hDRlFkUFdaWmZCaXpIbEpXUEJBPT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_aarch64.AppImage" }, "linux-aarch64-appimage": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RBUnh4VUlWRE1UenNSamhTQkNGL2VIcC9IZElSUnR6WkNINUk4SXRjNmExdHVYcUlwN3ExdFpxTko4UHNmU0hhUStQVDZRT1M5NC9BdGI3clFOY0FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NzQ5CWZpbGU6UmVkaXNNRV80LjIuMV9hYXJjaDY0LkFwcEltYWdlCjVjTCtkSkdvUFc3SjRNcUdoeSsxaDg1Sk52VHMxOWdQNmg0cGk5TzhpUGtQbGU4aXJZZXAwdlREa2h5RjBIME1vVnNxSlNVU1lrRHFiQ1ZoSUZDUENRPT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_aarch64.AppImage" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2VMZnlYcGk0d0JRTE9zUGtjQU1MWUlzdjdua3pxU2FJV0gzZy9OZFphQ3FTZUR1L2xQMFowL3FiNHpSUmt6WWxacFlDRmd6VFg1VGtrb29kZ2JldlFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTM0CWZpbGU6UmVkaXNNRV80LjMuMF9hYXJjaDY0LkFwcEltYWdlCi9nL3NlTDJsaWlDSVlKYlVwcWZ6anFqVm1PU3Fmd2dxRjBVZysrRk94cDdidEJIQTNDblhlaHVCVEY2d055V3NjQ1hDRlFkUFdaWmZCaXpIbEpXUEJBPT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_aarch64.AppImage" }, "linux-aarch64-deb": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2FCaTBTSkFBWnVNU3duU0N0aVdYRDhUS21ZSnRreUhIZ2tGTFRDNDVEalpIdzdCMlB0MlQ3U0E0eC9NeVBCSmNHTkszdWJZRXViUnNabHhvZXBMWVFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NzQ5CWZpbGU6UmVkaXNNRV80LjIuMV9hcm02NC5kZWIKWUY5aE1Xdldud0FobWtTQTBYaE9hK1p6bnltM2kvT1BBb3BRVENKaE5RbmpWTUZkVG9nYzN3dFgydEJJbkZnTktUcVg4MW0wTUhzb2cvaHl1SDdkRGc9PQo=", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64.deb" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2FSbkdMdTBCVitpcStXbk9ZWEhEamk3VmwrVjhHckllOHJNKytxZzhkckRucGJ1UHJsUjgrNGlEMHRCeE5HUlExNDlEOUlsb0Q2OUdUc2RuY2I2SkFvPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTM0CWZpbGU6UmVkaXNNRV80LjMuMF9hcm02NC5kZWIKbU12ZTZ4Y0RNK2pialRoZ2RvOEM3K3Z0OTRQMVlNZHNxODU5T2R2QzNGTTlDN1ZCK3hucFp3Wm5lTGFIS3lMNDNVemdBOEJkOUE1SkVqcnJoYkN5QlE9PQo=", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64.deb" }, "linux-aarch64-rpm": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1pKZmg2VXVnYzZvK1ludGIrTk9HTVhwbTlrZEpnMm9neUZQNXV4ZnJrUXVnWkRMZm43VTdLeC9pOHRkSi9CVnNxaTBja3FLcG9wUFVPb2F2Ui9Cb3d3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NzQ5CWZpbGU6UmVkaXNNRS00LjIuMS0xLmFhcmNoNjQucnBtCmpxT0Q2RkI0alpjZXNZdG5HOWhORW03VS9jN2FHbHRZa01wTTh1cS9BZUY4WE56aWY5K29ScDd2UElTdHZ4citWVzFZZzRhK2I5YXViL1FmNkZPaUJBPT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME-4.2.1-1.aarch64.rpm" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1MySGc0dHBxZWRNMWFBbUxGeWllWExDU0N4RVdQY1RFSXJDZldnM2FDbitjRlI3N1dURnordnhmcmh5SVU2cU9hQUk1dEwxY1h0a2pNc2w1VVdXc3d3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTM0CWZpbGU6UmVkaXNNRS00LjMuMC0xLmFhcmNoNjQucnBtCmkrbE81eXpnNU0vLzA1VDFXZ0xPbXRjcjdzMzF0T0s5cUwvaUFWRFpUSmdtSDZvWDFBTksrV000UlV3WEpYVWUzTUtvTW1zWHFzV2tKREdrd3JKOEFBPT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME-4.3.0-1.aarch64.rpm" }, "linux-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1psSi9OMkVJcElyNm1HNkc5WkpkNjZiZHNtOFhTdlkycS9vVG5NQlZYK28rckluQU1VV052K1lleWJrbE5xU3hGZlRBOGFsYkUydnFseWdKY1Nvd2dnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0ODE4CWZpbGU6UmVkaXNNRV80LjIuMV9hbWQ2NC5BcHBJbWFnZQpVV05IYkMrMCs1Z3Bub2UvcVJMMWZ1cU9YNkl6STBuekhDZFFWQ3VCTU1mYVZGN3hYbEgvU0I2SUNTdXUyYUtYeDJBMFErV3doRzdBQVJJTENweVZBUT09Cg==", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.AppImage" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1VGVzNqeDBRRFp2eGx2TThCV2NGanhMYzVEUVNCeHM5UXlRZEZhUmEyTVV4VGRYb2padUNvbUt1Z0R1bnVnd2o3MlA5Y0VOeitBUDFrak5iaHpKalFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTcyCWZpbGU6UmVkaXNNRV80LjMuMF9hbWQ2NC5BcHBJbWFnZQpkTjFhYjlXbDVvN3RrNzRaUlBuSEhMQlI4LzUzUHU3NDhSY1U5TWhxd0g4R3dnem5uZHUwbkFXVjRiak9iSlVaZFhUbUdrR0Z5T3ZjcHpSR3NNamZEdz09Cg==", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.AppImage" }, "linux-x86_64-appimage": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1psSi9OMkVJcElyNm1HNkc5WkpkNjZiZHNtOFhTdlkycS9vVG5NQlZYK28rckluQU1VV052K1lleWJrbE5xU3hGZlRBOGFsYkUydnFseWdKY1Nvd2dnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0ODE4CWZpbGU6UmVkaXNNRV80LjIuMV9hbWQ2NC5BcHBJbWFnZQpVV05IYkMrMCs1Z3Bub2UvcVJMMWZ1cU9YNkl6STBuekhDZFFWQ3VCTU1mYVZGN3hYbEgvU0I2SUNTdXUyYUtYeDJBMFErV3doRzdBQVJJTENweVZBUT09Cg==", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.AppImage" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1VGVzNqeDBRRFp2eGx2TThCV2NGanhMYzVEUVNCeHM5UXlRZEZhUmEyTVV4VGRYb2padUNvbUt1Z0R1bnVnd2o3MlA5Y0VOeitBUDFrak5iaHpKalFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTcyCWZpbGU6UmVkaXNNRV80LjMuMF9hbWQ2NC5BcHBJbWFnZQpkTjFhYjlXbDVvN3RrNzRaUlBuSEhMQlI4LzUzUHU3NDhSY1U5TWhxd0g4R3dnem5uZHUwbkFXVjRiak9iSlVaZFhUbUdrR0Z5T3ZjcHpSR3NNamZEdz09Cg==", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.AppImage" }, "linux-x86_64-deb": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1dPSUJoTXd4bUppSkhZNlFRTUxGRTBiT1dhM2xLWWM4RXVicjRKZm5qRUh2Ync0NEtCYWtWcit3eWJzNG1yYVgvRzl4aDhXUGI3cXEwWk9SVERyd3dRPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0ODE4CWZpbGU6UmVkaXNNRV80LjIuMV9hbWQ2NC5kZWIKM0wxNG1BcjJmWFdXMklEY2t4WnY3T29mMHNidUxxRHdWTlo4SkoxdU4xaEl3Z0Uvd0s5UlFMbHBrejFWWVNQMCtvTGVCUm1ZbDdDcysxcTFDZWJ2RHc9PQo=", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.deb" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2FSZ1FqNDRPdEVuVVE3eXoxa1NzKzZ6bVJrT1lGRFNKdzVPRTh2eDJ4bUwwU1NSTm9IYXBKOW5VOXJrcnlobENiL0ZMMlQ3WmNzMlFJVGY3NnVUa0FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTcyCWZpbGU6UmVkaXNNRV80LjMuMF9hbWQ2NC5kZWIKQVdQU21XNGpDZ3g0UHpBZnNVUVpRRUEwSTQwZ0ljenVjaS8xWU1tK21sR2xrKzBWV25RMWlFeVNZd250Y25hTWJXcmZiMExZZ3YwUUhPSUtlcjdmQnc9PQo=", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.deb" }, "linux-x86_64-rpm": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1ZhZlRSNEVPL09sYzFOaHNPUmVjTUFQWkszcUJQeDAxSFRmZWdCeEw0bjY1WG5zRjBQMnZvcktiMjFFTEhLL24zRURRcytnYVpUd2NvUm1IWk1ZMFFzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0ODE4CWZpbGU6UmVkaXNNRS00LjIuMS0xLng4Nl82NC5ycG0KblBaRTJ3YlRUZHNWSzJ6aVp0blpjMjVZVklXdGx6ZWYzbm5Oa3IySU1yb3lhQUloL3RlbUNBaVZLWVk1eTVjWXoya1ZsR2ZHbzdBaEluZTJ3OGFLRGc9PQo=", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME-4.2.1-1.x86_64.rpm" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1dEQkFNWUdPMmNwYkozZExHYXBKTjRyY2JsMnExeldwRDJVcnhBSFdUbGhaMG1GcnBOQWNOMEpScWxBZktuZjA1cDhDZkdPMitGVHF2N2dJbkZPSVFFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTcyCWZpbGU6UmVkaXNNRS00LjMuMC0xLng4Nl82NC5ycG0KSTdhQ1VvMDlrZXNXQkVuOUk5Tlh0UnlqV0dWN0JXVVJQZXlnL3o1UjZUaGZkR0o1MGdrMnBtcW5oRlVXWDZSU1J2dEpleExBWWxDRUpmK3BMdVptQWc9PQo=", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME-4.3.0-1.x86_64.rpm" }, "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1lvTHMwQzRxaUhTdHNPaVNpMWFtQzNPUklqK3QvYXRCNjBUV1hGWlpQY1lmbFRXQko5bFJkTjZScHNuTHhxNXJYc1JsOEEzeWJLRDRuRTQ3K2owY3dNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0OTY1CWZpbGU6UmVkaXNNRV80LjIuMV94NjQtc2V0dXAuZXhlCjBPY0NEL2VnNDc0a293dUVFRGwvMmMzbjlGSVB2TEQxYmhFQWhVaGt2aHYzUmtzWDQ3SzB6Y2oxdXFqRHU3YndqTWpXSkRGQlE3d1FvL2tjUUNJT0NnPT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_x64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RHdEpsOEQxV2VyaTU3RTVzb0pJUWVwZU5HKzdmbFJkd3JTL24vRlhXeGx6ZlZBM0JVNFh1YnFjRUY4bkMyZ29GOVdHbDlCVnFBcytPazdhblorMnd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMjcyCWZpbGU6UmVkaXNNRV80LjMuMF94NjQtc2V0dXAuZXhlCjZRL0FMVUJ3V095ZGF1RmRWc1dVazRRR2RTMHQvL0VTUXdPY0F2eEhHMmdRNmNhdXBFbitiYWN6aDliM05ZZk5FOGliZ0t3dldoRTVEVGJpMkMvQUJnPT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_x64-setup.exe" }, "windows-x86_64-nsis": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1lvTHMwQzRxaUhTdHNPaVNpMWFtQzNPUklqK3QvYXRCNjBUV1hGWlpQY1lmbFRXQko5bFJkTjZScHNuTHhxNXJYc1JsOEEzeWJLRDRuRTQ3K2owY3dNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0OTY1CWZpbGU6UmVkaXNNRV80LjIuMV94NjQtc2V0dXAuZXhlCjBPY0NEL2VnNDc0a293dUVFRGwvMmMzbjlGSVB2TEQxYmhFQWhVaGt2aHYzUmtzWDQ3SzB6Y2oxdXFqRHU3YndqTWpXSkRGQlE3d1FvL2tjUUNJT0NnPT0K", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_x64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RHdEpsOEQxV2VyaTU3RTVzb0pJUWVwZU5HKzdmbFJkd3JTL24vRlhXeGx6ZlZBM0JVNFh1YnFjRUY4bkMyZ29GOVdHbDlCVnFBcytPazdhblorMnd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMjcyCWZpbGU6UmVkaXNNRV80LjMuMF94NjQtc2V0dXAuZXhlCjZRL0FMVUJ3V095ZGF1RmRWc1dVazRRR2RTMHQvL0VTUXdPY0F2eEhHMmdRNmNhdXBFbitiYWN6aDliM05ZZk5FOGliZ0t3dldoRTVEVGJpMkMvQUJnPT0K", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_x64-setup.exe" }, "windows-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2ZBRm1pdmRUdVREOGIzT0xTOENDb3A5YzVxTDlhRXFGeGE0SDhLekZBOXNiak9ESGQwUEVKTjVnQ3JoM1dFeDlUUlgvK3RDMHlIQmVJSzJOeTBnY1FVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0OTkwCWZpbGU6UmVkaXNNRV80LjIuMV9hcm02NC1zZXR1cC5leGUKRXlZY3VXcnduSzE3YmpRN0tZdzVUdktrVXlmWG40b2JpVjc4bzM1akNaR0MvY0JMSGl1MTBhOGE3YjdIbHpFaDJLVnVxdzF4V3VBWlNTeDJ2Z3dyQ2c9PQo=", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1hoN2IrdGlTdTdDbVRiV293NUlIOWxsL2xQSFh2N2Z5NXJPM2hnQ0VNUStwYUNjQ3RvU2ZHYjRUTkVUVG85ZkFxVXprZ1dXSVlSS3lVZS9ndGpPMUEwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMjkwCWZpbGU6UmVkaXNNRV80LjMuMF9hcm02NC1zZXR1cC5leGUKd2tZWTJDeWFhSjdqVUVZVS9BSGNYNUpjRVd6dmxpY053LzBMRFVEL3NQUVYyMHZHSldzaFdUaitXTGo2bXVGelRpK1FkZ2kyQW8wTXpPQmcrdHdwQ0E9PQo=", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64-setup.exe" }, "windows-aarch64-nsis": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2ZBRm1pdmRUdVREOGIzT0xTOENDb3A5YzVxTDlhRXFGeGE0SDhLekZBOXNiak9ESGQwUEVKTjVnQ3JoM1dFeDlUUlgvK3RDMHlIQmVJSzJOeTBnY1FVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0OTkwCWZpbGU6UmVkaXNNRV80LjIuMV9hcm02NC1zZXR1cC5leGUKRXlZY3VXcnduSzE3YmpRN0tZdzVUdktrVXlmWG40b2JpVjc4bzM1akNaR0MvY0JMSGl1MTBhOGE3YjdIbHpFaDJLVnVxdzF4V3VBWlNTeDJ2Z3dyQ2c9PQo=", - "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1hoN2IrdGlTdTdDbVRiV293NUlIOWxsL2xQSFh2N2Z5NXJPM2hnQ0VNUStwYUNjQ3RvU2ZHYjRUTkVUVG85ZkFxVXprZ1dXSVlSS3lVZS9ndGpPMUEwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMjkwCWZpbGU6UmVkaXNNRV80LjMuMF9hcm02NC1zZXR1cC5leGUKd2tZWTJDeWFhSjdqVUVZVS9BSGNYNUpjRVd6dmxpY053LzBMRFVEL3NQUVYyMHZHSldzaFdUaitXTGo2bXVGelRpK1FkZ2kyQW8wTXpPQmcrdHdwQ0E9PQo=", + "url": "https://gitee.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64-setup.exe" } } } diff --git a/docs/public/lastest/latest-github.json b/docs/public/lastest/latest-github.json index eb31e4a0..9b780ce2 100644 --- a/docs/public/lastest/latest-github.json +++ b/docs/public/lastest/latest-github.json @@ -1,71 +1,71 @@ { - "version": "4.2.1", - "notes": "| OS | Download |\n| --- | --- |\n| Windows | [Installer (x64)](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_x64-setup.exe) · [Portable (x64)](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_portable_x64.zip) · [Installer (Arm64)](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64-setup.exe) · [Portable (Arm64)](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_portable_arm64.zip) |\n| Mac | [Apple Silicon](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_aarch64.dmg) · [Intel Mac](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_x64.dmg) |\n| Linux | [amd64 deb](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.deb) · [arm64 deb](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64.deb) · [x86_64 rpm](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME-4.2.1-1.x86_64.rpm) · [aarch64 rpm](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME-4.2.1-1.aarch64.rpm) · [amd64 AppImage](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.AppImage) · [aarch64 AppImage](https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_aarch64.AppImage) |", - "pub_date": "2026-07-06T13:36:33.646Z", + "version": "4.3.0", + "notes": "| OS | Download |\n| --- | --- |\n| Windows | [Installer (x64)](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_x64-setup.exe) · [Portable (x64)](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_portable_x64.zip) · [Installer (Arm64)](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64-setup.exe) · [Portable (Arm64)](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_portable_arm64.zip) |\n| Mac | [Apple Silicon](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_aarch64.dmg) · [Intel Mac](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_x64.dmg) |\n| Linux | [amd64 deb](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.deb) · [arm64 deb](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64.deb) · [x86_64 rpm](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME-4.3.0-1.x86_64.rpm) · [aarch64 rpm](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME-4.3.0-1.aarch64.rpm) · [amd64 AppImage](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.AppImage) · [aarch64 AppImage](https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_aarch64.AppImage) |", + "pub_date": "2026-07-10T14:21:33.364Z", "platforms": { - "darwin-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1dnUytBdDJXU1kzRS9YaFNVZUl1cVpVQ0t2MERqWlBIMzdSN1FxMTRDZHl5RlhoSnZNdEQ5blc3ajZQTllMWWRVU1IrRHg4a2ZmeVBJT2FDa05GN1FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NjAyCWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6CjEwaGd1dFY2akpQTXNJdXd2cEJmbmtQZzk2bEh1ek5MRDVVMTlaV2tRaFBtVVVMMzJ6NFNONUxMWEFhOVFjNXlOS3E0akkxOGxmQSsvdDhFUEJ1dkN3PT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_x64.app.tar.gz" - }, - "darwin-x86_64-app": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1dnUytBdDJXU1kzRS9YaFNVZUl1cVpVQ0t2MERqWlBIMzdSN1FxMTRDZHl5RlhoSnZNdEQ5blc3ajZQTllMWWRVU1IrRHg4a2ZmeVBJT2FDa05GN1FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NjAyCWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6CjEwaGd1dFY2akpQTXNJdXd2cEJmbmtQZzk2bEh1ek5MRDVVMTlaV2tRaFBtVVVMMzJ6NFNONUxMWEFhOVFjNXlOS3E0akkxOGxmQSsvdDhFUEJ1dkN3PT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_x64.app.tar.gz" - }, "darwin-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2ZXVmREREVWeUpiNjVLdWtvZ1ZOU1ZSYVJRUkZUYWJXb3FQaTkyZFNXZWowSWNqdjNKTjA1c0t0VGMvM3Z5eEVqK2djQVhZbkNCN05EbnU0YmRndmcwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NjU2CWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6Ckt6dTVSL3BwMU43ak1pMDBzMytYS0g2Z3ZFeGFZbkFoOStLdkljT3lSdkhBbTlZMUkxYzRDQVEyczBlT0NUM2ttNGxNUE9NSTdhL1U2R29NSmFVeUNRPT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_aarch64.app.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RmNXJ5T2ZyZlJkM3FoSDdsYTJkWFh5cDAzVk9Hc005YmQycWEzZ2hBU2c3WVZVS2FGb29KRCtNTW4xN3g3ZkcyYVQzaUo3RXA0a3BlQXh4RnhmQUFzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkyOTc0CWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6CmJMZzBScnlhYkl0eG5qZ1F3aHBHUWowemFvQWJ0NmVnUHJjSnNWN3BaVlNqODllclV6eTlydjk3cVVGMStDQmUyVUV6M0VDRnBNOWt0aEdGOXlZNER3PT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_aarch64.app.tar.gz" }, "darwin-aarch64-app": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2ZXVmREREVWeUpiNjVLdWtvZ1ZOU1ZSYVJRUkZUYWJXb3FQaTkyZFNXZWowSWNqdjNKTjA1c0t0VGMvM3Z5eEVqK2djQVhZbkNCN05EbnU0YmRndmcwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NjU2CWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6Ckt6dTVSL3BwMU43ak1pMDBzMytYS0g2Z3ZFeGFZbkFoOStLdkljT3lSdkhBbTlZMUkxYzRDQVEyczBlT0NUM2ttNGxNUE9NSTdhL1U2R29NSmFVeUNRPT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_aarch64.app.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RmNXJ5T2ZyZlJkM3FoSDdsYTJkWFh5cDAzVk9Hc005YmQycWEzZ2hBU2c3WVZVS2FGb29KRCtNTW4xN3g3ZkcyYVQzaUo3RXA0a3BlQXh4RnhmQUFzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkyOTc0CWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6CmJMZzBScnlhYkl0eG5qZ1F3aHBHUWowemFvQWJ0NmVnUHJjSnNWN3BaVlNqODllclV6eTlydjk3cVVGMStDQmUyVUV6M0VDRnBNOWt0aEdGOXlZNER3PT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_aarch64.app.tar.gz" + }, + "darwin-x86_64": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1d1VkU0WEptOWRjM1VLMjhEdFpTVnpHOU5tY2FLTTg1aHJQdUdHUXdwMStWUnY4WkRac0RzZmpER3V5Q1Bsbnhya2ErNUtMa3NmbXVtNXFhSlhCa0FnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMDMwCWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6Ci9DOVJ2aVFRS1BXejRTWjhZcjV0SWtISFU0OVlnTXU4ZlAxMFdCRmVLcWVpdDgzRnZGUEZWdGtGZ0RuUG1ZV0JGWnlRMHJia2ZRbEljeFpyQ3hJSUJ3PT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_x64.app.tar.gz" + }, + "darwin-x86_64-app": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1d1VkU0WEptOWRjM1VLMjhEdFpTVnpHOU5tY2FLTTg1aHJQdUdHUXdwMStWUnY4WkRac0RzZmpER3V5Q1Bsbnhya2ErNUtMa3NmbXVtNXFhSlhCa0FnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMDMwCWZpbGU6UmVkaXNNRS5hcHAudGFyLmd6Ci9DOVJ2aVFRS1BXejRTWjhZcjV0SWtISFU0OVlnTXU4ZlAxMFdCRmVLcWVpdDgzRnZGUEZWdGtGZ0RuUG1ZV0JGWnlRMHJia2ZRbEljeFpyQ3hJSUJ3PT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_x64.app.tar.gz" }, "linux-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RBUnh4VUlWRE1UenNSamhTQkNGL2VIcC9IZElSUnR6WkNINUk4SXRjNmExdHVYcUlwN3ExdFpxTko4UHNmU0hhUStQVDZRT1M5NC9BdGI3clFOY0FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NzQ5CWZpbGU6UmVkaXNNRV80LjIuMV9hYXJjaDY0LkFwcEltYWdlCjVjTCtkSkdvUFc3SjRNcUdoeSsxaDg1Sk52VHMxOWdQNmg0cGk5TzhpUGtQbGU4aXJZZXAwdlREa2h5RjBIME1vVnNxSlNVU1lrRHFiQ1ZoSUZDUENRPT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_aarch64.AppImage" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2VMZnlYcGk0d0JRTE9zUGtjQU1MWUlzdjdua3pxU2FJV0gzZy9OZFphQ3FTZUR1L2xQMFowL3FiNHpSUmt6WWxacFlDRmd6VFg1VGtrb29kZ2JldlFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTM0CWZpbGU6UmVkaXNNRV80LjMuMF9hYXJjaDY0LkFwcEltYWdlCi9nL3NlTDJsaWlDSVlKYlVwcWZ6anFqVm1PU3Fmd2dxRjBVZysrRk94cDdidEJIQTNDblhlaHVCVEY2d055V3NjQ1hDRlFkUFdaWmZCaXpIbEpXUEJBPT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_aarch64.AppImage" }, "linux-aarch64-appimage": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RBUnh4VUlWRE1UenNSamhTQkNGL2VIcC9IZElSUnR6WkNINUk4SXRjNmExdHVYcUlwN3ExdFpxTko4UHNmU0hhUStQVDZRT1M5NC9BdGI3clFOY0FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NzQ5CWZpbGU6UmVkaXNNRV80LjIuMV9hYXJjaDY0LkFwcEltYWdlCjVjTCtkSkdvUFc3SjRNcUdoeSsxaDg1Sk52VHMxOWdQNmg0cGk5TzhpUGtQbGU4aXJZZXAwdlREa2h5RjBIME1vVnNxSlNVU1lrRHFiQ1ZoSUZDUENRPT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_aarch64.AppImage" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2VMZnlYcGk0d0JRTE9zUGtjQU1MWUlzdjdua3pxU2FJV0gzZy9OZFphQ3FTZUR1L2xQMFowL3FiNHpSUmt6WWxacFlDRmd6VFg1VGtrb29kZ2JldlFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTM0CWZpbGU6UmVkaXNNRV80LjMuMF9hYXJjaDY0LkFwcEltYWdlCi9nL3NlTDJsaWlDSVlKYlVwcWZ6anFqVm1PU3Fmd2dxRjBVZysrRk94cDdidEJIQTNDblhlaHVCVEY2d055V3NjQ1hDRlFkUFdaWmZCaXpIbEpXUEJBPT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_aarch64.AppImage" }, "linux-aarch64-deb": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2FCaTBTSkFBWnVNU3duU0N0aVdYRDhUS21ZSnRreUhIZ2tGTFRDNDVEalpIdzdCMlB0MlQ3U0E0eC9NeVBCSmNHTkszdWJZRXViUnNabHhvZXBMWVFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NzQ5CWZpbGU6UmVkaXNNRV80LjIuMV9hcm02NC5kZWIKWUY5aE1Xdldud0FobWtTQTBYaE9hK1p6bnltM2kvT1BBb3BRVENKaE5RbmpWTUZkVG9nYzN3dFgydEJJbkZnTktUcVg4MW0wTUhzb2cvaHl1SDdkRGc9PQo=", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64.deb" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2FSbkdMdTBCVitpcStXbk9ZWEhEamk3VmwrVjhHckllOHJNKytxZzhkckRucGJ1UHJsUjgrNGlEMHRCeE5HUlExNDlEOUlsb0Q2OUdUc2RuY2I2SkFvPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTM0CWZpbGU6UmVkaXNNRV80LjMuMF9hcm02NC5kZWIKbU12ZTZ4Y0RNK2pialRoZ2RvOEM3K3Z0OTRQMVlNZHNxODU5T2R2QzNGTTlDN1ZCK3hucFp3Wm5lTGFIS3lMNDNVemdBOEJkOUE1SkVqcnJoYkN5QlE9PQo=", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64.deb" }, "linux-aarch64-rpm": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1pKZmg2VXVnYzZvK1ludGIrTk9HTVhwbTlrZEpnMm9neUZQNXV4ZnJrUXVnWkRMZm43VTdLeC9pOHRkSi9CVnNxaTBja3FLcG9wUFVPb2F2Ui9Cb3d3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0NzQ5CWZpbGU6UmVkaXNNRS00LjIuMS0xLmFhcmNoNjQucnBtCmpxT0Q2RkI0alpjZXNZdG5HOWhORW03VS9jN2FHbHRZa01wTTh1cS9BZUY4WE56aWY5K29ScDd2UElTdHZ4citWVzFZZzRhK2I5YXViL1FmNkZPaUJBPT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME-4.2.1-1.aarch64.rpm" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1MySGc0dHBxZWRNMWFBbUxGeWllWExDU0N4RVdQY1RFSXJDZldnM2FDbitjRlI3N1dURnordnhmcmh5SVU2cU9hQUk1dEwxY1h0a2pNc2w1VVdXc3d3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTM0CWZpbGU6UmVkaXNNRS00LjMuMC0xLmFhcmNoNjQucnBtCmkrbE81eXpnNU0vLzA1VDFXZ0xPbXRjcjdzMzF0T0s5cUwvaUFWRFpUSmdtSDZvWDFBTksrV000UlV3WEpYVWUzTUtvTW1zWHFzV2tKREdrd3JKOEFBPT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME-4.3.0-1.aarch64.rpm" }, "linux-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1psSi9OMkVJcElyNm1HNkc5WkpkNjZiZHNtOFhTdlkycS9vVG5NQlZYK28rckluQU1VV052K1lleWJrbE5xU3hGZlRBOGFsYkUydnFseWdKY1Nvd2dnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0ODE4CWZpbGU6UmVkaXNNRV80LjIuMV9hbWQ2NC5BcHBJbWFnZQpVV05IYkMrMCs1Z3Bub2UvcVJMMWZ1cU9YNkl6STBuekhDZFFWQ3VCTU1mYVZGN3hYbEgvU0I2SUNTdXUyYUtYeDJBMFErV3doRzdBQVJJTENweVZBUT09Cg==", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.AppImage" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1VGVzNqeDBRRFp2eGx2TThCV2NGanhMYzVEUVNCeHM5UXlRZEZhUmEyTVV4VGRYb2padUNvbUt1Z0R1bnVnd2o3MlA5Y0VOeitBUDFrak5iaHpKalFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTcyCWZpbGU6UmVkaXNNRV80LjMuMF9hbWQ2NC5BcHBJbWFnZQpkTjFhYjlXbDVvN3RrNzRaUlBuSEhMQlI4LzUzUHU3NDhSY1U5TWhxd0g4R3dnem5uZHUwbkFXVjRiak9iSlVaZFhUbUdrR0Z5T3ZjcHpSR3NNamZEdz09Cg==", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.AppImage" }, "linux-x86_64-appimage": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1psSi9OMkVJcElyNm1HNkc5WkpkNjZiZHNtOFhTdlkycS9vVG5NQlZYK28rckluQU1VV052K1lleWJrbE5xU3hGZlRBOGFsYkUydnFseWdKY1Nvd2dnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0ODE4CWZpbGU6UmVkaXNNRV80LjIuMV9hbWQ2NC5BcHBJbWFnZQpVV05IYkMrMCs1Z3Bub2UvcVJMMWZ1cU9YNkl6STBuekhDZFFWQ3VCTU1mYVZGN3hYbEgvU0I2SUNTdXUyYUtYeDJBMFErV3doRzdBQVJJTENweVZBUT09Cg==", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.AppImage" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1VGVzNqeDBRRFp2eGx2TThCV2NGanhMYzVEUVNCeHM5UXlRZEZhUmEyTVV4VGRYb2padUNvbUt1Z0R1bnVnd2o3MlA5Y0VOeitBUDFrak5iaHpKalFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTcyCWZpbGU6UmVkaXNNRV80LjMuMF9hbWQ2NC5BcHBJbWFnZQpkTjFhYjlXbDVvN3RrNzRaUlBuSEhMQlI4LzUzUHU3NDhSY1U5TWhxd0g4R3dnem5uZHUwbkFXVjRiak9iSlVaZFhUbUdrR0Z5T3ZjcHpSR3NNamZEdz09Cg==", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.AppImage" }, "linux-x86_64-deb": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1dPSUJoTXd4bUppSkhZNlFRTUxGRTBiT1dhM2xLWWM4RXVicjRKZm5qRUh2Ync0NEtCYWtWcit3eWJzNG1yYVgvRzl4aDhXUGI3cXEwWk9SVERyd3dRPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0ODE4CWZpbGU6UmVkaXNNRV80LjIuMV9hbWQ2NC5kZWIKM0wxNG1BcjJmWFdXMklEY2t4WnY3T29mMHNidUxxRHdWTlo4SkoxdU4xaEl3Z0Uvd0s5UlFMbHBrejFWWVNQMCtvTGVCUm1ZbDdDcysxcTFDZWJ2RHc9PQo=", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_amd64.deb" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2FSZ1FqNDRPdEVuVVE3eXoxa1NzKzZ6bVJrT1lGRFNKdzVPRTh2eDJ4bUwwU1NSTm9IYXBKOW5VOXJrcnlobENiL0ZMMlQ3WmNzMlFJVGY3NnVUa0FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTcyCWZpbGU6UmVkaXNNRV80LjMuMF9hbWQ2NC5kZWIKQVdQU21XNGpDZ3g0UHpBZnNVUVpRRUEwSTQwZ0ljenVjaS8xWU1tK21sR2xrKzBWV25RMWlFeVNZd250Y25hTWJXcmZiMExZZ3YwUUhPSUtlcjdmQnc9PQo=", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_amd64.deb" }, "linux-x86_64-rpm": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1ZhZlRSNEVPL09sYzFOaHNPUmVjTUFQWkszcUJQeDAxSFRmZWdCeEw0bjY1WG5zRjBQMnZvcktiMjFFTEhLL24zRURRcytnYVpUd2NvUm1IWk1ZMFFzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0ODE4CWZpbGU6UmVkaXNNRS00LjIuMS0xLng4Nl82NC5ycG0KblBaRTJ3YlRUZHNWSzJ6aVp0blpjMjVZVklXdGx6ZWYzbm5Oa3IySU1yb3lhQUloL3RlbUNBaVZLWVk1eTVjWXoya1ZsR2ZHbzdBaEluZTJ3OGFLRGc9PQo=", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME-4.2.1-1.x86_64.rpm" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1dEQkFNWUdPMmNwYkozZExHYXBKTjRyY2JsMnExeldwRDJVcnhBSFdUbGhaMG1GcnBOQWNOMEpScWxBZktuZjA1cDhDZkdPMitGVHF2N2dJbkZPSVFFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMTcyCWZpbGU6UmVkaXNNRS00LjMuMC0xLng4Nl82NC5ycG0KSTdhQ1VvMDlrZXNXQkVuOUk5Tlh0UnlqV0dWN0JXVVJQZXlnL3o1UjZUaGZkR0o1MGdrMnBtcW5oRlVXWDZSU1J2dEpleExBWWxDRUpmK3BMdVptQWc9PQo=", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME-4.3.0-1.x86_64.rpm" }, "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1lvTHMwQzRxaUhTdHNPaVNpMWFtQzNPUklqK3QvYXRCNjBUV1hGWlpQY1lmbFRXQko5bFJkTjZScHNuTHhxNXJYc1JsOEEzeWJLRDRuRTQ3K2owY3dNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0OTY1CWZpbGU6UmVkaXNNRV80LjIuMV94NjQtc2V0dXAuZXhlCjBPY0NEL2VnNDc0a293dUVFRGwvMmMzbjlGSVB2TEQxYmhFQWhVaGt2aHYzUmtzWDQ3SzB6Y2oxdXFqRHU3YndqTWpXSkRGQlE3d1FvL2tjUUNJT0NnPT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_x64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RHdEpsOEQxV2VyaTU3RTVzb0pJUWVwZU5HKzdmbFJkd3JTL24vRlhXeGx6ZlZBM0JVNFh1YnFjRUY4bkMyZ29GOVdHbDlCVnFBcytPazdhblorMnd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMjcyCWZpbGU6UmVkaXNNRV80LjMuMF94NjQtc2V0dXAuZXhlCjZRL0FMVUJ3V095ZGF1RmRWc1dVazRRR2RTMHQvL0VTUXdPY0F2eEhHMmdRNmNhdXBFbitiYWN6aDliM05ZZk5FOGliZ0t3dldoRTVEVGJpMkMvQUJnPT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_x64-setup.exe" }, "windows-x86_64-nsis": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1lvTHMwQzRxaUhTdHNPaVNpMWFtQzNPUklqK3QvYXRCNjBUV1hGWlpQY1lmbFRXQko5bFJkTjZScHNuTHhxNXJYc1JsOEEzeWJLRDRuRTQ3K2owY3dNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0OTY1CWZpbGU6UmVkaXNNRV80LjIuMV94NjQtc2V0dXAuZXhlCjBPY0NEL2VnNDc0a293dUVFRGwvMmMzbjlGSVB2TEQxYmhFQWhVaGt2aHYzUmtzWDQ3SzB6Y2oxdXFqRHU3YndqTWpXSkRGQlE3d1FvL2tjUUNJT0NnPT0K", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_x64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1RHdEpsOEQxV2VyaTU3RTVzb0pJUWVwZU5HKzdmbFJkd3JTL24vRlhXeGx6ZlZBM0JVNFh1YnFjRUY4bkMyZ29GOVdHbDlCVnFBcytPazdhblorMnd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMjcyCWZpbGU6UmVkaXNNRV80LjMuMF94NjQtc2V0dXAuZXhlCjZRL0FMVUJ3V095ZGF1RmRWc1dVazRRR2RTMHQvL0VTUXdPY0F2eEhHMmdRNmNhdXBFbitiYWN6aDliM05ZZk5FOGliZ0t3dldoRTVEVGJpMkMvQUJnPT0K", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_x64-setup.exe" }, "windows-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2ZBRm1pdmRUdVREOGIzT0xTOENDb3A5YzVxTDlhRXFGeGE0SDhLekZBOXNiak9ESGQwUEVKTjVnQ3JoM1dFeDlUUlgvK3RDMHlIQmVJSzJOeTBnY1FVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0OTkwCWZpbGU6UmVkaXNNRV80LjIuMV9hcm02NC1zZXR1cC5leGUKRXlZY3VXcnduSzE3YmpRN0tZdzVUdktrVXlmWG40b2JpVjc4bzM1akNaR0MvY0JMSGl1MTBhOGE3YjdIbHpFaDJLVnVxdzF4V3VBWlNTeDJ2Z3dyQ2c9PQo=", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1hoN2IrdGlTdTdDbVRiV293NUlIOWxsL2xQSFh2N2Z5NXJPM2hnQ0VNUStwYUNjQ3RvU2ZHYjRUTkVUVG85ZkFxVXprZ1dXSVlSS3lVZS9ndGpPMUEwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMjkwCWZpbGU6UmVkaXNNRV80LjMuMF9hcm02NC1zZXR1cC5leGUKd2tZWTJDeWFhSjdqVUVZVS9BSGNYNUpjRVd6dmxpY053LzBMRFVEL3NQUVYyMHZHSldzaFdUaitXTGo2bXVGelRpK1FkZ2kyQW8wTXpPQmcrdHdwQ0E9PQo=", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64-setup.exe" }, "windows-aarch64-nsis": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL2ZBRm1pdmRUdVREOGIzT0xTOENDb3A5YzVxTDlhRXFGeGE0SDhLekZBOXNiak9ESGQwUEVKTjVnQ3JoM1dFeDlUUlgvK3RDMHlIQmVJSzJOeTBnY1FVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzMzQ0OTkwCWZpbGU6UmVkaXNNRV80LjIuMV9hcm02NC1zZXR1cC5leGUKRXlZY3VXcnduSzE3YmpRN0tZdzVUdktrVXlmWG40b2JpVjc4bzM1akNaR0MvY0JMSGl1MTBhOGE3YjdIbHpFaDJLVnVxdzF4V3VBWlNTeDJ2Z3dyQ2c9PQo=", - "url": "https://github.com/hepengju/redis-me/releases/download/v4.2.1/RedisME_4.2.1_arm64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTMFViKzlOQXVhL1hoN2IrdGlTdTdDbVRiV293NUlIOWxsL2xQSFh2N2Z5NXJPM2hnQ0VNUStwYUNjQ3RvU2ZHYjRUTkVUVG85ZkFxVXprZ1dXSVlSS3lVZS9ndGpPMUEwPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzgzNjkzMjkwCWZpbGU6UmVkaXNNRV80LjMuMF9hcm02NC1zZXR1cC5leGUKd2tZWTJDeWFhSjdqVUVZVS9BSGNYNUpjRVd6dmxpY053LzBMRFVEL3NQUVYyMHZHSldzaFdUaitXTGo2bXVGelRpK1FkZ2kyQW8wTXpPQmcrdHdwQ0E9PQo=", + "url": "https://github.com/hepengju/redis-me/releases/download/v4.3.0/RedisME_4.3.0_arm64-setup.exe" } } } diff --git a/docs/zh/changelog/latest.md b/docs/zh/changelog/latest.md index 0e78f223..1d2e4109 100644 --- a/docs/zh/changelog/latest.md +++ b/docs/zh/changelog/latest.md @@ -4,7 +4,7 @@ ### ✨ 新增优化 -- 终端: **输出对齐 redis-cli** TTY/Raw/JSON/CSV 格式 +- 终端: **输出对齐 redis-cli** TTY/Raw/JSON/CSV 格式 #134 - 空值: 新增键、值编辑、字段编辑均支持空字符串保存 - 键值页 - 表格支持**单行刷新和单行复制为命令** From 4d8352365e217ec93a5dcc358aaa026371ff2de5 Mon Sep 17 00:00:00 2001 From: hepengju Date: Sat, 11 Jul 2026 16:08:23 +0800 Subject: [PATCH 35/82] =?UTF-8?q?feat:=20=E5=AD=97=E6=AE=B5=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E6=94=AF=E6=8C=81=20pattern/exact=20=E4=B8=8E?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- plans/11_field-scan-pattern.md | 456 +++++++++++++++++++++++++++ plans/README.md | 6 + src-tauri/src/client/client_trait.rs | 280 ++++++++-------- src-tauri/src/client/mod.rs | 4 +- src-tauri/src/utils/model.rs | 9 +- src/locales/lang/en.ts | 8 +- src/locales/lang/zh-cn.ts | 8 +- src/types/tauri-specta.ts | 12 +- src/utils/redis-glob.ts | 39 +++ src/views/KeyMain.vue | 39 ++- src/views/tab/RedisTauri.vue | 11 +- src/views/tab/RedisValue.vue | 431 +++++++++++++++++++------ 12 files changed, 1031 insertions(+), 272 deletions(-) create mode 100644 plans/11_field-scan-pattern.md diff --git a/plans/11_field-scan-pattern.md b/plans/11_field-scan-pattern.md new file mode 100644 index 00000000..a6cebadf --- /dev/null +++ b/plans/11_field-scan-pattern.md @@ -0,0 +1,456 @@ +# Hash/Set/ZSet 字段扫描改造方案 + +> **实现状态**:待实现 +> **关联 backlog**:[docs/zh/changelog/future.md](../docs/zh/changelog/future.md) 第 4 行「Hash/Set/ZSet的扫描模式支持」 +> **参考实现**:键扫描 [06_scan-keys-optimization.md](./06_scan-keys-optimization.md)、[KeyMain.vue](../src/views/KeyMain.vue) + +## 背景 + +Hash / Set / ZSet 等集合类型的字段扫描目前: + +- **无 pattern / 精确查询**:表格上方 `tableKeyword` 仅在前端做本地模糊筛选,不会传给 Redis HSCAN/SSCAN/ZSCAN。 +- **循环在后端**:`field_scan0` 内层 `loop` 多轮扫描,直到凑够 `count` 或 `load_all` 扫完;与键扫描(前端 `scanKeyCore` / `scanKeyAuto` 驱动)不一致。 + +目标:改为与键扫描相同的形式——**pattern 输入 + 是否精确查询 + 前端控制循环**。 + +## 已确认范围 + +| 项 | 结论 | +| -------------------- | -------------------------------------------------------------------------------------------------------- | +| Hash pattern / exact | **仅匹配 field 名**(Redis HSCAN 语义),不匹配 value | +| **输入框规则** | **与键扫描完全一致**(见下节 `buildScanPattern`);勾选 exact 后走 HGET / SISMEMBER / ZSCORE,不走 HSCAN | +| List / Stream | 纳入**前端循环**改造;Stream 范围用 **minId/maxId**;List/Stream 表格仍用**本地模糊筛选** | +| `load_all` 字段 | **删除**;「加载全部」改由前端 `fieldScanAll` 循环驱动 | +| **顶栏 hashKey** | **删除**(Hash/Stream 顶栏子键/ID 输入及 `withHashKey` 单条模式);见下节 | + +--- + +## 输入框与 pattern 规则(与键扫描一致) + +**复用** [redis-glob.ts](../src/utils/redis-glob.ts) 的 `buildScanPattern(keyword, exact)`,**不传** `loadFolder`(字段扫描无目录模式)。 + +与 [KeyMain.vue](../src/views/KeyMain.vue) 相同的前端计算: + +```typescript +const fieldMatch = computed(() => buildScanPattern(fieldKeyword.value, fieldExact.value)) +// invoke 时:match: fieldMatch.value,exact: fieldExact.value +``` + +### 未勾选「完全匹配」(走 HSCAN / SSCAN / ZSCAN + MATCH) + +| 用户输入 | 传给后端的 pattern | 含义 | +| -------- | ------------------ | -------------------------------------------- | +| (空) | `*` | 全量扫描 | +| `app` | `*app*` | 包含 app | +| `app*` | `app*` | 以 app 开头 | +| `*app` | `*app` | 以 app 结尾 | +| `app?x` | `app?x` | 含 `*` / `?` / `[` 时**原样**作为 Redis glob | + +后端:`exact=false`,`field_scan_1_cmd` 带 `MATCH pattern`(pattern 为 `*` 时可省略 MATCH,与键扫描一致)。 + +### 勾选「完全匹配」(不走 SCAN,走单条命令) + +| 类型 | 后端命令 | 说明 | +| ---- | ---------------------- | --------------------------------------------------------- | +| Hash | `HGET key field` | 输入为**完整 field 名**(含 `*` 也按字面量,不展开 glob) | +| Set | `SISMEMBER key member` | 输入为**完整 member** | +| ZSet | `ZSCORE key member` | 输入为**完整 member**;命中则返回 `{value, score}` | + +- `exact=true` 时:**不**进入 HSCAN/SSCAN/ZSCAN;一次 invoke 返回 0/1 条,`cursor.finished=true`。 +- 未命中:空列表,finished=true(与键扫描 `EXISTS` 未命中一致)。 + +### 触发方式(对齐键扫描) + +- **Enter** 或 suffix **搜索图标** → 从头 scan(`cursor=null`,`fieldScanAuto` 凑满一页阈值)。 +- **切换 exact 勾选**:与键扫描相同——**不立即重新 scan**,仅更新本地展示过滤(见已确认决策 #1);按 Enter / 点搜索才重新请求后端。 +- 扫描进行中:输入框 `readonly`(与键扫描 `loading` 时一致)。 + +### tooltip 文案 + +与 `keyMain.exactSearch` 结构相同,对象改为 field / member(Hash 注明只匹配 field 名)。可新增 `redisValue.fieldExactSearch`,或参数化复用。 + +--- + +## 现状 vs 目标 + +```mermaid +flowchart LR + subgraph now [当前] + FE1[RedisValue.refreshKey] -->|一次 invoke| BE1[field_scan0 内层 loop] + BE1 -->|多轮 HSCAN| Redis1[(Redis)] + FE1 -->|tableKeyword| LocalFilter[前端本地 filterDataList] + end + + subgraph target [目标] + FE2[fieldScanCore 单次] -->|一次 HSCAN| BE2[field_scan0 无 loop] + FE2 --> FE2Auto[fieldScanAuto / fieldScanAll] + FE2Auto --> FE2 + Pattern[buildScanPattern + exact] --> FE2 + end +``` + +| 维度 | 键扫描(已实现) | 字段扫描(当前) | 字段扫描(目标) | +| ----------- | ------------------------------------------------- | -------------------------------- | ------------------------------------------------------- | +| Pattern | `buildScanPattern` + `exact` → `ScanParam` | 无;`tableKeyword` 仅本地 filter | Hash/Set/ZSet:`match` + `exact` 传后端 MATCH | +| 循环 | 前端 `scanKeyCore` / `scanKeyAuto` / `scanKeyAll` | 后端 `field_scan0` 内 `loop` | 前端 `fieldScanCore` / `fieldScanAuto` / `fieldScanAll` | +| 精确查询 | `EXISTS` 一次返回 | 无 | Hash:`HGET` / Set:`SISMEMBER` / ZSet:`ZSCORE` | +| List/Stream | 不适用 | 后端 `load_all` 一次拉完 | 前端循环;不加 pattern/exact | + +--- + +## 关于 `load_all`:建议直接删除 + +当前 `FieldScanParam.load_all` 仅被以下位置使用: + +| 文件 | 用途 | +| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [client_trait.rs](../src-tauri/src/client/client_trait.rs) | Hash/Set/ZSet 内层 loop 扫到完;List LRANGE `-1`;Stream 不传 COUNT | +| [RedisValue.vue](../src/views/tab/RedisValue.vue) | 「加载剩余所有」按钮传 `loadAll: true` | +| [RedisTauri.vue](../src/views/tab/RedisTauri.vue) / [mod.rs](../src-tauri/src/client/mod.rs) | mock / 测试默认值 | +| [tauri-specta.ts](../src/types/tauri-specta.ts) | 类型定义 | + +**无其他调用方**。前端改为 `fieldScanAll()` 递归 invoke 后,后端不再需要该字段。 + +改造约定: + +- 从 `FieldScanParam` **移除** `load_all`。 +- 前端 `refreshKey(..., loadAll)` 的第三参可保留为**纯前端语义**(是否循环到 `cursor.finished`),但 **不再写入 invoke 参数**。 +- `buildFieldScanParam` 不再传 `loadAll` 字段。 + +--- + +## 一、后端改造(Rust / Tauri) + +### 1. 扩展并精简 `FieldScanParam` — [model.rs](../src-tauri/src/utils/model.rs) + +**新增**(与 `ScanParam` 对齐): + +```rust +/// HSCAN/SSCAN/ZSCAN 的 MATCH pattern(前端 serde 字段名 match,与 ScanParam 一致) +#[serde(rename = "match")] +pattern: String, +/// 完全匹配:true 时走 HGET / SISMEMBER / ZSCORE,false 时走 *SCAN + MATCH +exact: bool, +``` + +前端 invoke 与键扫描对齐:`match: fieldMatch.value`,`exact: fieldExact.value`。 + +**删除**: + +```rust +load_all: bool, // 由前端 fieldScanAll 循环替代 +hash_key: Option, // 顶栏单 field / Stream ID;改由 exact 与 meta.min_id/max_id 替代 +``` + +`field_scan_0_get` 中依赖 `hash_key` 的分支一并删除: + +- Hash:`HGET` 单 field → 改由 `exact=true` + `field_scan_0_exact` +- Stream:`XRANGE id id` 单 entry → 改由 **minId=maxId=目标 ID**(表格 meta)或本地 `tableKeyword` 筛 id + +`field_scan0` / `field_scan_4_return` 中的 `with_field_key` 逻辑可随之简化(field_scan 路径恒为 false)。 + +Specta 重生成后同步 [tauri-specta.ts](../src/types/tauri-specta.ts)。 + +### 2. 单次扫描 + 精确路径 — [client_trait.rs](../src-tauri/src/client/client_trait.rs) + +**新增 `field_scan_0_exact`**(类比 `scan_0_exact`,**仅在 `exact=true` 时调用**): + +| 类型 | exact 实现 | 返回 | +| ---- | ---------------------------------------------------- | ------------------------ | +| Hash | `HGET key field`(不存在则空列表;有 HTTL 则补 ttl) | 单条 `{key,value[,ttl]}` | +| Set | `SISMEMBER` → 存在则取 member 本身组装一行 | 0/1 条 member | +| ZSet | `ZSCORE` → 存在则 `{value, score}` | 0/1 条 | + +- `cursor.finished = true`;`length` 仍走 `resolve_field_scan_length`(键内总条数,非当前结果数)。 + +**改造 `field_scan_1_cmd`**: + +```rust +// 现状:HSCAN key cursor COUNT n +// 目标:HSCAN key cursor [MATCH pattern] COUNT batch_count +``` + +- `exact=true` 时走 exact 路径,不进入 scan。 +- pattern 为空/`*` 时可省略 MATCH(与键扫描一致)。 +- COUNT 可复用 `scan_0_batch_count(pattern)`;**前端累积阈值**仍用 settings `fieldScanCount`。 + +**改造 `field_scan0`**: + +- Hash/Set/ZSet:**删除**内层 `loop`(约 L215–247),每次 API 只执行**一轮** HSCAN/SSCAN/ZSCAN。 +- 入口先调 `field_scan_0_exact`,有结果则直接返回。 +- List:去掉 `load_all` 分支;每次 LRANGE 只取 `count` 条,cursor 为起始下标。 +- Stream:去掉 `load_all` 时不传 COUNT 的全量 XREVRANGE;每次固定 COUNT。 + +**无需改动的委托层**:[impl_single.rs](../src-tauri/src/client/impl_single.rs)、[impl_cluster.rs](../src-tauri/src/client/impl_cluster.rs)、[api.rs](../src-tauri/src/api.rs)(命令名 `field_scan` 不变)。 + +### 3. 测试 — [mod.rs](../src-tauri/src/client/mod.rs) + +- `test_field_scan_param`:补 `match` / `exact`,去掉 `load_all`、`hash_key`。 +- 新增:带 MATCH 的 hash 分页(两次 invoke cursor 递增)、exact 命中/未命中、List/Stream 单页 + cursor 续扫。 + +--- + +## 二、前端改造(Vue / TS) + +### 1. 核心文件 — [RedisValue.vue](../src/views/tab/RedisValue.vue) + +**状态**(参考 [KeyMain.vue](../src/views/KeyMain.vue)) + +| 新增/调整 | 说明 | +| -------------------------------------------- | ----------------------------------------------------------------- | +| `fieldKeyword` / `fieldExact` / `fieldMatch` | Hash/Set/ZSet 服务端搜索(UI 对齐 KeyMain) | +| `filterFieldPattern` / `filterFieldList` | 切换 exact 未 Enter 时本地 minimatch | +| `fieldScanPaused` / 进度环等 | 与 KeyMain 一致 | +| `tableKeyword` | **List/Stream** 本地模糊筛选(Stream 可筛 entry id) | +| ~~`hashKey` / `withHashKey`~~ | **删除** | +| ~~`stringTypeOrWithHashKey`~~ | **删除**;`stringTypeOrWithHashKey` 相关分支还原为仅 `stringType` | + +**扫描函数**(对齐键扫描命名): + +```typescript +async function fieldScanCore(useCursor = false): Promise +async function fieldScanAuto(fetchedCount = 0): Promise // 累积至 fieldScanCount 停止 +async function fieldScanAll(): Promise // 直到 cursor.finished +async function refreshFieldScan(useCursor, loadAll, reset): Promise +``` + +- `buildFieldScanParam`:增加 `match`、`exact`;**不再传 `loadAll` / `hashKey`**。 +- `resetParam`:去掉 `hashKey` / `withHashKey` 清理。 +- **删除顶栏** `value-header-hash` 输入框(`v-if="hashType || streamType"` 整块)。 +- 底部 segmented(json/table)条件中去掉 `withHashKey` 判断。 + +**表格数据** + +- Hash/Set/ZSet:`dataList` 直接来自 `redisValue`,**移除** `filterDataList` 对这三类的 keyword 过滤。 +- List/Stream:保留 `tableKeyword` + `filterDataList`。 + +**UI**(表格视图工具栏,约 L1231–1254) + +- Hash/Set/ZSet:**输入框交互对齐 KeyMain 键搜索框**(suffix:进度环 + 暂停/继续 + 搜索图标 + exact checkbox);placeholder 参考 `keyMain.keyword`。 +- 进度环逻辑对齐 [KeyMain.vue](../src/views/KeyMain.vue):`scanProgress` 可按 `redisValue.length`(HLEN/SCARD/ZCARD)估算;`showScanControl` / `onScanAction` / `scanPaused` 同名语义。 +- Enter / 搜索图标 → 从头 scan。 +- List/Stream:仍为「模糊筛选」,不显示 exact checkbox。 + +**其他调用点**(保存/删字段后的 `refreshKey(false)`):无 keyword 时 match=`*`;**有 keyword 时保留 keyword 并带同一 match 重新 scan**。 + +### 2. 工具复用 — [redis-glob.ts](../src/utils/redis-glob.ts) + +- 复用 `buildScanPattern` / `isRedisGlob`;无需 `loadFolder`。 + +### 3. 类型与 mock + +- [tauri-specta.ts](../src/types/tauri-specta.ts):Specta 重生成。 +- [RedisTauri.vue](../src/views/tab/RedisTauri.vue):`minimalFieldScan` 去掉 `loadAll` / `hashKey`,补 `match` / `exact`。 + +### 4. i18n — [zh-cn.ts](../src/locales/lang/zh-cn.ts) / [en.ts](../src/locales/lang/en.ts) + +- 拆分或调整 `tableKeyword` → `fieldScanPlaceholder`、`fieldExactSearch`。 +- 复用或镜像 `keyMain.scanning` / `pauseScan` / `resumeScan`(字段扫描上下文)。 +- [Setting.vue](../src/views/ext/Setting.vue):`fieldScanCountTip` 改为「每轮扫描累积条数阈值(前端控制循环)」。 + +--- + +## 三、行为说明与边界 + +| 场景 | 行为 | +| --------------------------------------- | ------------------------------------------------------------------- | +| 打开 Hash 键、无 keyword | match=`*`,扫一页(fieldScanCount 条)→ 「加载更多」 | +| 输入 `app` + Enter | match=`*app*`,前端 auto 循环直到凑满 fieldScanCount 或 finished | +| 输入 `app*` + Enter | match=`app*`,只匹配以 app 开头的 field/member | +| 勾选 exact + `field1` + Enter | 后端 HGET(或 SISMEMBER/ZSCORE),0/1 条,finished=true | +| 有 keyword 时点「加载更多/全部」 | **沿用当前 match + exact**,cursor 续扫 | +| 切换键 / reset(`KEY_REFRESH`、选新键) | 清空 fieldKeyword、fieldExact、cursor、列表 | +| Hash 查单 field | 表格 **exact + field 名** → HGET,双击/FieldSet 编辑 | +| Stream 查单 entry | **minId = maxId = 目标 ID** 后 refresh;或 `tableKeyword` 本地筛 id | +| List 加载全部 | 前端 `fieldScanAll` 多次 LRANGE | +| Stream 加载全部 | 前端循环 XREVRANGE + cursor | +| 集群模式 | 与现 field_scan 相同;HSCAN MATCH 行为一致 | + +**刻意不做(本次)** + +- Hash **value** 维度搜索(留给 future「Hash仅查询键」等)。 +- Stream entry 的 **exact 模式**(按 ID 精确查仍用 minId/maxId 或本地筛选,不新增 Stream exact API)。 +- 键扫描的**搜索历史**。 + +--- + +## 四、已确认决策 + +| # | 结论 | +| --- | ------------------------------------------------------------------------------------------------------ | +| 1 | 切换 exact 未 Enter:**加** `filterFieldList` + minimatch,Enter 才 re-scan | +| 2 | **进度环 + 暂停/继续**:**本次 P0 做**,与 KeyMain 保持一致 | +| 3 | 搜索历史:**不做** | +| 4 | API 字段名:**`match`**(serde rename,与 ScanParam 一致) | +| 5 | **顶栏 hashKey 整块删除**(含 `FieldScanParam.hash_key`、后端单条分支、`withHashKey` UI) | +| 6 | JSON 视图:**不提供** field 搜索(仅 table) | +| 7 | exact 未命中:**静默空表** | +| 8 | 服务端 MATCH 区分大小写;本地 filter **nocase** | +| 9 | bytesFormat / 保存字段后:**保留 keyword 并 re-scan** | +| 10 | 手动「刷新列表」(菜单/F5 若绑定):**保留 keyword**,`restart` 重扫(与 KeyMain F5 一致,**不清空**) | + +--- + +## 五、移除顶栏 hashKey(Hash + Stream) + +### 决策 + +**整块去掉**顶栏 hashKey / Stream ID 输入及相关逻辑,降低维护成本: + +- **Hash**:单 field 查询与 **exact + HGET** 等价;编辑走表格行 + FieldSet。 +- **Stream**:已有 **minId / maxId** 限制 XREVRANGE 范围;查固定 entry 可设 `minId = maxId = id`,或在表格 **tableKeyword** 本地筛 id。 + +### 前端删除清单 — [RedisValue.vue](../src/views/tab/RedisValue.vue) + +| 移除项 | 说明 | +| ----------------------------------- | --------------------------------------------------------------------------------------------------- | +| `hashKey` / `withHashKey` ref | 状态 | +| `stringTypeOrWithHashKey` | 改回仅用 `stringType`;`watchEffect` / `applyDefaultViewType` / `showValue` / `setValue` 等分支收紧 | +| 顶栏 `value-header-hash` `el-input` | 模板 | +| `buildFieldScanParam` 的 `hashKey` | invoke 参数 | +| `resetParam` 内 hashKey 清理 | | +| 注释「带有 hashKey 不显示」等 | 底部 toolbar / segmented 条件 | + +**保留**:`FieldSet.vue` / `FieldAdd.vue` 里表单 label「哈希键」——那是**增删改行**时的 field 名,与顶栏无关。 + +### 后端删除清单 — [client_trait.rs](../src-tauri/src/client/client_trait.rs) / [model.rs](../src-tauri/src/utils/model.rs) + +| 移除项 | 说明 | +| ---------------------------------------------------------------- | ----------------------- | +| `FieldScanParam.hash_key` | 模型 + Specta | +| `field_scan_0_get` Hash 分支:`hash_key` 非空 → `HGET` | | +| `field_scan_0_get` Stream 分支:`hash_key` 非空 → `XRANGE id id` | | +| `field_scan0` 中 `with_field_key` 派生自 `hash_key` | 可内联为 false 或删参数 | + +`FieldNotFound`(field_get 用)保留;`FieldNotFoundStream` 仍供 **field_get** 使用,不删。 + +### 用户操作替代 + +| 原顶栏 hashKey 用途 | 替代方式 | +| ------------------------ | ------------------------------------------------------------ | +| Hash 查看/编辑单个 field | 表格搜索 **exact + field 名** → 双击行 / FieldSet | +| Stream 查看单条 entry | minId=maxId=entryId + Enter refresh;或 tableKeyword 过滤 id | + +--- + +## 六、改造文件清单 + +| 优先级 | 文件 | 改动摘要 | +| ------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------- | +| P0 | [model.rs](../src-tauri/src/utils/model.rs) | +match/exact,**-load_all -hash_key** | +| P0 | [client_trait.rs](../src-tauri/src/client/client_trait.rs) | 去 loop、exact/MATCH、List/Stream 单页、**hash_key 单条分支** | +| P0 | [RedisValue.vue](../src/views/tab/RedisValue.vue) | 前端循环 + 搜索 UI + 进度环 + **删顶栏 hashKey** | +| P0 | [tauri-specta.ts](../src/types/tauri-specta.ts) | 类型同步 | +| P1 | [zh-cn.ts](../src/locales/lang/zh-cn.ts) / [en.ts](../src/locales/lang/en.ts) | 文案与 tooltip | +| P1 | [mod.rs](../src-tauri/src/client/mod.rs) | 测试 | +| P1 | [RedisTauri.vue](../src/views/tab/RedisTauri.vue) | mock:去 loadAll/hashKey,补 match/exact | +| P1 | [Setting.vue](../src/views/ext/Setting.vue) | fieldScanCount 说明 | +| P2 | [future.md](../docs/zh/changelog/future.md) | 完成后勾选第 4 项 | + +**预计不改**:`impl_single.rs` / `impl_cluster.rs`(仅委托)、`FieldSet.vue` / `FieldAdd.vue` 表单内 field 名 label(非顶栏)。 + +**i18n 可选清理**:`redisValue.hashKey` / `streamId` 顶栏文案若再无引用可删;`fieldSet.hashKey` 等表单文案保留。 + +--- + +## 七、实施顺序建议 + +1. **后端契约**:model(删 load_all、hash_key;加 match/exact)+ `field_scan0` 单次化 → Rust 测试。 +2. **前端循环 + 进度环**:`fieldScanCore/Auto/All` + KeyMain 同款暂停/进度。 +3. **搜索 UI + 删顶栏 hashKey**:fieldKeyword/exact、`filterFieldList`;移除 `withHashKey` 分支。 +4. **联调**:Hash exact、Stream minId=maxId、List loadAll、暂停/继续。 +5. **收尾**:更新本文「实现状态」、future.md 勾选。 + +--- + +## 八、风险与验证 + +| 风险 | 说明 | +| ----------- | ------------------------------------------------------------------- | +| API 变更 | 删除 `load_all`、`hash_key`;同 PR 内改 RedisValue / 测试 / mock | +| 行为变更 | 失去 Hash「整页当 String 编辑单 field」;改为 exact + 表格/FieldSet | +| Stream 单条 | 顶栏 ID 改为 minId=maxId 或本地筛选;需在 tooltip/文档略作说明 | +| 性能 | pattern 稀疏时前端多轮 invoke(与键扫描相同) | + +**验证**:`vp check`、`vp test`;手动验证 Hash pattern、Set exact、List/Stream 加载全部。 + +--- + +## 九、方案复核(2026-07-11) + +对照当前代码库审阅,**整体可实施**;下列为一致性核对结果与实现时注意点。 + +### 与代码现状对齐 ✓ + +| 方案描述 | 代码现状 | 结论 | +| ---------------------------------- | ------------------------------------------------------------------- | -------------- | +| 后端 `field_scan0` 内层 loop | [client_trait.rs L215–247](../src-tauri/src/client/client_trait.rs) | 准确 | +| `load_all` / `hash_key` 调用面 | 仅 RedisValue、RedisTauri、mod 测试、Specta | 可安全删除 | +| 键扫描参考实现 | KeyMain `scanKeyCore/Auto/All`、`buildScanPattern` | 可直接照搬模式 | +| 顶栏 hashKey 删除面 | RedisValue ~15 处 `hashKey`/`withHashKey`/`stringTypeOrWithHashKey` | 清单完整 | +| FieldSet/FieldAdd 的 hashKey label | 表单 field 名,非 `FieldScanParam` | 正确保留 | + +### 已修正的方案笔误 + +| 项 | 原方案 | 修正 | +| ---------- | --------------- | ------------------------------------------------------------------------------------ | +| 已确认 #10 | F5 清空 keyword | 与 KeyMain 一致:**保留 keyword + restart 重扫**;仅**切换键**(`reset=true`)时清空 | + +### 实现时注意(方案未写细但必做) + +1. **双 COUNT 语义**(对齐键扫描) + - Redis 单次 `HSCAN/SSCAN/ZSCAN` 的 **COUNT** → `scan_0_batch_count(match)`(hint)。 + - 前端 **停止阈值** → `settings.fieldScanCount`(`fieldScanAuto` 累计条数)。 + - 现状后端把 `param.count` 既当 HSCAN COUNT 又当凑满页目标,改造时需拆开。 + +2. **List / Stream 分页探针** + 去掉 `load_all` 后**保留**现有「多取 1 条判 finished」逻辑: + - List:`LRANGE start (start+count)`,返回 `>count` 则 `finished=false`、`cursor+=count`。 + - Stream:首页 `COUNT count+1`,续页 `COUNT count`,弹出末条作 `stream_cursor`。 + +3. **进度环配套状态**(KeyMain 全套,非仅 `scanPaused`) + `scanCancelled`、`scanPaused`、`scanLoadAll`、`scanBatchCount`、`SCAN_CONTROL_MIN_BATCHES`、`showScanControl`、`onScanAction`;暂停后继续应用 `useCursor=true` + 原 `scanLoadAll` 语义。 + +4. **表格数据源拆分** + - Hash/Set/ZSet:`:data="filterFieldList"`(服务端 match + 本地 minimatch 切换 exact)。 + - List/Stream:`:data="filterDataList"`(`tableKeyword` 本地筛)。 + 不宜继续混用一个 `filterDataList` 兼做两类事。 + +5. **`exact` 与类型** + UI 仅 hash/set/zset 展示 exact;invoke 时 List/Stream **固定 `exact: false`**。后端若收到 List/Stream + `exact=true`,应忽略 exact、走原 LRANGE/XREVRANGE(防误用)。 + +6. **`field_scan_0_exact` 的 Hash HTTL** + 批量扫描在 `field_scan_2_value` 里补 HTTL;exact 单条路径需**单独**调 HTTL(与现有 HGET 分支一致)。 + +7. **Set exact 返回值** + `SISMEMBER` 只返回 bool,需在 Rust 侧用**用户输入的 member 字面量**(`match` 字段,非 `*app*` 展开结果)组装表格行。 + +8. **样式** + KeyMain 的 `.scan-control` / `.scan-ring` 等 scoped 样式需复制或抽公共,否则进度环只有逻辑无布局。 + +9. **`resetParam` 扩展** + 除删 hashKey 外,显式清空 `fieldKeyword`、`fieldExact`(切换键时)。 + +10. **Stream minId=maxId** + 现有 XREVRANGE 闭区间语义下可行;需在 Stream 工具栏 tooltip 补一句「查单条 entry:minId 与 maxId 填同一 ID」。 + +### 刻意不做的边界 ✓ 合理 + +- Hash value 搜索、Stream ID exact API、搜索历史:与范围一致。 +- [future.md](../docs/zh/changelog/future.md)「Hash仅查询键」与本次「Hash field 名 pattern/exact」相关但不重复(future 更偏只展示 key 列/不拉 value)。 + +### 风险补充 + +| 项 | 说明 | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| 改造面集中 | 主要动 4 个 P0 文件,但 **RedisValue.vue 改动量大**(循环 + 搜索 UI + 删 hashKey + 进度环),建议分 commit:后端 → 前端循环 → 搜索 UI → 删 hashKey | +| 回归 | `mergeFieldScanPage`、`field_get` 单行刷新、Stream meta Enter 刷新需回归 | +| Specta | 改 model 后需重新生成 `tauri-specta.ts`(项目惯用构建命令) | + +### 复核结论 + +**方案完整、决策闭环,可进入实现。** 实施前以本节「实现时注意」补全细节;#10 已按 KeyMain 行为修正。 + +--- + +_方案文档写于 2026-07-11;2026-07-11 确认进度环 P0、移除顶栏 hashKey(含 FieldScanParam.hash_key 与 withHashKey 全链路)。_ diff --git a/plans/README.md b/plans/README.md index b7524bea..984889df 100644 --- a/plans/README.md +++ b/plans/README.md @@ -24,6 +24,12 @@ | 09 | [09_command-export-format.md](./09_command-export-format.md) | 批量导出/导入 CSV 与 CMD | | 10 | [10_cluster-numbered-databases.md](./10_cluster-numbered-databases.md) | 集群多库(Valkey 9,连接时指定) | +## 待实现(编号) + +| 编号 | 文件 | 概要 | +| ---- | ------------------------------------------------------ | ----------------------------------------- | +| 11 | [11_field-scan-pattern.md](./11_field-scan-pattern.md) | Hash/Set/ZSet 字段扫描 pattern + 前端循环 | + ## 分析 / 参考 | 文件 | 说明 | diff --git a/src-tauri/src/client/client_trait.rs b/src-tauri/src/client/client_trait.rs index 63673543..10f0ba6e 100644 --- a/src-tauri/src/client/client_trait.rs +++ b/src-tauri/src/client/client_trait.rs @@ -152,6 +152,11 @@ pub fn scan_0_batch_count(pattern: &str) -> u64 { } } +/** fieldScan 单次 HSCAN/SSCAN/ZSCAN/LRANGE 的 COUNT,来自 settings.fieldScanCount */ +pub fn field_scan_batch_count(count: u64) -> u64 { + if count == 0 { 20 } else { count } +} + /// 完全匹配时用 EXISTS 判断键是否存在;否则返回 None 走 SCAN /// 注意:EXISTS 路径不校验 scan_type,精确查完整键名时更符合实际使用场景 pub fn scan_0_exact( @@ -197,6 +202,73 @@ pub fn scan_1_cmd(cursor: u64, pattern: &str, batch_count: u64, scan_type: Optio cmd } +pub fn field_scan_0_exact( + conn: &mut impl Commands, + key: &RedisKey, + key_type: &ValueType, + param: &FieldScanParam, + bytes_format: &BytesFormat, + httl_supported: bool, +) -> AnyResult> { + if !param.exact { + return Ok(None); + } + let cc = ScanCursor { + finished: true, + ..Default::default() + }; + let member = ¶m.pattern; + let json = match key_type { + ValueType::Hash => { + let value: Option> = conn.hget(key, member)?; + let mut items = match value { + Some(bytes) => ui_hash_value(&[(member.as_bytes().to_vec(), bytes)], bytes_format), + None => Vec::new(), + }; + if httl_supported && !items.is_empty() { + let field_bytes = member.as_bytes().to_vec(); + if let Ok(ttl_values) = + conn.httl::<_, _, Vec>(key, &[&field_bytes]) + { + if let (Some(item), Some(ttl_reply)) = (items.first_mut(), ttl_values.first()) + { + item.ttl = match ttl_reply { + IntegerReplyOrNoOp::IntegerReply(ttl) => Some(*ttl as i64), + IntegerReplyOrNoOp::NotExists => Some(-2), + IntegerReplyOrNoOp::ExistsButNotRelevant => Some(-1), + _ => None, + }; + } + } + } + serde_json::to_value(items)? + } + ValueType::Set => { + let exists: bool = conn.sismember(key, member)?; + let set = if exists { + ui_set_value( + HashSet::from([member.as_bytes().to_vec()]), + bytes_format, + ) + } else { + Vec::new() + }; + serde_json::to_value(set)? + } + ValueType::ZSet => { + let score: Option = conn.zscore(key, member)?; + let zset = score + .map(|score| { + ui_zset_value(vec![(member.as_bytes().to_vec(), score)], bytes_format) + }) + .unwrap_or_default(); + serde_json::to_value(zset)? + } + _ => return Ok(None), + }; + Ok(Some((json, cc))) +} + pub fn field_scan0( mut conn: MutexGuard, param: FieldScanParam, @@ -204,64 +276,56 @@ pub fn field_scan0( ) -> AnyResult { let bytes_format = param.bytes_format.as_ref().cloned().unwrap_or_default(); - // String, Json, List, Hash(WithKey), Stream(WithKey), Stream 直接获取得到值 + // String, Json, List, Stream 直接获取;Hash/Set/ZSet 走 exact 或 *SCAN let (mut value, key_type, mut cc, length, value_truncated) = field_scan_0_get(&mut conn, ¶m, &bytes_format)?; - // Hash, Set, Zset 进行扫描(hscan, sscan, zscan) - let key = param.key; if value.is_none() { - let mut scan_value = FieldScanValue::default(); - let mut ready_count = 0; - loop { - // 优化字段扫描个数 - let count = if param.load_all { - 1000 - } else if param.count == 0 { - 20 - } else { - param.count - }; - - let cmd = field_scan_1_cmd(&key_type, &key, cc.now_cursor, count)?; + if let Some((exact_value, exact_cc)) = field_scan_0_exact( + &mut conn, + ¶m.key, + &key_type, + ¶m, + &bytes_format, + httl_supported, + )? { + value = Some(exact_value); + cc = exact_cc; + } else { + // 每次 API 只执行一轮 HSCAN/SSCAN/ZSCAN,循环由前端控制;COUNT 用 fieldScanCount,非键扫描 batch + let batch_count = field_scan_batch_count(param.count); + let cmd = field_scan_1_cmd( + &key_type, + ¶m.key, + cc.now_cursor, + ¶m.pattern, + batch_count, + )?; let (next_cursor, new_value): (u64, Value) = cmd.query(&mut conn)?; - let new_count = field_scan_2_value( + let mut scan_value = FieldScanValue::default(); + field_scan_2_value( &mut conn, &key_type, &mut scan_value, new_value, - &key, + ¶m.key, &bytes_format, httl_supported, )?; - - ready_count += new_count; cc.now_cursor = next_cursor; - if next_cursor == 0 { cc.finished = true; - break; - } - - if !param.load_all && ready_count >= param.count as usize { - break; } + value = Some(field_scan_3_json(&key_type, &scan_value)?); } - value = Some(field_scan_3_json(&key_type, &scan_value)?) } - let with_field_key = param - .hash_key - .as_ref() - .is_some_and(|k| !k.is_empty()); - // 返回值添加 TTL、内存占用与元素总数 field_scan_4_return( conn, - key, + param.key, key_type, value.unwrap_or_default(), cc, length, - with_field_key, value_truncated, ) } @@ -294,7 +358,6 @@ pub fn field_scan_0_get( bytes_format: &BytesFormat, ) -> AnyResult<(Option, ValueType, ScanCursor, usize, bool)> { let key = ¶m.key; - let hash_key = param.hash_key.clone(); let key_type: ValueType = conn.key_type(key)?; let mut cc = param.cursor.clone().unwrap_or_default(); @@ -320,41 +383,18 @@ pub fn field_scan_0_get( ValueType::JSON => { let value: Value = redis::cmd("JSON.GET").arg(key).query(&mut conn)?; cc.finished = true; - //Some(serde_json::to_value(redis_value_to_string(value, "\n"))?) Some(serde_json::from_str(&redis_value_to_string(value, "\n"))?) } - ValueType::Hash => { - if let Some(hash_key) = hash_key - && !hash_key.is_empty() - { - let value: Option> = conn.hget(key, &hash_key)?; - match value { - Some(str) => { - length = str.len(); - cc.finished = true; - Some(serde_json::to_value(format_bytes(&str, bytes_format))?) - } - None => bail!(AppError::FieldNotFound { hash_key }), - } - } else { - None - } - } + ValueType::Hash => None, ValueType::List => { - // 如果你有一个从 0 到 100 的数字列表,LRANGE list 0 10 将返回 11 个元素,也就是说,最右边的项是包含在内的 - // 超出范围的索引不会产生错误。如果 start 大于列表的末尾,将返回一个空列表。如果 stop 大于列表的实际末尾,Redis 会将其视为列表的最后一个元素。 - let end_index: isize = if param.load_all { - -1 - } else { - (cc.now_cursor + param.count) as isize - }; + let count = field_scan_batch_count(param.count); + let end_index: isize = (cc.now_cursor + count) as isize; let value: Vec> = conn.lrange(key, cc.now_cursor as isize, end_index)?; - // 0 ~ 10 获取到11元素, 表示还没有获取到全部数据。序号为10的元素本次不取 - let value: Vec = if !param.load_all && value.len() > param.count as usize { + let value: Vec = if value.len() > count as usize { cc.finished = false; - cc.now_cursor += param.count; - ui_list_value(&value[0..param.count as usize], bytes_format) + cc.now_cursor += count; + ui_list_value(&value[0..count as usize], bytes_format) } else { cc.finished = true; ui_list_value(&value, bytes_format) @@ -362,72 +402,41 @@ pub fn field_scan_0_get( Some(serde_json::to_value(value)?) } ValueType::Stream => { - // stream的id, 复用hash_key字段 - if let Some(hash_key) = hash_key - && !hash_key.is_empty() - { - let mut reply: StreamRangeReply = conn.xrange(key, &hash_key, &hash_key)?; - match reply.ids.pop() { - Some(entry) => { - cc.finished = true; - Some(serde_json::to_value(ui_stream_id(entry.map))?) - } - None => bail!(AppError::FieldNotFoundStream { - stream_id: hash_key - }), + let count = field_scan_batch_count(param.count); + let end = if cc.stream_cursor.is_empty() { + match param.meta.as_ref() { + Some(meta) if !meta.max_id.is_empty() => &meta.max_id, + _ => "+", } } else { - // https://redis.ac.cn/docs/latest/commands/xrange/ - // XRANGE key start end [COUNT count] 注意start/end是包含在内的 - // 起始参数: - 表示最小值, 游标有值则取值 - // 结束参数: + 表示最大值 - // 加载所有: 不追加count参数,否则追加count + 1参数,多获取1个用于判断是否已扫描结束 - //let start = if cc.stream_cursor.is_empty() { "-" } else { &cc.stream_cursor }; - //let end = "+"; - //let count = if cc.stream_cursor.is_empty() { param.count + 1 } else { param.count }; - //let mut cmd = redis::cmd("XRANGE"); - //cmd.arg(key).arg(start).arg(end); - - // 倒序: 更符合实际的使用习惯, 即查看最新的消息。TinyRDM/AnotherRDM都是倒序的 - // XREVRANGE key end start [COUNT count] - let end = if cc.stream_cursor.is_empty() { - match param.meta.as_ref() { - Some(meta) if !meta.max_id.is_empty() => &meta.max_id, - _ => "+", - } - } else { - &cc.stream_cursor - }; + &cc.stream_cursor + }; - let start = match param.meta.as_ref() { - Some(meta) if !meta.min_id.is_empty() => &meta.min_id, - _ => "-", - }; + let start = match param.meta.as_ref() { + Some(meta) if !meta.min_id.is_empty() => &meta.min_id, + _ => "-", + }; - let count = if cc.stream_cursor.is_empty() { - param.count + 1 - } else { - param.count - }; + let scan_count = if cc.stream_cursor.is_empty() { + count + 1 + } else { + count + }; - let mut cmd = redis::cmd("XREVRANGE"); - cmd.arg(key).arg(end).arg(start); - if !param.load_all { - cmd.arg("COUNT").arg(count); - } - let reply: StreamRangeReply = cmd.query(&mut conn)?; - let mut value = ui_stream_value(reply); - - if !param.load_all && value.len() > param.count as usize { - cc.finished = false; - cc.stream_cursor = value.pop().unwrap().id; // 弹出最后1个元素,作为下次游标 - } else { - cc.finished = true; - }; - Some(serde_json::to_value(value)?) - } + let mut cmd = redis::cmd("XREVRANGE"); + cmd.arg(key).arg(end).arg(start); + cmd.arg("COUNT").arg(scan_count); + let reply: StreamRangeReply = cmd.query(&mut conn)?; + let mut value = ui_stream_value(reply); + + if value.len() > count as usize { + cc.finished = false; + cc.stream_cursor = value.pop().unwrap().id; + } else { + cc.finished = true; + }; + Some(serde_json::to_value(value)?) } - // 注意此处SET/ZSET等是支持的,只是需要进行扫描,不能直接使用通用的: handle_other_value_type ValueType::Unknown(_) => { handle_other_value_type(&key_type, key)?; None @@ -441,7 +450,8 @@ pub fn field_scan_1_cmd( key_type: &ValueType, key: &RedisKey, cursor: u64, - count: u64, + pattern: &str, + batch_count: u64, ) -> AnyResult { let scan_command = match key_type { ValueType::Hash => "hscan", @@ -452,12 +462,12 @@ pub fn field_scan_1_cmd( }), }; - // SCAN cursor [MATCH pattern] [COUNT count] [TYPE type] - // HSCAN key cursor [MATCH pattern] [COUNT count] [NOVALUES] - // SSCAN key cursor [MATCH pattern] [COUNT count] - // ZSCAN key cursor [MATCH pattern] [COUNT count] let mut cmd = redis::cmd(scan_command); - cmd.arg(key).arg(cursor).arg("count").arg(count); + cmd.arg(key).arg(cursor); + if !pattern.is_empty() && pattern != "*" { + cmd.arg("MATCH").arg(pattern); + } + cmd.arg("COUNT").arg(batch_count); Ok(cmd) } @@ -529,17 +539,13 @@ pub fn field_scan_3_json( Ok(value) } -/// 集合类型用 HLEN/LLEN 等填充 length;String/单字段仍用已算好的 bytes 长度 +/// 集合类型用 HLEN/LLEN 等填充 length;String 仍用已算好的 bytes 长度 fn resolve_field_scan_length( conn: &mut MutexGuard, key: &RedisKey, key_type: &ValueType, field_byte_len: usize, - with_field_key: bool, ) -> AnyResult { - if with_field_key { - return Ok(field_byte_len); - } let len = match key_type { ValueType::String => field_byte_len, ValueType::Hash => conn.hlen(key)?, @@ -559,7 +565,6 @@ pub fn field_scan_4_return( value: serde_json::Value, cursor: ScanCursor, length: usize, - with_field_key: bool, value_truncated: bool, ) -> AnyResult { let ttl: i64 = conn.ttl(&key)?; @@ -567,9 +572,8 @@ pub fn field_scan_4_return( .arg("usage") .arg(&key) .query(&mut conn) - // 兼容腾讯云Redis等不支持memory usage的第三方缓存数据库 #81 .unwrap_or(0); - let length = resolve_field_scan_length(&mut conn, &key, &key_type, length, with_field_key)?; + let length = resolve_field_scan_length(&mut conn, &key, &key_type, length)?; Ok(FieldScanResult { key_type: ui_key_type(key_type), diff --git a/src-tauri/src/client/mod.rs b/src-tauri/src/client/mod.rs index 464fa8de..b56f0540 100644 --- a/src-tauri/src/client/mod.rs +++ b/src-tauri/src/client/mod.rs @@ -209,10 +209,10 @@ mod tests { key: key.to_string(), bytes: vec![], }, - hash_key: None, count: 150, cursor: None, - load_all: false, + pattern: "*".into(), + exact: false, meta: None, bytes_format: Some(BytesFormat::Base64), value_byte_limit: None, diff --git a/src-tauri/src/utils/model.rs b/src-tauri/src/utils/model.rs index 6a608925..4976233b 100644 --- a/src-tauri/src/utils/model.rs +++ b/src-tauri/src/utils/model.rs @@ -315,11 +315,14 @@ impl ScanParam { api_model!(FieldScanParam { key: RedisKey, - hash_key: Option, // Hash键 或 StreamId count: u64, cursor: Option, - load_all: bool, - meta: Option, // 扩展参数 + /// HSCAN/SSCAN/ZSCAN 的 MATCH pattern(前端字段名 match) + #[serde(rename = "match")] + pattern: String, + /// 完全匹配:true 时走 HGET / SISMEMBER / ZSCORE + exact: bool, + meta: Option, // Stream 范围 min_id / max_id bytes_format: Option, // 扫描/展示用字节格式 /// STRING 全量加载字节上限;超过且未 force 时仅 GETRANGE 预览前 value_preview_bytes value_byte_limit: Option, diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index e9cc5029..41278ba1 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -76,7 +76,8 @@ export default { fieldScanCount: 'Field Scan', keyScanCountTip: 'Number of keys loaded per SCAN; too large may affect performance. Range {min}–{max}', - fieldScanCountTip: 'Fields loaded per HSCAN or SSCAN. Range {min}~{max}', + fieldScanCountTip: + 'HSCAN/SSCAN/ZSCAN COUNT per request; also the cumulative threshold for frontend auto-load. Range {min}~{max}', commandTimeout: 'Cmd Timeout', commandTimeoutTip: 'Max wait for one Redis command on an open connection; takes effect after reconnect. Range {min}–{max} s', @@ -804,7 +805,10 @@ export default { locationTitle: 'Cluster Node', slotHint: 'View the cluster slot where the key is located', slotTitle: 'Cluster Slot', - tableKeyword: 'Fuzzy Filter', + tableKeyword: 'Local filter', + fieldScanPlaceholder: 'Scan params / local filter', + fieldExactSearch: + 'Scan (unchecked)
app: contains app
app*: starts with app
*app: ends with app
Use * ? [ as glob when present

Exact (checked)
Whether a field/member exactly matches input (Hash matches field name only)', insertRow: 'Insert Row', id: 'ID', key: 'Key', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index 115aa5eb..6b76fdcb 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -70,7 +70,8 @@ export default { keyScanCount: '键扫描', fieldScanCount: '字段扫描', keyScanCountTip: '键列表每次 SCAN 加载的数量;过大可能影响性能。范围 {min}–{max} 个', - fieldScanCountTip: '每次 HSCAN、SSCAN 加载的数量。范围 {min}~{max} 个', + fieldScanCountTip: + 'HSCAN/SSCAN/ZSCAN 每次 COUNT;同时作为前端自动续扫的累积条数阈值。范围 {min}~{max} 个', commandTimeout: '命令超时', commandTimeoutTip: '单次 Redis 命令读写的最长等待时间;修改后需重连生效。范围 {min}–{max} 秒', scriptTimeout: '脚本超时', @@ -791,7 +792,10 @@ export default { locationTitle: '集群节点', slotHint: '查看键所在集群槽位', slotTitle: '集群槽位', - tableKeyword: '模糊筛选', + tableKeyword: '本地过滤', + fieldScanPlaceholder: '扫描参数 / 本地过滤', + fieldExactSearch: + '扫描(未勾选)
app: 包含 app
app*: 以 app 开头
*app: 以 app 结尾
含 * ? [ 时原样作为模式

精确(勾选)
判断 field/member 与输入完全一致是否存在(Hash 仅匹配 field 名)', insertRow: '插入行', id: 'ID', key: '键', diff --git a/src/types/tauri-specta.ts b/src/types/tauri-specta.ts index 7a6fc71c..79c4ddfa 100644 --- a/src/types/tauri-specta.ts +++ b/src/types/tauri-specta.ts @@ -160,10 +160,12 @@ export type FieldScanParam = FieldScanParam_Serialize | FieldScanParam_Deseriali export type FieldScanParam_Deserialize = { key: RedisKey_Deserialize, - hashKey: string | null, count: number, cursor: ScanCursor | null, - loadAll: boolean, + /** HSCAN/SSCAN/ZSCAN 的 MATCH pattern(前端字段名 match) */ + match: string, + /** 完全匹配:true 时走 HGET / SISMEMBER / ZSCORE */ + exact: boolean, meta: FiledScanMeta | null, bytesFormat: BytesFormat | null, /** STRING 全量加载字节上限;超过且未 force 时仅 GETRANGE 预览前 value_preview_bytes */ @@ -174,10 +176,12 @@ export type FieldScanParam_Deserialize = { export type FieldScanParam_Serialize = { key: RedisKey_Serialize, - hashKey: string | null, count: number, cursor: ScanCursor | null, - loadAll: boolean, + /** HSCAN/SSCAN/ZSCAN 的 MATCH pattern(前端字段名 match) */ + match: string, + /** 完全匹配:true 时走 HGET / SISMEMBER / ZSCORE */ + exact: boolean, meta: FiledScanMeta | null, bytesFormat: BytesFormat | null, /** STRING 全量加载字节上限;超过且未 force 时仅 GETRANGE 预览前 value_preview_bytes */ diff --git a/src/utils/redis-glob.ts b/src/utils/redis-glob.ts index c44479ca..a6d17c07 100644 --- a/src/utils/redis-glob.ts +++ b/src/utils/redis-glob.ts @@ -8,6 +8,45 @@ export function escapeMinimatchLiteral(s: string): string { return s.replace(/[\\?*[\]{}()!@+]/g, '\\$&') } +/** 本地 minimatch 过滤(与 KeyMain / RedisValue 表格筛选一致) */ +export const MINIMATCH_SCAN_OPTS = { + nobrace: true, + noglobstar: true, + noext: true, + nocase: true, +} as const + +/** + * 未 Enter 重扫时的本地过滤 pattern:exact 转义字面,否则用服务端 match。 + */ +export function buildLocalFilterPattern(keyword: string, exact: boolean, match: string): string { + const key = keyword.trim() + if (!key) return '' + if (exact) return escapeMinimatchLiteral(key) + return match +} + +/** 与后端 scan_0_batch_count 一致:pattern 去 * 后 ≤1 字符 COUNT=1000,否则 10000 */ +export function computeScanBatchSize(match: string): number { + const stripped = match.replace(/\*/g, '') + return stripped.length <= 1 ? 1000 : 10000 +} + +/** 扫描进度环:按批次估算,finished 时 100% */ +export function computeScanProgress( + batchCount: number, + batchSize: number, + totalEstimate: number, + finished: boolean, +): number { + if (finished) return 100 + if (batchCount === 0) return 0 + if (totalEstimate > 0) { + return Math.min(99, Math.round(((batchCount * batchSize) / totalEstimate) * 100)) + } + return Math.min(99, batchCount * 5) +} + /** * 构建 SCAN 模式(目录扫描 loadFolder 时固定为 keyword:*) * diff --git a/src/views/KeyMain.vue b/src/views/KeyMain.vue index 79efe627..e9ef74f1 100644 --- a/src/views/KeyMain.vue +++ b/src/views/KeyMain.vue @@ -27,7 +27,13 @@ import { isFavorited, } from '@/utils/favorite' import { clearKeyTypeCacheForConn } from '@/utils/key-type-cache' -import { buildScanPattern, escapeMinimatchLiteral } from '@/utils/redis-glob' +import { + buildScanPattern, + buildLocalFilterPattern, + computeScanBatchSize, + computeScanProgress, + MINIMATCH_SCAN_OPTS, +} from '@/utils/redis-glob' import { bus, CONN_REFRESH, @@ -221,10 +227,7 @@ function onKeyListRefreshHotkey(e: KeyboardEvent) { const match = computed(() => buildScanPattern(keyword.value, exact.value, loadFolder.value)) // 与后端 scan_0_batch_count 一致:pattern 去 * 后 ≤1 字符 COUNT=1000,否则 10000 -const scanBatchSize = computed(() => { - const stripped = match.value.replace(/\*/g, '') - return stripped.length <= 1 ? 1000 : 10000 -}) +const scanBatchSize = computed(() => computeScanBatchSize(match.value)) /** 当前库键总量:单机取 INFO dbN;集群为单 master 键数 × master 节点数 */ const dbSize = computed(() => { @@ -237,13 +240,13 @@ const dbSize = computed(() => { // 扫描进度:按 SCAN 批次估算(与匹配结果数量无关,稀有键搜索时进度仍正常推进) const scanProgress = computed(() => { - if (cursor.value?.finished) return 100 - if (!share.conn || scanBatchCount.value === 0) return 0 - if (dbSize.value > 0) { - const scanned = scanBatchCount.value * scanBatchSize.value - return Math.min(99, Math.round((scanned / dbSize.value) * 100)) - } - return Math.min(99, scanBatchCount.value * 5) + if (!share.conn) return 0 + return computeScanProgress( + scanBatchCount.value, + scanBatchSize.value, + dbSize.value, + Boolean(cursor.value?.finished), + ) }) const cursor = ref(null) @@ -253,12 +256,9 @@ const showLoadMoreButtons = computed( ) // 本地过滤模式:精确转义字面,扫描用 match(切换勾选仅更新过滤,回车/查询才重新扫描) -const filterPattern = computed(() => { - const key = keyword.value.trim() - if (!key) return '' - if (exact.value && !loadFolder.value) return escapeMinimatchLiteral(key) - return match.value -}) +const filterPattern = computed(() => + buildLocalFilterPattern(keyword.value, exact.value && !loadFolder.value, match.value), +) const keyList = ref([]) const filterKeyList = computed(() => { @@ -266,8 +266,7 @@ const filterKeyList = computed(() => { let source: RedisKey_Deserialize[] = favoriteMode.value ? currentFavorites.value : keyList.value if (!filterPattern.value) return source - const opts = { nobrace: true, noglobstar: true, noext: true, nocase: true } - return source.filter(k => minimatch(k.key, filterPattern.value, opts)) + return source.filter(k => minimatch(k.key, filterPattern.value, MINIMATCH_SCAN_OPTS)) }) // 搜索自动加载的停止阈值:使用设置中的 keyScanCount diff --git a/src/views/tab/RedisTauri.vue b/src/views/tab/RedisTauri.vue index 9ffef8c4..dd648be9 100644 --- a/src/views/tab/RedisTauri.vue +++ b/src/views/tab/RedisTauri.vue @@ -77,16 +77,21 @@ const emptyScanCursor: ScanCursor = { finished: false, } -const minimalScanParam: ScanParam = { match: '*', type: null, cursor: emptyScanCursor } +const minimalScanParam: ScanParam = { + match: '*', + type: null, + cursor: emptyScanCursor, + exact: false, +} const dummyKey: RedisKey_Deserialize = { key: 'k', bytes: '' } const minimalFieldScan: FieldScanParam_Deserialize = { key: dummyKey, - hashKey: null, count: 100, cursor: null, - loadAll: false, + match: '*', + exact: false, meta: null, bytesFormat: null, valueByteLimit: null, diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index 9870b6be..3dc7c42f 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -5,6 +5,7 @@ */ // #region 导入 import dayjs from 'dayjs' +import { minimatch } from 'minimatch' import { computed, inject, @@ -47,6 +48,12 @@ import { viewFmtForField, type ViewBytesFormat, } from '@/utils/format' +import { + buildScanPattern, + buildLocalFilterPattern, + computeScanProgress, + MINIMATCH_SCAN_OPTS, +} from '@/utils/redis-glob' import { getValueShortcuts } from '@/utils/shortcut' import { bus, @@ -63,6 +70,7 @@ import { meJsonNormal, meOk, meWarn, + sleep, } from '@/utils/util' import TableGroup from '@/views/ext/TableGroup.vue' import TTLSet from '@/views/ext/TTLSet.vue' @@ -107,10 +115,36 @@ const canEdit = computed(() => !share.readonly) const redisValue = ref(null) const cursor = ref(null) // list/hash/set/zset/stream 分页游标 const loading = ref(false) -const hashKey = ref('') // hash 子键 / stream 起始 ID -const withHashKey = ref(false) // 是否处于「单字段」模式(hashKey 非空) const isPretty = ref(true) const tableKeyword = ref('') +/** Hash/Set/ZSet 服务端 field 扫描关键词 */ +const fieldKeyword = ref('') +const fieldExact = ref(false) +const fieldMatch = computed(() => buildScanPattern(fieldKeyword.value, fieldExact.value)) +const scanCancelled = ref(false) +const scanPaused = ref(false) +const scanLoadAll = ref(false) +const scanBatchCount = ref(0) +const SCAN_CONTROL_MIN_BATCHES = 10 +const showScanControl = computed( + () => + supportsFieldServerScan(redisValue.value?.type) && + (scanPaused.value || (loading.value && scanBatchCount.value >= SCAN_CONTROL_MIN_BATCHES)), +) +const scanToggleTip = computed(() => + loading.value ? t('keyMain.pauseScan') : t('keyMain.resumeScan'), +) +const FIELD_SCAN_FETCH_COUNT = computed(() => meTauri.settings.fieldScanCount as number) +/** 进度环估算:与 settings.fieldScanCount 一致,不用键扫描的 scan_0_batch_count */ +const scanBatchSize = computed(() => FIELD_SCAN_FETCH_COUNT.value) +const scanProgress = computed(() => + computeScanProgress( + scanBatchCount.value, + scanBatchSize.value, + redisValue.value?.length ?? 0, + Boolean(cursor.value?.finished), + ), +) const suppressCodeUpdate = ref(false) /** fieldScan 成功后递增,强制 me-code 与服务器同步(未保存时 modelValue 字符串可能不变) */ const valueEditorRemountKey = ref(0) @@ -133,13 +167,9 @@ const meta = ref({ maxId: '', minId: '' }) // #endregion // #region 键类型(派生) -const hashType = computed(() => 'hash' === redisValue.value?.type) const stringType = computed(() => 'string' === redisValue.value?.type) const jsonType = computed(() => 'json' === redisValue.value?.type) const streamType = computed(() => 'stream' === redisValue.value?.type) -const stringTypeOrWithHashKey = computed( - () => 'string' === redisValue.value?.type || withHashKey.value, -) const canSave = computed( () => canEdit.value && @@ -153,6 +183,38 @@ type FieldViewType = 'json' | 'table' const viewTypeList: FieldViewType[] = ['json', 'table'] const viewType = ref('json') +function supportsFieldServerScan(type: string | undefined) { + return type === 'hash' || type === 'set' || type === 'zset' +} + +function pauseFieldScan() { + scanCancelled.value = true + scanPaused.value = true +} + +function onFieldScanAction() { + if (loading.value) pauseFieldScan() + else if (scanPaused.value) { + scanPaused.value = false + void refreshKey(false, true, scanLoadAll.value, false) + } +} + +/** Enter / 搜索图标:保留 keyword,中断进行中的扫描后重扫(无 F5 快捷键) */ +function restartFieldScan() { + return refreshKey(false, false, false, true) +} + +async function onFieldSearch() { + await restartFieldScan() +} + +/** 菜单 / 底部按钮手动刷新:保留 fieldKeyword,可 restart 中断扫描 */ +function manualRefreshKey() { + prepareManualKeyRefresh() + return restartFieldScan() +} + /** 支持表格视图的类型(与底部 segmented 可见条件一致) */ function supportsTableView(type: string | undefined) { return ( @@ -168,7 +230,7 @@ function supportsFieldRowRefresh(type: string | undefined) { /** 切换键或 reset 时,按 settings.fieldShow 决定默认视图 */ function applyDefaultViewType() { const rv = redisValue.value - if (!rv || stringTypeOrWithHashKey.value || jsonType.value) { + if (!rv || stringType.value || jsonType.value) { viewType.value = 'json' return } @@ -194,7 +256,7 @@ function onViewTypeChange(val: string | number | boolean) { // string / json 仅支持 JSON 视图,强制切回 watchEffect(() => { - if (stringTypeOrWithHashKey.value || jsonType.value) { + if (stringType.value || jsonType.value) { viewType.value = 'json' } }) @@ -285,7 +347,7 @@ function syncDisplaySnapshot() { } async function refreshResolvedWireView() { - if (!stringTypeOrWithHashKey.value || !isCustomView(displayBytesFormat.value)) { + if (!stringType.value || !isCustomView(displayBytesFormat.value)) { resolvedWireView.value = '' customCodecFailed.value = false return @@ -322,7 +384,7 @@ const showValue = computed(() => { if (obj === null || obj === undefined) return '' if (isPretty.value) { - if (stringTypeOrWithHashKey.value) { + if (stringType.value) { const str = stringWireDisplayText(displayWire.value) return meFormatDisplayValue(str, isPretty.value) } @@ -330,14 +392,14 @@ const showValue = computed(() => { } if ( - ('hash' === redisValue.value?.type && !withHashKey.value) || + 'hash' === redisValue.value?.type || 'zset' === redisValue.value?.type || 'json' === redisValue.value?.type || 'stream' === redisValue.value?.type ) { return JSON.stringify(obj) } - if (stringTypeOrWithHashKey.value) { + if (stringType.value) { return stringWireDisplayText(displayWire.value) } return obj.toString() @@ -363,11 +425,10 @@ const dataList = computed(() => { if (rv === null || rv === undefined || rv.value === null || rv.value === undefined) return [] const data: ValueTableRow[] = [] - if (rv.type === 'list' || rv.type === 'set') { - fieldValueRows(rv.value).forEach(value => data.push({ value })) - } else if (rv.type === 'zset' || rv.type === 'stream' || rv.type === 'hash') { - fieldValueRows(rv.value).forEach(value => data.push(value as ValueTableRow)) - } + fieldValueRows(rv.value).forEach(value => { + if (rv.type === 'list' || rv.type === 'set') data.push({ value }) + else data.push(value as ValueTableRow) + }) return data }) @@ -383,25 +444,46 @@ const filterDataList = computed(() => { return false }) }) + +/** 切换 exact 未 Enter 时本地 minimatch(与 KeyMain filterKeyList 一致) */ +const filterFieldPattern = computed(() => + buildLocalFilterPattern(fieldKeyword.value, fieldExact.value, fieldMatch.value), +) + +const filterFieldList = computed(() => { + if (!filterFieldPattern.value) return dataList.value + return dataList.value.filter(row => { + const name = row.key ? formatTableCell(row.key) : formatTableCell(row.value) + return minimatch(name, filterFieldPattern.value, MINIMATCH_SCAN_OPTS) + }) +}) + +const tableDisplayList = computed(() => { + const type = redisValue.value?.type + if (type === 'hash' || type === 'set' || type === 'zset') return filterFieldList.value + return filterDataList.value +}) // #endregion // #region 键刷新 fieldScan -/** 切换键或全量刷新时清空 hash 子键、表格筛选等 UI 状态 */ +/** 切换键或全量刷新时清空表格筛选等 UI 状态 */ function resetParam() { tableKeyword.value = '' - hashKey.value = '' - withHashKey.value = false + fieldKeyword.value = '' + fieldExact.value = false } -/** 组装 fieldScan 参数:游标分页、Stream 范围 meta、wire 字节格式 */ -function buildFieldScanParam(loadAll: boolean) { +/** 组装 fieldScan 参数:count 来自 settings.fieldScanCount(HSCAN COUNT + 前端续扫阈值) */ +function buildFieldScanParam() { + const type = redisValue.value?.type + const serverScan = supportsFieldServerScan(type) return { key: share.redisKey!, - hashKey: hashKey.value, count: meTauri.settings.fieldScanCount ?? 10, cursor: cursor.value, - loadAll, + match: serverScan ? fieldMatch.value : '*', + exact: serverScan ? fieldExact.value : false, meta: meta.value, bytesFormat: toWireFormat(bytesFormat.value), valueByteLimit: VALUE_BYTE_LIMIT, @@ -472,60 +554,104 @@ async function finalizeAfterFieldScan(reset: boolean, replaceData?: FieldScanRes loading.value = false } +async function fieldScanCore( + useCursor: boolean, +): Promise<{ count: number; replaceData?: FieldScanResult }> { + const data = await meCommands.fieldScan(share.conn!.id, buildFieldScanParam()) + cursor.value = data.cursor + scanBatchCount.value++ + + if (useCursor) { + const prev = redisValue.value + if (prev && mergeFieldScanPage(prev, data)) { + return { count: fieldValueRows(data.value).length } + } + } + return { count: fieldValueRows(data.value).length, replaceData: data } +} + +async function fieldScanAuto(fetchedCount = 0): Promise { + if (!cursor.value || cursor.value.finished) return + if (scanCancelled.value) return + if (fetchedCount >= FIELD_SCAN_FETCH_COUNT.value) return + + const { count } = await fieldScanCore(true) + await fieldScanAuto(fetchedCount + count) +} + +async function fieldScanAll(): Promise { + if (!cursor.value || cursor.value.finished) return + if (scanCancelled.value) return + + await fieldScanCore(true) + await fieldScanAll() +} + +function shouldFieldScanAuto(type: string | undefined, exact: boolean) { + if (exact || !type) return false + // Hash/Set/ZSet pattern 扫描、List/Stream 前端分页循环 + return supportsFieldServerScan(type) || type === 'list' || type === 'stream' +} + /** * 拉取/刷新当前键(fieldScan → 更新 redisValue → 同步编辑器)。 - * - * 典型调用: - * - refreshKey() / refreshKey(true):选中新键或切换 hashKey,从头 scan - * - refreshKey(false):同键刷新(改 bytesFormat、保存/删字段后) - * - refreshKey(false, true):加载更多(useCursor,追加一页) - * - refreshKey(false, true, true):加载全部 - * - * @param reset 是否 resetParam + 重算 json/table 默认视图 - * @param useCursor 为 true 时保留 cursor,请求下一页并尝试 merge - * @param loadAll 为 true 时后端一次返回剩余全部页(仍走 cursor 协议) + * - reset=true:切换键,清空 fieldKeyword / tableKeyword + * - restart=true:手动刷新 / Enter 搜索,保留 keyword 并中断进行中的扫描 + * 值面板无 F5;F5 仅 KeyMain 刷新键列表。 */ async function refreshKey( reset: boolean = true, useCursor: boolean = false, loadAll: boolean = false, + restart: boolean = false, ) { + if (!share.conn || !share.redisKey) return + + if (loading.value) { + if (!restart) return + scanCancelled.value = true + scanPaused.value = false + while (loading.value) { + await sleep(20) + } + } + fieldSetInit() - // 刷新过程中 me-code 的 modelValue 会变,避免误写入 newValue suppressCodeUpdate.value = true + scanLoadAll.value = loadAll if (reset) { resetParam() forceFullValue.value = false valueTruncatedDismissed.value = false } - // 非「加载更多」时从第一页重新 scan if (!useCursor) cursor.value = null loading.value = true - // 有值则 finally 整包替换;undefined 表示 merge 成功或仅更新了 prev 字段 - let replaceData: FieldScanResult | undefined + scanCancelled.value = false + if (!useCursor) scanPaused.value = false + try { - const data = await meCommands.fieldScan(share.conn!.id, buildFieldScanParam(loadAll)) - cursor.value = data.cursor - // hashKey 非空时进入「单字段」模式,UI 按 string 展示 - withHashKey.value = !!hashKey.value - - if (useCursor) { - const prev = redisValue.value - if (!prev || !mergeFieldScanPage(prev, data)) { - replaceData = data - } - } else { - replaceData = data + if (!useCursor) scanBatchCount.value = 0 + + const first = await fieldScanCore(useCursor) + if (first.replaceData) { + redisValue.value = toViewState(first.replaceData) + } + + const scanType = redisValue.value?.type + if (loadAll) { + await fieldScanAll() + } else if (shouldFieldScanAuto(scanType, fieldExact.value)) { + await fieldScanAuto(first.count) } showMore.value = !cursor.value?.finished - // setTimer 用 ttl 启本地倒计时;merge 场景直接用 prev - const rvDone = replaceData ? toViewState(replaceData) : redisValue.value + const rvDone = redisValue.value if (rvDone) await setTimer(rvDone.ttl) } finally { - await finalizeAfterFieldScan(reset, replaceData) + await finalizeAfterFieldScan(reset) + if (cursor.value?.finished) scanPaused.value = false } } // #endregion @@ -606,9 +732,7 @@ async function copyAsCommand() { } async function onFooterRefreshKey() { - if (loading.value) return - prepareManualKeyRefresh() - await refreshKey(false) + await manualRefreshKey() meOk(t('redisValue.refreshKeyOk')) } @@ -670,8 +794,7 @@ function toggleFavorite() { function onKeyMoreCommand(command: string) { if (command === 'refreshKey') { - prepareManualKeyRefresh() - void refreshKey(false) + void manualRefreshKey() } else if (command === 'copyKey') { meCopy(showKey.value) } else if (command === 'copyValue') { @@ -1025,8 +1148,8 @@ const textMemory = computed(() => { /** 与 textLength 同一位置:String/单字段为字节长度,集合类型为总数 */ const textLength = computed(() => { const rv = redisValue.value - if (!rv || jsonType.value || (streamType.value && withHashKey.value)) return '' - if (stringTypeOrWithHashKey.value) { + if (!rv || jsonType.value) return '' + if (stringType.value) { return t('redisValue.textLength') + rv.length } if (rv.length <= 0) return '' @@ -1035,8 +1158,8 @@ const textLength = computed(() => { const textEntries = computed(() => { const rv = redisValue.value - if (!rv || jsonType.value || stringTypeOrWithHashKey.value) return '' - const filtered = filterDataList.value.length + if (!rv || jsonType.value || stringType.value) return '' + const filtered = tableDisplayList.value.length const loaded = fieldValueRows(rv.value).length return t('redisValue.textEntries') + `${filtered} / ${loaded}` }) @@ -1075,6 +1198,7 @@ const keyShortcuts = computed(() => getValueShortcuts(t)) // #endregion // #region 事件总线与生命周期 +/** 选中键时加载值(KEY_REFRESH);与 KeyMain F5 刷新键列表无关 */ const onKeyRefreshBus = () => { bytesFormat.value = 'utf8' void refreshKey() @@ -1094,8 +1218,8 @@ onUnmounted(() => { - - - { + + + { icon="el-icon-refresh-right" :name="t('redisValue.refreshFieldRow')" /> - + + + + + + + + :name="t('redisValue.copyStreamId')" /> - + + :name="t('redisValue.copyScore')" /> Date: Mon, 13 Jul 2026 22:00:13 +0800 Subject: [PATCH 66/82] =?UTF-8?q?RedisValue=E8=A1=A8=E6=A0=BC=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=B1=BB=E5=9E=8B=E6=97=B6=E9=80=9A=E8=BF=87key?= =?UTF-8?q?=E5=BC=BA=E5=88=B6=E9=87=8D=E6=96=B0=E6=8C=82=E8=BD=BD=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9Ddefault-sort=E9=BB=98=E8=AE=A4=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E6=AD=A3=E7=A1=AE=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/tab/RedisValue.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index 151a1222..6191a786 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -1654,6 +1654,7 @@ onUnmounted(() => {
Date: Mon, 13 Jul 2026 22:09:48 +0800 Subject: [PATCH 67/82] =?UTF-8?q?=E5=88=B7=E6=96=B0=E9=94=AE=E7=9A=84?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/lang/en.ts | 2 +- src/locales/lang/zh-cn.ts | 2 +- src/views/tab/RedisValue.vue | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index c04ea596..b6789139 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -850,7 +850,7 @@ export default { copyAsCommand: 'Copy Command', refreshFieldRow: 'Refresh Row', refreshFieldRowOk: 'Row refreshed', - refreshKeyOk: 'Refreshed', + refreshKeyOk: 'Key refreshed', copyCommandOk: 'Command copied', copyCommandEmpty: 'Empty key, nothing to copy', refreshKey: 'Refresh Key', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index 6d389596..d4d34bfd 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -836,7 +836,7 @@ export default { copyAsCommand: '复制为命令', refreshFieldRow: '刷新行', refreshFieldRowOk: '行已刷新', - refreshKeyOk: '已刷新', + refreshKeyOk: '键已刷新', copyCommandOk: '命令已复制', copyCommandEmpty: '空键,无可复制命令', refreshKey: '刷新键', diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index 6191a786..93b4614a 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -915,9 +915,9 @@ function toggleFavorite() { } } -function onKeyMoreCommand(command: string) { +async function onKeyMoreCommand(command: string) { if (command === 'refreshKey') { - void manualRefreshKey() + await onFooterRefreshKey() } else if (command === 'copyKey') { meCopy(showKey.value) } else if (command === 'copyValue') { From e82ada994b1cf23ddf5450ad241ba5c96bc51bde Mon Sep 17 00:00:00 2001 From: hepengju Date: Mon, 13 Jul 2026 22:11:17 +0800 Subject: [PATCH 68/82] =?UTF-8?q?=E9=87=8D=E7=BD=AE=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E7=9A=84=E6=8F=90=E7=A4=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/lang/en.ts | 2 +- src/locales/lang/zh-cn.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index b6789139..a54716c2 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -116,7 +116,7 @@ export default { openDir: 'Open', resetWindow: 'Reset Window', resetWindowTip: 'Restore default size and center', - resetWindowOk: 'Window restored to default size', + resetWindowOk: 'Window restored to default position and size', shortcuts: 'Shortcuts', appFullscreen: 'Fullscreen', shortcutTips: 'Shortcuts', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index d4d34bfd..82ba3591 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -108,7 +108,7 @@ export default { openDir: '打开', resetWindow: '重置窗口', resetWindowTip: '恢复为默认大小并居中', - resetWindowOk: '窗口已恢复默认大小', + resetWindowOk: '窗口已恢复默认位置和大小', shortcuts: '快捷键', appFullscreen: '全屏应用', shortcutTips: '快捷键', From fb8d9f5f75fc1828a4c164e4ee65fa2d06f5e295 Mon Sep 17 00:00:00 2001 From: hepengju Date: Mon, 13 Jul 2026 22:39:16 +0800 Subject: [PATCH 69/82] =?UTF-8?q?feat:=20ZSet=20=E8=A1=8C=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E8=8F=9C=E5=8D=95=E6=96=B0=E5=A2=9E=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E5=8A=9F=E8=83=BD=EF=BC=88ZRANK/ZREVRANK?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 zset_rank API,调用 Redis ZRANK/ZREVRANK 获取正序/倒序索引 - 前端行扩展菜单添加'查看索引'选项,结果弹窗展示 - 添加 me-icon-rank 自定义图标 - 补充中英文 i18n 文案 --- src-tauri/src/api.rs | 1 + src-tauri/src/client/client_trait.rs | 14 ++++++++++++++ src-tauri/src/client/impl_cluster.rs | 4 ++++ src-tauri/src/client/impl_single.rs | 4 ++++ src-tauri/src/lib.rs | 1 + src-tauri/src/utils/model.rs | 12 ++++++++++++ src/assets/icons/rank.svg | 1 + src/locales/lang/en.ts | 5 +++++ src/locales/lang/zh-cn.ts | 5 +++++ src/types/tauri-specta.ts | 20 ++++++++++++++++++++ src/views/tab/RedisValue.vue | 26 ++++++++++++++++++++++++++ 11 files changed, 93 insertions(+) create mode 100644 src/assets/icons/rank.svg diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index 5b9aeaa4..de5e35e3 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -145,6 +145,7 @@ api_commands!( hash_values(param: RedisHashKeys) -> Vec; // Hash 全量字段值(HVALS) field_pop(param: RedisPop) -> String; // List/Set/ZSet 弹出元素(LPOP/RPOP/SPOP/ZPOPMIN/ZPOPMAX) field_del(param: RedisFieldDel) -> (); // 删除字段 + zset_rank(param: RedisZsetRank) -> RedisZsetRankResult; // ZSet 排名查询(ZRANK/ZREVRANK) execute_command(param: RedisCommand) -> String; // 执行命令 config_get(pattern: &str, node: Option) -> HashMap; // 获取配置 config_set(key: &str, value: &str, node: Option) -> (); // 设置配置 diff --git a/src-tauri/src/client/client_trait.rs b/src-tauri/src/client/client_trait.rs index 06765997..aa5c2ae0 100644 --- a/src-tauri/src/client/client_trait.rs +++ b/src-tauri/src/client/client_trait.rs @@ -81,6 +81,8 @@ pub trait MeClient: Send + Sync { fn field_del(&self, param: RedisFieldDel) -> AnyResult<()>; + fn zset_rank(&self, param: RedisZsetRank) -> AnyResult; + fn execute_command(&self, param: RedisCommand) -> AnyResult; fn config_get(&self, pattern: &str, node: Option) @@ -1153,6 +1155,18 @@ pub fn field_del0(mut conn: MutexGuard, param: RedisFieldDel) -> Ok(()) } +pub fn zset_rank0( + mut conn: MutexGuard, + param: RedisZsetRank, +) -> AnyResult { + let key: RedisKey = param.key; + let val_fmt = param.val_fmt.as_ref().cloned().unwrap_or_default(); + let member_bytes = parse_bytes(¶m.member, &val_fmt)?; + let rank: Option = conn.zrank(&key, &member_bytes)?; + let rev_rank: Option = conn.zrevrank(&key, &member_bytes)?; + Ok(RedisZsetRankResult { rank, rev_rank }) +} + pub fn publish0( mut conn: MutexGuard, channel: &str, diff --git a/src-tauri/src/client/impl_cluster.rs b/src-tauri/src/client/impl_cluster.rs index fb714670..c906b843 100644 --- a/src-tauri/src/client/impl_cluster.rs +++ b/src-tauri/src/client/impl_cluster.rs @@ -253,6 +253,10 @@ impl MeClient for MeCluster { field_del0(self.get_conn()?, param) } + fn zset_rank(&self, param: RedisZsetRank) -> AnyResult { + zset_rank0(self.get_conn()?, param) + } + fn execute_command(&self, param: RedisCommand) -> AnyResult { let (cmd_name, args) = parse_command(param.command.as_str())?; if cmd_name.is_empty() { diff --git a/src-tauri/src/client/impl_single.rs b/src-tauri/src/client/impl_single.rs index 8465e2c0..fc934df9 100644 --- a/src-tauri/src/client/impl_single.rs +++ b/src-tauri/src/client/impl_single.rs @@ -193,6 +193,10 @@ impl MeClient for MeSingle { field_del0(self.get_conn()?, param) } + fn zset_rank(&self, param: RedisZsetRank) -> AnyResult { + zset_rank0(self.get_conn()?, param) + } + fn execute_command(&self, param: RedisCommand) -> AnyResult { let (cmd, args) = parse_command(param.command.as_str())?; if cmd.is_empty() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4c604924..7e4ee5ca 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,6 +46,7 @@ fn tauri_specta_commands() -> Commands { hash_values, field_pop, field_del, + zset_rank, execute_command, acl_users, acl_list_users, diff --git a/src-tauri/src/utils/model.rs b/src-tauri/src/utils/model.rs index 199eeb30..a6b5a5f3 100644 --- a/src-tauri/src/utils/model.rs +++ b/src-tauri/src/utils/model.rs @@ -643,6 +643,18 @@ api_model!(RedisFieldValue { field_ttl: i64, // 字段 TTL(秒),仅 Redis/Valkey >= 7.4 }); +// ZSet 排名查询 +api_model!(RedisZsetRank { + key: RedisKey, + member: String, + val_fmt: Option, +}); + +api_model!(RedisZsetRankResult { + rank: Option, + rev_rank: Option, +}); + // 字段删除 api_model!(RedisFieldDel { key: RedisKey, diff --git a/src/assets/icons/rank.svg b/src/assets/icons/rank.svg new file mode 100644 index 00000000..d48af66b --- /dev/null +++ b/src/assets/icons/rank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index a54716c2..d97cef88 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -847,6 +847,11 @@ export default { copyKey: 'Copy Key', copyStreamId: 'Copy ID', copyScore: 'Copy Score', + showZsetRank: 'View Index', + rank: 'Index (ZRANK)', + revRank: 'Rev Index (ZREVRANK)', + rankTitle: 'Member Rank', + rankNotFound: 'Not Found', copyAsCommand: 'Copy Command', refreshFieldRow: 'Refresh Row', refreshFieldRowOk: 'Row refreshed', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index 82ba3591..d7e8d962 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -833,6 +833,11 @@ export default { copyKey: '复制键', copyStreamId: '复制ID', copyScore: '复制分数', + showZsetRank: '查看索引', + rank: '正序索引 (ZRANK)', + revRank: '倒序索引 (ZREVRANK)', + rankTitle: '成员索引', + rankNotFound: '未找到', copyAsCommand: '复制为命令', refreshFieldRow: '刷新行', refreshFieldRowOk: '行已刷新', diff --git a/src/types/tauri-specta.ts b/src/types/tauri-specta.ts index 951da6c3..3d503ce5 100644 --- a/src/types/tauri-specta.ts +++ b/src/types/tauri-specta.ts @@ -37,6 +37,7 @@ export const commands = { hashValues: (id: string, param: RedisHashKeys_Deserialize) => typedError(__TAURI_INVOKE("hash_values", { id, param })), fieldPop: (id: string, param: RedisPop_Deserialize) => typedError(__TAURI_INVOKE("field_pop", { id, param })), fieldDel: (id: string, param: RedisFieldDel_Deserialize) => typedError(__TAURI_INVOKE("field_del", { id, param })), + zsetRank: (id: string, param: RedisZsetRank_Deserialize) => typedError(__TAURI_INVOKE("zset_rank", { id, param })), executeCommand: (id: string, param: RedisCommand) => typedError(__TAURI_INVOKE("execute_command", { id, param })), aclUsers: (id: string) => typedError(__TAURI_INVOKE("acl_users", { id })), aclListUsers: (id: string) => typedError(__TAURI_INVOKE("acl_list_users", { id })), @@ -596,6 +597,25 @@ export type RedisSlowLog = { clientName: string, }; +export type RedisZsetRank = RedisZsetRank_Serialize | RedisZsetRank_Deserialize; + +export type RedisZsetRankResult = { + rank: number | null, + revRank: number | null, +}; + +export type RedisZsetRank_Deserialize = { + key: RedisKey_Deserialize, + member: string, + valFmt: BytesFormat | null, +}; + +export type RedisZsetRank_Serialize = { + key: RedisKey_Serialize, + member: string, + valFmt: BytesFormat | null, +}; + export type ScanCursor = { readyNodes: string[], nowNode: string, diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index 93b4614a..b463cd15 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -27,6 +27,7 @@ import type { RedisFieldGet_Deserialize, RedisFieldValue, RedisKey_Deserialize, + RedisZsetRankResult, ScanCursor, } from '@/types/tauri-specta' import { useFavorites, addFavorite, removeFavorite, isFavorited } from '@/utils/favorite' @@ -1192,9 +1193,31 @@ function onFieldRowMoreCommand(command: string, row: ValueTableRow) { meCopy(String(row.score ?? '')) } else if (command === 'copyAsCommand') { void copyFieldAsCommand(row) + } else if (command === 'showZsetRank') { + void showZsetRank(row) } } +async function showZsetRank(row: ValueTableRow) { + const conn = share.conn + const rk = share.redisKey + if (!conn || !rk) return + const member = fieldRowDisplayValue(row) + const data: RedisZsetRankResult = await meCommands.zsetRank(conn.id, { + key: rk, + member, + valFmt: toWireFormat(viewFmtForField(bytesFormat.value)), + }) + const rankText = data.rank !== null ? String(data.rank) : t('redisValue.rankNotFound') + const revRankText = data.revRank !== null ? String(data.revRank) : t('redisValue.rankNotFound') + meOk( + `${t('redisValue.rank')}: ${rankText}
${t('redisValue.revRank')}: ${revRankText}`, + true, + t('redisValue.rankTitle'), + { dangerouslyUseHTMLString: true }, + ) +} + function onFieldSetRefreshed(data: RedisFieldValue) { const rv = redisValue.value const row = fieldSetRow.value @@ -1813,6 +1836,9 @@ onUnmounted(() => { icon="me-icon-copy-command" :name="t('redisValue.copyAsCommand')" /> + + + From 948a4abac1eb995f14fc662a2aae071c6a0fe3e6 Mon Sep 17 00:00:00 2001 From: hepengju Date: Mon, 13 Jul 2026 23:21:42 +0800 Subject: [PATCH 70/82] =?UTF-8?q?feat:=20List=E7=B1=BB=E5=9E=8B=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=8B=AC=E7=AB=8B=E7=B4=A2=E5=BC=95=E5=88=97=E5=8F=8A?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=B4=A2=E5=BC=95=E8=8F=9C=E5=8D=95=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/lang/en.ts | 2 ++ src/locales/lang/zh-cn.ts | 2 ++ src/views/tab/RedisValue.vue | 22 ++++++++++++++++++---- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index d97cef88..0081e7ec 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -832,6 +832,7 @@ export default { hashValuesEmpty: 'No values', id: 'ID', key: 'Key', + index: 'Index', ttl: 'TTL', value: 'Value', score: 'Score', @@ -845,6 +846,7 @@ export default { duplicateKey: 'Create Duplicate', copyValue: 'Copy Value', copyKey: 'Copy Key', + copyIndex: 'Copy Index', copyStreamId: 'Copy ID', copyScore: 'Copy Score', showZsetRank: 'View Index', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index d7e8d962..a66bad00 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -818,6 +818,7 @@ export default { hashValuesEmpty: '暂无值', id: 'ID', key: '键', + index: '索引', ttl: '过期', value: '值', score: '分数', @@ -831,6 +832,7 @@ export default { duplicateKey: '创建副本', copyValue: '复制值', copyKey: '复制键', + copyIndex: '复制索引', copyStreamId: '复制ID', copyScore: '复制分数', showZsetRank: '查看索引', diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index b463cd15..3f613bd0 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -1187,6 +1187,8 @@ function onFieldRowMoreCommand(command: string, row: ValueTableRow) { meCopy(String(row.key ?? '')) } else if (command === 'copyValue') { meCopy(fieldRowDisplayValue(row)) + } else if (command === 'copyIndex') { + meCopy(String(row.index ?? '')) } else if (command === 'copyStreamId') { meCopy(String(row.id ?? '')) } else if (command === 'copyScore') { @@ -1699,9 +1701,7 @@ onUnmounted(() => { - + { + + + { + + + Date: Tue, 14 Jul 2026 21:48:14 +0800 Subject: [PATCH 71/82] =?UTF-8?q?feat:=20List=E7=B4=A2=E5=BC=95=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E6=9F=A5=E8=AF=A2=E6=94=AF=E6=8C=81=E8=B4=9F=E6=95=B0?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=EF=BC=8C=E8=B6=85=E5=87=BA=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E8=BF=94=E5=9B=9E=E7=A9=BA=E6=95=B0=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/client/client_trait.rs | 52 +++++++++++++++++----------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/client/client_trait.rs b/src-tauri/src/client/client_trait.rs index aa5c2ae0..b8c0b2b5 100644 --- a/src-tauri/src/client/client_trait.rs +++ b/src-tauri/src/client/client_trait.rs @@ -419,11 +419,24 @@ fn list_scan_desc(param: &FieldScanParam) -> bool { fn resolve_list_scan_range(param: &FieldScanParam, list_len: usize) -> (i64, i64) { let max_default = list_len.saturating_sub(1) as i64; let meta = param.meta.as_ref(); - let min = meta.and_then(|m| m.list_min_index).unwrap_or(0).max(0); - let max = meta - .and_then(|m| m.list_max_index) - .unwrap_or(max_default) - .clamp(min, max_default); + let raw_min = meta.and_then(|m| m.list_min_index); + let raw_max = meta.and_then(|m| m.list_max_index); + + let min = raw_min.map_or(0, |v| { + if v < 0 { + (list_len as i64 + v).max(0) + } else { + v + } + }); + let max = raw_max.map_or(max_default, |v| { + if v < 0 { + (list_len as i64 + v).max(0) + } else { + v + } + }); + (min, max) } @@ -441,30 +454,32 @@ fn field_scan_list_page( return Ok(Vec::new()); } let (range_min, range_max) = resolve_list_scan_range(param, list_len); - let range_len = (range_max - range_min + 1).max(0) as u64; - if range_len == 0 { - cc.finished = true; - return Ok(Vec::new()); - } - let fetched = cc.now_cursor; - if fetched >= range_len { + if range_min > range_max { cc.finished = true; return Ok(Vec::new()); } - let batch = count.min(range_len - fetched); let desc = list_scan_desc(param); let (start, end) = if desc { - let end = range_max - fetched as i64; - let start = end - batch as i64 + 1; + let potential_end = range_max - cc.now_cursor as i64; + if potential_end < range_min { + cc.finished = true; + return Ok(Vec::new()); + } + let end = potential_end; + let start = (end - count as i64 + 1).max(range_min); (start, end) } else { - let start = range_min + fetched as i64; - let end = start + batch as i64 - 1; + let start = range_min + cc.now_cursor as i64; + let end = (start + count as i64 - 1).min(range_max); (start, end) }; let raw: Vec> = conn.lrange(key, start as isize, end as isize)?; + if raw.is_empty() { + cc.finished = true; + return Ok(Vec::new()); + } let mut items = ui_list_items(start, &raw, bytes_format); // LRANGE 始终按索引升序返回;降序扫描时反转,使结果页从高索引到低索引 if desc { @@ -472,9 +487,6 @@ fn field_scan_list_page( } cc.now_cursor += items.len() as u64; - if cc.now_cursor >= range_len { - cc.finished = true; - } Ok(items) } From 00ad633c40c4751e238dedaabfbbacf5587d72a8 Mon Sep 17 00:00:00 2001 From: hepengju Date: Tue, 14 Jul 2026 22:22:38 +0800 Subject: [PATCH 72/82] =?UTF-8?q?feat:=20ZSet=E6=96=B0=E5=A2=9ETopN?= =?UTF-8?q?=E6=8E=92=E5=90=8D=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 zset_range 命令,支持 ZRANGE/ZREVRANGE - 前端新增 TableZsetRange 弹窗组件 - 支持输入数量、正序/倒序选择、模糊筛选 - RedisValue.vue 添加 TopN 按钮,位于更多操作左侧 - 添加中英文国际化文案 --- src-tauri/src/api.rs | 1 + src-tauri/src/client/client_trait.rs | 26 +++++ src-tauri/src/client/impl_cluster.rs | 4 + src-tauri/src/client/impl_single.rs | 4 + src-tauri/src/lib.rs | 1 + src-tauri/src/utils/model.rs | 16 +++ src/locales/lang/en.ts | 8 ++ src/locales/lang/zh-cn.ts | 8 ++ src/types/tauri-specta.ts | 28 +++++ src/views/ext/TableZsetRange.vue | 158 +++++++++++++++++++++++++++ src/views/tab/RedisValue.vue | 15 +++ 11 files changed, 269 insertions(+) create mode 100644 src/views/ext/TableZsetRange.vue diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index de5e35e3..08be3a74 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -146,6 +146,7 @@ api_commands!( field_pop(param: RedisPop) -> String; // List/Set/ZSet 弹出元素(LPOP/RPOP/SPOP/ZPOPMIN/ZPOPMAX) field_del(param: RedisFieldDel) -> (); // 删除字段 zset_rank(param: RedisZsetRank) -> RedisZsetRankResult; // ZSet 排名查询(ZRANK/ZREVRANK) + zset_range(param: RedisZsetRange) -> Vec; // ZSet Top/Bottom 范围查询(ZRANGE/ZREVRANGE) execute_command(param: RedisCommand) -> String; // 执行命令 config_get(pattern: &str, node: Option) -> HashMap; // 获取配置 config_set(key: &str, value: &str, node: Option) -> (); // 设置配置 diff --git a/src-tauri/src/client/client_trait.rs b/src-tauri/src/client/client_trait.rs index b8c0b2b5..e81b1a00 100644 --- a/src-tauri/src/client/client_trait.rs +++ b/src-tauri/src/client/client_trait.rs @@ -83,6 +83,8 @@ pub trait MeClient: Send + Sync { fn zset_rank(&self, param: RedisZsetRank) -> AnyResult; + fn zset_range(&self, param: RedisZsetRange) -> AnyResult>; + fn execute_command(&self, param: RedisCommand) -> AnyResult; fn config_get(&self, pattern: &str, node: Option) @@ -1179,6 +1181,30 @@ pub fn zset_rank0( Ok(RedisZsetRankResult { rank, rev_rank }) } +/// ZSet Top/Bottom 范围查询:ZRANGE/ZREVRANGE ... WITHSCORES +pub fn zset_range0( + mut conn: MutexGuard, + param: RedisZsetRange, +) -> AnyResult> { + let key: RedisKey = param.key; + let val_fmt = param.val_fmt.as_ref().cloned().unwrap_or_default(); + let cmd_name = if param.reverse { "ZREVRANGE" } else { "ZRANGE" }; + let end = (param.count as isize).saturating_sub(1); + let values: Vec<(Vec, f64)> = redis::cmd(cmd_name) + .arg(&key) + .arg(0) + .arg(end) + .arg("WITHSCORES") + .query(&mut conn)?; + Ok(values + .into_iter() + .map(|(v, s)| RedisZsetRangeItem { + value: format_bytes(&v, &val_fmt), + score: s, + }) + .collect()) +} + pub fn publish0( mut conn: MutexGuard, channel: &str, diff --git a/src-tauri/src/client/impl_cluster.rs b/src-tauri/src/client/impl_cluster.rs index c906b843..95a23c9f 100644 --- a/src-tauri/src/client/impl_cluster.rs +++ b/src-tauri/src/client/impl_cluster.rs @@ -257,6 +257,10 @@ impl MeClient for MeCluster { zset_rank0(self.get_conn()?, param) } + fn zset_range(&self, param: RedisZsetRange) -> AnyResult> { + zset_range0(self.get_conn()?, param) + } + fn execute_command(&self, param: RedisCommand) -> AnyResult { let (cmd_name, args) = parse_command(param.command.as_str())?; if cmd_name.is_empty() { diff --git a/src-tauri/src/client/impl_single.rs b/src-tauri/src/client/impl_single.rs index fc934df9..f28e7124 100644 --- a/src-tauri/src/client/impl_single.rs +++ b/src-tauri/src/client/impl_single.rs @@ -197,6 +197,10 @@ impl MeClient for MeSingle { zset_rank0(self.get_conn()?, param) } + fn zset_range(&self, param: RedisZsetRange) -> AnyResult> { + zset_range0(self.get_conn()?, param) + } + fn execute_command(&self, param: RedisCommand) -> AnyResult { let (cmd, args) = parse_command(param.command.as_str())?; if cmd.is_empty() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7e4ee5ca..43731be0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -47,6 +47,7 @@ fn tauri_specta_commands() -> Commands { field_pop, field_del, zset_rank, + zset_range, execute_command, acl_users, acl_list_users, diff --git a/src-tauri/src/utils/model.rs b/src-tauri/src/utils/model.rs index a6b5a5f3..268a8f9b 100644 --- a/src-tauri/src/utils/model.rs +++ b/src-tauri/src/utils/model.rs @@ -655,6 +655,22 @@ api_model!(RedisZsetRankResult { rev_rank: Option, }); +// ZSet Top/Bottom 范围查询 +api_model!(RedisZsetRange { + key: RedisKey, + /// true: ZREVRANGE (分数从高到低); false: ZRANGE (分数从低到高) + reverse: bool, + /// 返回数量限制 + count: u64, + /// 值解码格式 + val_fmt: Option, +}); + +api_model!(RedisZsetRangeItem { + value: String, + score: f64, +}); + // 字段删除 api_model!(RedisFieldDel { key: RedisKey, diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index 0081e7ec..898a7816 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -854,6 +854,14 @@ export default { revRank: 'Rev Index (ZREVRANK)', rankTitle: 'Member Rank', rankNotFound: 'Not Found', + zsetRange: 'TopN', + zsetRangeTitle: 'ZSet Rank Query', + zsetRangeCount: 'Count', + zsetRangeAsc: 'Asc', + zsetRangeDesc: 'Desc', + zsetRangeQuery: 'Query', + zsetRangeFilter: 'Filter (value, score)', + zsetRangeEmpty: 'No data', copyAsCommand: 'Copy Command', refreshFieldRow: 'Refresh Row', refreshFieldRowOk: 'Row refreshed', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index a66bad00..5d13e4e4 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -840,6 +840,14 @@ export default { revRank: '倒序索引 (ZREVRANK)', rankTitle: '成员索引', rankNotFound: '未找到', + zsetRange: 'TopN', + zsetRangeTitle: 'ZSet 排名查询', + zsetRangeCount: '数量', + zsetRangeAsc: '正序', + zsetRangeDesc: '倒序', + zsetRangeQuery: '查询', + zsetRangeFilter: '模糊筛选(值、分数)', + zsetRangeEmpty: '暂无数据', copyAsCommand: '复制为命令', refreshFieldRow: '刷新行', refreshFieldRowOk: '行已刷新', diff --git a/src/types/tauri-specta.ts b/src/types/tauri-specta.ts index 3d503ce5..c0a79676 100644 --- a/src/types/tauri-specta.ts +++ b/src/types/tauri-specta.ts @@ -38,6 +38,7 @@ export const commands = { fieldPop: (id: string, param: RedisPop_Deserialize) => typedError(__TAURI_INVOKE("field_pop", { id, param })), fieldDel: (id: string, param: RedisFieldDel_Deserialize) => typedError(__TAURI_INVOKE("field_del", { id, param })), zsetRank: (id: string, param: RedisZsetRank_Deserialize) => typedError(__TAURI_INVOKE("zset_rank", { id, param })), + zsetRange: (id: string, param: RedisZsetRange_Deserialize) => typedError(__TAURI_INVOKE("zset_range", { id, param })), executeCommand: (id: string, param: RedisCommand) => typedError(__TAURI_INVOKE("execute_command", { id, param })), aclUsers: (id: string) => typedError(__TAURI_INVOKE("acl_users", { id })), aclListUsers: (id: string) => typedError(__TAURI_INVOKE("acl_list_users", { id })), @@ -597,6 +598,33 @@ export type RedisSlowLog = { clientName: string, }; +export type RedisZsetRange = RedisZsetRange_Serialize | RedisZsetRange_Deserialize; + +export type RedisZsetRangeItem = { + value: string, + score: number | null, +}; + +export type RedisZsetRange_Deserialize = { + key: RedisKey_Deserialize, + /** true: ZREVRANGE (分数从高到低); false: ZRANGE (分数从低到高) */ + reverse: boolean, + /** 返回数量限制 */ + count: number, + /** 值解码格式 */ + valFmt: BytesFormat | null, +}; + +export type RedisZsetRange_Serialize = { + key: RedisKey_Serialize, + /** true: ZREVRANGE (分数从高到低); false: ZRANGE (分数从低到高) */ + reverse: boolean, + /** 返回数量限制 */ + count: number, + /** 值解码格式 */ + valFmt: BytesFormat | null, +}; + export type RedisZsetRank = RedisZsetRank_Serialize | RedisZsetRank_Deserialize; export type RedisZsetRankResult = { diff --git a/src/views/ext/TableZsetRange.vue b/src/views/ext/TableZsetRange.vue new file mode 100644 index 00000000..898d89b9 --- /dev/null +++ b/src/views/ext/TableZsetRange.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index 3f613bd0..be8b7503 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -73,6 +73,7 @@ import { } from '@/utils/util' import TableGroup from '@/views/ext/TableGroup.vue' import TableHashKeys from '@/views/ext/TableHashKeys.vue' +import TableZsetRange from '@/views/ext/TableZsetRange.vue' import TTLSet from '@/views/ext/TTLSet.vue' import ValueShortcut from '@/views/ext/ValueShortcut.vue' import KeyRename from '@/views/key/KeyRename.vue' @@ -1304,6 +1305,11 @@ function showAllHashKeys() { function showAllHashValues() { hashKeysRef.value?.open(hashListValFmt(), 'values') } + +const zsetRangeRef = useTemplateRef('zsetRangeRef') +function showZsetRange() { + zsetRangeRef.value?.open(hashListValFmt()) +} // #endregion // #region 底部信息栏(内存 / 条数 / 槽位) @@ -1654,6 +1660,13 @@ onUnmounted(() => { style="margin-left: 10px"> {{ listDescAsc ? t('redisValue.listSortAsc') : t('redisValue.listSortDesc') }} + + {{ t('redisValue.zsetRange') }} + { + + From 611bfc948352dee3ee5fbfecb1d09f55cf8986c4 Mon Sep 17 00:00:00 2001 From: hepengju Date: Tue, 14 Jul 2026 22:22:53 +0800 Subject: [PATCH 73/82] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20meCommands=20?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E9=87=8D=E5=A4=8D=E5=BC=B9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit meCommands 已自带统一错误处理,组件内再 catch + meErr 会导致弹两次框。 移除以下组件中多余的 catch meErr: - FieldSet.vue: fieldSet 调用 - TableGroup.vue: xinfoGroups 调用 - TableHashKeys.vue: hashKeys/hashValues 调用 - TableZsetRange.vue: zsetRange 调用 (新组件中已正确处理) --- src/views/ext/FieldSet.vue | 2 -- src/views/ext/TableGroup.vue | 5 +---- src/views/ext/TableHashKeys.vue | 5 +---- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/views/ext/FieldSet.vue b/src/views/ext/FieldSet.vue index 5e25f951..b573321b 100644 --- a/src/views/ext/FieldSet.vue +++ b/src/views/ext/FieldSet.vue @@ -217,8 +217,6 @@ function submit() { visible.value = false emit('success') meOk(t('editOk')) - } catch (e) { - meErr(e instanceof Error ? e.message : String(e)) } finally { isSaving.value = false } diff --git a/src/views/ext/TableGroup.vue b/src/views/ext/TableGroup.vue index 3b4a39eb..cf0fbf12 100644 --- a/src/views/ext/TableGroup.vue +++ b/src/views/ext/TableGroup.vue @@ -6,7 +6,7 @@ import { useI18n } from 'vue-i18n' import { shareProvideKey } from '@/types/me-interface' import type { XInfoConsumer, XInfoGroup } from '@/types/tauri-specta' -import { meCommands, meErr } from '@/utils/util' +import { meCommands } from '@/utils/util' const { t } = useI18n() const share = inject(shareProvideKey)! @@ -28,9 +28,6 @@ async function open() { consumerData.value = [] try { dataList.value = await meCommands.xinfoGroups(conn.id, redisKey) - } catch (e) { - visible.value = false - meErr(e) } finally { loading.value = false } diff --git a/src/views/ext/TableHashKeys.vue b/src/views/ext/TableHashKeys.vue index e8164ad3..1f1a648d 100644 --- a/src/views/ext/TableHashKeys.vue +++ b/src/views/ext/TableHashKeys.vue @@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n' import { shareProvideKey } from '@/types/me-interface' import type { BytesFormat } from '@/types/tauri-specta' -import { meCommands, meErr } from '@/utils/util' +import { meCommands } from '@/utils/util' type HashListMode = 'keys' | 'values' @@ -56,9 +56,6 @@ async function open(valFmt: BytesFormat | null, listMode: HashListMode = 'keys') listMode === 'keys' ? await meCommands.hashKeys(conn.id, param) : await meCommands.hashValues(conn.id, param) - } catch (e) { - visible.value = false - meErr(e) } finally { loading.value = false } From d733bd6a7254ad6dcd90a9c25c4d27860749f059 Mon Sep 17 00:00:00 2001 From: hepengju Date: Wed, 15 Jul 2026 23:06:36 +0800 Subject: [PATCH 74/82] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=94=AE?= =?UTF-8?q?=E5=8C=BA=E9=95=BF=E5=90=8D=E7=A7=B0=E5=B8=83=E5=B1=80=E9=94=99?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/views/key/KeyTree.vue | 62 ++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/src/views/key/KeyTree.vue b/src/views/key/KeyTree.vue index b075fa9e..31d7e71a 100644 --- a/src/views/key/KeyTree.vue +++ b/src/views/key/KeyTree.vue @@ -382,6 +382,7 @@ const isContextNodeFavorited = computed(() => { :item-size="keyHeight" :show-checkbox="showCheckbox"> @@ -530,6 +530,11 @@ const isContextNodeFavorited = computed(() => { background-color: var(--el-color-info-light-8); } +/* 自定义节点可收缩,长名才能 ellipsis,右侧数量/图标不被挤出 */ +:deep(.el-tree-node__content) { + overflow: hidden; +} + /* 右键选中的键 */ :deep(.context-key) { outline: 1px dashed var(--el-color-primary); @@ -541,9 +546,13 @@ const isContextNodeFavorited = computed(() => { margin-left: -20px; } -.key-leaf-row { - width: 100%; +/* 占满 content 剩余宽度(勿用 width:100%,会和展开图标叠宽溢出) */ +.key-leaf-row, +.folder-row { + flex: 1; + min-width: 0; align-items: center; + overflow: hidden; } .key-leaf-main { @@ -551,23 +560,54 @@ const isContextNodeFavorited = computed(() => { min-width: 0; align-items: center; justify-content: flex-start; + overflow: hidden; } -.key-leaf-label { +.key-leaf-main :deep(.el-tag) { + flex-shrink: 0; +} + +.key-leaf-label, +.folder-label { flex: 1; min-width: 0; - margin-left: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.key-leaf-label { + margin-left: 5px; +} + +.folder-icon { + flex-shrink: 0; +} + +.folder-label { + margin: 0 5px; +} + +/* 数量区固定右侧;操作图标右缘大致对齐到 [ n ] 的最后一个数字 */ +.folder-count { + flex-shrink: 0; + margin-right: 10px; + color: var(--el-color-info); +} + +.key-leaf-actions { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 5px; + margin-right: 15px; +} + /* 删除图标:hover 行时显示 */ :deep(.el-tree-node__content:hover) .key-delete-btn { visibility: visible; } -/* 删除图标右缘与文件夹 [数量] 末位数字对齐(略大于数量块的 margin-right) */ .key-delete-btn { flex-shrink: 0; visibility: hidden; From e5933722fa7975fa88d6694a1ea3cd777c989a9b Mon Sep 17 00:00:00 2001 From: hepengju Date: Wed, 15 Jul 2026 23:27:22 +0800 Subject: [PATCH 75/82] =?UTF-8?q?fix:=20Stream=20=E8=A1=8C=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E5=A4=8D=E5=88=B6ID=E4=B8=8E=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E5=80=BC=E9=A1=BA=E5=BA=8F=E4=B8=8E=E8=A1=A8=E6=A0=BC=E5=88=97?= =?UTF-8?q?=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/views/tab/RedisValue.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index be8b7503..90029e8d 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -1843,15 +1843,15 @@ onUnmounted(() => { icon="el-icon-document-copy" :name="t('redisValue.copyIndex')" /> - + + :name="t('redisValue.copyStreamId')" /> - + + :name="t('redisValue.copyValue')" /> Date: Wed, 15 Jul 2026 23:27:31 +0800 Subject: [PATCH 76/82] =?UTF-8?q?chore:=20=E6=9A=82=E6=97=B6=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E7=A4=BE=E4=BA=A4=E8=8F=9C=E5=8D=95=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E4=BB=A5=E5=87=8F=E5=B0=8F=E6=89=93=E5=8C=85=E4=BD=93=E7=A7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/views/KeyHeader.vue | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/views/KeyHeader.vue b/src/views/KeyHeader.vue index 02d63f27..9348661d 100644 --- a/src/views/KeyHeader.vue +++ b/src/views/KeyHeader.vue @@ -8,7 +8,7 @@ import { bus, CONN_REFRESH, meCommands, meOk, openNewWindow } from '@/utils/util import About from '@/views/ext/About.vue' import AppShortcut from '@/views/ext/AppShortcut.vue' import CommandLog from '@/views/ext/CommandLog.vue' -import Official from '@/views/ext/Official.vue' +// import Official from '@/views/ext/Official.vue' import Setting from '@/views/ext/Setting.vue' const share = inject(shareProvideKey)! @@ -19,7 +19,7 @@ const dialog = reactive({ commandLog: false }) const settingRef = ref>() const shortcutRef = ref>() const aboutRef = ref>() -const officialRef = ref>() +// const officialRef = ref>() function openSetting(): void { settingRef.value?.open() @@ -57,8 +57,8 @@ async function handleCommand(command: string): Promise { await openNewWindow() } else if ('info' === command) { aboutRef.value?.open() - } else if ('social' === command) { - officialRef.value?.open() + // } else if ('social' === command) { + // officialRef.value?.open() } else { meOk(`TODO: ${command}`) } @@ -110,9 +110,10 @@ async function handleCommand(command: string): Promise { - + + @@ -123,7 +124,7 @@ async function handleCommand(command: string): Promise { - +
From f46a029c2d4ffa05461f4b8bf3e372215d35f9a5 Mon Sep 17 00:00:00 2001 From: hepengju Date: Thu, 16 Jul 2026 22:19:35 +0800 Subject: [PATCH 77/82] =?UTF-8?q?feat:=20=E9=94=AE=E5=80=BC=E9=A1=B5?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E8=8F=9C=E5=8D=95=E6=96=B0=E5=A2=9E=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E8=87=AA=E7=9C=81=EF=BC=88OBJECT=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/zh/changelog/latest.md | 28 ++++++ src-tauri/src/api.rs | 1 + src-tauri/src/client/client_trait.rs | 37 ++++++++ src-tauri/src/client/impl_cluster.rs | 4 + src-tauri/src/client/impl_single.rs | 4 + src-tauri/src/lib.rs | 1 + src-tauri/src/utils/model.rs | 12 +++ src/locales/lang/en.ts | 38 ++++++++ src/locales/lang/zh-cn.ts | 37 ++++++++ src/types/tauri-specta.ts | 12 +++ src/views/ext/ObjectInfo.vue | 125 +++++++++++++++++++++++++++ src/views/tab/RedisValue.vue | 11 ++- 12 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 src/views/ext/ObjectInfo.vue diff --git a/docs/zh/changelog/latest.md b/docs/zh/changelog/latest.md index 1d2e4109..4be1f2cd 100644 --- a/docs/zh/changelog/latest.md +++ b/docs/zh/changelog/latest.md @@ -1,5 +1,33 @@ # 4.x 更新日志 +## v4.4.0 (2026-07-18) + +### ✨ 新增优化 + +- 键值页: 扫描优化与根据键类型进行针对性的功能扩展 + - **字段扫描优化: Hash/Set/ZSet支持扫描模式,扫描进度和是否精确匹配** + - **字段表格根据不同键类型设置合适的默认排序** + > Hash键升序,Set值升序,ZSet分数升序,List索引升序,Stream主键升序 + - 扩展菜单新增命令帮助,根据键类型的不同进行默认分组过滤 + - 扩展菜单新增**对象自省**(OBJECT ENCODING / IDLETIME / REFCOUNT / FREQ) + - Hash: 新增可选HTTL支持,**所有键**、所有值、复制键 + - List: 新增**索引范围查询**、升降序与 LPOP/RPOP,表格新增独立索引列和复制索引 + - Set: 新增SPOP支持 + - ZSet: 新增 **TopN 排名查询**;行菜单支持查看索引(ZRANK/ZREVRANK) + - Stream: 范围输入样式优化,支持升序/降序扫描,复制ID +- 键区: 优化键名称过长时的布局样式 +- 设置: 新增**安全阈值**与**预览字节**配置,界面布局优化 +- 其他细节优化 + - DB 选择器宽度等样式微调 + - 键 TTL 图标前增加分隔符 + - 字段默认扫描数改为 100 + - 刷新键、重置窗口提示优化 + - 隐藏社交菜单入口以减小打包体积 + +### 🐞 修复问题 + +- 修复特定场景下 meCommands 的错误重复弹框问题 + ## [v4.3.0](https://mp.weixin.qq.com/s/k3KMigBxg3fJraezIMdP5Q) (2026-07-11) ### ✨ 新增优化 diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index 08be3a74..2e2229df 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -147,6 +147,7 @@ api_commands!( field_del(param: RedisFieldDel) -> (); // 删除字段 zset_rank(param: RedisZsetRank) -> RedisZsetRankResult; // ZSet 排名查询(ZRANK/ZREVRANK) zset_range(param: RedisZsetRange) -> Vec; // ZSet Top/Bottom 范围查询(ZRANGE/ZREVRANGE) + object_info(key: RedisKey) -> RedisObjectInfo; // OBJECT 自省(ENCODING/IDLETIME/REFCOUNT/FREQ) execute_command(param: RedisCommand) -> String; // 执行命令 config_get(pattern: &str, node: Option) -> HashMap; // 获取配置 config_set(key: &str, value: &str, node: Option) -> (); // 设置配置 diff --git a/src-tauri/src/client/client_trait.rs b/src-tauri/src/client/client_trait.rs index e81b1a00..a2dc8fd3 100644 --- a/src-tauri/src/client/client_trait.rs +++ b/src-tauri/src/client/client_trait.rs @@ -85,6 +85,8 @@ pub trait MeClient: Send + Sync { fn zset_range(&self, param: RedisZsetRange) -> AnyResult>; + fn object_info(&self, key: RedisKey) -> AnyResult; + fn execute_command(&self, param: RedisCommand) -> AnyResult; fn config_get(&self, pattern: &str, node: Option) @@ -1181,6 +1183,41 @@ pub fn zset_rank0( Ok(RedisZsetRankResult { rank, rev_rank }) } +/// OBJECT ENCODING / IDLETIME / REFCOUNT / FREQ;IDLETIME/FREQ 受 maxmemory-policy 限制时写入 *_error +pub fn object_info0( + mut conn: MutexGuard, + key: RedisKey, +) -> AnyResult { + let encoding: Option = conn.object_encoding(&key)?; + let refcount_raw: Option = conn.object_refcount(&key)?; + let refcount = refcount_raw.map(|n| n as u64); + + // LFU 策略下 IDLETIME 报错;非 LFU 下 FREQ 报错 —— 记录原因供前端提示 + let (idle_time, idle_time_error) = match conn.object_idletime(&key) { + Ok(v) => { + let n: Option = v; + (n.map(|x| x as u64), None) + } + Err(e) => (None, Some(e.to_string())), + }; + let (freq, freq_error) = match conn.object_freq(&key) { + Ok(v) => { + let n: Option = v; + (n.map(|x| x as u64), None) + } + Err(e) => (None, Some(e.to_string())), + }; + + Ok(RedisObjectInfo { + encoding, + idle_time, + idle_time_error, + refcount, + freq, + freq_error, + }) +} + /// ZSet Top/Bottom 范围查询:ZRANGE/ZREVRANGE ... WITHSCORES pub fn zset_range0( mut conn: MutexGuard, diff --git a/src-tauri/src/client/impl_cluster.rs b/src-tauri/src/client/impl_cluster.rs index 95a23c9f..9d76dd3a 100644 --- a/src-tauri/src/client/impl_cluster.rs +++ b/src-tauri/src/client/impl_cluster.rs @@ -261,6 +261,10 @@ impl MeClient for MeCluster { zset_range0(self.get_conn()?, param) } + fn object_info(&self, key: RedisKey) -> AnyResult { + object_info0(self.get_conn()?, key) + } + fn execute_command(&self, param: RedisCommand) -> AnyResult { let (cmd_name, args) = parse_command(param.command.as_str())?; if cmd_name.is_empty() { diff --git a/src-tauri/src/client/impl_single.rs b/src-tauri/src/client/impl_single.rs index f28e7124..5b127c94 100644 --- a/src-tauri/src/client/impl_single.rs +++ b/src-tauri/src/client/impl_single.rs @@ -201,6 +201,10 @@ impl MeClient for MeSingle { zset_range0(self.get_conn()?, param) } + fn object_info(&self, key: RedisKey) -> AnyResult { + object_info0(self.get_conn()?, key) + } + fn execute_command(&self, param: RedisCommand) -> AnyResult { let (cmd, args) = parse_command(param.command.as_str())?; if cmd.is_empty() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 43731be0..7035f06b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -48,6 +48,7 @@ fn tauri_specta_commands() -> Commands { field_del, zset_rank, zset_range, + object_info, execute_command, acl_users, acl_list_users, diff --git a/src-tauri/src/utils/model.rs b/src-tauri/src/utils/model.rs index 268a8f9b..bb125816 100644 --- a/src-tauri/src/utils/model.rs +++ b/src-tauri/src/utils/model.rs @@ -655,6 +655,18 @@ api_model!(RedisZsetRankResult { rev_rank: Option, }); +// OBJECT 自省:ENCODING / IDLETIME / REFCOUNT / FREQ(*_error 为策略限制等原因) +api_model!(RedisObjectInfo { + encoding: Option, + idle_time: Option, + /// IDLETIME 因 maxmemory-policy 等不可用时的错误信息 + idle_time_error: Option, + refcount: Option, + freq: Option, + /// FREQ 因 maxmemory-policy 等不可用时的错误信息 + freq_error: Option, +}); + // ZSet Top/Bottom 范围查询 api_model!(RedisZsetRange { key: RedisKey, diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index 898a7816..cd92a457 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -883,6 +883,44 @@ export default { valueTruncatedLoadAll: 'Load Full Value', viewCodec: 'Codec', commandHelp: 'Command Help', + objectInfo: 'Object Introspection', + objectInfoCommand: 'Command', + objectInfoItem: 'Item', + objectInfoValue: 'Value', + objectEncoding: 'Internal Encoding', + objectIdleTime: 'Idle Time', + objectRefcount: 'Reference Count', + objectFreq: 'Access Frequency', + objectInfoNA: 'N/A', + objectIdleTimeUnavailable: 'OBJECT IDLETIME is unavailable when maxmemory-policy is LFU-based', + objectFreqUnavailable: + 'OBJECT FREQ is unavailable when maxmemory-policy is not LFU (including no eviction policy)', + objectEncodingTip: `Redis objects can be encoded in different ways

+Strings
+• raw: normal string encoding
+• int: a string representing a 64-bit signed integer, encoded this way to save space
+• embstr: embedded string; an object where the SDS is an immutable string allocated in the same memory block as the object itself; length limited by OBJ_ENCODING_EMBSTR_SIZE_LIMIT (44 bytes)

+Lists
+• linkedlist: simple list encoding (old, no longer used)
+• ziplist: (Redis ≤ 6.2) space-efficient encoding for small lists
+• listpack: (Redis ≥ 7.0) space-efficient encoding for small lists
+• quicklist: a linked list of ziplist or listpack

+Sets
+• hashtable: normal set encoding
+• intset: special encoding for small sets of integers only
+• listpack: (Redis ≥ 7.2) space-efficient encoding for small sets

+Hashes
+• zipmap: old hash encoding (no longer used)
+• hashtable: normal hash table encoding
+• ziplist: (Redis ≤ 6.2) space-efficient encoding for small hashes
+• listpack: (Redis ≥ 7.0) space-efficient encoding for small hashes

+Sorted Sets
+• skiplist: normal sorted set encoding
+• ziplist: (Redis ≤ 6.2) space-efficient encoding for small sorted sets
+• listpack: (Redis ≥ 7.0) space-efficient encoding for small sorted sets

+Streams
+• stream: a radix tree encoded as listpack

+Once an operation prevents Redis from keeping the space-saving encoding, special encodings are automatically converted to the general type`, keyShortHint: 'View KeyShort', keyShort: { fullscreen: 'Fullscreen Editor', diff --git a/src/locales/lang/zh-cn.ts b/src/locales/lang/zh-cn.ts index 5d13e4e4..526418f9 100644 --- a/src/locales/lang/zh-cn.ts +++ b/src/locales/lang/zh-cn.ts @@ -869,6 +869,43 @@ export default { valueTruncatedLoadAll: '仍要加载全部', viewCodec: '数据编码', commandHelp: '命令帮助', + objectInfo: '对象自省', + objectInfoCommand: '命令', + objectInfoItem: '项目', + objectInfoValue: '值', + objectEncoding: '内部编码', + objectIdleTime: '空闲时间', + objectRefcount: '引用计数', + objectFreq: '访问频率', + objectInfoNA: '不可用', + objectIdleTimeUnavailable: '当 maxmemory-policy 为 LFU 策略时,OBJECT IDLETIME 不可用', + objectFreqUnavailable: '当 maxmemory-policy 非 LFU 策略(含无淘汰策略)时,OBJECT FREQ 不可用', + objectEncodingTip: `Redis 对象可以使用不同的方式进行编码

+字符串
+• raw:普通字符串编码
+• int:表示 64 位有符号整数的字符串,以此方式编码以节省空间
+• embstr:嵌入式字符串;内部 SDS 与对象本身分配在同一内存块的不可变字符串,长度受 OBJ_ENCODING_EMBSTR_SIZE_LIMIT(44 字节)限制

+列表
+• linkedlist:简单列表编码(旧编码,已不再使用)
+• ziplist:(Redis ≤ 6.2)小列表的高效空间编码
+• listpack:(Redis ≥ 7.0)小列表的高效空间编码
+• quicklist:由 ziplist 或 listpack 编码的链表

+集合
+• hashtable:普通集合编码
+• intset:仅含整数的小集合的特殊编码
+• listpack:(Redis ≥ 7.2)小集合的高效空间编码

+哈希表
+• zipmap:旧哈希编码(已不再使用)
+• hashtable:普通哈希表编码
+• ziplist:(Redis ≤ 6.2)小哈希的高效空间编码
+• listpack:(Redis ≥ 7.0)小哈希的高效空间编码

+有序集合
+• skiplist:普通有序集合编码
+• ziplist:(Redis ≤ 6.2)小有序集合的高效空间编码
+• listpack:(Redis ≥ 7.0)小有序集合的高效空间编码

+
+• stream:编码为 listpack 的基数树

+一旦执行的操作导致 Redis 无法保留节省空间的编码,所有特殊编码类型都会自动转换为通用类型`, keyShortHint: '查看快捷键', keyShort: { fullscreen: '全屏编辑器', diff --git a/src/types/tauri-specta.ts b/src/types/tauri-specta.ts index c0a79676..79d1f250 100644 --- a/src/types/tauri-specta.ts +++ b/src/types/tauri-specta.ts @@ -39,6 +39,7 @@ export const commands = { fieldDel: (id: string, param: RedisFieldDel_Deserialize) => typedError(__TAURI_INVOKE("field_del", { id, param })), zsetRank: (id: string, param: RedisZsetRank_Deserialize) => typedError(__TAURI_INVOKE("zset_rank", { id, param })), zsetRange: (id: string, param: RedisZsetRange_Deserialize) => typedError(__TAURI_INVOKE("zset_range", { id, param })), + objectInfo: (id: string, key: RedisKey_Deserialize) => typedError(__TAURI_INVOKE("object_info", { id, key })), executeCommand: (id: string, param: RedisCommand) => typedError(__TAURI_INVOKE("execute_command", { id, param })), aclUsers: (id: string) => typedError(__TAURI_INVOKE("acl_users", { id })), aclListUsers: (id: string) => typedError(__TAURI_INVOKE("acl_list_users", { id })), @@ -552,6 +553,17 @@ export type RedisNode = { slaveOfNode: string | null, }; +export type RedisObjectInfo = { + encoding: string | null, + idleTime: number | null, + /** IDLETIME 因 maxmemory-policy 等不可用时的错误信息 */ + idleTimeError: string | null, + refcount: number | null, + freq: number | null, + /** FREQ 因 maxmemory-policy 等不可用时的错误信息 */ + freqError: string | null, +}; + export type RedisPop = RedisPop_Serialize | RedisPop_Deserialize; export type RedisPop_Deserialize = { diff --git a/src/views/ext/ObjectInfo.vue b/src/views/ext/ObjectInfo.vue new file mode 100644 index 00000000..e3739b08 --- /dev/null +++ b/src/views/ext/ObjectInfo.vue @@ -0,0 +1,125 @@ + + + diff --git a/src/views/tab/RedisValue.vue b/src/views/tab/RedisValue.vue index 90029e8d..1c97967e 100644 --- a/src/views/tab/RedisValue.vue +++ b/src/views/tab/RedisValue.vue @@ -71,6 +71,7 @@ import { meWarn, sleep, } from '@/utils/util' +import ObjectInfo from '@/views/ext/ObjectInfo.vue' import TableGroup from '@/views/ext/TableGroup.vue' import TableHashKeys from '@/views/ext/TableHashKeys.vue' import TableZsetRange from '@/views/ext/TableZsetRange.vue' @@ -832,6 +833,8 @@ function renameKey() { keyRenameRef.value?.open({ redisKey: share.redisKey }) } +const objectInfoRef = useTemplateRef>('objectInfoRef') + function duplicateKey() { if (!share.redisKey) return connUi.openKeyCopy(share.redisKey) @@ -930,6 +933,8 @@ async function onKeyMoreCommand(command: string) { renameKey() } else if (command === 'duplicateKey') { duplicateKey() + } else if (command === 'objectInfo') { + objectInfoRef.value?.open() } else if (command === 'showSlot') { void showSlot() } else if (command === 'showLocation') { @@ -1496,7 +1501,10 @@ onUnmounted(() => { - + + + + @@ -2037,6 +2045,7 @@ onUnmounted(() => { + From 5b656136447c1080d1eeba75a925544498ac4afd Mon Sep 17 00:00:00 2001 From: hepengju Date: Thu, 16 Jul 2026 22:28:01 +0800 Subject: [PATCH 78/82] =?UTF-8?q?feat:=20=E5=91=BD=E4=BB=A4=E5=B8=AE?= =?UTF-8?q?=E5=8A=A9=E8=A1=A8=E6=A0=BC=E6=94=AF=E6=8C=81=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E5=90=84=E5=AE=98=E7=BD=91=E5=91=BD=E4=BB=A4=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/components/MeWebsite.vue | 25 ++++++++++++++++++++----- src/views/ext/CommandHelp.vue | 7 ++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/MeWebsite.vue b/src/components/MeWebsite.vue index 6bd6df11..74b7d51e 100644 --- a/src/components/MeWebsite.vue +++ b/src/components/MeWebsite.vue @@ -1,5 +1,5 @@