update
This commit is contained in:
parent
a82f855743
commit
422a4b9d71
8
.env
8
.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
|
||||
|
||||
@ -26,3 +26,7 @@ WHITELIST=d:\
|
||||
|
||||
# 路径黑名单(优先级最高,逗号分隔;留空表示无)
|
||||
BLACKLIST=
|
||||
|
||||
# DuckDB:内置 CLI 可执行文件路径与本地数据库文件(无需原生 .node 依赖)
|
||||
DUCKDB_EXE=bin/duckdb.exe
|
||||
DUCKDB_FILE=data/duck.db
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -13,6 +13,10 @@ _be.*
|
||||
# 会话记忆数据
|
||||
conversations/
|
||||
|
||||
# 内置 duckdb 可执行文件与运行时数据库
|
||||
bin/
|
||||
data/
|
||||
|
||||
# 环境变量(含密钥,勿提交;参考 .env.example)
|
||||
.env
|
||||
.env.local
|
||||
|
||||
74
server.ts
74
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<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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user