opsx
71d5afff03
fix(query): 结尾分号导致 LIMIT 拼接成两条语句(MySQL 1064)+ SQL 编辑器光标异常与无法滚动
...
=== 一、点表生成的语句带分号 → 查询必报 1064 ===
用户报错:
Error 1064 (42000): You have an error in your SQL syntax; ...
near 'LIMIT 100 OFFSET 0' at line 2
复现语句(点表自动生成的原文,天然带分号):
SELECT * FROM `cex_activity`.`concert_goods_language`;
根因在 instance_detail.go 的自动补 LIMIT:
sqlStr = fmt.Sprintf("%s LIMIT %d OFFSET %d", sqlStr, limit, offset)
原语句已以 ';' 结尾 → 拼完变成
SELECT * FROM ...; LIMIT 100 OFFSET 0
→ 两条语句,报错位置就是第 2 行的 LIMIT(与报错文案完全吻合)。
修复:新增 normalizeSQLForExec(),去掉首尾空白并反复剥掉结尾分号,
在两个「会拼 LIMIT」的入口统一调用:
① executeSQLQuery(查询)—— 归一化后再生效 COUNT(*) 统计与 LIMIT 注入
② exportSQLCSV(导出 CSV)—— 同样先归一化再拼 LIMIT
顺带收益:COUNT(*) 统计以前也会带上分号(虽然 MySQL 容忍),现在干净了。
安全边界未被放松:多语句仍被 isReadOnlySQL 拦住
SELECT 1; SELECT 2 → 拒绝「不允许一次执行多条语句」✅
SELECT 1; LIMIT 5 → 拒绝 ✅ (这是一次性两条语句,不是尾分号)
双分号 ;; → 归一化后正常执行 ✅
=== 二、SQL / Redis / Mongo 编辑器:光标异常 + 只有 13 行且滚不动 ===
用户反馈:
「默认还是有一个很长的竖线 一闪一闪,需要回车后第 2 行才正常」
「输入框下面还有空白,最后只有 13 行可以输入,至少需要加一个滚动」
根因 1(滚不动,13 行):appTheme 的 '&' 没有 height:100%。
.cm-editor 会随内容无限增高,而外层 div 固定 200px 且裁切,
CodeMirror 内部的 .cm-scroller 永远拿不到受限高度 → 不产生滚动条。
13 行 = 200px ÷ 15px 行高,正好对上。
根因 2(光标拉长):appTheme 在 extensions 里排在 oneDark 【之前】。
CodeMirror theme 是后者覆盖前者,oneDark 的默认行高/内边距盖掉了我们的
line-height,首次测量拿到的行高不对 → 空文档时光标高度算成好几行。
修复:
① appTheme '&' 增加 height:'100%';外层容器 style 改为
height + minHeight:120px + resize:vertical + overflow:hidden
(.cm-scroller 立刻有确定高度 → 内部滚动生效;右下角还能拖动拉高)
② appTheme 移到 oneDark 之后(顺序即优先级)
③ 行高统一写成 1.55em,并在 .cm-scroller / .cm-content / .cm-line /
.cm-gutters 四处保持一致(CM 靠读 .cm-content 的 line-height 算光标高度)
④ 挂载后 requestMeasure() + 下一帧再测一次 + document.fonts.ready 后再测一次,
修掉「首帧测量错乱」这一大类问题
⑤ SqlEditorSection 默认高度 200px → 240px(约 15 行可见)
注意:仍然不要给 .cm-cursor 写死 height / marginTop ——
CM 自己按行高算,写死会溢出到相邻行(历史上已经踩过一次)。
=== 三、实测 ===
表本身为空:cex_activity.concert_goods_language COUNT(*)=0
→ SELECT * 返回 0 行是正确结果,不是故障
分号归一化:带分号 / 不带分号 / 双分号 → rows=100 total=17447 完全一致,无 1064 ✅
安全边界:两条语句、分号后接 LIMIT 均被拒 ✅
编辑器产物:SqlEditor-*.js 内含 1.55em 与 resize:"vertical" ✅
镜像:lghost/opsxsql:latest + v0.1 = sha256:72a00dda2419
2026-10-27 09:22:11 +00:00
opsx
7dfbc0b680
feat(redis): 大 key 分页 + 流式导出(含票据鉴权)+ 查询行数上限默认 5000
...
=== 一、问题:大 key 会把内存打爆,且查询结果没有导出 ===
RedisGetKeyDetail 原来对每种类型都是全量读取:
string → GET list → LRANGE 0 -1 set → SMEMBERS
zset → ZRANGE 0 -1 hash → HGETALL
然后整包序列化返回。像 PERP:market:* 这类盘口/kline key,
一个 key 几十万条就会:Go 侧整值入内存 → JSON 序列化翻倍 → 浏览器渲染卡死。
=== 二、后端:键详情分页(统一令牌,兼容四种类型)===
GET /redis/:id/keys/:key?db=&count=&token=
count 默认 200,硬上限 2000
token 语义按类型区分,前端无感知:
list / zset → 十进制 offset(LRANGE / ZRANGE 区间)
hash / set → Redis SCAN 游标(HSCAN / SSCAN)
string → 忽略
返回 total / count / nextToken / truncated / previewCap
string 特殊处理:不能分页,改为预览上限 256KB
- 用 STRLEN 拿真实大小(O(1))
- 超过上限时用 GETRANGE 只取前 256KB,truncated=true
- 完整内容只能靠导出
新增 wrapper 方法:HScan / ZScan / HLen / ZCard / StrLen / GetRange
(HGETALL、SMEMBERS、ZRANGE 0 -1 一律不再用于详情读取)
=== 三、后端:流式导出(两段式票据,避免令牌进 URL 日志)===
① POST /redis/:id/keys/:key/export-ticket (走 Bearer 鉴权)
→ HMAC-SHA256(JWTSecret, instance|db|key|exp) 生成票据,2 分钟过期,
绑定具体 instance/db/key,不能跨 key 复用
② GET /redis/:id/keys/:key/export?db=&format=csv|jsonl&ticket=
→ 注册在鉴权组【之外】,靠票据校验
为什么这么做:浏览器原生下载(<a download>/window.open)带不了
Authorization 头;若前端用 axios 取 blob,整份导出会进浏览器内存,
与「大 key 不爆内存」的目标相悖。
导出实现全程流式(服务端内存 = 单批 + 64KB 缓冲):
string → GETRANGE 256KB 分块
list → LRANGE 每批 1000
zset → ZRANGE WITHSCORES 每批 1000
hash → HSCAN 游标每批 1000
set → SSCAN 游标每批 1000
单次上限 200 万行;CSV 带 UTF-8 BOM(Excel 直接打开不乱码);
列顺:hash(field,value) / list(index,value) / set(member) / zset(rank,member,score)
=== 四、前端:详情面板加分页条 + 4 个导出按钮 ===
RedisKeyDetailPanel:
- 「共 N 条 · 第 X 页 · 每页 [100/200/500/1000/2000]」+ 上一页/下一页
- SCAN 游标不可逆,上一页用令牌栈回放
- string 超预览上限时显示黄色提示「仅预览前 256KB,共 N 字节」
- 导出四档:当前页 CSV / 当前页 JSON(纯前端,瞬时)
导出全部 CSV / 导出全部 JSONL(服务端流式,边读边写)
顺带修掉头注释里「已知限制:大值不分页」的过时说明。
=== 五、查询行数上限:默认 1000 → 5000,并修掉硬编码 ===
用户要求:「默认设置里面有查询最大上限吧 比如 mysql pg 查询最大Limit限制 5000」
发现两个问题:
1. seed 默认值只有 1000(main.go query_limit_max)
2. ExecuteQuery 里还有一段硬编码:
if input.Limit <= 0 { input.Limit = maxLimit; if input.Limit > 1000 { input.Limit = 1000 } }
→ 管理员把设置改成 5000 也白搭,前端不传 limit 时永远被压回 1000
修复:
- seed 默认值 1000 → 5000(Value + DefaultValue)
- getSettingInt("query_limit_max", 1000) → 5000
- 删掉硬编码 1000 封顶,唯一口径就是设置值
- 线上已有记录的 value 已通过 PUT /settings 改成 5000(seed 只在缺失时插入,不会自动更新旧值)
对 MySQL / PostgreSQL / Redis / MongoDB 统一生效(ExecuteQuery 按 db_type 分发,
limit 在入口处统一 clamp)。
=== 六、实测结果(线上)===
分页:
zset PERP:market:kline:symbol_kline_12h_AAVEUSDT_PERP_MARK
total=83,count=5 → 第1页 5 条 nextToken=5 → 第2页 5 条 nextToken=10,数据不同 ✅
hash ST:market:ticker:symbol_ticker
total=27,count=3 → 第1页 3 字段 nextToken=24 → 第2页 3 字段(完全不同)✅
string PERP:market:depth:symbol_depth_BTCUSDT_PERP
STRLEN=14192 → 导出 14192 字节,完全一致 ✅ (实时 key 两次采样有差异属正常)
导出:
zset CSV 200 / 30970 字节 / 84 行(header+83)✅
zset JSONL 200 / 84 行 ✅
string CSV 200 / 分块 GETRANGE 拼接完整 ✅
无票据 → 403 ✅ 伪造票据 → 403 ✅
查询上限:
limit=99999 → 5000 行 ✅ limit=0(默认)→ 5000 行 ✅
SQL 导出 CSV(原已存在 ExportQueryCSV,支持 mysql/pg/redis/mongo)200 / 101 行 ✅
镜像:lghost/opsxsql:latest + v0.1 = sha256:68fc996681c6
2026-10-27 09:14:02 +00:00
Logan.Li
2c96902fbd
fix: make postgresql actually usable (4 critical bugs)
...
- CORS whitelist only allowed origin localhost:5173; browsers send Origin
on same-origin POST too, so every deployment on any other port/origin
got empty 403 on ALL POST/PUT/DELETE (curl tests masked it) - now
echoes any origin (bearer-token auth, no cookie CSRF surface)
- connection_tester pg DSN had a literal *** password placeholder - pg
connection test and query path could never authenticate
- instance detail endpoints (databases/tables/db-schema/table-detail)
ran MySQL SHOW statements against pg - added pg branches
(pg_database/pg_class/information_schema/pg_indexes, search_path
semantics, PRI detection); schema filter uses non-system schemas
- frontend normalizeDbType missed the literal 'postgresql' value so pg
instances were treated as mysql (backtick quoting, no column hints);
genSql no longer emits db.table prefix for pg (schema semantics)
- also: query page footer warnings condensed to a single line per db type
verified live: mysql table tree/context menu/preview-execute; pg table
tree/columns/PK, double-click preview with masking working (email/phone
masked), query history persisted
2026-10-26 19:47:08 +08:00
Logan.Li
16f1f7d65f
feat(backend): unified response helpers, refresh token, security hardening
...
- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
envelope {detail, code?}, sanitized 500s (full error logged server-side,
generic message to client - stops DB error leakage), pagination envelope
{items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
POST /auth/refresh with single-use rotation, POST /auth/logout revokes
all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
Instance.MarshalJSON guarantees encrypted passwords never leave the API
(removes reliance on per-handler manual masking); production (DEBUG=false)
with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
2026-10-26 17:21:00 +08:00
hermes-$(hostname)
0fcdbc5f6c
update
2026-07-08 08:53:48 +00:00
Logan.Li
617c15dbfe
feat: harden query read-only workflow
2026-07-07 17:20:44 +08:00
Logan
644981eef4
feat: improve datasource query audit workflows
2026-07-06 23:01:04 +08:00
logan
8fbcb8647d
feat: SQL安全防护体系 — 危险SQL黑名单 + 事务回滚 + LIMIT修复
...
🔴 P0 安全漏洞修复:
- MySQL: 黑名单拦截 DROP DATABASE/DROP SCHEMA
- MySQL: 拦截无WHERE的UPDATE/DELETE(全表操作)
- MySQL: 多语句中含危险SQL也会被检测
- Redis: 拦截 FLUSHALL/FLUSHDB/CONFIG/DEBUG/SHUTDOWN/SCRIPT/SAVE/BGSAVE/KEYS/CLUSTER/REPLICAOF
🔄 事务支持:
- MySQL多语句执行包裹在事务中
- 任一SQL失败自动回滚全部已执行语句
- 验证通过: INSERT成功+UNIQUE冲突=全部回滚
🔧 Bug修复:
- query.go: LIMIT只追加SELECT语句(SHOW/CREATE等不再追加LIMIT报错)
- SHOW VARIABLES/STATUS 恢复正常
📊 测试验证:
- 29/29 安全拦截测试全部通过
- MySQL 8项危险操作全部拦截
- Redis 11项危险命令全部拦截
- 10项正常SQL全部放行
- 事务回滚验证通过
- 类型错误/参数错误正确报告
2026-06-03 02:49:07 +08:00
logan
bc2092a980
feat: 平台配置全面生效 — 配置策略接入实际逻辑
...
- query组: ExecuteQuery读取query_limit_max/query_timeout/query_dangerous_pattern
- 查询超时通过context.WithTimeout生效,db.QueryContext/QueryRowContext
- 危险模式检测拦截无WHERE的DELETE/UPDATE/DROP/TRUNCATE
- LIMIT上限从settings读取,不再硬编码1000
- security组: Login读取login_max_attempts/login_lockout_minutes
- 新增User字段: failed_login_attempts + locked_until
- 登录失败达到阈值自动锁定,成功时清除计数
- JWT Token过期时间读取session_timeout_hours设置
- executor组: ExecuteTicket读取executor_timeout/executor_dry_run
- 执行超时通过context.WithTimeout控制
- 开启dry_run时先EXPLAIN预检查SQL语法
- db.ExecContext替代db.Exec
- cleanup组: SettingsCleanupPreview/Run从占位改为真实实现
- 预览统计4类数据(工单/查询历史/慢日志/审计日志)待清理数量
- 执行清理按retention_days配置删除过期数据
- ghost组: 13项gh-ost配置加入seedDefaultSettings
- enabled/path/mode/osc_priority/env/cloud/trigger/chunk/load/lag等
- 已被ghost.go中getGhostConfig等函数读取
- 前端: PlatformConfigPage新增Ghost/OSC分组显示
2026-06-01 01:00:23 +08:00
logan
24c633d9f0
feat: ZRANGE WITHSCORES JSON自动解析为结构化表格 + Redis审计规则 + 前端安全检查
...
- ZRANGE WITHSCORES 自动检测JSON member,展开为 open/close/high/low/volume 等独立列
- Redis 审计: 只读命令安全,写命令warning,FLUSHALL等危险命令error
- TicketCreatePage: auditResult.findings 添加 undefined 安全检查
- 审计API支持 instance_id 自动解析 db_type
2026-05-31 02:07:10 +08:00
logan
e9a700b9d9
fix: Redis 查询格式修复
...
- rows nil → 空数组: rowsOrDefault() 确保 HGETALL/LRANGE 空集合返回 [] 而非 null
- 扩展只读白名单: PING/CONFIG/CLIENT/SLOWLOG/CLUSTER/MEMORY/OBJECT/DEBUG 等
- 前端 sortedResult: Array.isArray() 检查防止 raw_text 模式报 not iterable
2026-05-31 00:34:38 +08:00
logan
433e7e2909
feat: ARDM风格查询页优化 — 可拖拽Sidebar + Redis差异化结果展示 + KeyHeader\n\n1. Sidebar可拖拽宽度(200-600px, localStorage持久化)\n2. Redis raw_text展示加ARDM KeyHeader: key名+类型标签+TTL+格式标签+复制按钮\n3. HGETALL结果用field/value紧凑双列展示替代通用表格\n4. 后端raw_text模式自动查询key TYPE和TTL返回raw_type/raw_ttl
2026-05-30 18:20:22 +08:00
logan
4e6f9eb8fd
fix: Redis集群SCAN遍历所有master节点 + 查询结果布局调整\n\n1. ScanAllKeys(): 集群模式用ForEachMaster遍历所有master节点SCAN合并去重\n2. RedisScanKeys/KEYS命令均改用ScanAllKeys,确保集群不遗漏key\n3. QueriesPage布局: sidebar独立左侧,查询结果只在编辑器下方不被sidebar占据宽度
2026-05-30 15:28:38 +08:00
logan
430a667455
fix: Redis命令解析支持引号参数(parseCommandParts替代strings.Fields)\n\nstrings.Fields()会将引号当作参数的一部分,导致HGET带引号查询失败\n新增parseCommandParts()函数支持双引号和单引号包裹的参数
2026-05-30 14:47:21 +08:00
logan
3af65f2350
feat: Redis cluster support + version auto-detect + metrics API
2026-05-30 01:44:58 +08:00
logan
557d88d3ed
feat: 数据脱敏体系重构 — 全局设置+masked_columns+前端去硬编码
...
后端:
- applyDataMasking 新增 masked_columns 返回字段,告诉前端哪些列被脱敏
- seedDefaultSettings 添加 data_masking 分组(启用开关+敏感字段列表)
- 脱敏逻辑: 全局敏感字段 + 实例级 mask_fields 合并,开关关闭则跳过
前端:
- QueriesPage 去掉硬编码 sensitiveKeywords 和 maskData()
- 改用后端返回的 masked_columns 显示'敏'标签
- 脱敏值由后端统一处理(替换为*)
- PlatformConfigPage 数据脱敏分组独立显示
- EditInstanceDialog 脱敏字段简化为逗号分隔输入
验证:
- 设置页开关可正常开关/保存
- 查询 ghost_test: email列被脱敏为*,列头显示'敏'标签
- 非敏感列(name, id, status)正常显示
2026-05-28 23:26:10 +08:00
logan
b8df2c0027
fix: Redis KEYS命令自动补通配符(*)
...
输入 KEYS match 自动变为 match* 模糊匹配,
无需手动加 *,同时匹配 ? [] 等 Redis 原生通配符
2026-05-28 16:07:16 +08:00
logan
3b3cc32d45
feat: Redis/MongoDB分页+CSV导出全数据库类型支持
...
后端:
- Redis 分页: KEYS→SCAN分页, LRANGE→offset/limit, SMEMBERS→SSCAN分页
- MongoDB 新增 executeMongoQuery: db.collection.find/aggregate 解析
- find: skip/limit分页 + CountDocuments总数
- aggregate: $facet并行取count+data
- bson.M扁平化处理嵌套文档
- ExportQueryCSV 改为多分支: exportSQLCSV/exportRedisCSV/exportMongoCSV
- Redis: 复用executeRedisQuery取数据写CSV
- MongoDB: 复用executeMongoQuery取数据写CSV
- 新增 mongo-driver v2 依赖
- 前端: Redis也显示导出CSV按钮(去掉dbType过滤)
2026-05-28 15:58:42 +08:00
logan
5ea86fe334
feat: 查询分页(limit/offset)+流式CSV导出+去复制按钮
...
后端:
- ExecuteQuery 添加 limit/offset 参数,MySQL 自动注入 LIMIT/OFFSET
- 首页自动 COUNT 查总数(total),翻页页不再重复 count
- 新增 POST /query/export 流式 CSV 导出端点,游标分批写不占内存
- 默认 limit=100, max=1000,防止大数据量 OOM
前端:
- 查询结果加分页控件(首页/上一页/下一页/末页 + 每页条数选择)
- CSV 导出改走流式端点(全量数据),不限于当前页
- 去掉复制按钮(Copy/Check/coped state),Redis 单值也不显示复制
- 工具栏显示 总行数/行每页/列数
2026-05-28 15:51:22 +08:00
logan
03ff845485
feat: Redis单值结果直接展示+JSON中文格式化+悬浮复制
...
1. GET/HGET/TTL等单值命令: 直接展示raw值, 不再套table
2. JSON自动检测+格式化(中文正常显示, 2空格缩进)
3. 非JSON值(如false/数字)直接文本展示
4. 代码块右上角悬浮复制按钮(hover显示)
5. 工具栏显示值类型(JSON/String)替代行列数
2026-05-28 15:20:22 +08:00
logan
18430f3717
feat: Redis查询结果优化 — 复制按钮+JSON自动格式化+语义化列名
...
1. GET/HGET/TTL等单值命令: columns改为[key,value]替代[command,result]
2. DBSIZE等无key命令保持[command,result]格式
3. 前端添加复制按钮(Clipboard API), 2秒反馈
4. JSON值自动检测并格式化显示(whitespace-pre-wrap)
5. 结果区最大高度350px→500px
6. cost_ms字段兼容处理
2026-05-28 15:09:37 +08:00
logan
828a0597c5
fix: Redis查询核心bug修复 — 命令拆分+HGETALL结构化+DB号+插入换行
...
1. executeRedisQuery: client.Do()没传命令名导致ERR unknown command
2. HGETALL结果用reflect处理map类型,结构化输出field/value表格
3. 支持指定Redis DB号(之前固定DB=0)
4. 前端Redis命令插入用换行分隔,MySQL保持空格
5. cost_ms到execution_time_ms字段映射修复
2026-05-28 14:53:12 +08:00
logan
8469df359f
fix: SHOW TABLE STATUS 列名小写化修复前端 key 警告 + table-detail 400 错误
...
根因: Go 后端 DetailListTables 返回 SHOW TABLE STATUS 原始列名(如 Name, Engine, Rows)
前端 TableMeta 使用小写 key(name, engine, rows), 导致 t.name = undefined
结果: key 警告 + handleExpandTable(dbName, undefined) 触发 table-detail 400
修复: instance_detail.go 中 DetailListTables 将列名统一 strings.ToLower()
2026-05-26 21:55:37 +08:00
logan
f2e93a09c0
feat: 细节打磨 - 全面修复核心功能
...
后端修复:
- ticket: 添加独立 CancelTicket/RollbackTicket/GetTicketTimeline/GetTicketApprovals handler
(之前 cancel/rollback 都错误调用 ApproveTicket, timeline/approvals 调用 GetTicket)
- instance_detail: 7个 TODO placeholder 端点全部实现真实 MySQL 查询
(databases/tables/db-schema/table-detail/processlist/status/users)
- instance_detail: ExecuteQuery 添加真实 Redis 命令执行(只读命令白名单)
- redis: 全部 handler 从 placeholder 改为真实 go-redis 实现
(info/dbsize/keys/key-detail/slowlog/clients/command/create-key/delete-key)
- router: 添加 sql-assistant/format, sql-assistant/analyze, tickets/audit 路由
- router: 添加 dictionary/*, mongo/* stub 路由(前端不报 404)
- stub_handlers: 新增 SQL 审计/格式化/分析 handler + dictionary/mongo stub
- model: TicketStatistics.Completed JSON tag 改为 success 匹配前端
- ticket: ExecuteTicket 完成 status 改为 success 而非 completed
前端兼容:
- 所有前端 API 路径后端都有对应路由(无 404)
- TicketStatistics 返回 success 字段与前端 stats?.success 对齐
2026-05-26 20:40:45 +08:00
logan
88e5f4e7ee
feat: Go后端LDAP真实对接 — AES-256-GCM密码加解密 + 登录LDAP fallback
...
- 新增 crypto.go: AES-256-GCM 加解密,与 Python backend 兼容
- auth.go: Login 添加 LDAP fallback(本地优先→LDAP绑定→自动创建本地用户)
- ldap.go: 实现真实 LDAP 连接/用户搜索/密码验证/OU列表/用户列表
- 所有 LDAP 操作解密 admin_password 后使用
- 新增/更新密码存储时加密
- router.go: 补充 delete-user 路由
- 添加 go-ldap/v3 依赖
2026-05-25 23:32:15 +08:00