@@ -39,10 +39,12 @@ const config = {
TEMPERATURE : parseFloat ( process . env . TEMPERATURE || ENV . TEMPERATURE || "0.3" ) ,
WHITELIST : parseList ( process . env . WHITELIST || ENV . WHITELIST ) . length ? parseList ( process . env . WHITELIST || ENV . WHITELIST ) : DEFAULT_WHITELIST ,
BLACKLIST : parseList ( process . env . BLACKLIST || ENV . BLACKLIST ) ,
DUCKDB_EXE : process.env.DUCKDB_EXE || ENV . DUCKDB_EXE || resolve ( "bin" , "duckdb.exe" ) ,
DUCKDB_FILE : process.env.DUCKDB_FILE || ENV . DUCKDB_FILE || resolve ( "data" , "duck.db" ) ,
} ;
// 持久化配置到 .env(WORKSPACE 为内部默认值,不在设置页暴露)
const ENV_KEYS = [ "BASE_URL" , "API_KEY" , "MODEL" , "WORKSPACE" , "CMD_APPROVE_LEVEL" , "PORT" , "LOG_LEVEL" , "TEMPERATURE" , "WHITELIST" , "BLACKLIST" ] as const ;
const ENV_KEYS = [ "BASE_URL" , "API_KEY" , "MODEL" , "WORKSPACE" , "CMD_APPROVE_LEVEL" , "PORT" , "LOG_LEVEL" , "TEMPERATURE" , "WHITELIST" , "BLACKLIST" , "DUCKDB_EXE" , "DUCKDB_FILE" ] as const ;
function saveEnv() {
const lines = ENV_KEYS . map ( ( k ) = > ` ${ k } = ${ String ( ( config as any ) [ k ] ) } ` ) ;
const header = "# Mini AI Assistant 配置(可由设置页面修改)\n" ;
@@ -88,9 +90,9 @@ function clip(s: any, n = 600): string {
// ---------- 依赖 ----------
import { readFile , writeFile , readdir , rename , unlink , stat , mkdir } from "fs/promises" ;
import { resolve , normalize , extname , sep } from "path" ;
import { resolve , normalize , extname , sep , dirname } from "path" ;
import { read as xlsxRead , write as xlsxWrite , utils } from "xlsx" ;
import duckdb from "duckdb ";
import { spawn } from "child_process ";
// ---------- 路径安全(白名单 + 黑名单,黑名单优先) ----------
// 判断 target 是否等于 base 或位于 base 的子目录下(跨平台)
@@ -125,33 +127,38 @@ function isDangerousCommand(cmd: string): boolean {
return DANGEROUS_PATTERNS . some ( ( p ) = > lower . includes ( p ) ) ;
}
// ---------- DuckDB( Excel 数据处理 ) ----------
let duckDb : any = null ;
function getDuck ( ) : Promise < any > {
// ---------- DuckDB( 内置 duckdb.exe 子进程 + 本地数据库文件,无原生 .node 依赖 ) ----------
function duckRun ( sql : string ) : Promise < string > {
return new Promise ( ( resolve , reject ) = > {
if ( duckDb ) return resolve ( duckDb ) ;
duckDb = new duckdb . Database ( ":memory:" , ( err : any ) = > {
if ( err ) return reject ( err ) ;
resolve ( duckDb ) ;
mkdirSync ( dirname ( config . DUCKDB_FILE ) , { recursive : true } ) ;
const child = spawn ( config . DUCKDB_EXE , [ config . DUCKDB_FILE , "-json" ] , { stdio : [ "pipe" , "pipe" , "pipe" ] } ) ;
let out = "" , err = "" ;
child . stdout . on ( "data" , ( d ) = > ( out += d . toString ( "utf8" ) ) ) ;
child . stderr . on ( "data" , ( d ) = > ( err += d . toString ( "utf8" ) ) ) ;
child . on ( "error" , ( e ) = > reject ( new Error ( "无法启动 duckdb: " + e . message ) ) ) ;
child . on ( "close" , ( code ) = > {
if ( code === 0 ) resolve ( out ) ;
else reject ( new Error ( err . trim ( ) || ` duckdb 退出码 ${ code } ` ) ) ;
} ) ;
child . stdin . write ( sql , "utf8" ) ;
child . stdin . end ( ) ;
} ) ;
}
function duckExec ( db : any , sql : string ) : Promise < void > {
return new Promise ( ( resolve , reject ) = > db . exec ( sql , ( e : any ) = > ( e ? reject ( e ) : resolve ( ) ) ) ) ;
async function duckExec ( sql : string ) : Promise < void > {
await duckRun ( sql ) ;
}
function duckAll ( db : any , sql : string ) : Promise < any [ ] > {
return new Promise ( ( resolve , reject ) = > db . all ( sql , ( e : any , rows : any ) = > ( e ? reject ( e ) : resolve ( rows || [ ] ) ) ) ) ;
async function duckAll ( sql : string ) : Promise < any [ ] > {
const out = await duckRun ( sql ) ;
const t = out . trim ( ) ;
if ( ! t ) return [ ] ;
try { return JSON . parse ( t ) ; } catch { return [ ] ; }
}
function sqlVal ( v : any ) : string {
if ( v === null || v === undefined || v === "" ) return "NULL " ;
if ( typeof v === "number" ) return Number . isFinite ( v ) ? String ( v ) : "NULL " ;
if ( typeof v === "boolean" ) return v ? "TRUE" : "FALSE" ;
if ( v instanceof Date ) return ` ' ${ v . toISOString ( ) . replace ( /'/g , "''" ) } ' ` ;
return ` ' ${ String ( v ) . replace ( /' /g , "''" ) } ' ` ;
}
function safeCol ( c : string ) : string {
const s = String ( c ) . trim ( ) || "col" ;
return s . replace ( /[^\w一-龥]/g , "_" ) ;
function csvCell ( v : any ) : string {
if ( v === null || v === undefined ) return "" ;
if ( typeof v === "number" ) return Number . isFinite ( v ) ? String ( v ) : "" ;
if ( v instanceof Date ) return '"' + v . toISOString ( ) . replace ( /"/g , '""' ) + '"' ;
const s = String ( v ) ;
return /[",\n\r]/ . test ( s ) ? '"' + s . replace ( /" /g , '""' ) + '"' : s ;
}
// ---------- 工具执行 ----------
@@ -248,7 +255,6 @@ async function execTool(name: string, args: any): Promise<{ ok: boolean; result:
if ( ! ws ) throw new Error ( "工作表不存在: " + sheetName ) ;
const rows : any [ ] = utils . sheet_to_json ( ws , { defval : null } ) ;
const table = args . table || "excel_data" ;
const db = await getDuck ( ) ;
if ( ! rows . length ) return { ok : true , result : ` 工作表 " ${ sheetName } " 无数据行 ` } ;
const rawCols = Object . keys ( rows [ 0 ] ) ;
const seen = new Set < string > ( ) ;
@@ -272,11 +278,20 @@ async function execTool(name: string, args: any): Promise<{ ok: boolean; result:
types [ c ] = hasVal && allNum ? "DOUBLE" : "VARCHAR" ;
}
const colDefs = cols . map ( ( c ) = > ` " ${ c } " ${ types [ c ] } ` ) . join ( ", " ) ;
await duckExec ( db , ` CREATE OR REPLACE TABLE " ${ table } " ( ${ colDefs } ) ` ) ;
const MAX = 50000 ;
const slice = rows . slice ( 0 , MAX ) ;
const values = slice . map ( ( r ) = > ` ( ${ cols . map ( ( c ) = > sqlVal ( r [ c ] ) ) . join ( ", " ) } ) ` ) . join ( ", " ) ;
if ( values ) await duckExec ( db , ` INSERT INTO " ${ table } " VALUES ${ values } ` ) ;
// 写 UTF-8 CSV → duckdb.exe COPY 导入
const csvPath = resolve ( config . DUCKDB_FILE , ".." , ` load_ ${ Date . now ( ) } .csv ` ) ;
mkdirSync ( dirname ( csvPath ) , { recursive : true } ) ;
const csvLines = [ cols . join ( "," ) ] ;
for ( const r of slice ) csvLines . push ( cols . map ( ( c ) = > csvCell ( r [ c ] ) ) . join ( "," ) ) ;
writeFileSync ( csvPath , csvLines . join ( "\n" ) , "utf-8" ) ;
const csvSql = csvPath . replace ( /\\/g , "/" ) ;
try {
await duckExec ( ` CREATE OR REPLACE TABLE " ${ table } " ( ${ colDefs } ); COPY " ${ table } " FROM ' ${ csvSql } ' (FORMAT csv, HEADER true, DELIMITER ','); ` ) ;
} finally {
try { unlinkSync ( csvPath ) ; } catch { }
}
return {
ok : true ,
result : {
@@ -290,9 +305,8 @@ async function execTool(name: string, args: any): Promise<{ ok: boolean; result:
case "query_duckdb" : {
const sql = String ( args . sql || "" ) . trim ( ) ;
if ( ! sql ) throw new Error ( "缺少 SQL 语句" ) ;
const db = await getDuck ( ) ;
try {
const rows = await duckAll ( db , sql ) ;
const rows = await duckAll ( sql ) ;
const cols = rows . length ? Object . keys ( rows [ 0 ] ) : [ ] ;
return { ok : true , result : { rowCount : rows.length , columns : cols , data : rows.slice ( 0 , 200 ) } } ;
} catch ( e : any ) {