From 8a401c11b4447f26eee7e3b77c44ffa7db7863f1 Mon Sep 17 00:00:00 2001 From: digua Date: Thu, 8 Jan 2026 01:18:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SQL=E5=AE=9E=E9=AA=8C=E5=AE=A4=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/worker/query/sql.ts | 47 +-- src/components.d.ts | 1 + .../analysis/SQLLab/AIGenerateModal.vue | 2 + .../analysis/SQLLab/ResultTable.vue | 319 +++++++++++++++--- .../analysis/quotes/HotRepeatTab.vue | 4 +- .../common/settings/AIPromptConfigTab.vue | 34 +- src/i18n/locales/en-US/settings.json | 4 + src/i18n/locales/zh-CN/settings.json | 4 + src/stores/prompt.ts | 8 +- src/utils/index.ts | 1 + src/utils/sqlExport.ts | 66 ++++ 11 files changed, 408 insertions(+), 82 deletions(-) create mode 100644 src/utils/sqlExport.ts diff --git a/electron/main/worker/query/sql.ts b/electron/main/worker/query/sql.ts index 2ea40d4..94c6104 100644 --- a/electron/main/worker/query/sql.ts +++ b/electron/main/worker/query/sql.ts @@ -5,9 +5,6 @@ import { openDatabase } from '../core' -// 最大返回行数限制 -const MAX_LIMIT = 1000 - // 查询超时时间(毫秒) const QUERY_TIMEOUT_MS = 10000 @@ -81,41 +78,17 @@ export function getSchema(sessionId: string): TableSchema[] { } /** - * 解析并强制添加 LIMIT - * 如果 SQL 没有 LIMIT 或 LIMIT 超过最大值,强制设置为 MAX_LIMIT + * 检查 SQL 是否包含 LIMIT 子句 */ -function enforceLimit(sql: string): { sql: string; limited: boolean } { - const trimmedSQL = sql.trim() - - // 检查是否是 SELECT 语句 - if (!trimmedSQL.toUpperCase().startsWith('SELECT')) { - return { sql: trimmedSQL, limited: false } - } - - // 使用正则匹配 LIMIT 子句 - const limitMatch = trimmedSQL.match(/\bLIMIT\s+(\d+)\s*(?:,\s*\d+)?(?:\s+OFFSET\s+\d+)?/i) - - if (limitMatch) { - const currentLimit = parseInt(limitMatch[1], 10) - if (currentLimit > MAX_LIMIT) { - // 替换超出的 LIMIT - const newSQL = trimmedSQL.replace(/\bLIMIT\s+\d+/i, `LIMIT ${MAX_LIMIT}`) - return { sql: newSQL, limited: true } - } - return { sql: trimmedSQL, limited: false } - } else { - // 没有 LIMIT,追加 - // 需要处理可能存在的分号 - const sqlWithoutSemicolon = trimmedSQL.replace(/;\s*$/, '') - return { sql: `${sqlWithoutSemicolon} LIMIT ${MAX_LIMIT}`, limited: true } - } +function hasLimit(sql: string): boolean { + return /\bLIMIT\s+\d+/i.test(sql) } /** * 执行用户 SQL 查询 * - 只支持 SELECT 语句 - * - 强制 LIMIT 不超过 MAX_LIMIT - * - 带超时控制 + * - 不强制 LIMIT,由用户自行控制 + * - 带超时控制(由 Worker 管理器控制) */ export function executeRawSQL(sessionId: string, sql: string): SQLResult { const db = openDatabase(sessionId) @@ -130,16 +103,12 @@ export function executeRawSQL(sessionId: string, sql: string): SQLResult { throw new Error('只支持 SELECT 查询语句') } - // 强制 LIMIT - const { sql: limitedSQL, limited } = enforceLimit(trimmedSQL) - // 执行查询 const startTime = Date.now() try { - // better-sqlite3 是同步的,我们通过 Worker 实现"超时" - // 这里先执行,超时由 Worker 管理器控制 - const stmt = db.prepare(limitedSQL) + // better-sqlite3 是同步的,超时由 Worker 管理器控制 + const stmt = db.prepare(trimmedSQL) const rows = stmt.all() const duration = Date.now() - startTime @@ -154,7 +123,7 @@ export function executeRawSQL(sessionId: string, sql: string): SQLResult { rows: rowData, rowCount: rows.length, duration, - limited: limited || rows.length >= MAX_LIMIT, + limited: false, // 不再强制限制 } } catch (error) { if (error instanceof Error) { diff --git a/src/components.d.ts b/src/components.d.ts index 2f33b04..bf98e63 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -19,6 +19,7 @@ declare module 'vue' { UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default'] UChatPrompt: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/ChatPrompt.vue')['default'] UChatPromptSubmit: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/ChatPromptSubmit.vue')['default'] + UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default'] UContextMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/ContextMenu.vue')['default'] UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default'] UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default'] diff --git a/src/components/analysis/SQLLab/AIGenerateModal.vue b/src/components/analysis/SQLLab/AIGenerateModal.vue index 5cd9a3b..9fe8371 100644 --- a/src/components/analysis/SQLLab/AIGenerateModal.vue +++ b/src/components/analysis/SQLLab/AIGenerateModal.vue @@ -72,6 +72,8 @@ ${schemaDesc} OR m.aliases LIKE '%人名%' \`\`\` 5. 显示成员名称时,使用 COALESCE(m.group_nickname, m.account_name, m.platform_id) 来获取最佳显示名 +6. **查询具体消息时包含消息 ID**:当用户需要查看具体的聊天记录时,SELECT 应包含 msg.id 作为第一个字段,这样用户可以点击查看完整上下文。注意是 msg.id(消息 ID),不是 m.id(成员 ID)。 +7. **统计查询不需要消息 ID**:当用户需要统计分析(如"统计发言数量"、"分析活跃度")时,不需要返回 msg.id ## 用户需求 diff --git a/src/components/analysis/SQLLab/ResultTable.vue b/src/components/analysis/SQLLab/ResultTable.vue index 1b3a802..f61b1eb 100644 --- a/src/components/analysis/SQLLab/ResultTable.vue +++ b/src/components/analysis/SQLLab/ResultTable.vue @@ -1,11 +1,84 @@