From e31dc4e7f18a9ea82f46f943fe34845f42d2963e Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 11 Feb 2026 10:41:22 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(redis-stream):=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20Redis=20Stream=20=E7=B1=BB=E5=9E=8B=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E4=B8=8E=E6=B6=88=E6=81=AF=E5=A2=9E=E5=88=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端扩展 RedisClient 接口,新增 StreamEntry 与 Stream 操作定义 - Redis 实现新增 XADD/XDEL/XRANGE 封装并接入 RedisGetValue 的 stream 分支 - App 层新增 RedisStreamAdd 与 RedisStreamDelete 方法并返回操作结果 - 前端新增 stream 类型视图,支持消息新增、删除与字段复制 - refs #92 --- frontend/src/components/RedisViewer.tsx | 210 +++++++++++++++++++++++- frontend/src/types.ts | 7 +- frontend/wailsjs/go/app/App.d.ts | 4 + frontend/wailsjs/go/app/App.js | 8 + internal/app/methods_redis.go | 34 ++++ internal/redis/redis.go | 13 +- internal/redis/redis_impl.go | 105 ++++++++++++ 7 files changed, 378 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/RedisViewer.tsx b/frontend/src/components/RedisViewer.tsx index c618447..cb102ed 100644 --- a/frontend/src/components/RedisViewer.tsx +++ b/frontend/src/components/RedisViewer.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { Table, Input, Button, Space, Tag, Tree, Spin, message, Modal, Form, InputNumber, Popconfirm, Tooltip, Radio } from 'antd'; import { ReloadOutlined, DeleteOutlined, PlusOutlined, EditOutlined, SearchOutlined, ClockCircleOutlined, CopyOutlined, FolderOpenOutlined, KeyOutlined } from '@ant-design/icons'; import { useStore } from '../store'; -import { RedisKeyInfo, RedisValue } from '../types'; +import { RedisKeyInfo, RedisValue, StreamEntry } from '../types'; import Editor from '@monaco-editor/react'; import type { DataNode } from 'antd/es/tree'; @@ -625,6 +625,7 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { case 'list': return 'orange'; case 'set': return 'purple'; case 'zset': return 'magenta'; + case 'stream': return 'cyan'; default: return 'default'; } }; @@ -1468,6 +1469,212 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { ); }; + const renderStreamValue = () => { + const processValue = (value: string) => { + if (viewMode === 'hex') { + return { displayValue: toHexDisplay(value), isBinary: true, isJson: false, encoding: 'HEX' }; + } else if (viewMode === 'text') { + return { displayValue: value, isBinary: false, isJson: false, encoding: 'Text' }; + } else if (viewMode === 'utf8') { + try { + const bytes = new Uint8Array(value.length); + for (let i = 0; i < value.length; i++) { + bytes[i] = value.charCodeAt(i) & 0xFF; + } + const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes); + return { displayValue: decoded, isBinary: false, isJson: false, encoding: 'UTF-8' }; + } catch (e) { + return { displayValue: value, isBinary: false, isJson: false, encoding: 'UTF-8 (失败)' }; + } + } else { + return formatStringValue(value); + } + }; + + const data = (keyValue.value as StreamEntry[]).map((item, index) => { + const rawFieldsText = JSON.stringify(item.fields ?? {}, null, 2); + const { displayValue, isBinary, isJson, encoding } = processValue(rawFieldsText); + return { + index, + id: item.id, + rawFieldsText, + displayFields: displayValue, + isBinary, + isJson, + encoding, + }; + }); + + const handleAddStreamEntry = async (fieldsText: string, id: string) => { + const config = getConfig(); + if (!config) return; + + let parsed: unknown; + try { + parsed = JSON.parse(fieldsText); + } catch (e) { + message.error('字段 JSON 格式不正确'); + return; + } + + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + message.error('字段必须是 JSON 对象'); + return; + } + + const fieldMap: Record = {}; + Object.entries(parsed as Record).forEach(([field, value]) => { + fieldMap[field] = value == null ? '' : String(value); + }); + + if (Object.keys(fieldMap).length === 0) { + message.error('至少提供一个字段'); + return; + } + + try { + const res = await (window as any).go.app.App.RedisStreamAdd(config, selectedKey, fieldMap, id || '*'); + if (res.success) { + const newID = res.data?.id ? ` (${res.data.id})` : ''; + message.success(`添加成功${newID}`); + loadKeyValue(selectedKey); + } else { + message.error('添加失败: ' + res.message); + } + } catch (e: any) { + message.error('添加失败: ' + (e?.message || String(e))); + } + }; + + const handleDeleteStreamEntry = async (id: string) => { + const config = getConfig(); + if (!config) return; + + try { + const res = await (window as any).go.app.App.RedisStreamDelete(config, selectedKey, [id]); + if (res.success) { + const deleted = Number(res.data?.deleted ?? 0); + if (deleted > 0) { + message.success('删除成功'); + } else { + message.warning('未删除任何消息,可能已不存在'); + } + loadKeyValue(selectedKey); + } else { + message.error('删除失败: ' + res.message); + } + } catch (e: any) { + message.error('删除失败: ' + (e?.message || String(e))); + } + }; + + return ( +
+
+ + setViewMode(e.target.value)}> + 自动 + 原始文本 + UTF-8 + 十六进制 + +
+ { + const tooltipContent = record.encoding && record.encoding !== 'UTF-8' + ? `[${record.encoding}]\n${text}` + : text; + + return ( + {tooltipContent}} styles={{ root: { maxWidth: 720 } }}> + + {text} + + + ); + } + }, + { + title: '操作', + key: 'action', + width: 140, + render: (_: any, record: any) => ( + + +