improve(qmd-brain): harden command execution against injection

This commit is contained in:
2026-03-14 15:03:34 +08:00
parent f1a6df4ca4
commit 4cc98e7c0f

View File

@@ -7,13 +7,15 @@
* - embed_to_pg.py (Python venv at /home/selig/apps/qmd-pg/) * - embed_to_pg.py (Python venv at /home/selig/apps/qmd-pg/)
*/ */
import { execSync, exec } from 'child_process'; import { exec, execFile } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
const QMD_CMD = '/home/selig/.nvm/versions/node/v24.13.1/bin/qmd'; const QMD_CMD = '/home/selig/.nvm/versions/node/v24.13.1/bin/qmd';
const EMBED_PY = '/home/selig/apps/qmd-pg/venv/bin/python3 /home/selig/apps/qmd-pg/embed_to_pg.py'; const EMBED_PY_BIN = '/home/selig/apps/qmd-pg/venv/bin/python3';
const EMBED_PY_SCRIPT = '/home/selig/apps/qmd-pg/embed_to_pg.py';
const MAX_SEARCH_LEN = 1500; // 回覆中搜尋結果最大字數 const MAX_SEARCH_LEN = 1500; // 回覆中搜尋結果最大字數
interface SearchResult { interface SearchResult {
@@ -23,17 +25,12 @@ interface SearchResult {
similarity?: number; similarity?: number;
} }
interface QmdResult {
path: string;
text?: string;
score?: number;
}
/** 執行 qmd BM25 全文搜尋 */ /** 執行 qmd BM25 全文搜尋 */
async function qmdSearch(query: string, topK = 5): Promise<string> { async function qmdSearch(query: string, topK = 5): Promise<string> {
try { try {
const { stdout } = await execAsync( const { stdout } = await execFileAsync(
`${QMD_CMD} search ${JSON.stringify(query)} --output markdown --limit ${topK}`, QMD_CMD,
['search', query, '--output', 'markdown', '--limit', String(topK)],
{ timeout: 15000, env: { ...process.env, HOME: '/home/selig' } } { timeout: 15000, env: { ...process.env, HOME: '/home/selig' } }
); );
return stdout.trim() || '(無結果)'; return stdout.trim() || '(無結果)';
@@ -45,8 +42,9 @@ async function qmdSearch(query: string, topK = 5): Promise<string> {
/** 執行 PostgreSQL 向量語意搜尋 */ /** 執行 PostgreSQL 向量語意搜尋 */
async function pgSearch(query: string, topK = 5): Promise<SearchResult[]> { async function pgSearch(query: string, topK = 5): Promise<SearchResult[]> {
try { try {
const { stdout } = await execAsync( const { stdout } = await execFileAsync(
`${EMBED_PY} search ${JSON.stringify(query)} --top-k ${topK} --json`, EMBED_PY_BIN,
[EMBED_PY_SCRIPT, 'search', query, '--top-k', String(topK), '--json'],
{ timeout: 20000 } { timeout: 20000 }
); );
return JSON.parse(stdout) as SearchResult[]; return JSON.parse(stdout) as SearchResult[];
@@ -71,7 +69,7 @@ async function triggerEmbed(): Promise<string> {
try { try {
// 背景執行,不等待完成 // 背景執行,不等待完成
exec( exec(
`${QMD_CMD} embed 2>&1 >> /tmp/qmd-embed.log & ${EMBED_PY} embed 2>&1 >> /tmp/qmd-embed.log &`, `${QMD_CMD} embed 2>&1 >> /tmp/qmd-embed.log & ${EMBED_PY_BIN} ${EMBED_PY_SCRIPT} embed 2>&1 >> /tmp/qmd-embed.log &`,
{ env: { ...process.env, HOME: '/home/selig' } } { env: { ...process.env, HOME: '/home/selig' } }
); );
return '✅ 索引更新已在背景啟動,約需 1-5 分鐘完成。'; return '✅ 索引更新已在背景啟動,約需 1-5 分鐘完成。';
@@ -86,8 +84,9 @@ async function getStats(): Promise<string> {
// qmd collection list // qmd collection list
try { try {
const { stdout } = await execAsync( const { stdout } = await execFileAsync(
`${QMD_CMD} collection list`, QMD_CMD,
['collection', 'list'],
{ timeout: 5000, env: { ...process.env, HOME: '/home/selig' } } { timeout: 5000, env: { ...process.env, HOME: '/home/selig' } }
); );
results.push(`**qmd Collections:**\n\`\`\`\n${stdout.trim()}\n\`\`\``); results.push(`**qmd Collections:**\n\`\`\`\n${stdout.trim()}\n\`\`\``);
@@ -97,8 +96,9 @@ async function getStats(): Promise<string> {
// pgvector stats // pgvector stats
try { try {
const { stdout } = await execAsync( const { stdout } = await execFileAsync(
`${EMBED_PY} stats`, EMBED_PY_BIN,
[EMBED_PY_SCRIPT, 'stats'],
{ timeout: 10000 } { timeout: 10000 }
); );
results.push(`**PostgreSQL pgvector:**\n\`\`\`\n${stdout.trim()}\n\`\`\``); results.push(`**PostgreSQL pgvector:**\n\`\`\`\n${stdout.trim()}\n\`\`\``);