'use strict'; // 從 telegram-codex-bot(0.1.x)升級: // 1) ~/.tgcodex → ~/.tgbot(設定、registry、會話全部搬過去,registry 裡的路徑一併改寫) // 2) 跑在舊套件路徑(telegram-codex-bot/src/index.js)上的 bot 程序重新掛到新套件 // 3) 舊控制台程序 tgcodex-console 移除(ensureConsoleOnPm2 會掛新的 tgbot-console) // 每一步都是冪等的:沒有舊東西就什麼都不做。搬家途中任何失敗都會把 bot 用原路徑拉回來,不留下停機狀態。 const fs = require('fs'); const path = require('path'); const pm2 = require('./pm2util'); function rewritePrefix(p, from, to) { if (typeof p !== 'string') return p; const norm = path.resolve(p); const fromNorm = path.resolve(from); if (norm.toLowerCase().startsWith(fromNorm.toLowerCase())) return path.join(to, norm.slice(fromNorm.length)); return p; } function underDir(p, dir) { return path.resolve(p).toLowerCase().startsWith(path.resolve(dir).toLowerCase()); } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // Windows 下剛被 pm2 刪掉的程序可能還握著檔案幾百毫秒;rename 失敗就等一下重試, // 真的搬不動就整份複製過去(舊目錄留著,之後可手動刪) async function moveDir(from, to, notes) { let lastErr = null; for (let i = 0; i < 6; i++) { try { fs.renameSync(from, to); return; } catch (err) { lastErr = err; if (!/EBUSY|EPERM|EACCES/.test(err.code || '')) throw err; await sleep(500 * (i + 1)); } } fs.cpSync(from, to, { recursive: true }); try { fs.rmSync(from, { recursive: true, force: true }); } catch { notes.push(`⚠️ 舊目錄 ${from} 被其他程序鎖住(${lastErr?.code}),已複製到新位置,舊目錄請之後手動刪除`); } } async function restartBots(bots, notes, label) { const { loadConfig } = require('./config'); for (const b of bots) { if (!b.online) { notes.push(`⏸ ${b.name} 原本是停止狀態,已從 pm2 移除,可在控制台重新啟動`); continue; } try { await pm2.startBotOnPm2(loadConfig(b.cfgPath)); notes.push(`🔄 ${b.name} ${label}`); } catch (err) { notes.push(`⚠️ ${b.name} 重新啟動失敗:${err.message}(請在控制台手動啟動)`); } } } async function migrateLegacy() { const notes = []; let list = await pm2.jlist().catch(() => []); const isOnline = (p) => p.pm2_env?.status === 'online'; // 1) 家目錄搬家 if (fs.existsSync(pm2.LEGACY_HOME) && !fs.existsSync(pm2.NEW_HOME)) { // 舊控制台的 cwd 與 log 都在舊目錄裡,會把目錄鎖住(Windows EBUSY),先拿掉 if (list.some((p) => p.name === pm2.LEGACY_CONSOLE_NAME)) { await pm2.pm2Exec(`delete ${JSON.stringify(pm2.LEGACY_CONSOLE_NAME)}`).catch(() => {}); notes.push(`🗑 已移除舊控制台程序 ${pm2.LEGACY_CONSOLE_NAME}`); } // 跑在舊家目錄上的 bot 也先從 pm2 拿掉(記住哪些在線上,搬完再啟) const affected = []; for (const p of list) { const cfgPath = pm2.botConfigPathOf(p); if (!cfgPath || !underDir(cfgPath, pm2.LEGACY_HOME)) continue; affected.push({ name: p.name, online: isOnline(p), oldCfgPath: cfgPath, cfgPath: rewritePrefix(cfgPath, pm2.LEGACY_HOME, pm2.NEW_HOME), }); await pm2.pm2Exec(`delete ${JSON.stringify(p.name)}`).catch(() => {}); } try { await moveDir(pm2.LEGACY_HOME, pm2.NEW_HOME, notes); } catch (err) { // 搬不動:bot 照原路徑拉回來,控制台由後續 ensureConsoleOnPm2 掛(會沿用舊目錄) notes.push(`❌ 設定目錄搬家失敗(${err.message}),維持使用 ${pm2.LEGACY_HOME}`); await restartBots(affected.map((b) => ({ ...b, cfgPath: b.oldCfgPath })), notes, '已用原路徑重新啟動'); return notes; } notes.push(`📦 設定目錄已搬到 ${pm2.NEW_HOME}(原 ${pm2.LEGACY_HOME})`); // registry 裡的設定檔路徑改寫 const regFile = path.join(pm2.NEW_HOME, 'registry.json'); try { const reg = JSON.parse(fs.readFileSync(regFile, 'utf-8')); for (const entry of Object.values(reg.bots || {})) { entry.configPath = rewritePrefix(entry.configPath, pm2.LEGACY_HOME, pm2.NEW_HOME); } fs.writeFileSync(regFile, JSON.stringify(reg, null, 2) + '\n'); } catch { /* 沒有 registry 就算了 */ } await restartBots(affected, notes, '已用新路徑重新啟動'); list = await pm2.jlist().catch(() => []); } // 2) 還跑在舊套件(或舊安裝路徑)上的 bot:重新掛到目前套件的 index.js const { loadConfig } = require('./config'); for (const p of list) { const cfgPath = pm2.botConfigPathOf(p); if (!cfgPath) continue; const script = p.pm2_env?.pm_exec_path || ''; if (path.resolve(script) === path.resolve(pm2.BOT_SCRIPT)) continue; if (!fs.existsSync(cfgPath)) continue; if (!isOnline(p)) { await pm2.pm2Exec(`delete ${JSON.stringify(p.name)}`).catch(() => {}); notes.push(`⏸ ${p.name} 原本是停止狀態(舊套件),已從 pm2 移除,可在控制台重新啟動`); continue; } try { await pm2.startBotOnPm2(loadConfig(cfgPath)); notes.push(`🔄 ${p.name} 已從舊套件切換到 telegram-bot 並重新啟動`); } catch (err) { notes.push(`⚠️ ${p.name} 切換失敗:${err.message}`); } } // 3) 舊控制台程序由 ensureConsoleOnPm2 處理(會刪掉 tgcodex-console 再掛 tgbot-console) return notes; } module.exports = { migrateLegacy };