Files
EJClaw/scripts/migrate-unify.cjs
2026-03-23 08:09:15 +09:00

221 lines
8.6 KiB
JavaScript

/**
* Data Unification Migration Script
* Merges codex data into the primary (claude) directories.
*
* Run AFTER stopping both services.
*/
const Database = require('/home/clone-ej/EJClaw/node_modules/better-sqlite3');
const fs = require('fs');
const path = require('path');
const BASE = '/home/clone-ej/EJClaw';
const CLAUDE_DB = path.join(BASE, 'store/messages.db');
const CODEX_DB = path.join(BASE, 'store-codex/messages.db');
console.log('=== EJClaw Data Unification ===\n');
// 1. Open both DBs
const primary = new Database(CLAUDE_DB);
const codex = new Database(CODEX_DB);
primary.pragma('journal_mode = WAL');
primary.pragma('busy_timeout = 5000');
// 2. Merge registered_groups (codex → primary, skip JID conflicts)
console.log('--- Merging registered_groups ---');
const codexGroups = codex.prepare('SELECT * FROM registered_groups').all();
const insertGroup = primary.prepare(`
INSERT OR IGNORE INTO registered_groups
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger, is_main, agent_type, work_dir)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
let groupsAdded = 0;
let groupsSkipped = 0;
for (const g of codexGroups) {
// Check if JID already exists with different agent_type
const existing = primary.prepare('SELECT agent_type FROM registered_groups WHERE jid = ?').get(g.jid);
if (existing) {
// Same JID, different bots — both need to be registered
// Since JID is PK, we can't have duplicates. The group with claude-code type stays,
// and the codex one is already differentiated by agent_type in the same row.
// But wait — in the unified DB, the same JID can only appear once.
// For shared channels (both bots respond), we keep the claude registration
// and the codex service will also load it IF we adjust the filter.
// Actually, the codex service filters by agent_type='codex', so it won't see claude-code groups.
// We need BOTH registrations for shared channels.
// Solution: codex groups with different folder names get inserted.
// Same JID + same folder = skip (duplicate)
// Same JID + different folder = need a unique folder for codex
if (existing.agent_type !== g.agent_type) {
// Same JID but different agent types — need both
// Change JID to make it unique: append agent type suffix
const codexJid = g.jid + ':codex';
const existsCodex = primary.prepare('SELECT 1 FROM registered_groups WHERE jid = ?').get(codexJid);
if (!existsCodex) {
insertGroup.run(codexJid, g.name, g.folder, g.trigger_pattern, g.added_at,
g.container_config, g.requires_trigger, g.is_main, g.agent_type, g.work_dir);
groupsAdded++;
console.log(` + ${g.folder} (codex, jid=${codexJid})`);
} else {
groupsSkipped++;
}
} else {
groupsSkipped++;
}
} else {
insertGroup.run(g.jid, g.name, g.folder, g.trigger_pattern, g.added_at,
g.container_config, g.requires_trigger, g.is_main, g.agent_type, g.work_dir);
groupsAdded++;
console.log(` + ${g.folder} (${g.agent_type})`);
}
}
console.log(` Added: ${groupsAdded}, Skipped: ${groupsSkipped}\n`);
// 3. Merge sessions (codex → primary, agent_type='codex')
console.log('--- Merging sessions ---');
const codexSessions = codex.prepare('SELECT * FROM sessions').all();
const insertSession = primary.prepare(
'INSERT OR IGNORE INTO sessions (group_folder, agent_type, session_id) VALUES (?, ?, ?)'
);
let sessionsAdded = 0;
for (const s of codexSessions) {
insertSession.run(s.group_folder, s.agent_type, s.session_id);
sessionsAdded++;
console.log(` + ${s.group_folder} [${s.agent_type}]`);
}
console.log(` Added: ${sessionsAdded}\n`);
// 4. Merge router_state (codex → primary, already prefixed)
console.log('--- Merging router_state ---');
const codexState = codex.prepare('SELECT * FROM router_state').all();
const insertState = primary.prepare(
'INSERT OR IGNORE INTO router_state (key, value) VALUES (?, ?)'
);
for (const s of codexState) {
insertState.run(s.key, s.value);
console.log(` + ${s.key}`);
}
console.log('');
// 5. Merge messages (codex → primary, skip duplicates by PK)
console.log('--- Merging messages ---');
const codexMsgs = codex.prepare('SELECT * FROM messages').all();
const insertMsg = primary.prepare(`
INSERT OR IGNORE INTO messages
(id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
let msgsAdded = 0;
let msgsSkipped = 0;
const batchInsert = primary.transaction((msgs) => {
for (const m of msgs) {
const result = insertMsg.run(m.id, m.chat_jid, m.sender, m.sender_name,
m.content, m.timestamp, m.is_from_me, m.is_bot_message);
if (result.changes > 0) msgsAdded++;
else msgsSkipped++;
}
});
batchInsert(codexMsgs);
console.log(` Added: ${msgsAdded}, Skipped (duplicates): ${msgsSkipped}\n`);
// 6. Merge chats (codex → primary, UPSERT with newer timestamp)
console.log('--- Merging chats ---');
const codexChats = codex.prepare('SELECT * FROM chats').all();
const upsertChat = primary.prepare(`
INSERT INTO chats (jid, name, last_message_time, channel, is_group) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(jid) DO UPDATE SET
name = COALESCE(excluded.name, name),
last_message_time = MAX(last_message_time, excluded.last_message_time),
channel = COALESCE(excluded.channel, channel),
is_group = COALESCE(excluded.is_group, is_group)
`);
let chatsUpserted = 0;
for (const c of codexChats) {
upsertChat.run(c.jid, c.name, c.last_message_time, c.channel, c.is_group);
chatsUpserted++;
}
console.log(` Upserted: ${chatsUpserted}\n`);
// 7. Merge scheduled_tasks (codex → primary, skip duplicates)
const codexTasks = codex.prepare('SELECT * FROM scheduled_tasks').all();
if (codexTasks.length > 0) {
console.log('--- Merging scheduled_tasks ---');
const insertTask = primary.prepare(`
INSERT OR IGNORE INTO scheduled_tasks
(id, group_folder, chat_jid, prompt, schedule_type, schedule_value, next_run, last_run, last_result, status, created_at, context_mode)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const t of codexTasks) {
insertTask.run(t.id, t.group_folder, t.chat_jid, t.prompt, t.schedule_type,
t.schedule_value, t.next_run, t.last_run, t.last_result, t.status, t.created_at, t.context_mode);
console.log(` + ${t.id}`);
}
console.log('');
}
// Close DBs
primary.close();
codex.close();
// 8. Merge groups directories
console.log('--- Merging groups-codex/ → groups/ ---');
const codexGroupsDir = path.join(BASE, 'groups-codex');
const claudeGroupsDir = path.join(BASE, 'groups');
if (fs.existsSync(codexGroupsDir)) {
const entries = fs.readdirSync(codexGroupsDir);
for (const entry of entries) {
const src = path.join(codexGroupsDir, entry);
const dst = path.join(claudeGroupsDir, entry);
if (fs.existsSync(dst)) {
// Merge: copy files that don't exist in dst
if (fs.statSync(src).isDirectory()) {
const files = fs.readdirSync(src);
for (const f of files) {
const srcFile = path.join(src, f);
const dstFile = path.join(dst, f);
if (!fs.existsSync(dstFile)) {
fs.cpSync(srcFile, dstFile, { recursive: true });
console.log(` cp ${entry}/${f}`);
}
}
}
} else {
fs.cpSync(src, dst, { recursive: true });
console.log(` + ${entry}/`);
}
}
}
console.log('');
// 9. Merge data-codex/sessions/ → data/sessions/
console.log('--- Merging data-codex/sessions/ → data/sessions/ ---');
const codexDataSessions = path.join(BASE, 'data-codex/sessions');
const claudeDataSessions = path.join(BASE, 'data/sessions');
if (fs.existsSync(codexDataSessions)) {
const entries = fs.readdirSync(codexDataSessions);
for (const entry of entries) {
const src = path.join(codexDataSessions, entry);
const dst = path.join(claudeDataSessions, entry);
if (fs.existsSync(dst)) {
// Both exist — merge .codex/ subdirectory
const codexSubdir = path.join(src, '.codex');
const dstCodexSubdir = path.join(dst, '.codex');
if (fs.existsSync(codexSubdir) && !fs.existsSync(dstCodexSubdir)) {
fs.cpSync(codexSubdir, dstCodexSubdir, { recursive: true });
console.log(` cp ${entry}/.codex/`);
}
} else {
fs.cpSync(src, dst, { recursive: true });
console.log(` + ${entry}/`);
}
}
}
console.log('');
console.log('=== Migration complete ===');
console.log('Next steps:');
console.log('1. Update .env.codex to remove EJCLAW_STORE_DIR, EJCLAW_DATA_DIR, EJCLAW_GROUPS_DIR');
console.log('2. Restart both services');
console.log('3. Verify, then rename old dirs to .bak');