feat: custom formatter support
- format.ts: customFormat() uses child_process.execFile
- Template params: {KEY} {VALUE} {HEX_FILE}
- Value written to temp file, path passed via {HEX_FILE}
- 10s timeout, error handling
- StringEditor: 'custom' format option + command input
- Command persisted to localStorage
- i18n keys (zh-CN + en)
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@
|
||||
|
||||
- [x] **多格式查看器** — Text / Hex / Json / Binary / Msgpack / PHP Serialize / Java Serialize / Pickle / Brotli / Gzip / Deflate / DeflateRaw / Protobuf(13 种格式)
|
||||
- [x] **自动格式检测** — 检查内容 magic bytes 自动选择查看器
|
||||
- [ ] **自定义格式化器** — 外部可执行文件 + 模板参数 {KEY} {VALUE} {HEX} {HEX_FILE}
|
||||
- [x] **自定义格式化器** — 外部可执行文件 + 模板参数 {KEY} {VALUE} {HEX} {HEX_FILE}
|
||||
- [ ] **ReJson / TairJson 支持** — JSON.GET / JSON.SET / JSON.DEL 编辑
|
||||
- [ ] **二进制 key 支持** — [Hex] 前缀输入,hex ↔ buffer 双向转换
|
||||
- [ ] **虚拟化滚动** — 大 key 集合(200k+)的性能优化,KeyList 树形视图虚拟化
|
||||
|
||||
@@ -264,6 +264,9 @@ export function registerIpcHandlers(): void {
|
||||
ipcMain.handle('redis:detectFormat', (_e, value: string) => {
|
||||
return redis.detectFormat(value)
|
||||
})
|
||||
ipcMain.handle('redis:customFormat', (_e, value: string, command: string, key: string) => {
|
||||
return redis.customFormat(value, command, key)
|
||||
})
|
||||
|
||||
// Pub/Sub
|
||||
ipcMain.handle('redis:subscribe', async (_e, id: string, channels: string[], isPattern: boolean) => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// electron/main/redis/format.ts
|
||||
// 值格式转换(解压/编码)
|
||||
// 值格式转换(解压/编码/自定义)
|
||||
import { gunzipSync, inflateSync, brotliDecompressSync } from 'zlib'
|
||||
import { execFile } from 'child_process'
|
||||
import { writeFile, unlink, mkdtemp } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
export type FormatType = 'text' | 'hex' | 'json' | 'binary' | 'gzip' | 'deflate' | 'brotli' | 'deflateRaw'
|
||||
|
||||
@@ -44,3 +48,33 @@ export function detectFormat(value: string): FormatType {
|
||||
if (hasNonPrintable) return 'hex'
|
||||
return 'text'
|
||||
}
|
||||
|
||||
export async function customFormat(
|
||||
value: string,
|
||||
command: string,
|
||||
key?: string
|
||||
): Promise<string> {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), 'jrdm-fmt-'))
|
||||
const tmpFile = join(tmpDir, 'value.bin')
|
||||
await writeFile(tmpFile, value, 'utf-8')
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const cmd = command
|
||||
.replace(/\{KEY\}/g, key || '')
|
||||
.replace(/\{VALUE\}/g, value)
|
||||
.replace(/\{HEX_FILE\}/g, tmpFile)
|
||||
|
||||
const parts = cmd.split(/\s+/)
|
||||
const exe = parts[0]
|
||||
const args = parts.slice(1)
|
||||
|
||||
execFile(exe, args, { timeout: 10000, maxBuffer: 10 * 1024 * 1024 }, async (err, stdout) => {
|
||||
await unlink(tmpFile).catch(() => {})
|
||||
if (err) {
|
||||
resolve(`[Custom formatter error: ${err.message}]`)
|
||||
} else {
|
||||
resolve(stdout)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -111,6 +111,7 @@ export interface ElectronAPI {
|
||||
clearCommandLog: () => Promise<void>
|
||||
decodeValue: (value: string, format: string) => Promise<string>
|
||||
detectFormat: (value: string) => Promise<string>
|
||||
customFormat: (value: string, command: string, key: string) => Promise<string>
|
||||
subscribe: (id: string, channels: string[], isPattern: boolean) => Promise<{ success: boolean; error?: string }>
|
||||
unsubscribe: (id: string) => Promise<void>
|
||||
onSubscribeMessage: (callback: (msg: any) => void) => void
|
||||
|
||||
@@ -135,6 +135,8 @@ const electronAPI = {
|
||||
ipcRenderer.invoke('redis:decodeValue', value, format),
|
||||
detectFormat: (value: string): Promise<string> =>
|
||||
ipcRenderer.invoke('redis:detectFormat', value),
|
||||
customFormat: (value: string, command: string, key: string): Promise<string> =>
|
||||
ipcRenderer.invoke('redis:customFormat', value, command, key),
|
||||
// Pub/Sub
|
||||
subscribe: (id: string, channels: string[], isPattern: boolean): Promise<any> =>
|
||||
ipcRenderer.invoke('redis:subscribe', id, channels, isPattern),
|
||||
|
||||
@@ -34,8 +34,11 @@ const formatOptions = [
|
||||
{ value: 'gzip', labelKey: 'editor.formatGzip' },
|
||||
{ value: 'deflate', labelKey: 'editor.formatDeflate' },
|
||||
{ value: 'brotli', labelKey: 'editor.formatBrotli' },
|
||||
{ value: 'custom', labelKey: 'editor.formatCustom' },
|
||||
]
|
||||
|
||||
const customCommand = ref(localStorage.getItem('customFormatter') || '/bin/cat {HEX_FILE}')
|
||||
|
||||
const effectiveFormat = computed(() => {
|
||||
if (selectedFormat.value === 'auto') return detectedFormat.value
|
||||
return selectedFormat.value
|
||||
@@ -70,6 +73,11 @@ async function decodeAndDisplay(raw: string) {
|
||||
}
|
||||
}
|
||||
// For other formats, use IPC decode
|
||||
if (fmt === 'custom') {
|
||||
displayValue.value = await window.electronAPI.redis.customFormat(raw, customCommand.value, keyStore.selectedKey || '')
|
||||
isJson.value = false
|
||||
return
|
||||
}
|
||||
displayValue.value = await window.electronAPI.redis.decodeValue(raw, fmt)
|
||||
isJson.value = false
|
||||
}
|
||||
@@ -90,6 +98,8 @@ watch(selectedFormat, () => {
|
||||
if (editedValue.value) decodeAndDisplay(editedValue.value)
|
||||
})
|
||||
|
||||
watch(customCommand, (v) => localStorage.setItem('customFormatter', v))
|
||||
|
||||
const editorOptions = computed(() => ({
|
||||
readOnly: !isEditing.value || effectiveFormat.value !== 'json' && effectiveFormat.value !== 'text',
|
||||
minimap: { enabled: false },
|
||||
@@ -133,6 +143,13 @@ async function save() {
|
||||
:label="t(opt.labelKey)"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-if="selectedFormat === 'custom'"
|
||||
v-model="customCommand"
|
||||
size="small"
|
||||
style="width:240px"
|
||||
:placeholder="t('editor.customCommandHint')"
|
||||
/>
|
||||
<span v-if="selectedFormat === 'auto'" class="format-detected">
|
||||
{{ t('editor.format') }}: {{ detectedFormat }}
|
||||
</span>
|
||||
|
||||
@@ -141,6 +141,9 @@ export default {
|
||||
formatGzip: 'Gzip',
|
||||
formatDeflate: 'Deflate',
|
||||
formatBrotli: 'Brotli',
|
||||
formatCustom: 'Custom',
|
||||
customCommand: 'Custom Command',
|
||||
customCommandHint: 'Templates: {KEY} {VALUE} {HEX_FILE}',
|
||||
consumerGroups: 'Consumer Groups',
|
||||
group: 'Group',
|
||||
consumers: 'Consumers',
|
||||
|
||||
@@ -141,6 +141,9 @@ export default {
|
||||
formatGzip: 'Gzip',
|
||||
formatDeflate: 'Deflate',
|
||||
formatBrotli: 'Brotli',
|
||||
formatCustom: '自定义',
|
||||
customCommand: '自定义命令',
|
||||
customCommandHint: '模板: {KEY} {VALUE} {HEX_FILE}',
|
||||
consumerGroups: '消费者组',
|
||||
group: '组名',
|
||||
consumers: '消费者',
|
||||
|
||||
Reference in New Issue
Block a user