mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 16:24:33 +00:00
132 lines
4.9 KiB
TypeScript
132 lines
4.9 KiB
TypeScript
import type { ConnectionConfig } from "@/types/database";
|
|
|
|
export type ConnectionEnvironment = "production" | "non_production" | "unknown";
|
|
export type AiSqlExecutionAction = "auto_execute" | "confirm" | "block";
|
|
export type AiSqlExecutionCategory = "read" | "low_risk_write" | "write" | "schema_change" | "dangerous" | "unknown";
|
|
|
|
export interface AiSqlExecutionDecision {
|
|
action: AiSqlExecutionAction;
|
|
environment: ConnectionEnvironment;
|
|
category: AiSqlExecutionCategory;
|
|
reasons: string[];
|
|
}
|
|
|
|
const READ_RE = /^(SELECT|WITH|SHOW|DESCRIBE|DESC|EXPLAIN)\b/i;
|
|
const INSERT_RE = /^INSERT\b/i;
|
|
const UPDATE_RE = /^UPDATE\b/i;
|
|
const DELETE_RE = /^DELETE\b/i;
|
|
const CONFIRM_WRITE_RE = /^(MERGE|REPLACE)\b/i;
|
|
const BLOCK_RE = /^(DROP|TRUNCATE|ALTER|RENAME)\b/i;
|
|
const SCHEMA_RE = /^(CREATE)\b/i;
|
|
|
|
const PRODUCTION_RE = /\b(prod|prd|production)\b|生产|正式/i;
|
|
const NON_PRODUCTION_RE =
|
|
/\b(local|localhost|dev|develop|development|test|testing|stage|staging|sandbox|demo)\b|本地|开发|测试|预发/i;
|
|
const LOCAL_HOST_RE = /^(localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|::1)$/i;
|
|
const NEGATIVE_EXECUTION_RE =
|
|
/(不要|别|不用|禁止|只生成|仅生成|只写|仅写).{0,12}(执行|运行)|do\s+not\s+execute|don't\s+execute|dont\s+execute|without\s+executing|only\s+(generate|write|return)/i;
|
|
const ACTION_INTENT_RE =
|
|
/(查一下|查询|查看|看一下|统计|列出|执行|运行|跑一下|show|list|query|find|count|get|fetch|execute|run)/i;
|
|
|
|
export function stripAiSqlComments(sql: string): string {
|
|
return sql
|
|
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
.replace(/--.*$/gm, " ")
|
|
.replace(/#.*$/gm, " ");
|
|
}
|
|
|
|
function sqlStatements(sql: string): string[] {
|
|
return stripAiSqlComments(sql)
|
|
.split(";")
|
|
.map((stmt) => stmt.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function classifyStatement(statement: string): AiSqlExecutionCategory {
|
|
if (READ_RE.test(statement)) return "read";
|
|
if (BLOCK_RE.test(statement)) return "dangerous";
|
|
if (SCHEMA_RE.test(statement)) return "schema_change";
|
|
if (INSERT_RE.test(statement)) return "low_risk_write";
|
|
if (UPDATE_RE.test(statement)) return isScopedUpdate(statement) ? "low_risk_write" : "dangerous";
|
|
if (DELETE_RE.test(statement) || CONFIRM_WRITE_RE.test(statement)) return "write";
|
|
return "unknown";
|
|
}
|
|
|
|
function isScopedUpdate(statement: string): boolean {
|
|
const whereMatch = statement.match(/\bWHERE\b([\s\S]*)$/i);
|
|
if (!whereMatch) return false;
|
|
const where = whereMatch[1];
|
|
if (/\b1\s*=\s*1\b|\btrue\b/i.test(where)) return false;
|
|
return /\b[\w"`.[\]]*(?:id|_id|uuid|key)[\w"`.[\]]*\s*=\s*(?:'[^']+'|"[^"]+"|`[^`]+`|[\w.-]+)/i.test(where);
|
|
}
|
|
|
|
export function classifyConnectionEnvironment(connection?: ConnectionConfig): ConnectionEnvironment {
|
|
if (!connection) return "unknown";
|
|
|
|
const parts = [connection.name, connection.host, connection.database, connection.connection_string].filter(Boolean);
|
|
const signal = parts.join(" ");
|
|
if (PRODUCTION_RE.test(signal)) return "production";
|
|
if (LOCAL_HOST_RE.test(connection.host) || NON_PRODUCTION_RE.test(signal)) return "non_production";
|
|
return "unknown";
|
|
}
|
|
|
|
export function classifyAiSqlExecution(sql: string, connection?: ConnectionConfig): AiSqlExecutionDecision {
|
|
const environment = classifyConnectionEnvironment(connection);
|
|
const statements = sqlStatements(sql);
|
|
const reasons: string[] = [];
|
|
|
|
if (!statements.length) {
|
|
return { action: "block", environment, category: "unknown", reasons: ["empty_sql"] };
|
|
}
|
|
|
|
const categories = statements.map(classifyStatement);
|
|
const hasMultipleStatements = statements.length > 1;
|
|
if (hasMultipleStatements) reasons.push("multi_statement");
|
|
|
|
if (categories.includes("dangerous")) {
|
|
return { action: "block", environment, category: "dangerous", reasons };
|
|
}
|
|
|
|
if (categories.includes("unknown")) {
|
|
return { action: "confirm", environment, category: "unknown", reasons };
|
|
}
|
|
|
|
if (categories.every((category) => category === "read")) {
|
|
return { action: "auto_execute", environment, category: "read", reasons };
|
|
}
|
|
|
|
if (hasMultipleStatements) {
|
|
return { action: "confirm", environment, category: "write", reasons };
|
|
}
|
|
|
|
const [category] = categories;
|
|
if (category === "low_risk_write") {
|
|
return {
|
|
action: environment === "non_production" ? "auto_execute" : "confirm",
|
|
environment,
|
|
category,
|
|
reasons,
|
|
};
|
|
}
|
|
|
|
return {
|
|
action: "confirm",
|
|
environment,
|
|
category,
|
|
reasons,
|
|
};
|
|
}
|
|
|
|
export function shouldAttemptAiAutoExecute(instruction: string, action: string): boolean {
|
|
if (action !== "generate") return false;
|
|
const normalized = instruction.trim();
|
|
if (!normalized || NEGATIVE_EXECUTION_RE.test(normalized)) return false;
|
|
return ACTION_INTENT_RE.test(normalized);
|
|
}
|
|
|
|
export function extractFirstSqlCodeBlock(content: string): string | undefined {
|
|
const match = content.match(/```(?:sql|mysql|postgresql|sqlite|tsql|clickhouse)?\s*\n([\s\S]*?)```/i);
|
|
const sql = match?.[1]?.trim();
|
|
return sql || undefined;
|
|
}
|