/**
* バックアップ・差分ツール
*/
import { z } from 'zod';
import { requireMTClient, getActiveConnectionId } from '../utils/connection-manager.js';
import {
createBackupSession,
addBackupItem,
setTransformedHtml,
getCurrentSession,
getTransformedItems,
generateDiffReport,
generateHtmlDiffReport,
listBackupSessions,
loadBackupSession,
deleteBackupSession,
updateSessionStatus,
} from '../utils/backup-manager.js';
import type { BackupItem } from '../types/index.js';
// バックアップ作成のスキーマ
export const createBackupSchema = z.object({
siteId: z.number().describe('サイトID'),
targetType: z.enum(['entries', 'pages', 'all']).default('all').describe('対象タイプ(entries, pages, all)'),
status: z.string().optional().describe('ステータスフィルタ(例: Publish)'),
});
// 変換結果設定のスキーマ
export const setTransformSchema = z.object({
itemType: z.enum(['entry', 'page', 'content_data']).describe('アイテムタイプ'),
itemId: z.number().describe('アイテムID'),
afterHtml: z.string().describe('変換後のHTML'),
});
// 一括変換結果設定のスキーマ
export const setBulkTransformSchema = z.object({
transforms: z.array(z.object({
itemType: z.enum(['entry', 'page', 'content_data']),
itemId: z.number(),
afterHtml: z.string(),
})).describe('変換結果の配列'),
});
// 差分レポート生成のスキーマ
export const generateReportSchema = z.object({
format: z.enum(['json', 'html']).default('html').describe('レポート形式'),
});
// セッション操作のスキーマ
export const sessionIdSchema = z.object({
sessionId: z.string().describe('セッションID'),
});
// 変更適用のスキーマ
export const applyChangesSchema = z.object({
dryRun: z.boolean().default(false).describe('ドライランモード(実際には適用しない)'),
});
// リストア対象のスキーマ
export const restoreSchema = z.object({
sessionId: z.string().optional().describe('リストアするセッションID(省略時はアクティブセッション)'),
});
/**
* バックアップを作成
*/
export async function handleCreateBackup(args: z.infer<typeof createBackupSchema>) {
const connectionId = getActiveConnectionId();
if (!connectionId) {
return {
content: [{
type: 'text' as const,
text: 'アクティブな接続がありません。mt_use_connection で接続を選択してください。',
}],
isError: true,
};
}
const client = await requireMTClient();
const session = await createBackupSession(connectionId);
let totalItems = 0;
// 記事をバックアップ
if (args.targetType === 'entries' || args.targetType === 'all') {
let offset = 0;
const limit = 50;
while (true) {
const result = await client.getEntries(args.siteId, { limit, offset, status: args.status });
for (const entry of result.items) {
if (entry.body) {
const item: BackupItem = {
type: 'entry',
id: entry.id,
siteId: args.siteId,
title: entry.title,
beforeHtml: entry.body,
fieldName: 'body',
};
await addBackupItem(item);
totalItems++;
}
if (entry.more) {
const item: BackupItem = {
type: 'entry',
id: entry.id,
siteId: args.siteId,
title: entry.title + ' (続き)',
beforeHtml: entry.more,
fieldName: 'more',
};
await addBackupItem(item);
totalItems++;
}
}
if (result.items.length < limit) break;
offset += limit;
}
}
// ウェブページをバックアップ
if (args.targetType === 'pages' || args.targetType === 'all') {
let offset = 0;
const limit = 50;
while (true) {
const result = await client.getPages(args.siteId, { limit, offset, status: args.status });
for (const page of result.items) {
if (page.body) {
const item: BackupItem = {
type: 'page',
id: page.id,
siteId: args.siteId,
title: page.title,
beforeHtml: page.body,
fieldName: 'body',
};
await addBackupItem(item);
totalItems++;
}
if (page.more) {
const item: BackupItem = {
type: 'page',
id: page.id,
siteId: args.siteId,
title: page.title + ' (続き)',
beforeHtml: page.more,
fieldName: 'more',
};
await addBackupItem(item);
totalItems++;
}
}
if (result.items.length < limit) break;
offset += limit;
}
}
return {
content: [{
type: 'text' as const,
text: `バックアップを作成しました。\n\nセッションID: ${session.sessionId}\nバックアップ件数: ${totalItems}件\n\n次のステップ:\n1. mt_get_backup_items でバックアップ内容を確認\n2. HTMLを変換\n3. mt_set_transform または mt_set_bulk_transform で変換結果を設定\n4. mt_generate_diff_report で差分を確認\n5. mt_apply_changes で変更を適用`,
}],
};
}
/**
* バックアップアイテム一覧を取得
*/
export async function handleGetBackupItems() {
const session = getCurrentSession();
if (!session) {
return {
content: [{
type: 'text' as const,
text: 'アクティブなバックアップセッションがありません。mt_create_backup でバックアップを作成してください。',
}],
isError: true,
};
}
if (session.items.length === 0) {
return {
content: [{
type: 'text' as const,
text: 'バックアップアイテムがありません。',
}],
};
}
// 全文を含めたアイテム一覧を生成
const items = session.items.map((item, index) => {
return {
index: index + 1,
type: item.type,
id: item.id,
siteId: item.siteId,
title: item.title,
fieldName: item.fieldName,
beforeHtml: item.beforeHtml, // 全文を含める
};
});
// 見やすい形式でテキスト出力
const textList = items.map(item => {
return `## ${item.index}. [${item.type}#${item.id}] ${item.title}
フィールド: ${item.fieldName}
### HTML内容:
\`\`\`html
${item.beforeHtml}
\`\`\`
`;
}).join('\n---\n\n');
return {
content: [{
type: 'text' as const,
text: `# バックアップアイテム (${session.items.length}件)
セッションID: ${session.sessionId}
${textList}
---
変換後のHTMLを設定するには mt_set_bulk_transform を使用してください。`,
}],
};
}
/**
* 変換結果を設定
*/
export async function handleSetTransform(args: z.infer<typeof setTransformSchema>) {
await setTransformedHtml(args.itemType, args.itemId, args.afterHtml);
return {
content: [{
type: 'text' as const,
text: `変換結果を設定しました: ${args.itemType}#${args.itemId}`,
}],
};
}
/**
* 一括変換結果を設定
*/
export async function handleSetBulkTransform(args: z.infer<typeof setBulkTransformSchema>) {
for (const transform of args.transforms) {
await setTransformedHtml(transform.itemType, transform.itemId, transform.afterHtml);
}
return {
content: [{
type: 'text' as const,
text: `${args.transforms.length}件の変換結果を設定しました。`,
}],
};
}
/**
* 差分レポートを生成
*/
export async function handleGenerateDiffReport(args: z.infer<typeof generateReportSchema>) {
if (args.format === 'html') {
const htmlPath = await generateHtmlDiffReport();
return {
content: [{
type: 'text' as const,
text: `HTMLレポートを生成しました:\n${htmlPath}\n\nブラウザで開いて差分を確認してください。`,
}],
};
} else {
const report = await generateDiffReport();
return {
content: [{
type: 'text' as const,
text: `差分レポート:\n\n変更あり: ${report.summary.changed}件\n変更なし: ${report.summary.unchanged}件\n合計: ${report.summary.total}件\n\n詳細:\n${JSON.stringify(report.items.map(i => ({
type: i.type,
id: i.id,
title: i.title,
hasChanges: i.hasChanges,
})), null, 2)}`,
}],
};
}
}
/**
* 変更を適用
*/
export async function handleApplyChanges(args: z.infer<typeof applyChangesSchema>) {
const session = getCurrentSession();
if (!session) {
return {
content: [{
type: 'text' as const,
text: 'アクティブなバックアップセッションがありません。',
}],
isError: true,
};
}
const transformedItems = getTransformedItems();
if (transformedItems.length === 0) {
return {
content: [{
type: 'text' as const,
text: '変換されたアイテムがありません。mt_set_transform で変換結果を設定してください。',
}],
isError: true,
};
}
if (args.dryRun) {
return {
content: [{
type: 'text' as const,
text: `[ドライラン] ${transformedItems.length}件のアイテムが更新される予定です。\n\n実際に適用するには dryRun: false で再度実行してください。`,
}],
};
}
const client = await requireMTClient();
let successCount = 0;
let errorCount = 0;
const errors: string[] = [];
for (const item of transformedItems) {
try {
if (item.type === 'entry') {
const updateData = item.fieldName === 'body'
? { body: item.afterHtml }
: { more: item.afterHtml };
await client.updateEntry(item.siteId, item.id, updateData);
} else if (item.type === 'page') {
const updateData = item.fieldName === 'body'
? { body: item.afterHtml }
: { more: item.afterHtml };
await client.updatePage(item.siteId, item.id, updateData);
}
successCount++;
} catch (error) {
errorCount++;
errors.push(`${item.type}#${item.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
await updateSessionStatus('applied');
let message = `変更を適用しました。\n\n成功: ${successCount}件\nエラー: ${errorCount}件`;
if (errors.length > 0) {
message += `\n\nエラー詳細:\n${errors.join('\n')}`;
}
return {
content: [{
type: 'text' as const,
text: message,
}],
};
}
/**
* バックアップからリストア
*/
export async function handleRestore(args: z.infer<typeof restoreSchema>) {
let session = getCurrentSession();
if (args.sessionId) {
session = await loadBackupSession(args.sessionId);
}
if (!session) {
return {
content: [{
type: 'text' as const,
text: 'リストアするセッションが見つかりません。',
}],
isError: true,
};
}
const client = await requireMTClient();
let successCount = 0;
let errorCount = 0;
const errors: string[] = [];
for (const item of session.items) {
try {
if (item.type === 'entry') {
const updateData = item.fieldName === 'body'
? { body: item.beforeHtml }
: { more: item.beforeHtml };
await client.updateEntry(item.siteId, item.id, updateData);
} else if (item.type === 'page') {
const updateData = item.fieldName === 'body'
? { body: item.beforeHtml }
: { more: item.beforeHtml };
await client.updatePage(item.siteId, item.id, updateData);
}
successCount++;
} catch (error) {
errorCount++;
errors.push(`${item.type}#${item.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
await updateSessionStatus('restored');
let message = `リストアを完了しました。\n\n成功: ${successCount}件\nエラー: ${errorCount}件`;
if (errors.length > 0) {
message += `\n\nエラー詳細:\n${errors.join('\n')}`;
}
return {
content: [{
type: 'text' as const,
text: message,
}],
};
}
/**
* セッション一覧を取得
*/
export async function handleListSessions() {
const sessions = await listBackupSessions();
if (sessions.length === 0) {
return {
content: [{
type: 'text' as const,
text: 'バックアップセッションがありません。',
}],
};
}
const currentSession = getCurrentSession();
const list = sessions.map(s => {
const isActive = s.sessionId === currentSession?.sessionId;
return `${isActive ? '✓ ' : ' '}[${s.sessionId}]\n 接続: ${s.connectionId}\n 作成日時: ${s.createdAt}\n アイテム数: ${s.items.length}\n ステータス: ${s.status}`;
}).join('\n\n');
return {
content: [{
type: 'text' as const,
text: `バックアップセッション一覧 (${sessions.length}件):\n\n${list}`,
}],
};
}
/**
* セッションを読み込み
*/
export async function handleLoadSession(args: z.infer<typeof sessionIdSchema>) {
const session = await loadBackupSession(args.sessionId);
if (!session) {
return {
content: [{
type: 'text' as const,
text: `セッション '${args.sessionId}' が見つかりません。`,
}],
isError: true,
};
}
return {
content: [{
type: 'text' as const,
text: `セッション '${args.sessionId}' を読み込みました。\n\nアイテム数: ${session.items.length}\nステータス: ${session.status}`,
}],
};
}
/**
* セッションを削除
*/
export async function handleDeleteSession(args: z.infer<typeof sessionIdSchema>) {
const success = await deleteBackupSession(args.sessionId);
if (!success) {
return {
content: [{
type: 'text' as const,
text: `セッション '${args.sessionId}' が見つかりません。`,
}],
isError: true,
};
}
return {
content: [{
type: 'text' as const,
text: `セッション '${args.sessionId}' を削除しました。`,
}],
};
}