Skip to main content
Glama

Scratchpad MCP

by pc035860
merge-databases.cjs13.6 kB
#!/usr/bin/env node /** * Scratchpad 資料庫合併工具 * * 支援三種合併策略: * 1. smart - 智慧合併(推薦) * 2. namespace - 命名空間合併 * 3. simple - 簡單合併 * * 使用方法: * node scripts/merge-databases.cjs --source=/path/to/source.db --strategy=smart [--project-scope=new-project] */ const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); // 命令行參數解析 const args = process.argv.slice(2); const config = { target: path.join(process.cwd(), 'scratchpad.db'), source: null, strategy: 'smart', projectScope: null, prefix: 'MERGED' }; args.forEach(arg => { if (arg.startsWith('--source=')) { config.source = arg.substring(9); } else if (arg.startsWith('--target=')) { config.target = arg.substring(9); } else if (arg.startsWith('--strategy=')) { config.strategy = arg.substring(11); } else if (arg.startsWith('--project-scope=')) { config.projectScope = arg.substring(16); } else if (arg.startsWith('--prefix=')) { config.prefix = arg.substring(9); } }); // 驗證參數 if (!config.source) { console.error('❌ 請指定來源資料庫路徑:--source=/path/to/source.db'); process.exit(1); } if (!fs.existsSync(config.source)) { console.error(`❌ 來源資料庫不存在:${config.source}`); process.exit(1); } if (!fs.existsSync(config.target)) { console.error(`❌ 目標資料庫不存在:${config.target}`); process.exit(1); } if (!['smart', 'namespace', 'simple'].includes(config.strategy)) { console.error('❌ 不支援的合併策略,請使用:smart, namespace, simple'); process.exit(1); } console.log('🔄 Scratchpad 資料庫合併工具'); console.log(`📁 來源資料庫: ${config.source}`); console.log(`📁 目標資料庫: ${config.target}`); console.log(`📋 合併策略: ${config.strategy}`); if (config.projectScope) { console.log(`🏷️ 專案範圍: ${config.projectScope}`); } // 檢查資料庫是否正在使用 function checkDatabaseInUse(dbPath) { try { const { execSync } = require('child_process'); const lsofResult = execSync( `lsof "${dbPath}" "${dbPath}-shm" "${dbPath}-wal" 2>/dev/null || true`, { encoding: 'utf8' } ); if (lsofResult.trim()) { console.error(`❌ 資料庫正在使用中:${dbPath}`); console.error('請先關閉所有 MCP 伺服器!'); process.exit(1); } } catch (error) { // lsof 可能不可用,忽略 } } // 生成新的 UUID(不與現有 ID 衝突) function generateNewId(existingIds) { let newId; do { newId = crypto.randomUUID(); } while (existingIds.has(newId)); existingIds.add(newId); return newId; } // 智慧合併策略 function smartMerge(sourceDb, targetDb) { console.log('\n🧠 執行智慧合併...'); // 1. 載入目標資料庫現有 ID const existingWorkflowIds = new Set( targetDb.prepare('SELECT id FROM workflows').all().map(row => row.id) ); const existingScratchpadIds = new Set( targetDb.prepare('SELECT id FROM scratchpads').all().map(row => row.id) ); // 2. 載入來源資料 const sourceWorkflows = sourceDb.prepare(` SELECT * FROM workflows ORDER BY created_at `).all(); const sourceScratchpads = sourceDb.prepare(` SELECT * FROM scratchpads ORDER BY created_at `).all(); console.log(`📊 來源資料:${sourceWorkflows.length} workflows, ${sourceScratchpads.length} scratchpads`); // 3. ID 映射表 const workflowIdMap = new Map(); // 4. 合併 workflows const insertWorkflow = targetDb.prepare(` INSERT INTO workflows (id, name, description, created_at, updated_at, scratchpad_count, is_active, project_scope) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); let mergedWorkflows = 0; sourceWorkflows.forEach(workflow => { const newId = existingWorkflowIds.has(workflow.id) ? generateNewId(existingWorkflowIds) : workflow.id; workflowIdMap.set(workflow.id, newId); const projectScope = config.projectScope || workflow.project_scope; try { insertWorkflow.run( newId, workflow.name, workflow.description, workflow.created_at, workflow.updated_at, workflow.scratchpad_count, workflow.is_active, projectScope ); mergedWorkflows++; } catch (error) { console.warn(`⚠️ 跳過 workflow ${workflow.name}: ${error.message}`); } }); // 5. 合併 scratchpads const insertScratchpad = targetDb.prepare(` INSERT INTO scratchpads (id, workflow_id, title, content, created_at, updated_at, size_bytes) VALUES (?, ?, ?, ?, ?, ?, ?) `); let mergedScratchpads = 0; sourceScratchpads.forEach(scratchpad => { const newId = existingScratchpadIds.has(scratchpad.id) ? generateNewId(existingScratchpadIds) : scratchpad.id; const newWorkflowId = workflowIdMap.get(scratchpad.workflow_id); if (!newWorkflowId) { console.warn(`⚠️ 跳過 scratchpad ${scratchpad.title}: 找不到對應的 workflow`); return; } try { insertScratchpad.run( newId, newWorkflowId, scratchpad.title, scratchpad.content, scratchpad.created_at, scratchpad.updated_at, scratchpad.size_bytes ); mergedScratchpads++; } catch (error) { console.warn(`⚠️ 跳過 scratchpad ${scratchpad.title}: ${error.message}`); } }); console.log(`✅ 智慧合併完成:${mergedWorkflows} workflows, ${mergedScratchpads} scratchpads`); return { workflows: mergedWorkflows, scratchpads: mergedScratchpads }; } // 命名空間合併策略 function namespaceMerge(sourceDb, targetDb) { console.log('\n🏷️ 執行命名空間合併...'); const existingWorkflowIds = new Set( targetDb.prepare('SELECT id FROM workflows').all().map(row => row.id) ); const existingScratchpadIds = new Set( targetDb.prepare('SELECT id FROM scratchpads').all().map(row => row.id) ); const sourceWorkflows = sourceDb.prepare('SELECT * FROM workflows ORDER BY created_at').all(); const sourceScratchpads = sourceDb.prepare('SELECT * FROM scratchpads ORDER BY created_at').all(); const workflowIdMap = new Map(); const insertWorkflow = targetDb.prepare(` INSERT INTO workflows (id, name, description, created_at, updated_at, scratchpad_count, is_active, project_scope) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); let mergedWorkflows = 0; sourceWorkflows.forEach(workflow => { const newId = generateNewId(existingWorkflowIds); workflowIdMap.set(workflow.id, newId); const prefixedName = `[${config.prefix}] ${workflow.name}`; const prefixedScope = config.projectScope || (workflow.project_scope ? `${config.prefix.toLowerCase()}-${workflow.project_scope}` : null); try { insertWorkflow.run( newId, prefixedName, workflow.description, workflow.created_at, workflow.updated_at, workflow.scratchpad_count, workflow.is_active, prefixedScope ); mergedWorkflows++; } catch (error) { console.warn(`⚠️ 跳過 workflow ${workflow.name}: ${error.message}`); } }); const insertScratchpad = targetDb.prepare(` INSERT INTO scratchpads (id, workflow_id, title, content, created_at, updated_at, size_bytes) VALUES (?, ?, ?, ?, ?, ?, ?) `); let mergedScratchpads = 0; sourceScratchpads.forEach(scratchpad => { const newId = generateNewId(existingScratchpadIds); const newWorkflowId = workflowIdMap.get(scratchpad.workflow_id); if (newWorkflowId) { try { insertScratchpad.run( newId, newWorkflowId, scratchpad.title, scratchpad.content, scratchpad.created_at, scratchpad.updated_at, scratchpad.size_bytes ); mergedScratchpads++; } catch (error) { console.warn(`⚠️ 跳過 scratchpad ${scratchpad.title}: ${error.message}`); } } }); console.log(`✅ 命名空間合併完成:${mergedWorkflows} workflows, ${mergedScratchpads} scratchpads`); return { workflows: mergedWorkflows, scratchpads: mergedScratchpads }; } // 簡單合併策略 function simpleMerge(sourceDb, targetDb) { console.log('\n⚡ 執行簡單合併...'); try { // 直接複製,忽略衝突 targetDb.exec(` INSERT OR IGNORE INTO workflows SELECT * FROM source.workflows `); targetDb.exec(` INSERT OR IGNORE INTO scratchpads SELECT * FROM source.scratchpads `); const mergedWorkflows = targetDb.changes; console.log(`✅ 簡單合併完成(忽略衝突)`); return { workflows: '未知', scratchpads: '未知' }; } catch (error) { console.error(`❌ 簡單合併失敗: ${error.message}`); throw error; } } // 重建 FTS5 索引 function rebuildFTS5Index(db) { console.log('\n🔍 重建 FTS5 索引...'); try { // 清理現有 FTS5 db.exec('DROP TABLE IF EXISTS scratchpads_fts'); // 重建 FTS5 表 db.exec(` CREATE VIRTUAL TABLE scratchpads_fts USING fts5( id UNINDEXED, workflow_id UNINDEXED, title, content, content='scratchpads', content_rowid='rowid', tokenize='porter unicode61' ) `); // 重建索引內容 db.exec(`INSERT INTO scratchpads_fts(scratchpads_fts) VALUES('rebuild')`); const ftsCount = db.prepare('SELECT COUNT(*) as count FROM scratchpads_fts').get(); console.log(`✅ FTS5 索引重建完成,記錄數:${ftsCount.count}`); // 重建觸發器 const triggers = [ `CREATE TRIGGER scratchpads_fts_insert AFTER INSERT ON scratchpads BEGIN INSERT INTO scratchpads_fts(rowid, id, workflow_id, title, content) VALUES (NEW.rowid, NEW.id, NEW.workflow_id, NEW.title, NEW.content); END`, `CREATE TRIGGER scratchpads_fts_delete AFTER DELETE ON scratchpads BEGIN DELETE FROM scratchpads_fts WHERE rowid = OLD.rowid; END`, `CREATE TRIGGER scratchpads_fts_update AFTER UPDATE ON scratchpads BEGIN INSERT OR REPLACE INTO scratchpads_fts(rowid, id, workflow_id, title, content) VALUES (NEW.rowid, NEW.id, NEW.workflow_id, NEW.title, NEW.content); END` ]; triggers.forEach(trigger => db.exec(trigger)); console.log('✅ FTS5 觸發器重建完成'); } catch (error) { console.warn(`⚠️ FTS5 索引重建失敗:${error.message}`); console.warn('搜尋功能可能受影響,但基本功能正常'); } } // 主程序 async function main() { checkDatabaseInUse(config.source); checkDatabaseInUse(config.target); const sourceDb = new Database(config.source, { readonly: true }); const targetDb = new Database(config.target); try { console.log('\n📊 合併前狀態檢查...'); // 來源資料庫狀態 const sourceWorkflowCount = sourceDb.prepare('SELECT COUNT(*) as count FROM workflows').get(); const sourceScratchpadCount = sourceDb.prepare('SELECT COUNT(*) as count FROM scratchpads').get(); console.log(`來源:${sourceWorkflowCount.count} workflows, ${sourceScratchpadCount.count} scratchpads`); // 目標資料庫狀態 const targetWorkflowCount = targetDb.prepare('SELECT COUNT(*) as count FROM workflows').get(); const targetScratchpadCount = targetDb.prepare('SELECT COUNT(*) as count FROM scratchpads').get(); console.log(`目標:${targetWorkflowCount.count} workflows, ${targetScratchpadCount.count} scratchpads`); // 設定事務 targetDb.pragma('journal_mode = DELETE'); // 暫時切換到 DELETE 模式確保穩定性 targetDb.exec('BEGIN TRANSACTION'); // 對於簡單模式,需要 ATTACH if (config.strategy === 'simple') { targetDb.exec(`ATTACH DATABASE '${config.source}' AS source`); } let result; switch (config.strategy) { case 'smart': result = smartMerge(sourceDb, targetDb); break; case 'namespace': result = namespaceMerge(sourceDb, targetDb); break; case 'simple': result = simpleMerge(sourceDb, targetDb); break; } targetDb.exec('COMMIT'); // 重建 FTS5 索引 rebuildFTS5Index(targetDb); // 最終狀態檢查 console.log('\n📊 合併後狀態檢查...'); const finalWorkflowCount = targetDb.prepare('SELECT COUNT(*) as count FROM workflows').get(); const finalScratchpadCount = targetDb.prepare('SELECT COUNT(*) as count FROM scratchpads').get(); console.log(`最終:${finalWorkflowCount.count} workflows, ${finalScratchpadCount.count} scratchpads`); console.log('\n🎉 資料庫合併成功!'); console.log(`✨ 新增:${result.workflows} workflows, ${result.scratchpads} scratchpads`); } catch (error) { targetDb.exec('ROLLBACK'); console.error('\n❌ 合併過程發生錯誤:'); console.error(error.message); console.error('\n🔧 建議操作:'); console.error('1. 檢查來源資料庫完整性'); console.error('2. 確保目標資料庫有足夠空間'); console.error('3. 備份後重試'); process.exit(1); } finally { sourceDb.close(); targetDb.close(); } } main().catch(console.error);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pc035860/scratchpad-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server