From 422a4b9d7153226e262c5596ff7847dfe4f8a291 Mon Sep 17 00:00:00 2001 From: Cheney Date: Sun, 16 Aug 2026 21:48:49 +0800 Subject: [PATCH] update --- .env | 8 +++--- .env.example | 4 +++ .gitignore | 4 +++ server.ts | 74 +++++++++++++++++++++++++++++++--------------------- 4 files changed, 57 insertions(+), 33 deletions(-) diff --git a/.env b/.env index 0ceba47..1c9b660 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # Mini AI Assistant 配置(可由设置页面修改) -BASE_URL=https://opencode.ai/zen/v1 -API_KEY=sk-ihICiTIZTPdQ0FlhA7ru1kFMpVnxcvHNaK0oxem0ecnz7CpPx0xRoBMCPQhPDmmf -MODEL=deepseek-v4-flash-free +BASE_URL=https://ark.cn-beijing.volces.com/api/coding/v3 +API_KEY=ark-ef9b970f-5301-4691-a95e-3f01597ca3f2-b430e +MODEL=deepseek-v4-flash WORKSPACE=D:\workbench\miniai CMD_APPROVE_LEVEL=medium PORT=3100 @@ -9,3 +9,5 @@ LOG_LEVEL=debug TEMPERATURE=0.3 WHITELIST=d:\ BLACKLIST= +DUCKDB_EXE=D:\workbench\miniai\bin\duckdb.exe +DUCKDB_FILE=D:\workbench\miniai\data\duck.db diff --git a/.env.example b/.env.example index 91dabd1..7467f04 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,7 @@ WHITELIST=d:\ # 路径黑名单(优先级最高,逗号分隔;留空表示无) BLACKLIST= + +# DuckDB:内置 CLI 可执行文件路径与本地数据库文件(无需原生 .node 依赖) +DUCKDB_EXE=bin/duckdb.exe +DUCKDB_FILE=data/duck.db diff --git a/.gitignore b/.gitignore index f40891a..ca355b9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ _be.* # 会话记忆数据 conversations/ +# 内置 duckdb 可执行文件与运行时数据库 +bin/ +data/ + # 环境变量(含密钥,勿提交;参考 .env.example) .env .env.local diff --git a/server.ts b/server.ts index 9e9c4c4..5064b62 100644 --- a/server.ts +++ b/server.ts @@ -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 { +// ---------- DuckDB(内置 duckdb.exe 子进程 + 本地数据库文件,无原生 .node 依赖) ---------- +function duckRun(sql: string): Promise { 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 { - return new Promise((resolve, reject) => db.exec(sql, (e: any) => (e ? reject(e) : resolve()))); +async function duckExec(sql: string): Promise { + await duckRun(sql); } -function duckAll(db: any, sql: string): Promise { - return new Promise((resolve, reject) => db.all(sql, (e: any, rows: any) => (e ? reject(e) : resolve(rows || [])))); +async function duckAll(sql: string): Promise { + 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(); @@ -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) {