feat(ai): 新增 SQL 编辑器事务状态探针

- 新增 inspect_sql_editor_transaction 内置探针,返回提交模式、待提交事务和当前 SQL 页签事务语义

- 将 SQL 编辑器待提交事务状态登记到 store,供 AI 只读诊断使用

- 增加 /tx 斜杠菜单、工具目录、系统引导和回归测试
This commit is contained in:
Syngnat
2026-06-10 18:53:24 +08:00
parent 156631c263
commit cf8f9be8dc
16 changed files with 427 additions and 8 deletions

View File

@@ -763,6 +763,7 @@ type PendingSqlEditorTransaction = {
commitMode: 'manual' | 'auto';
autoCommitDelayMs: number;
createdAt: number;
autoCommitDueAt?: number | null;
};
const normalizeEditorPosition = (position: any): { lineNumber: number; column: number } | null => {
@@ -2048,6 +2049,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const setQueryOptions = useStore(state => state.setQueryOptions);
const sqlEditorTransactionOptions = useStore(state => state.sqlEditorTransactionOptions);
const setSqlEditorTransactionOptions = useStore(state => state.setSqlEditorTransactionOptions);
const setSqlEditorPendingTransaction = useStore(state => state.setSqlEditorPendingTransaction);
const [isResultPanelVisible, setIsResultPanelVisible] = useState(
() => tab.resultPanelVisible === true
);
@@ -2110,7 +2112,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const updatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction | null) => {
pendingSqlTransactionRef.current = transaction;
setPendingSqlTransaction(transaction);
}, []);
setSqlEditorPendingTransaction(tab.id, transaction);
}, [setSqlEditorPendingTransaction, tab.id]);
const finishPendingSqlTransaction = useCallback(async (
action: 'commit' | 'rollback',
source: 'manual' | 'auto' = 'manual',
@@ -2145,11 +2148,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}, [clearSqlEditorAutoCommitTimer, updatePendingSqlTransaction]);
const activatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction) => {
clearSqlEditorAutoCommitTimer();
updatePendingSqlTransaction(transaction);
if (transaction.commitMode !== 'auto') {
const dueAt = transaction.commitMode === 'auto' ? Date.now() + transaction.autoCommitDelayMs : null;
const nextTransaction = { ...transaction, autoCommitDueAt: dueAt };
updatePendingSqlTransaction(nextTransaction);
if (nextTransaction.commitMode !== 'auto' || !dueAt) {
return;
}
const dueAt = Date.now() + transaction.autoCommitDelayMs;
const updateRemaining = () => {
setSqlEditorAutoCommitRemainingSeconds(Math.max(1, Math.ceil((dueAt - Date.now()) / 1000)));
};
@@ -2162,8 +2166,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
sqlEditorAutoCommitCountdownRef.current = null;
}
setSqlEditorAutoCommitRemainingSeconds(null);
void finishPendingSqlTransaction('commit', 'auto', transaction.id);
}, transaction.autoCommitDelayMs);
void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id);
}, nextTransaction.autoCommitDelayMs);
}, [clearSqlEditorAutoCommitTimer, finishPendingSqlTransaction, updatePendingSqlTransaction]);
useEffect(() => {
return () => {
@@ -2171,10 +2175,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const transaction = pendingSqlTransactionRef.current;
if (transaction?.id) {
pendingSqlTransactionRef.current = null;
setSqlEditorPendingTransaction(tab.id, null);
void DBRollbackTransaction(transaction.id);
}
};
}, [clearSqlEditorAutoCommitTimer]);
}, [clearSqlEditorAutoCommitTimer, setSqlEditorPendingTransaction, tab.id]);
const autoFetchVisible = useAutoFetchVisibility();
const currentSavedQuery = useMemo(() => {

View File

@@ -71,6 +71,9 @@ describe('AIBuiltinToolsCatalog', () => {
expect(markup).toContain('inspect_recent_sql_logs');
expect(markup).toContain('总结最近 SQL 活动');
expect(markup).toContain('inspect_recent_sql_activity');
expect(markup).toContain('核对 SQL 编辑器事务');
expect(markup).toContain('inspect_sql_editor_transaction');
expect(markup).toContain('待提交事务');
expect(markup).toContain('SQL 风险预检');
expect(markup).toContain('inspect_sql_risk');
expect(markup).toContain('WHERE 条件');

View File

@@ -145,6 +145,11 @@ const BUILTIN_TOOL_FLOWS = [
steps: 'inspect_recent_sql_activity → inspect_recent_sql_logs → inspect_current_connection',
description: '适合先看最近到底以读还是写为主、有没有 DDL 或删除、哪个库最近报错最多,再决定继续下钻哪条日志或哪个连接。',
},
{
title: '核对 SQL 编辑器事务',
steps: 'inspect_sql_editor_transaction → inspect_recent_sql_activity → inspect_sql_risk',
description: '适合先确认 SQL 编辑器 DML 是否会进入托管事务、当前是手动还是自动提交、有没有待提交事务,再解释 update/insert/delete 执行后的提交语义。',
},
{
title: 'SQL 风险预检',
steps: 'inspect_sql_risk → inspect_ai_safety → execute_sql',

View File

@@ -481,6 +481,61 @@ describe('aiLocalToolExecutor', () => {
expect(result.content).not.toContain('SELECT * FROM users LIMIT 10');
});
it('returns sql editor transaction settings, active dml semantics, and pending transactions', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_sql_editor_transaction', {}),
connections: [buildConnection()],
tabs: [{
id: 'tab-query-1',
title: '订单更新',
type: 'query',
connectionId: 'conn-1',
dbName: 'crm',
query: 'UPDATE orders SET status = \'paid\' WHERE id = 1',
resultPanelVisible: true,
}],
activeTabId: 'tab-query-1',
mcpTools: [],
toolContextMap: new Map(),
sqlLogs: [{
id: 'log-1',
timestamp: 10,
sql: 'UPDATE orders SET status = \'paid\' WHERE id = 1',
status: 'success',
duration: 42,
dbName: 'crm',
affectedRows: 1,
}],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getSqlEditorTransactionState: vi.fn().mockResolvedValue({
commitMode: 'auto',
autoCommitDelayMs: 3000,
pendingTransactions: {
'tab-query-1': {
id: 'tx-1',
tabId: 'tab-query-1',
commitMode: 'auto',
autoCommitDelayMs: 3000,
createdAt: 1000,
autoCommitDueAt: Date.now() + 3000,
},
},
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"commitMode":"auto"');
expect(result.content).toContain('"transactionAlwaysOnForDML":true');
expect(result.content).toContain('"usesManagedTransaction":true');
expect(result.content).toContain('"pendingTransactionCount":1');
expect(result.content).toContain('"activePendingTransaction"');
expect(result.content).toContain('自动提交,但 DML 仍会先进入托管事务');
expect(result.content).toContain('UPDATE orders SET status');
});
it('returns a database overview bundle with per-table column previews in one tool call', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_database_bundle', {

View File

@@ -105,6 +105,13 @@ export const buildDefaultLocalToolRuntime = (): AILocalToolRuntime => ({
}
return service.AIGetMCPClientInstallStatuses();
},
getSqlEditorTransactionState: async () => {
const state = useStore.getState();
return {
...state.sqlEditorTransactionOptions,
pendingTransactions: state.sqlEditorPendingTransactions,
};
},
getShortcutOptions: async () => useStore.getState().shortcutOptions,
getShortcutPlatform: async () => getShortcutPlatform(isMacLikePlatform()),
});

View File

@@ -20,6 +20,7 @@ describe('aiSlashCommands', () => {
expect(commands.some((command) => command.cmd === '/shortcuts')).toBe(true);
expect(commands.some((command) => command.cmd === '/applog')).toBe(true);
expect(commands.some((command) => command.cmd === '/airender')).toBe(true);
expect(commands.some((command) => command.cmd === '/tx')).toBe(true);
});
it('supports filtering by chinese keywords in addition to command prefix', () => {
@@ -49,6 +50,12 @@ describe('aiSlashCommands', () => {
expect(filterAISlashCommands('/air').map((command) => command.cmd)).toContain('/airender');
});
it('supports filtering sql editor transaction diagnostics by keyword and command prefix', () => {
expect(filterAISlashCommands('自动提交').map((command) => command.cmd)).toContain('/tx');
expect(filterAISlashCommands('未提交').map((command) => command.cmd)).toContain('/tx');
expect(filterAISlashCommands('/tx').map((command) => command.cmd)).toContain('/tx');
});
it('supports filtering mcp tool schema diagnostics by keyword and command prefix', () => {
expect(filterAISlashCommands('arguments').map((command) => command.cmd)).toContain('/mcptool');
expect(filterAISlashCommands('MCP工具参数').map((command) => command.cmd)).toContain('/mcptool');
@@ -77,6 +84,7 @@ describe('aiSlashCommands', () => {
expect(featured).toContain('/mcp');
expect(featured).toContain('/mcpadd');
expect(featured).toContain('/connfail');
expect(featured).toContain('/tx');
expect(featured).not.toContain('/shortcuts');
});
});

View File

@@ -58,6 +58,7 @@ export const DEFAULT_AI_SLASH_COMMANDS: AISlashCommandDefinition[] = [
{ cmd: '/airender', label: '🧯 AI 渲染异常', desc: '读取最近一次 AI 消息渲染失败记录', prompt: '请先调用 inspect_ai_last_render_error告诉我最近一次 AI 消息渲染失败记录里是哪条消息、报错摘要是什么,以及下一步该怎么排查。', category: 'diagnose', keywords: ['渲染失败', '气泡空白', 'ai消息', 'render', '白块'] },
{ cmd: '/safety', label: '🛡️ 查看写入安全', desc: '确认只读/写入边界和 allowMutating', prompt: '请先调用 inspect_ai_safety告诉我当前 AI 和 GoNavi MCP 的写入边界、是否只读,以及 execute_sql 是否需要 allowMutating。', category: 'diagnose', keywords: ['安全', '只读', 'allowmutating', 'ddl', 'dml'] },
{ cmd: '/activity', label: '🕘 最近 SQL 活动', desc: '总结最近执行、报错和热点', prompt: '请先调用 inspect_recent_sql_activity帮我总结最近 SQL 活动、错误热点和主要读写类型。', category: 'diagnose', keywords: ['activity', 'sql日志', '最近执行', '报错'] },
{ cmd: '/tx', label: '🔁 SQL 事务状态', desc: '查看 SQL 编辑器提交模式和待提交事务', prompt: '请先调用 inspect_sql_editor_transaction告诉我 SQL 编辑器当前 DML 托管事务语义、手动/自动提交设置、活动 SQL 页签是否会进入事务、是否有待提交事务,以及下一步应该提交、回滚还是继续执行。', category: 'diagnose', featured: true, keywords: ['事务', 'transaction', '提交', '自动提交', '手动提交', '未提交', 'dml'] },
];
const buildCommandSearchText = (command: AISlashCommandDefinition): string => [

View File

@@ -31,6 +31,7 @@ import {
buildRecentSqlActivitySnapshot,
buildRecentSqlLogsSnapshot,
} from './aiSqlLogInsights';
import { buildSqlEditorTransactionSnapshot } from './aiSqlEditorTransactionInsights';
import {
buildActiveTabSnapshot,
buildWorkspaceTabsSnapshot,
@@ -300,6 +301,22 @@ export async function executeSnapshotInspectionToolCall(
})),
success: true,
};
case 'inspect_sql_editor_transaction': {
const transactionState = typeof runtime?.getSqlEditorTransactionState === 'function'
? await runtime.getSqlEditorTransactionState()
: undefined;
return {
content: JSON.stringify(buildSqlEditorTransactionSnapshot({
transactionState,
tabs,
activeTabId,
connections,
sqlLogs,
includeSqlPreview: args.includeSqlPreview !== false,
})),
success: true,
};
}
case 'inspect_app_logs': {
const readResult = typeof runtime?.readAppLogTail === 'function'
? await runtime.readAppLogTail(Number(args.lineLimit) || 80, String(args.keyword || ''))
@@ -403,6 +420,7 @@ export async function executeSnapshotInspectionToolCall(
inspect_ai_context: '读取当前 AI 上下文失败',
inspect_recent_sql_logs: '获取最近 SQL 日志失败',
inspect_recent_sql_activity: '汇总最近 SQL 活动失败',
inspect_sql_editor_transaction: '读取 SQL 编辑器事务状态失败',
inspect_sql_risk: '检查 SQL 风险失败',
inspect_app_logs: '读取 GoNavi 应用日志失败',
inspect_recent_connection_failures: '汇总最近连接失败记录失败',

View File

@@ -16,12 +16,28 @@ export interface AISnapshotInspectionRuntimeState {
contextLevel?: string;
}
export interface AISqlEditorPendingTransactionRuntimeState {
id: string;
tabId: string;
commitMode: string;
autoCommitDelayMs: number;
createdAt: number;
autoCommitDueAt?: number | null;
}
export interface AISqlEditorTransactionRuntimeState {
commitMode?: string;
autoCommitDelayMs?: number;
pendingTransactions?: Record<string, AISqlEditorPendingTransactionRuntimeState>;
}
export interface AISnapshotInspectionRuntime {
getAIRuntimeState?: () => Promise<AISnapshotInspectionRuntimeState | undefined>;
getMCPServers?: () => Promise<AIMCPServerConfig[] | undefined>;
getMCPClientInstallStatuses?: () => Promise<AIMCPClientInstallStatus[] | undefined>;
getShortcutOptions?: () => Promise<ShortcutOptions | undefined>;
getShortcutPlatform?: () => Promise<ShortcutPlatform | undefined>;
getSqlEditorTransactionState?: () => Promise<AISqlEditorTransactionRuntimeState | undefined>;
readAppLogTail?: (lineLimit: number, keyword: string) => Promise<any>;
readSQLFile?: (filePath: string) => Promise<any>;
checkSQL?: (sql: string) => Promise<{ allowed?: boolean; operationType?: string } | undefined>;

View File

@@ -0,0 +1,235 @@
import type { SqlLog } from '../../store';
import type { SavedConnection, TabData } from '../../types';
import { findSqlStatementRanges } from '../../utils/sqlStatementSelection';
import { shouldUseSqlEditorManagedTransaction } from '../../utils/sqlEditorTransaction';
import type {
AISqlEditorPendingTransactionRuntimeState,
AISqlEditorTransactionRuntimeState,
} from './aiSnapshotInspectionToolTypes';
type SqlEditorCommitMode = 'manual' | 'auto';
const DEFAULT_AUTO_COMMIT_DELAY_MS = 5000;
const SQL_PREVIEW_LIMIT = 1200;
const normalizeCommitMode = (value: unknown): SqlEditorCommitMode =>
String(value || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual';
const normalizeDelayMs = (value: unknown): number => {
const delayMs = Number(value);
return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : DEFAULT_AUTO_COMMIT_DELAY_MS;
};
const splitStatements = (sql: string): string[] =>
findSqlStatementRanges(String(sql || ''))
.map((range) => String(range.text || '').trim())
.filter(Boolean);
const hasTransactionControlStatement = (statement: string): boolean =>
/^\s*(begin|commit|rollback|savepoint|release)\b/i.test(statement)
|| /^\s*start\s+transaction\b/i.test(statement);
const buildTabSummary = (
tab: TabData | undefined,
connections: SavedConnection[],
) => {
if (!tab) return null;
const connection = connections.find((item) => item.id === tab.connectionId);
return {
id: tab.id,
title: tab.title,
type: tab.type,
connectionId: tab.connectionId,
connectionName: connection?.name || '',
connectionType: connection?.config?.type || '',
dbName: tab.dbName || '',
filePath: tab.filePath || '',
resultPanelVisible: tab.resultPanelVisible === true,
readOnly: tab.readOnly === true,
};
};
const buildActiveSqlTabSnapshot = (params: {
tabs?: TabData[];
activeTabId?: string | null;
connections: SavedConnection[];
includeSqlPreview?: boolean;
}) => {
const { tabs = [], activeTabId = null, connections, includeSqlPreview = true } = params;
const activeTab = tabs.find((tab) => tab.id === activeTabId);
if (!activeTab) {
return {
hasActiveTab: false,
hasSql: false,
message: '当前没有活动页签',
};
}
if (activeTab.type !== 'query') {
return {
hasActiveTab: true,
hasSql: false,
message: '当前活动页签不是 SQL 编辑器页签',
tab: buildTabSummary(activeTab, connections),
};
}
const sql = String(activeTab.query || '').trim();
const statements = splitStatements(sql);
const hasExplicitTransactionControl = statements.some(hasTransactionControlStatement);
const usesManagedTransaction = shouldUseSqlEditorManagedTransaction(statements);
return {
hasActiveTab: true,
hasSql: sql.length > 0,
tab: buildTabSummary(activeTab, connections),
sqlPreview: includeSqlPreview ? sql.slice(0, SQL_PREVIEW_LIMIT) : '',
sqlCharCount: sql.length,
sqlTruncated: includeSqlPreview && sql.length > SQL_PREVIEW_LIMIT,
statementCount: statements.length,
hasExplicitTransactionControl,
usesManagedTransaction,
transactionSemantics: usesManagedTransaction
? '执行 INSERT/UPDATE/DELETE/MERGE/REPLACE 等 DML 时会先进入 SQL 编辑器托管事务;提交设置只决定事务执行成功后何时 COMMIT。'
: hasExplicitTransactionControl
? '检测到用户显式事务控制语句GoNavi 不会再包一层 SQL 编辑器托管事务。'
: '当前 SQL 不会触发 SQL 编辑器托管事务;只读查询仍走普通查询路径。',
};
};
const buildPendingTransactionPreview = (params: {
item: AISqlEditorPendingTransactionRuntimeState;
tabs?: TabData[];
connections: SavedConnection[];
now: number;
}) => {
const { item, tabs = [], connections, now } = params;
const tab = tabs.find((candidate) => candidate.id === item.tabId);
const commitMode = normalizeCommitMode(item.commitMode);
const dueAt = Number(item.autoCommitDueAt || 0);
const remainingMs = commitMode === 'auto' && dueAt > 0
? Math.max(0, dueAt - now)
: null;
return {
id: item.id,
tabId: item.tabId,
tab: buildTabSummary(tab, connections),
commitMode,
autoCommitDelayMs: normalizeDelayMs(item.autoCommitDelayMs),
createdAt: Number(item.createdAt) || 0,
autoCommitDueAt: dueAt || null,
autoCommitRemainingMs: remainingMs,
};
};
const isRelevantSqlEditorTransactionLog = (log: SqlLog): boolean => {
const sql = String(log.sql || '');
const statements = splitStatements(sql);
if (shouldUseSqlEditorManagedTransaction(statements)) return true;
if (statements.some(hasTransactionControlStatement)) return true;
return /\b(transaction|commit|rollback)\b/i.test(sql)
|| /事务|提交|回滚|transaction|commit|rollback/i.test(String(log.message || ''));
};
const buildRecentLogPreview = (log: SqlLog) => ({
id: log.id,
timestamp: log.timestamp,
status: log.status,
duration: log.duration,
dbName: log.dbName || '',
affectedRows: typeof log.affectedRows === 'number' ? log.affectedRows : null,
sqlPreview: String(log.sql || '').trim().slice(0, 1000),
message: log.message || '',
});
export const buildSqlEditorTransactionSnapshot = (params: {
transactionState?: AISqlEditorTransactionRuntimeState;
tabs?: TabData[];
activeTabId?: string | null;
connections: SavedConnection[];
sqlLogs?: SqlLog[];
includeSqlPreview?: boolean;
now?: number;
}) => {
const {
transactionState,
tabs = [],
activeTabId = null,
connections,
sqlLogs = [],
includeSqlPreview = true,
now = Date.now(),
} = params;
const commitMode = normalizeCommitMode(transactionState?.commitMode);
const autoCommitDelayMs = normalizeDelayMs(transactionState?.autoCommitDelayMs);
const pendingTransactions = Object.values(transactionState?.pendingTransactions || {})
.map((item) => buildPendingTransactionPreview({ item, tabs, connections, now }))
.sort((left, right) => right.createdAt - left.createdAt);
const activePendingTransaction = pendingTransactions.find((item) => item.tabId === activeTabId) || null;
const activeSqlTab = buildActiveSqlTabSnapshot({
tabs,
activeTabId,
connections,
includeSqlPreview,
});
const activeUsesManagedTransaction = activeSqlTab.hasActiveTab
&& activeSqlTab.hasSql
&& 'usesManagedTransaction' in activeSqlTab
&& activeSqlTab.usesManagedTransaction === true;
const activeHasExplicitTransactionControl = activeSqlTab.hasActiveTab
&& activeSqlTab.hasSql
&& 'hasExplicitTransactionControl' in activeSqlTab
&& activeSqlTab.hasExplicitTransactionControl === true;
const recentRelevantLogs = sqlLogs
.filter(isRelevantSqlEditorTransactionLog)
.slice(0, 8)
.map(buildRecentLogPreview);
const warnings: string[] = [];
if (pendingTransactions.length > 0) {
warnings.push(`当前有 ${pendingTransactions.length} 个 SQL 编辑器托管事务待提交或回滚`);
}
if (activePendingTransaction) {
warnings.push('当前活动 SQL 页签已有待处理事务,继续执行新的 DML 前应先提交或回滚');
}
if (activeUsesManagedTransaction && commitMode === 'auto') {
warnings.push('当前设置为自动提交,但 DML 仍会先进入托管事务,只是在延迟到期后自动 COMMIT');
}
if (activeHasExplicitTransactionControl) {
warnings.push('当前 SQL 已包含显式事务控制SQL 编辑器不会再接管提交/回滚');
}
const nextActions: string[] = [];
if (activePendingTransaction) {
nextActions.push('先让用户在结果区事务条点击“提交”或“回滚”,或等待自动提交倒计时结束');
} else if (pendingTransactions.length > 0) {
nextActions.push('如要继续执行 DML先切回对应 SQL 页签处理待提交事务');
}
if (activeUsesManagedTransaction) {
nextActions.push(commitMode === 'auto'
? `说明当前 DML 会先开启托管事务,执行成功后约 ${Math.round(autoCommitDelayMs / 1000)} 秒自动提交`
: '说明当前 DML 会先开启托管事务,执行成功后需要手动点击提交或回滚');
}
if (!activeSqlTab.hasActiveTab || !activeSqlTab.hasSql) {
nextActions.push('先切换到包含 SQL 草稿的查询页签,或让用户贴出要执行的 SQL');
}
if (recentRelevantLogs.length > 0) {
nextActions.push('结合 recentRelevantLogs 回看最近写入/事务执行结果,必要时再调用 inspect_recent_sql_activity 下钻');
}
return {
commitPolicy: {
commitMode,
autoCommitDelayMs,
transactionAlwaysOnForDML: true,
semantics: 'SQL 编辑器执行 INSERT/UPDATE/DELETE/MERGE/REPLACE 等 DML 时始终先进入托管事务;“手动/自动”只控制执行成功后的 COMMIT 时机,不控制是否开启事务。',
},
activeSqlTab,
pendingTransactionCount: pendingTransactions.length,
activePendingTransaction,
pendingTransactions,
recentRelevantLogs,
warnings,
nextActions,
};
};

View File

@@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => {
connections: [connections[0]],
tabs: [],
activeTabId: null,
availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', 'inspect_mcp_draft', 'inspect_mcp_tool_schema', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'],
availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', 'inspect_mcp_draft', 'inspect_mcp_tool_schema', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_sql_editor_transaction', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'],
skills,
userPromptSettings,
});
@@ -93,6 +93,7 @@ describe('buildAISystemContextMessages', () => {
expect(joined).toContain('inspect_external_sql_directories');
expect(joined).toContain('inspect_external_sql_file');
expect(joined).toContain('inspect_recent_sql_activity');
expect(joined).toContain('inspect_sql_editor_transaction 读取真实提交设置');
expect(joined).toContain('inspect_sql_risk');
expect(joined).toContain('inspect_recent_connection_failures 读取真实连接失败总结');
expect(joined).toContain('inspect_app_logs 读取真实应用日志尾部');

View File

@@ -176,6 +176,12 @@ export const appendDatabaseInspectionGuidanceMessages = (
'inspect_recent_sql_activity',
'如果用户提到“最近都执行了什么”“是不是刚删过数据”“最近主要在查还是在改”“哪个库最近报错最多”,优先调用 inspect_recent_sql_activity 先读最近 SQL 活动总结,再决定是否继续下钻 inspect_recent_sql_logs 看具体语句。',
);
appendGuidanceIfToolAvailable(
messages,
availableToolNames,
'inspect_sql_editor_transaction',
'如果用户提到“SQL 编辑器手动提交/自动提交”“当前有没有未提交事务”“执行 update/insert/delete 会不会自动提交”“事务语义是不是理解错了”,优先调用 inspect_sql_editor_transaction 读取真实提交设置、待提交事务和当前 SQL 页签是否会进入托管事务,不要凭记忆解释。',
);
appendGuidanceIfToolAvailable(
messages,
availableToolNames,

View File

@@ -53,6 +53,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
inspect_workspace_tabs: '盘点当前工作区页签',
inspect_recent_sql_logs: '回看最近 SQL 执行日志',
inspect_recent_sql_activity: '总结最近 SQL 活动',
inspect_sql_editor_transaction: '读取 SQL 编辑器事务状态',
inspect_app_logs: '回看 GoNavi 应用日志',
inspect_recent_connection_failures: '总结最近连接失败记录',
inspect_ai_last_render_error: '读取最近一次 AI 渲染异常',

View File

@@ -1124,6 +1124,15 @@ export interface SqlEditorTransactionOptions {
autoCommitDelayMs: number;
}
export interface SqlEditorPendingTransactionState {
id: string;
tabId: string;
commitMode: "manual" | "auto";
autoCommitDelayMs: number;
createdAt: number;
autoCommitDueAt?: number | null;
}
interface AppState {
connections: SavedConnection[];
connectionTags: ConnectionTag[];
@@ -1143,6 +1152,7 @@ interface AppState {
queryOptions: QueryOptions;
dataEditTransactionOptions: DataEditTransactionOptions;
sqlEditorTransactionOptions: SqlEditorTransactionOptions;
sqlEditorPendingTransactions: Record<string, SqlEditorPendingTransactionState>;
shortcutOptions: ShortcutOptions;
sqlSnippets: SqlSnippet[];
sqlLogs: SqlLog[];
@@ -1254,6 +1264,10 @@ interface AppState {
setSqlEditorTransactionOptions: (
options: Partial<SqlEditorTransactionOptions>,
) => void;
setSqlEditorPendingTransaction: (
tabId: string,
transaction: Omit<SqlEditorPendingTransactionState, "tabId"> | null,
) => void;
updateShortcut: (
action: ShortcutAction,
binding: Partial<ShortcutPlatformBinding>,
@@ -2051,6 +2065,7 @@ export const useStore = create<AppState>()(
commitMode: "manual",
autoCommitDelayMs: 5000,
},
sqlEditorPendingTransactions: {},
shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS),
sqlSnippets: DEFAULT_SQL_SNIPPETS,
sqlLogs: [],
@@ -2809,6 +2824,23 @@ export const useStore = create<AppState>()(
...options,
}),
})),
setSqlEditorPendingTransaction: (tabId, transaction) =>
set((state) => {
const safeTabId = String(tabId || "").trim();
if (!safeTabId) {
return {};
}
const next = { ...state.sqlEditorPendingTransactions };
if (!transaction) {
delete next[safeTabId];
return { sqlEditorPendingTransactions: next };
}
next[safeTabId] = {
...transaction,
tabId: safeTabId,
};
return { sqlEditorPendingTransactions: next };
}),
updateShortcut: (action, binding, platform) => {
runWithExplicitShortcutPersistence(() => {
const targetPlatform = platform ?? getShortcutPlatform();

View File

@@ -463,6 +463,28 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [
},
},
},
{
name: "inspect_sql_editor_transaction",
icon: "🔁",
desc: "查看 SQL 编辑器事务提交状态",
detail:
"返回 SQL 编辑器 DML 托管事务语义、当前手动/自动提交设置、活动 SQL 页签是否会进入托管事务、待提交事务以及最近写入/事务执行记录。适合用户问“手动/自动提交到底是什么意思”“当前有没有事务没提交”“执行 update/insert/delete 会不会自动提交”时先读真实状态。",
params: "includeSqlPreview?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_sql_editor_transaction",
description:
"读取 SQL 编辑器事务状态快照,包括 DML 始终进入托管事务的真实语义、当前提交模式、自动提交延迟、活动 SQL 页签是否会触发托管事务、待提交事务列表和最近写入/事务日志。适用于用户提到 SQL 编辑器手动提交、自动提交、未提交事务、DML 执行后是否提交或事务语义不清时,先读取真实状态再解释。",
parameters: {
type: "object",
properties: {
includeSqlPreview: { type: "boolean", description: "可选,是否返回活动 SQL 页签的 SQL 预览,默认 true" },
},
},
},
},
},
{
name: "inspect_sql_risk",
icon: "🛑",

View File

@@ -147,6 +147,7 @@ describe('aiToolRegistry', () => {
it('registers the recent-sql-activity, saved-query, and sql-snippet inspectors as builtin tools', () => {
const recentActivityTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_recent_sql_activity');
const sqlEditorTransactionTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_sql_editor_transaction');
const sqlRiskTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_sql_risk');
const appLogTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_app_logs');
const connectionFailureTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_recent_connection_failures');
@@ -158,6 +159,8 @@ describe('aiToolRegistry', () => {
expect(recentActivityTool?.desc).toContain('最近 SQL 活动');
expect(recentActivityTool?.tool.function.description).toContain('最近 SQL 活动');
expect(sqlEditorTransactionTool?.desc).toContain('SQL 编辑器事务');
expect(sqlEditorTransactionTool?.tool.function.description).toContain('托管事务');
expect(sqlRiskTool?.desc).toContain('SQL 的执行风险');
expect(sqlRiskTool?.tool.function.description).toContain('危险点');
expect(appLogTool?.desc).toContain('GoNavi 应用日志');
@@ -208,6 +211,7 @@ describe('aiToolRegistry', () => {
expect(tools.some((item) => item.function.name === 'inspect_external_sql_directories')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_external_sql_file')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_recent_sql_activity')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_sql_editor_transaction')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_sql_risk')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_app_logs')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_recent_connection_failures')).toBe(true);