friday-mcp-v2 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-server.js +802 -86
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -207,13 +207,15 @@ function checkRevisionMismatch(expectedRevision, currentState, mode, postId, ses
|
|
|
207
207
|
const currentRevision = computeRevision(currentState, mode);
|
|
208
208
|
if (expectedRevision === currentRevision) return null;
|
|
209
209
|
|
|
210
|
-
// 不一致 —
|
|
210
|
+
// 不一致 — snapshot をキャッシュに登録(AI が新 snapshotId で refetch 可能に)
|
|
211
|
+
// Phase 2: blocks 一覧は出力しない(コンテキスト軽量化)
|
|
211
212
|
const latestSnapshot = buildSnapshotFromState(currentState, mode, postId, sessionId, siteName);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
const snapshotLine = latestSnapshot
|
|
214
|
+
? `\n[snapshot:${latestSnapshot.snapshotId} rev:${latestSnapshot.revision}]`
|
|
215
|
+
: '';
|
|
216
|
+
const text = `❌ REVISION_MISMATCH: expected ${expectedRevision}, actual ${currentRevision}` +
|
|
217
|
+
snapshotLine +
|
|
218
|
+
`\n→ get_article_structure で最新を取得してください`;
|
|
217
219
|
return { content: [{ type: "text", text }], isError: true };
|
|
218
220
|
}
|
|
219
221
|
|
|
@@ -1005,11 +1007,11 @@ async function resolveRefsAndCheckRevision({
|
|
|
1005
1007
|
try {
|
|
1006
1008
|
if (ref) {
|
|
1007
1009
|
const index = resolveRefFromState(snapshotId, ref, mode, sessionId, postId, currentState);
|
|
1008
|
-
return { index };
|
|
1010
|
+
return { index, currentState };
|
|
1009
1011
|
}
|
|
1010
1012
|
if (refs) {
|
|
1011
1013
|
const indices = refs.map(r => resolveRefFromState(snapshotId, r, mode, sessionId, postId, currentState));
|
|
1012
|
-
return { indices };
|
|
1014
|
+
return { indices, currentState };
|
|
1013
1015
|
}
|
|
1014
1016
|
} catch (e) {
|
|
1015
1017
|
return { error: {
|
|
@@ -1021,10 +1023,11 @@ async function resolveRefsAndCheckRevision({
|
|
|
1021
1023
|
|
|
1022
1024
|
// Case 2: ref なし + expectedRevision のみ
|
|
1023
1025
|
if (expectedRevision) {
|
|
1024
|
-
const { error } = await acquireFreshState({
|
|
1026
|
+
const { currentState, error } = await acquireFreshState({
|
|
1025
1027
|
expectedRevision, mode, client, postId, sessionId, siteName,
|
|
1026
1028
|
});
|
|
1027
1029
|
if (error) return { error };
|
|
1030
|
+
return { currentState };
|
|
1028
1031
|
}
|
|
1029
1032
|
|
|
1030
1033
|
// Case 3: どちらもなし
|
|
@@ -1032,15 +1035,62 @@ async function resolveRefsAndCheckRevision({
|
|
|
1032
1035
|
}
|
|
1033
1036
|
|
|
1034
1037
|
/**
|
|
1035
|
-
*
|
|
1038
|
+
* 下流レスポンスの blocks を buildSnapshotBlocks / computeRevision が
|
|
1039
|
+
* 期待する state.allBlocks 形式に正規化する。
|
|
1040
|
+
* @param {Array} blocks - 下流 formatBlocksForSnapshot() の結果
|
|
1041
|
+
* @returns {{ allBlocks: Array }}
|
|
1042
|
+
*/
|
|
1043
|
+
function normalizeDownstreamBlocks(blocks) {
|
|
1044
|
+
return {
|
|
1045
|
+
allBlocks: blocks.map(b => ({
|
|
1046
|
+
index: b.index,
|
|
1047
|
+
type: b.type,
|
|
1048
|
+
html: b.html || '',
|
|
1049
|
+
attributes: b.attributes || {},
|
|
1050
|
+
section: b.section || null,
|
|
1051
|
+
depth: b.depth || 0,
|
|
1052
|
+
parentIndex: b.parentIndex ?? null,
|
|
1053
|
+
})),
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* 下流レスポンスの result.blocks から snapshot を構築する。
|
|
1059
|
+
* blocks がない場合は null を返す(呼び出し元でフォールバック判断)。
|
|
1060
|
+
* @param {object} result - 下流レスポンス
|
|
1061
|
+
* @param {string} mode
|
|
1062
|
+
* @param {number} postId
|
|
1063
|
+
* @param {string|null} sessionId
|
|
1064
|
+
* @param {string} siteName
|
|
1065
|
+
* @returns {object|null} { snapshotId, revision, blocks[] } or null
|
|
1066
|
+
*/
|
|
1067
|
+
function buildResponseSnapshotFromResult(result, mode, postId, sessionId, siteName) {
|
|
1068
|
+
const blocks = result?.blocks;
|
|
1069
|
+
if (!blocks || !Array.isArray(blocks) || blocks.length === 0) {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
const state = normalizeDownstreamBlocks(blocks);
|
|
1073
|
+
return buildSnapshotFromState(state, mode, postId, sessionId, siteName);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* 更新後のレスポンス snapshot を構築する。
|
|
1078
|
+
* result が渡され、result.blocks があればそこから構築(再取得不要)。
|
|
1079
|
+
* なければ従来通り getCurrentStructure() で再取得する。
|
|
1036
1080
|
* @param {string} mode
|
|
1037
1081
|
* @param {object} client
|
|
1038
1082
|
* @param {number} postId
|
|
1039
1083
|
* @param {string|null} sessionId
|
|
1040
1084
|
* @param {string} siteName
|
|
1085
|
+
* @param {object} [result] - 下流レスポンス(Phase 1: blocks 再利用)
|
|
1041
1086
|
* @returns {Promise<object|null>} { snapshotId, revision, blocks[] } or null
|
|
1042
1087
|
*/
|
|
1043
|
-
async function buildResponseSnapshot(mode, client, postId, sessionId, siteName) {
|
|
1088
|
+
async function buildResponseSnapshot(mode, client, postId, sessionId, siteName, result) {
|
|
1089
|
+
// Phase 1: 下流 result.blocks があればそこから構築(再取得不要)
|
|
1090
|
+
if (result) {
|
|
1091
|
+
const fromResult = buildResponseSnapshotFromResult(result, mode, postId, sessionId, siteName);
|
|
1092
|
+
if (fromResult) return fromResult;
|
|
1093
|
+
}
|
|
1044
1094
|
let newState;
|
|
1045
1095
|
try {
|
|
1046
1096
|
newState = await getCurrentStructure(mode, client, postId, sessionId);
|
|
@@ -1083,6 +1133,7 @@ async function buildResponseSnapshot(mode, client, postId, sessionId, siteName)
|
|
|
1083
1133
|
}
|
|
1084
1134
|
|
|
1085
1135
|
/**
|
|
1136
|
+
* @deprecated Phase 7 で削除予定。batch 経路および index 経路の後方互換用。
|
|
1086
1137
|
* snapshot 情報をテキストレスポンスに付加する
|
|
1087
1138
|
* blocks[] を含めて AI が新しい ref を取得できるようにする
|
|
1088
1139
|
* @param {string} text - 元のレスポンステキスト
|
|
@@ -1093,7 +1144,7 @@ async function buildResponseSnapshot(mode, client, postId, sessionId, siteName)
|
|
|
1093
1144
|
* @param {number} [refInfo.resolvedIndex] - 解決された元の index
|
|
1094
1145
|
* @returns {string} snapshot 行と blocks 一覧が付加されたテキスト
|
|
1095
1146
|
*/
|
|
1096
|
-
function
|
|
1147
|
+
function appendSnapshotToTextLegacy(text, snapshot, refInfo) {
|
|
1097
1148
|
if (!snapshot) return text;
|
|
1098
1149
|
|
|
1099
1150
|
// 1→N 展開チェック(単体操作時のみ)
|
|
@@ -1125,6 +1176,491 @@ function appendSnapshotToText(text, snapshot, refInfo) {
|
|
|
1125
1176
|
return out;
|
|
1126
1177
|
}
|
|
1127
1178
|
|
|
1179
|
+
/**
|
|
1180
|
+
* Phase 5: update_blocks / insert_block の差分/Legacy 自動分岐。
|
|
1181
|
+
* inputRef が null なら即 Legacy。差分構築失敗時も Legacy フォールバック。
|
|
1182
|
+
* @param {string} text - メッセージ本文
|
|
1183
|
+
* @param {object|null} snap - buildResponseSnapshot の結果
|
|
1184
|
+
* @param {object} result - 下流レスポンス
|
|
1185
|
+
* @param {object|null} preState - _preState
|
|
1186
|
+
* @param {string|null} inputRef - 入力 ref(null = Legacy 固定)
|
|
1187
|
+
* @param {boolean} isInsert - insertOnly 操作か
|
|
1188
|
+
* @param {object|null} refInfo - Legacy フォールバック用 _refInfo
|
|
1189
|
+
* @returns {string}
|
|
1190
|
+
*/
|
|
1191
|
+
function buildUpdateDiffResponse(text, snap, result, preState, inputRef, isInsert, refInfo) {
|
|
1192
|
+
if (!inputRef || !snap) {
|
|
1193
|
+
return appendSnapshotToTextLegacy(text, snap, refInfo);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (isInsert) {
|
|
1197
|
+
const changeInfo = buildChangeInfoFromResult('inserted', snap, result, preState);
|
|
1198
|
+
if (changeInfo) return appendRefChangesToText(text, changeInfo);
|
|
1199
|
+
return appendSnapshotToTextLegacy(text, snap, refInfo);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// update / expand 判定
|
|
1203
|
+
// success:false の change を除外(invalid regex, parse failure 等)
|
|
1204
|
+
const changes = result?.changes?.filter(c => c.success !== false);
|
|
1205
|
+
if (!changes || changes.length === 0) {
|
|
1206
|
+
return appendSnapshotToTextLegacy(text, snap, refInfo);
|
|
1207
|
+
}
|
|
1208
|
+
// filtered 済み changes で result を差し替え(buildChangeInfoFromResult が result.changes を直接参照するため)
|
|
1209
|
+
const filteredResult = { ...result, changes };
|
|
1210
|
+
if (changes.length === 1 && changes[0]?.newIndices?.length > 1) {
|
|
1211
|
+
// 1→N 展開(単体 target のみ)
|
|
1212
|
+
const changeInfo = buildChangeInfoFromResult('expanded', snap, filteredResult, preState, { inputRef });
|
|
1213
|
+
if (changeInfo) return appendRefChangesToText(text, changeInfo);
|
|
1214
|
+
} else if (changes.length === 1) {
|
|
1215
|
+
// 1→1 更新(単体 ref で成功 1 件のときだけ差分化)
|
|
1216
|
+
const changeInfo = buildChangeInfoFromResult('updated', snap, filteredResult, preState);
|
|
1217
|
+
if (changeInfo) return appendRefChangesToText(text, changeInfo);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return appendSnapshotToTextLegacy(text, snap, refInfo);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// ============================================================
|
|
1224
|
+
// Phase 2: ref 変化分類ロジック(差分通知方式の基盤)
|
|
1225
|
+
// ============================================================
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* @typedef {Object} DeletedChange
|
|
1229
|
+
* @property {string} snapshotId
|
|
1230
|
+
* @property {string} revision
|
|
1231
|
+
* @property {'deleted'} type
|
|
1232
|
+
* @property {string[]} deletedRefs - 削除された ref 配列 ['r5', 'r8']
|
|
1233
|
+
* @property {boolean} [refetchRequired]
|
|
1234
|
+
*/
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* @typedef {Object} InsertedChange
|
|
1238
|
+
* @property {string} snapshotId
|
|
1239
|
+
* @property {string} revision
|
|
1240
|
+
* @property {'inserted'} type
|
|
1241
|
+
* @property {Array<{index: number, ref: string, blockType: string, depth: number}>} inserted
|
|
1242
|
+
* @property {boolean} [refetchRequired]
|
|
1243
|
+
*/
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* @typedef {Object} MovedChange
|
|
1247
|
+
* @property {string} snapshotId
|
|
1248
|
+
* @property {string} revision
|
|
1249
|
+
* @property {'moved'} type
|
|
1250
|
+
* @property {Array<{oldRef: string, newRef: string}>} moved
|
|
1251
|
+
* @property {boolean} [refetchRequired]
|
|
1252
|
+
*/
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* @typedef {Object} UpdatedChange
|
|
1256
|
+
* @property {string} snapshotId
|
|
1257
|
+
* @property {string} revision
|
|
1258
|
+
* @property {'updated'} type
|
|
1259
|
+
* @property {string[]} updatedRefs - fingerprint 変化した ref 配列
|
|
1260
|
+
* @property {boolean} [refetchRequired]
|
|
1261
|
+
*/
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* @typedef {Object} ExpandedChange
|
|
1265
|
+
* @property {string} snapshotId
|
|
1266
|
+
* @property {string} revision
|
|
1267
|
+
* @property {'expanded'} type
|
|
1268
|
+
* @property {{oldRef: string, newRefs: string[]}} expanded
|
|
1269
|
+
* @property {boolean} [refetchRequired]
|
|
1270
|
+
*/
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* @typedef {Object} DuplicatedChange
|
|
1274
|
+
* @property {string} snapshotId
|
|
1275
|
+
* @property {string} revision
|
|
1276
|
+
* @property {'duplicated'} type
|
|
1277
|
+
* @property {string} sourceRef
|
|
1278
|
+
* @property {string} newRef
|
|
1279
|
+
* @property {number} sourceIndex
|
|
1280
|
+
* @property {number} newIndex
|
|
1281
|
+
* @property {boolean} [refetchRequired]
|
|
1282
|
+
*/
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* @typedef {DeletedChange|InsertedChange|MovedChange|UpdatedChange|ExpandedChange|DuplicatedChange} ChangeInfo
|
|
1286
|
+
*/
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* ref 変化情報をテキストに付加する��差分モード)��
|
|
1290
|
+
* blocks 一��は出力しない。
|
|
1291
|
+
* @param {string} text
|
|
1292
|
+
* @param {ChangeInfo|null} changeInfo
|
|
1293
|
+
* @returns {string}
|
|
1294
|
+
*/
|
|
1295
|
+
function appendRefChangesToText(text, changeInfo) {
|
|
1296
|
+
if (!changeInfo) return text;
|
|
1297
|
+
|
|
1298
|
+
let out = text + `\n\n[snapshot:${changeInfo.snapshotId} rev:${changeInfo.revision}]`;
|
|
1299
|
+
|
|
1300
|
+
switch (changeInfo.type) {
|
|
1301
|
+
case 'deleted':
|
|
1302
|
+
out += `\ndeleted: [${changeInfo.deletedRefs.join(', ')}] (invalid in new snapshot)`;
|
|
1303
|
+
break;
|
|
1304
|
+
case 'inserted':
|
|
1305
|
+
out += '\ninserted: ' + changeInfo.inserted
|
|
1306
|
+
.map(b => `[${b.index}|${b.ref}] ${b.blockType} (d:${b.depth})`)
|
|
1307
|
+
.join(', ');
|
|
1308
|
+
break;
|
|
1309
|
+
case 'moved':
|
|
1310
|
+
out += '\nmoved: ' + changeInfo.moved
|
|
1311
|
+
.map(m => `${m.oldRef}\u2192${m.newRef}`)
|
|
1312
|
+
.join(', ');
|
|
1313
|
+
break;
|
|
1314
|
+
case 'updated':
|
|
1315
|
+
out += `\nupdated: [${changeInfo.updatedRefs.join(', ')}] (use new snapshot)`;
|
|
1316
|
+
break;
|
|
1317
|
+
case 'expanded':
|
|
1318
|
+
out += `\nexpanded: ${changeInfo.expanded.oldRef} \u2192 [${changeInfo.expanded.newRefs.join(', ')}]`;
|
|
1319
|
+
break;
|
|
1320
|
+
case 'duplicated':
|
|
1321
|
+
out += `\nduplicated: ${changeInfo.sourceRef} \u2192 ${changeInfo.newRef} (source:[${changeInfo.sourceIndex}], new:[${changeInfo.newIndex}]) (use new snapshot)`;
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (changeInfo.refetchRequired) {
|
|
1326
|
+
out += '\n\u26a0 refetchRequired: \u5b50\u30d6\u30ed\u30c3\u30af\u5909\u66f4\u3042\u308a\u3002get_article_structure \u3067\u6700\u65b0\u3092\u53d6\u5f97\u3057\u3066\u304f\u3060\u3055\u3044\u3002';
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return out;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* @typedef {Object} BatchDiffEntry
|
|
1334
|
+
* @property {number} opIndex
|
|
1335
|
+
* @property {'updated'|'expanded'|'failed'|'skipped'} type
|
|
1336
|
+
* @property {string} [ref] - updated/failed/skipped 時
|
|
1337
|
+
* @property {string} [oldRef] - expanded 時
|
|
1338
|
+
* @property {string[]} [newRefs] - expanded 時
|
|
1339
|
+
* @property {string} [error] - failed 時
|
|
1340
|
+
* @property {boolean} [refetchRequired] - この entry 単体の refetch 判定
|
|
1341
|
+
*/
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* @typedef {Object} BatchDiffInfo
|
|
1345
|
+
* @property {string} snapshotId
|
|
1346
|
+
* @property {string} revision
|
|
1347
|
+
* @property {'batch'} type
|
|
1348
|
+
* @property {BatchDiffEntry[]} entries
|
|
1349
|
+
* @property {boolean} [refetchRequired] - batch 全体の OR
|
|
1350
|
+
*/
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* BatchDiffInfo をテキストに付加する(batch 差分モード)。
|
|
1354
|
+
* @param {string} text
|
|
1355
|
+
* @param {BatchDiffInfo|null} batchInfo
|
|
1356
|
+
* @returns {string}
|
|
1357
|
+
*/
|
|
1358
|
+
function appendBatchRefChangesToText(text, batchInfo) {
|
|
1359
|
+
if (!batchInfo) return text;
|
|
1360
|
+
|
|
1361
|
+
let out = text + `\n\n[snapshot:${batchInfo.snapshotId} rev:${batchInfo.revision}]`;
|
|
1362
|
+
out += '\nchanges:';
|
|
1363
|
+
|
|
1364
|
+
for (const entry of batchInfo.entries) {
|
|
1365
|
+
switch (entry.type) {
|
|
1366
|
+
case 'updated':
|
|
1367
|
+
out += `\n [${entry.opIndex}] updated: [${entry.ref}]`;
|
|
1368
|
+
break;
|
|
1369
|
+
case 'expanded':
|
|
1370
|
+
out += `\n [${entry.opIndex}] expanded: ${entry.oldRef} \u2192 [${entry.newRefs.join(', ')}]`;
|
|
1371
|
+
break;
|
|
1372
|
+
case 'failed':
|
|
1373
|
+
out += `\n [${entry.opIndex}] failed: ${entry.ref}`;
|
|
1374
|
+
if (entry.error) out += ` \u2014 ${entry.error}`;
|
|
1375
|
+
break;
|
|
1376
|
+
case 'skipped':
|
|
1377
|
+
out += `\n [${entry.opIndex}] skipped: ${entry.ref}`;
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
if (entry.refetchRequired) out += ' (refetch)';
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (batchInfo.refetchRequired) {
|
|
1384
|
+
out += '\n\u26a0 refetchRequired: \u5b50\u30d6\u30ed\u30c3\u30af\u5909\u66f4\u3042\u308a\u3002get_article_structure \u3067\u6700\u65b0\u3092\u53d6\u5f97\u3057\u3066\u304f\u3060\u3055\u3044\u3002';
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return out;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// ============================================================
|
|
1391
|
+
// Phase 7: batch 差分化 helper
|
|
1392
|
+
// ============================================================
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* allBlocks の parentIndex チェーンを辿り、resolvedOps 間に祖先/子孫関係があるか検出。
|
|
1396
|
+
* @param {Array<{resolvedIndex: number, ref: string}>} resolvedOps
|
|
1397
|
+
* @param {Array<{index: number, parentIndex: number|null}>} allBlocks
|
|
1398
|
+
* @returns {{childRef: string, childIndex: number, ancestorRef: string, ancestorIndex: number}|null}
|
|
1399
|
+
*/
|
|
1400
|
+
function checkAncestorOverlap(resolvedOps, allBlocks) {
|
|
1401
|
+
const targetMap = new Map(resolvedOps.map(o => [o.resolvedIndex, o.ref]));
|
|
1402
|
+
const parentMap = new Map(allBlocks.map(b => [b.index, b.parentIndex ?? null]));
|
|
1403
|
+
|
|
1404
|
+
for (const op of resolvedOps) {
|
|
1405
|
+
let current = parentMap.get(op.resolvedIndex);
|
|
1406
|
+
while (current != null) {
|
|
1407
|
+
if (targetMap.has(current)) {
|
|
1408
|
+
return {
|
|
1409
|
+
childRef: op.ref,
|
|
1410
|
+
childIndex: op.resolvedIndex,
|
|
1411
|
+
ancestorRef: targetMap.get(current),
|
|
1412
|
+
ancestorIndex: current,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
current = parentMap.get(current) ?? null;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* allBlocks 内の rootIndex を含む subtree の flat size を返す。
|
|
1423
|
+
* @param {Array<{index: number, parentIndex: number|null}>} allBlocks
|
|
1424
|
+
* @param {number} rootIndex
|
|
1425
|
+
* @returns {number}
|
|
1426
|
+
*/
|
|
1427
|
+
function countFlatSubtreeSize(allBlocks, rootIndex) {
|
|
1428
|
+
const childrenOf = new Map();
|
|
1429
|
+
for (const b of allBlocks) {
|
|
1430
|
+
if (b.parentIndex != null) {
|
|
1431
|
+
if (!childrenOf.has(b.parentIndex)) childrenOf.set(b.parentIndex, []);
|
|
1432
|
+
childrenOf.get(b.parentIndex).push(b.index);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
let count = 1;
|
|
1436
|
+
const stack = childrenOf.get(rootIndex) ? [...childrenOf.get(rootIndex)] : [];
|
|
1437
|
+
while (stack.length > 0) {
|
|
1438
|
+
const idx = stack.pop();
|
|
1439
|
+
count++;
|
|
1440
|
+
const children = childrenOf.get(idx);
|
|
1441
|
+
if (children) stack.push(...children);
|
|
1442
|
+
}
|
|
1443
|
+
return count;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* opResult.blocks から新 subtree の flat size を計算する。
|
|
1448
|
+
* rawNewIndices のうち集合内に親を持たないものを root とみなし、各 root の subtree size を合算。
|
|
1449
|
+
* editor(top-level のみ)/ headless(flat 全体)両対応。
|
|
1450
|
+
* @param {Array<{index: number, parentIndex?: number|null}>} opResultBlocks
|
|
1451
|
+
* @param {number[]} rawNewIndices
|
|
1452
|
+
* @returns {number}
|
|
1453
|
+
*/
|
|
1454
|
+
function computeNewFlatSize(opResultBlocks, rawNewIndices) {
|
|
1455
|
+
if (!rawNewIndices || rawNewIndices.length === 0) return 0;
|
|
1456
|
+
const indexSet = new Set(rawNewIndices);
|
|
1457
|
+
const newRoots = rawNewIndices.filter(idx => {
|
|
1458
|
+
const b = opResultBlocks.find(bl => bl.index === idx);
|
|
1459
|
+
return !b || b.parentIndex == null || !indexSet.has(b.parentIndex);
|
|
1460
|
+
});
|
|
1461
|
+
let total = 0;
|
|
1462
|
+
const blocksForSize = opResultBlocks.map(b => ({ index: b.index, parentIndex: b.parentIndex ?? null }));
|
|
1463
|
+
for (const root of newRoots) {
|
|
1464
|
+
total += countFlatSubtreeSize(blocksForSize, root);
|
|
1465
|
+
}
|
|
1466
|
+
return total;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* [pre-state 戦略] 操作前の状態で対象ブロックにネスト子があるか判定。
|
|
1471
|
+
* delete, move, duplicate 用。操作後は対象が消える/移動するため pre-state で見る。
|
|
1472
|
+
* 下流 hasChildren フラグがあればそちらを優先(duplicate, move が返す)。
|
|
1473
|
+
* @param {object|null} preState - 操作前の state
|
|
1474
|
+
* @param {number[]} targetIndices - 操作対象 index 配列
|
|
1475
|
+
* @param {object} [downstream] - 下流 result(hasChildren があれば優先)
|
|
1476
|
+
* @returns {boolean}
|
|
1477
|
+
*/
|
|
1478
|
+
function checkRefetchFromPreState(preState, targetIndices, downstream) {
|
|
1479
|
+
if (downstream?.hasChildren === true) return true;
|
|
1480
|
+
if (downstream?.hasChildren === false) return false;
|
|
1481
|
+
|
|
1482
|
+
if (!preState?.allBlocks) return true;
|
|
1483
|
+
|
|
1484
|
+
for (const idx of targetIndices) {
|
|
1485
|
+
const hasChild = preState.allBlocks.some(b => b.parentIndex === idx);
|
|
1486
|
+
if (hasChild) return true;
|
|
1487
|
+
}
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* [post-result 戦略] 操作結果にネストブロックが含ま���るか判定。
|
|
1493
|
+
* insert, update 用��非ネストだったブロックを nested HTML に更新したケースを検知。
|
|
1494
|
+
* 新 snapshot の blocks から、対象 index を parentIndex に持つブロックがあるかで判定。
|
|
1495
|
+
* @param {object} snapshot - buildResponseSnapshot() の結果(新 snapshot)
|
|
1496
|
+
* @param {number[]} resultIndices - 操作結果の index 配列(挿入/更新された位置)
|
|
1497
|
+
* @returns {boolean}
|
|
1498
|
+
*/
|
|
1499
|
+
function checkRefetchFromResult(snapshot, resultIndices) {
|
|
1500
|
+
if (!snapshot?.blocks || resultIndices.length === 0) return false;
|
|
1501
|
+
|
|
1502
|
+
for (const idx of resultIndices) {
|
|
1503
|
+
const hasNested = snapshot.blocks.some(b => b.parentIndex === idx);
|
|
1504
|
+
if (hasNested) return true;
|
|
1505
|
+
}
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* 下流 result + 新 snapshot → ChangeInfo を構築する。
|
|
1511
|
+
* 構築不能な場合は null を返す(呼び出し側は Legacy フォールバック必須)。
|
|
1512
|
+
* @param {'deleted'|'inserted'|'moved'|'updated'|'expanded'|'duplicated'} type
|
|
1513
|
+
* @param {object} snapshot - buildResponseSnapshot() の結果
|
|
1514
|
+
* @param {object} result - 下流レスポンス
|
|
1515
|
+
* @param {object|null} preState - _preState
|
|
1516
|
+
* @param {object} [opts] - 追加オプション
|
|
1517
|
+
* @returns {ChangeInfo|null} null = Legacy フォールバック
|
|
1518
|
+
*/
|
|
1519
|
+
function buildChangeInfoFromResult(type, snapshot, result, preState, opts) {
|
|
1520
|
+
if (!snapshot?.snapshotId || !result) return null;
|
|
1521
|
+
|
|
1522
|
+
const base = { snapshotId: snapshot.snapshotId, revision: snapshot.revision };
|
|
1523
|
+
|
|
1524
|
+
switch (type) {
|
|
1525
|
+
case 'deleted': {
|
|
1526
|
+
const deleted = result.deleted;
|
|
1527
|
+
if (!deleted || deleted.length === 0) return null;
|
|
1528
|
+
const deletedRefs = opts?.inputRefs || deleted.map(d => generateBlockRef(d.index));
|
|
1529
|
+
const refetch = checkRefetchFromPreState(preState, deleted.map(d => d.index), result);
|
|
1530
|
+
return { ...base, type: 'deleted', deletedRefs, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1531
|
+
}
|
|
1532
|
+
case 'inserted': {
|
|
1533
|
+
let indices = result.insertedIndices;
|
|
1534
|
+
if (!indices && result.changes) {
|
|
1535
|
+
indices = result.changes.flatMap(c => c.newIndices || []).filter(i => i != null);
|
|
1536
|
+
}
|
|
1537
|
+
if (!indices || indices.length === 0) return null;
|
|
1538
|
+
// refetchRequired は全 indices で判定(子孫含む)
|
|
1539
|
+
const refetch = checkRefetchFromResult(snapshot, indices);
|
|
1540
|
+
// top-level のみ: 他の inserted index を parentIndex に持つものを除外
|
|
1541
|
+
// (headless は subtree 全体の flat index を返すが、editor は top-level のみ。差分レスポンスは top-level に統一)
|
|
1542
|
+
const indexSet = new Set(indices);
|
|
1543
|
+
const topLevelIndices = indices.filter(idx => {
|
|
1544
|
+
const b = snapshot.blocks?.find(bl => bl.index === idx);
|
|
1545
|
+
return !b || b.parentIndex == null || !indexSet.has(b.parentIndex);
|
|
1546
|
+
});
|
|
1547
|
+
const inserted = topLevelIndices.map(idx => {
|
|
1548
|
+
const b = snapshot.blocks?.find(bl => bl.index === idx);
|
|
1549
|
+
return b
|
|
1550
|
+
? { index: b.index, ref: b.ref, blockType: b.type, depth: b.depth }
|
|
1551
|
+
: { index: idx, ref: generateBlockRef(idx), blockType: 'unknown', depth: 0 };
|
|
1552
|
+
});
|
|
1553
|
+
if (inserted.length === 0) return null;
|
|
1554
|
+
return { ...base, type: 'inserted', inserted, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1555
|
+
}
|
|
1556
|
+
case 'moved': {
|
|
1557
|
+
const mapping = result.movedMapping;
|
|
1558
|
+
if (!mapping || mapping.length === 0) return null;
|
|
1559
|
+
// inputRefs 長さ不一致 → Legacy フォールバック(count > 1 安全弁)
|
|
1560
|
+
if (opts?.inputRefs && opts.inputRefs.length !== mapping.length) return null;
|
|
1561
|
+
const moved = mapping.map((m, i) => ({
|
|
1562
|
+
oldRef: opts?.inputRefs ? opts.inputRefs[i] : generateBlockRef(m.oldIndex),
|
|
1563
|
+
newRef: generateBlockRef(m.newIndex),
|
|
1564
|
+
}));
|
|
1565
|
+
const refetch = checkRefetchFromPreState(preState, mapping.map(m => m.oldIndex), result);
|
|
1566
|
+
return { ...base, type: 'moved', moved, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1567
|
+
}
|
|
1568
|
+
case 'updated': {
|
|
1569
|
+
const changes = result.changes;
|
|
1570
|
+
if (!changes || changes.length === 0) return null;
|
|
1571
|
+
const updatedRefs = changes.map(c => generateBlockRef(c.newIndices?.[0] ?? c.oldIndex));
|
|
1572
|
+
const resultIndices = changes.flatMap(c => c.newIndices || []).filter(i => i != null);
|
|
1573
|
+
const refetch = checkRefetchFromResult(snapshot, resultIndices);
|
|
1574
|
+
return { ...base, type: 'updated', updatedRefs, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1575
|
+
}
|
|
1576
|
+
case 'expanded': {
|
|
1577
|
+
const changes = result.changes || [];
|
|
1578
|
+
if (changes.length !== 1) return null;
|
|
1579
|
+
const change = changes[0];
|
|
1580
|
+
if (!change || !change.newIndices || change.newIndices.length <= 1) return null;
|
|
1581
|
+
const oldRef = opts?.inputRef || generateBlockRef(change.oldIndex);
|
|
1582
|
+
const newRefs = change.newIndices.map(idx => generateBlockRef(idx));
|
|
1583
|
+
const refetch = checkRefetchFromResult(snapshot, change.newIndices);
|
|
1584
|
+
return { ...base, type: 'expanded', expanded: { oldRef, newRefs }, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1585
|
+
}
|
|
1586
|
+
case 'duplicated': {
|
|
1587
|
+
const srcIdx = result.sourceIndex ?? result.duplicated?.index;
|
|
1588
|
+
const newIdx = result.newIndex ?? result.duplicated?.newIndex;
|
|
1589
|
+
if (srcIdx == null || newIdx == null) return null;
|
|
1590
|
+
const refetch = checkRefetchFromPreState(preState, [srcIdx], result);
|
|
1591
|
+
return {
|
|
1592
|
+
...base, type: 'duplicated',
|
|
1593
|
+
sourceRef: opts?.inputRef || generateBlockRef(srcIdx),
|
|
1594
|
+
newRef: generateBlockRef(newIdx),
|
|
1595
|
+
sourceIndex: srcIdx, newIndex: newIdx,
|
|
1596
|
+
...(refetch ? { refetchRequired: true } : {}),
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
default:
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* 実行完了した resolvedOps + snapshot から BatchDiffInfo を構築する。
|
|
1606
|
+
* @param {Array} resolvedOps - rebasing 済みの op 配列
|
|
1607
|
+
* @param {object|null} snapshot - 最終 snapshot
|
|
1608
|
+
* @returns {BatchDiffInfo|null}
|
|
1609
|
+
*/
|
|
1610
|
+
function buildBatchDiffInfo(resolvedOps, snapshot) {
|
|
1611
|
+
if (!snapshot?.snapshotId) return null;
|
|
1612
|
+
|
|
1613
|
+
let refetchRequired = false;
|
|
1614
|
+
const entries = [];
|
|
1615
|
+
|
|
1616
|
+
// opIndex 順(入力順)でソート
|
|
1617
|
+
const opsByInput = [...resolvedOps].sort((a, b) => a.opIndex - b.opIndex);
|
|
1618
|
+
|
|
1619
|
+
for (const op of opsByInput) {
|
|
1620
|
+
if (op.status === 'failed') {
|
|
1621
|
+
entries.push({ opIndex: op.opIndex, type: 'failed', ref: op.ref, error: op.error || null });
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (op.status === 'skipped') {
|
|
1625
|
+
entries.push({ opIndex: op.opIndex, type: 'skipped', ref: op.ref });
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// rebased な新 ref を snapshot から取得
|
|
1630
|
+
const newRefs = (op.rebasedNewIndices || []).map(idx => {
|
|
1631
|
+
const b = snapshot.blocks?.find(bl => bl.index === idx);
|
|
1632
|
+
return b ? b.ref : generateBlockRef(idx);
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
// refetch 判定
|
|
1636
|
+
const opRefetch = checkRefetchFromResult(snapshot, op.rebasedNewIndices || []);
|
|
1637
|
+
if (opRefetch) refetchRequired = true;
|
|
1638
|
+
|
|
1639
|
+
if (op.status === 'expanded') {
|
|
1640
|
+
entries.push({
|
|
1641
|
+
opIndex: op.opIndex, type: 'expanded',
|
|
1642
|
+
oldRef: op.ref, newRefs,
|
|
1643
|
+
...(opRefetch ? { refetchRequired: true } : {}),
|
|
1644
|
+
});
|
|
1645
|
+
} else {
|
|
1646
|
+
// updated
|
|
1647
|
+
entries.push({
|
|
1648
|
+
opIndex: op.opIndex, type: 'updated',
|
|
1649
|
+
ref: newRefs[0] || op.ref,
|
|
1650
|
+
...(opRefetch ? { refetchRequired: true } : {}),
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
return {
|
|
1656
|
+
snapshotId: snapshot.snapshotId,
|
|
1657
|
+
revision: snapshot.revision,
|
|
1658
|
+
type: 'batch',
|
|
1659
|
+
entries,
|
|
1660
|
+
...(refetchRequired ? { refetchRequired: true } : {}),
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1128
1664
|
/**
|
|
1129
1665
|
* batch operations の実行
|
|
1130
1666
|
* @param {Array} operations - 各 operation: { target, newHTML?, replacements?, attributeUpdates? }
|
|
@@ -1221,17 +1757,30 @@ async function handleBatchOperations(operations, snapshotId, mode, client, postI
|
|
|
1221
1757
|
indexSet.add(op.resolvedIndex);
|
|
1222
1758
|
}
|
|
1223
1759
|
|
|
1224
|
-
//
|
|
1760
|
+
// ancestor/descendant overlap チェック(親子関係の target は batch 不可)
|
|
1761
|
+
const ancestorOverlap = checkAncestorOverlap(resolvedOps, currentState.allBlocks);
|
|
1762
|
+
if (ancestorOverlap) {
|
|
1763
|
+
return {
|
|
1764
|
+
content: [{ type: "text", text: `❌ operations に親子関係の target が含まれるため batch 不可です。ref ${ancestorOverlap.childRef} (index ${ancestorOverlap.childIndex}) は ref ${ancestorOverlap.ancestorRef} (index ${ancestorOverlap.ancestorIndex}) の子孫です。` }],
|
|
1765
|
+
isError: true,
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// 2. oldFlatSize を計算(rebasing 用、降順ソート前に実行)
|
|
1770
|
+
for (const op of resolvedOps) {
|
|
1771
|
+
op.oldFlatSize = countFlatSubtreeSize(currentState.allBlocks, op.resolvedIndex);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// 3. 降順ソート(index が大きい方から処理 — 1 op = 1 index なので安全)
|
|
1225
1775
|
resolvedOps.sort((a, b) => b.resolvedIndex - a.resolvedIndex);
|
|
1226
1776
|
|
|
1227
|
-
//
|
|
1228
|
-
|
|
1777
|
+
// 4. 各 operation を順次実行(結果情報を保持)
|
|
1778
|
+
let lastSuccessfulResult = null;
|
|
1229
1779
|
for (const op of resolvedOps) {
|
|
1230
1780
|
const index = op.resolvedIndex;
|
|
1231
1781
|
|
|
1232
1782
|
let opResult;
|
|
1233
1783
|
try {
|
|
1234
|
-
// batch は ref → index 解決済み。1 op = 1 index で下流に渡す
|
|
1235
1784
|
const downstreamParams = {
|
|
1236
1785
|
index,
|
|
1237
1786
|
replacements: op.replacements,
|
|
@@ -1246,39 +1795,95 @@ async function handleBatchOperations(operations, snapshotId, mode, client, postI
|
|
|
1246
1795
|
} else {
|
|
1247
1796
|
opResult = await client.sendEditorCommand("update_blocks", downstreamParams, 10000, postId, sessionId);
|
|
1248
1797
|
}
|
|
1249
|
-
|
|
1250
|
-
results.push({
|
|
1251
|
-
ref: op.ref,
|
|
1252
|
-
resolvedIndex: op.resolvedIndex,
|
|
1253
|
-
status: (opResult && opResult.success !== false && opResult.ok !== false) ? 'updated' : 'failed',
|
|
1254
|
-
error: opResult?.error || null,
|
|
1255
|
-
});
|
|
1256
1798
|
} catch (e) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1799
|
+
op.status = 'failed';
|
|
1800
|
+
op.error = e.message;
|
|
1801
|
+
op.rawNewIndices = [];
|
|
1802
|
+
op.newFlatSize = op.oldFlatSize;
|
|
1803
|
+
op.opResult = null;
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// op 成功判定
|
|
1808
|
+
const topLevelFail = !opResult || opResult.success === false || opResult.ok === false;
|
|
1809
|
+
if (topLevelFail) {
|
|
1810
|
+
op.status = 'failed';
|
|
1811
|
+
op.error = opResult?.error || 'unknown error';
|
|
1812
|
+
op.rawNewIndices = [];
|
|
1813
|
+
op.newFlatSize = op.oldFlatSize;
|
|
1814
|
+
op.opResult = opResult;
|
|
1815
|
+
continue;
|
|
1263
1816
|
}
|
|
1817
|
+
|
|
1818
|
+
// skipped 判定(attributeUpdates の filter mismatch)
|
|
1819
|
+
const successfulChanges = (opResult.changes || []).filter(c => c.success !== false);
|
|
1820
|
+
const hasSkippedResult = opResult.results?.some(r => r.skipped === true);
|
|
1821
|
+
if (successfulChanges.length === 0 && !topLevelFail && !opResult.error) {
|
|
1822
|
+
if (hasSkippedResult || op.attributeUpdates != null) {
|
|
1823
|
+
op.status = 'skipped';
|
|
1824
|
+
op.rawNewIndices = [];
|
|
1825
|
+
op.newFlatSize = op.oldFlatSize;
|
|
1826
|
+
op.opResult = opResult;
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
// changes 空で skipped でもない → failed
|
|
1830
|
+
op.status = 'failed';
|
|
1831
|
+
op.error = 'no successful changes';
|
|
1832
|
+
op.rawNewIndices = [];
|
|
1833
|
+
op.newFlatSize = op.oldFlatSize;
|
|
1834
|
+
op.opResult = opResult;
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// 成功: rawNewIndices を取得
|
|
1839
|
+
op.rawNewIndices = successfulChanges.flatMap(c => c.newIndices || []);
|
|
1840
|
+
op.newFlatSize = (opResult.blocks && opResult.blocks.length > 0)
|
|
1841
|
+
? computeNewFlatSize(opResult.blocks, op.rawNewIndices)
|
|
1842
|
+
: op.rawNewIndices.length || op.oldFlatSize;
|
|
1843
|
+
op.status = (op.rawNewIndices.length > 1) ? 'expanded' : 'updated';
|
|
1844
|
+
op.opResult = opResult;
|
|
1845
|
+
lastSuccessfulResult = opResult;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// 5. rebasing(resolvedIndex 昇順で走査、低 index の展開が高 index をずらす)
|
|
1849
|
+
const opsForRebase = [...resolvedOps].sort((a, b) => a.resolvedIndex - b.resolvedIndex);
|
|
1850
|
+
let cumulativeDelta = 0;
|
|
1851
|
+
for (const op of opsForRebase) {
|
|
1852
|
+
if (op.status === 'failed' || op.status === 'skipped') {
|
|
1853
|
+
op.rebasedNewIndices = [];
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
op.rebasedNewIndices = op.rawNewIndices.map(idx => idx + cumulativeDelta);
|
|
1857
|
+
cumulativeDelta += (op.newFlatSize - op.oldFlatSize);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// 6. 最終 snapshot 構築
|
|
1861
|
+
let snapshot;
|
|
1862
|
+
if (lastSuccessfulResult) {
|
|
1863
|
+
snapshot = buildResponseSnapshotFromResult(lastSuccessfulResult, mode, postId, sessionId, siteName);
|
|
1864
|
+
}
|
|
1865
|
+
if (!snapshot) {
|
|
1866
|
+
// 全 op 失敗/skipped or result.blocks なし → currentState から構築
|
|
1867
|
+
snapshot = buildSnapshotFromState(currentState, mode, postId, sessionId, siteName);
|
|
1264
1868
|
}
|
|
1869
|
+
if (!snapshot) {
|
|
1870
|
+
// 最終手段: 再取得
|
|
1871
|
+
snapshot = await buildResponseSnapshot(mode, client, postId, sessionId, siteName);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// 7. BatchDiffInfo 構築
|
|
1875
|
+
const batchDiff = buildBatchDiffInfo(resolvedOps, snapshot);
|
|
1265
1876
|
|
|
1266
|
-
//
|
|
1267
|
-
const
|
|
1877
|
+
// 8. レスポンス組み立て
|
|
1878
|
+
const successCount = resolvedOps.filter(r => r.status !== 'failed' && r.status !== 'skipped').length;
|
|
1879
|
+
const totalCount = resolvedOps.length;
|
|
1880
|
+
let text = `\u2705 batch \u5b8c\u4e86 (${successCount}/${totalCount} \u6210\u529f)`;
|
|
1268
1881
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
const successCount = results.filter(r => r.status !== 'failed').length;
|
|
1274
|
-
const failCount = results.filter(r => r.status === 'failed').length;
|
|
1275
|
-
let text = `✅ batch 完了\n\n成功: ${successCount} / 失敗: ${failCount}`;
|
|
1276
|
-
for (const r of results) {
|
|
1277
|
-
const refLabel = r.ref ? ` (${r.ref})` : '';
|
|
1278
|
-
text += `\n [${r.resolvedIndex}]${refLabel}: ${r.status}`;
|
|
1279
|
-
if (r.error) text += ` — ${r.error}`;
|
|
1882
|
+
if (batchDiff) {
|
|
1883
|
+
text = appendBatchRefChangesToText(text + _modeTag, batchDiff);
|
|
1884
|
+
} else {
|
|
1885
|
+
text = appendSnapshotToTextLegacy(text + _modeTag, snapshot);
|
|
1280
1886
|
}
|
|
1281
|
-
text = appendSnapshotToText(text + _modeTag, snapshot);
|
|
1282
1887
|
|
|
1283
1888
|
return { content: [{ type: "text", text }] };
|
|
1284
1889
|
}
|
|
@@ -1901,6 +2506,7 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1901
2506
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
1902
2507
|
});
|
|
1903
2508
|
if (resolved.error) return resolved.error;
|
|
2509
|
+
const _preState = resolved.currentState || null; // Phase 2: 差分計算用
|
|
1904
2510
|
if (resolved.index !== undefined) {
|
|
1905
2511
|
tp.index = resolved.index;
|
|
1906
2512
|
delete tp._ref;
|
|
@@ -1924,6 +2530,12 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1924
2530
|
_fromInsertBlock, appendToEnd } = (args || {});
|
|
1925
2531
|
let { newHTML } = (args || {});
|
|
1926
2532
|
|
|
2533
|
+
// Phase 5: 差分化オプション
|
|
2534
|
+
// - 単体 target.ref かつ snapshotId ありのみ差分化
|
|
2535
|
+
// - appendToEnd は Legacy 固定(早期リターンで自然に除外)
|
|
2536
|
+
const _inputRef = (snapshotId && args?.target?.ref) ? args.target.ref : null;
|
|
2537
|
+
const _isRefInsert = !!(insertOnly && _inputRef);
|
|
2538
|
+
|
|
1927
2539
|
// [INSERT_OBSERVE] Step 5: insert_block 経由時の送信パラメータログ
|
|
1928
2540
|
if (process.env.FRIDAY_DEBUG === '1' && _fromInsertBlock) {
|
|
1929
2541
|
console.log(`[INSERT_OBSERVE] mcp-send: index=${index}, postId=${postId}, sessionId=${_sessionId || 'none'}, mode=${mode}, appendToEnd=${appendToEnd}, htmlLen=${(newHTML || '').length}`);
|
|
@@ -2011,8 +2623,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2011
2623
|
_fromInsertBlock: _fromInsertBlock || false,
|
|
2012
2624
|
});
|
|
2013
2625
|
const count = result.results?.[0]?.count || 1;
|
|
2014
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2015
|
-
return { content: [{ type: "text", text:
|
|
2626
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2627
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
2016
2628
|
}
|
|
2017
2629
|
|
|
2018
2630
|
const result = await client.headlessUpdate(postId, {
|
|
@@ -2037,14 +2649,14 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2037
2649
|
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText}${deprecationWarning}` }] };
|
|
2038
2650
|
}
|
|
2039
2651
|
|
|
2040
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2652
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2041
2653
|
if (result.results) {
|
|
2042
2654
|
const successCount = result.results.filter(r => r.success).length;
|
|
2043
2655
|
const failCount = result.results.filter(r => !r.success).length;
|
|
2044
|
-
return { content: [{ type: "text", text:
|
|
2656
|
+
return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}${deprecationWarning}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
|
|
2045
2657
|
}
|
|
2046
2658
|
|
|
2047
|
-
return { content: [{ type: "text", text:
|
|
2659
|
+
return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 更新完了${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
|
|
2048
2660
|
} catch (e) {
|
|
2049
2661
|
const formatted = formatHeadlessConflictError(e);
|
|
2050
2662
|
if (formatted) return formatted;
|
|
@@ -2066,8 +2678,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2066
2678
|
if (!result || result.timeout) return timeoutResponse(name, client, args?.site);
|
|
2067
2679
|
if (!result.success) return errorResponse(name, result.error, args?.site);
|
|
2068
2680
|
const count = result.inserted || 1;
|
|
2069
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2070
|
-
return { content: [{ type: "text", text:
|
|
2681
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2682
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
2071
2683
|
}
|
|
2072
2684
|
|
|
2073
2685
|
// target: "selected" の場合 → エディタ状態から選択情報を取得してMCP側で処理
|
|
@@ -2121,8 +2733,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2121
2733
|
if (!result.success)
|
|
2122
2734
|
return errorResponse(name, result.error, args?.site);
|
|
2123
2735
|
{
|
|
2124
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2125
|
-
return { content: [{ type: "text", text:
|
|
2736
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2737
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${hasTextSelection ? "(カーソル選択モード)" : "(差分)"}${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2126
2738
|
}
|
|
2127
2739
|
}
|
|
2128
2740
|
|
|
@@ -2140,8 +2752,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2140
2752
|
if (!result.success)
|
|
2141
2753
|
return errorResponse(name, result.error, args?.site);
|
|
2142
2754
|
{
|
|
2143
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2144
|
-
return { content: [{ type: "text", text:
|
|
2755
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2756
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2145
2757
|
}
|
|
2146
2758
|
}
|
|
2147
2759
|
|
|
@@ -2157,8 +2769,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2157
2769
|
if (!result.success)
|
|
2158
2770
|
return errorResponse(name, result.error, args?.site);
|
|
2159
2771
|
{
|
|
2160
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2161
|
-
return { content: [{ type: "text", text:
|
|
2772
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2773
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ 属性更新完了${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2162
2774
|
}
|
|
2163
2775
|
}
|
|
2164
2776
|
|
|
@@ -2198,16 +2810,16 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2198
2810
|
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText || "(対象なし)"}${deprecationWarning}` }] };
|
|
2199
2811
|
}
|
|
2200
2812
|
|
|
2201
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2813
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2202
2814
|
if (result.results) {
|
|
2203
2815
|
const successCount = result.results.filter(r => r.success && !r.skipped).length;
|
|
2204
2816
|
const skipCount = result.results.filter(r => r.skipped).length;
|
|
2205
2817
|
const failCount = result.results.filter(r => !r.success).length;
|
|
2206
2818
|
const detailText = result.results.filter(r => r.success && r.replaceCount > 0).map(r => ` [${r.index}]: ${r.replaceCount}箇所`).join('\n');
|
|
2207
|
-
return { content: [{ type: "text", text:
|
|
2819
|
+
return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
|
|
2208
2820
|
}
|
|
2209
2821
|
|
|
2210
|
-
return { content: [{ type: "text", text:
|
|
2822
|
+
return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
|
|
2211
2823
|
}
|
|
2212
2824
|
|
|
2213
2825
|
// ツール実行のハンドラ
|
|
@@ -2763,11 +3375,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2763
3375
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2764
3376
|
});
|
|
2765
3377
|
if (resolved.error) return resolved.error;
|
|
3378
|
+
const _preState = resolved.currentState || null; // Phase 2: 差分計算用
|
|
2766
3379
|
|
|
2767
3380
|
let { index, count, indices } = args;
|
|
2768
3381
|
if (resolved.index !== undefined) { index = resolved.index; count = count || 1; }
|
|
2769
3382
|
if (resolved.indices !== undefined) { indices = resolved.indices; }
|
|
2770
3383
|
|
|
3384
|
+
// Phase 3: 入力 ref を保持(ref + count > 1 は差分化できないので Legacy)
|
|
3385
|
+
const _inputRefs = args?.refs ? [...new Set(args.refs)]
|
|
3386
|
+
: (args?.ref && (count || 1) <= 1) ? [args.ref]
|
|
3387
|
+
: null;
|
|
3388
|
+
|
|
2771
3389
|
// indices と index/count の排他バリデーション
|
|
2772
3390
|
if (indices !== undefined && (index !== undefined || count !== undefined)) {
|
|
2773
3391
|
return { content: [{ type: "text", text: "❌ indices と index/count は同時に指定できません。" }], isError: true };
|
|
@@ -2788,8 +3406,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2788
3406
|
try {
|
|
2789
3407
|
const result = await client.headlessDeleteMultiple(postId, uniqueIndices);
|
|
2790
3408
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
2791
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2792
|
-
|
|
3409
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3410
|
+
const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
|
|
3411
|
+
if (_inputRefs) {
|
|
3412
|
+
const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3413
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3414
|
+
}
|
|
3415
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2793
3416
|
} catch (e) {
|
|
2794
3417
|
const formatted = formatHeadlessConflictError(e);
|
|
2795
3418
|
if (formatted) return formatted;
|
|
@@ -2803,8 +3426,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2803
3426
|
if (!result.success)
|
|
2804
3427
|
return errorResponse(name, result.error, args?.site);
|
|
2805
3428
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
2806
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2807
|
-
|
|
3429
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3430
|
+
const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
|
|
3431
|
+
if (_inputRefs) {
|
|
3432
|
+
const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3433
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3434
|
+
}
|
|
3435
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2808
3436
|
}
|
|
2809
3437
|
|
|
2810
3438
|
// 既存モード(index + count)
|
|
@@ -2815,8 +3443,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2815
3443
|
try {
|
|
2816
3444
|
const result = await client.headlessDelete(postId, index, count || 1);
|
|
2817
3445
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
2818
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2819
|
-
|
|
3446
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3447
|
+
const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
|
|
3448
|
+
if (_inputRefs) {
|
|
3449
|
+
const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3450
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3451
|
+
}
|
|
3452
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2820
3453
|
} catch (e) {
|
|
2821
3454
|
const formatted = formatHeadlessConflictError(e);
|
|
2822
3455
|
if (formatted) return formatted;
|
|
@@ -2830,8 +3463,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2830
3463
|
if (!result.success)
|
|
2831
3464
|
return errorResponse(name, result.error, args?.site);
|
|
2832
3465
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
2833
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2834
|
-
|
|
3466
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3467
|
+
const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
|
|
3468
|
+
if (_inputRefs) {
|
|
3469
|
+
const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3470
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3471
|
+
}
|
|
3472
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2835
3473
|
}
|
|
2836
3474
|
|
|
2837
3475
|
case "move_block": {
|
|
@@ -2875,12 +3513,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2875
3513
|
|
|
2876
3514
|
// expectedRevision チェック(全モード共通)
|
|
2877
3515
|
// ref モード以外でも expectedRevision が指定されていれば revision チェックを実施
|
|
3516
|
+
let _preState = null; // Phase 2: 差分計算用
|
|
2878
3517
|
if (args?.expectedRevision && !hasRef) {
|
|
2879
|
-
const { error: _revError } = await acquireFreshState({
|
|
3518
|
+
const { currentState, error: _revError } = await acquireFreshState({
|
|
2880
3519
|
expectedRevision: args.expectedRevision,
|
|
2881
3520
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2882
3521
|
});
|
|
2883
3522
|
if (_revError) return _revError;
|
|
3523
|
+
_preState = currentState || null;
|
|
2884
3524
|
}
|
|
2885
3525
|
|
|
2886
3526
|
// ref モード: fromRef + beforeRef/afterRef
|
|
@@ -2904,6 +3544,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2904
3544
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2905
3545
|
});
|
|
2906
3546
|
if (_stateError) return _stateError;
|
|
3547
|
+
_preState = currentState || null; // Phase 2: 差分計算用
|
|
2907
3548
|
|
|
2908
3549
|
// ref 解決(同じ currentState で 2 つの ref を解決)
|
|
2909
3550
|
try {
|
|
@@ -2919,6 +3560,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2919
3560
|
// ref → flat に変換完了。以降 flat モードに合流
|
|
2920
3561
|
}
|
|
2921
3562
|
|
|
3563
|
+
// Phase 4: 入力 ref 保持(ref モード + count <= 1 のみ差分化)
|
|
3564
|
+
const _inputRefs = (hasRef && count <= 1) ? [args.fromRef] : null;
|
|
3565
|
+
|
|
2922
3566
|
// 移動結果のレスポンス生成ヘルパー
|
|
2923
3567
|
const moveMsg = (moved) => {
|
|
2924
3568
|
if (moved?.noop) return `✅ 移動不要(同じ位置)${_mt_mb}`;
|
|
@@ -2941,8 +3585,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2941
3585
|
if (mode === 'headless') {
|
|
2942
3586
|
try {
|
|
2943
3587
|
const result = await client.headlessMoveFlat(postId, fromFlat, toFlat, count);
|
|
2944
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2945
|
-
|
|
3588
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3589
|
+
const _msg = moveMsg(result.moved);
|
|
3590
|
+
if (_inputRefs) {
|
|
3591
|
+
const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3592
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3593
|
+
}
|
|
3594
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2946
3595
|
} catch (e) {
|
|
2947
3596
|
const formatted = formatHeadlessConflictError(e);
|
|
2948
3597
|
if (formatted) return formatted;
|
|
@@ -2955,8 +3604,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2955
3604
|
return timeoutResponse(name, client, args?.site);
|
|
2956
3605
|
if (!result.success)
|
|
2957
3606
|
return errorResponse(name, result.error, args?.site);
|
|
2958
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2959
|
-
|
|
3607
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3608
|
+
const _msg = moveMsg(result.moved);
|
|
3609
|
+
if (_inputRefs) {
|
|
3610
|
+
const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3611
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3612
|
+
}
|
|
3613
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2960
3614
|
}
|
|
2961
3615
|
|
|
2962
3616
|
// 既存モード(from/to トップレベル)
|
|
@@ -2967,8 +3621,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2967
3621
|
if (mode === 'headless') {
|
|
2968
3622
|
try {
|
|
2969
3623
|
const result = await client.headlessMove(postId, from, to, count);
|
|
2970
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2971
|
-
return { content: [{ type: "text", text:
|
|
3624
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3625
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(moveMsg(result.moved), _snap) }] };
|
|
2972
3626
|
} catch (e) {
|
|
2973
3627
|
const formatted = formatHeadlessConflictError(e);
|
|
2974
3628
|
if (formatted) return formatted;
|
|
@@ -2981,8 +3635,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2981
3635
|
return timeoutResponse(name, client, args?.site);
|
|
2982
3636
|
if (!result.success)
|
|
2983
3637
|
return errorResponse(name, result.error, args?.site);
|
|
2984
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2985
|
-
return { content: [{ type: "text", text:
|
|
3638
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3639
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(moveMsg(result.moved), _snap) }] };
|
|
2986
3640
|
}
|
|
2987
3641
|
|
|
2988
3642
|
case "undo": {
|
|
@@ -3041,9 +3695,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3041
3695
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
3042
3696
|
});
|
|
3043
3697
|
if (resolved.error) return resolved.error;
|
|
3698
|
+
const _preState = resolved.currentState || null; // Phase 2: 差分計算用
|
|
3044
3699
|
|
|
3045
3700
|
const index = resolved.index !== undefined ? resolved.index : args?.index;
|
|
3046
3701
|
|
|
3702
|
+
// Phase 3: 入力 ref を保持(index 経路は差分化しない)
|
|
3703
|
+
const _inputRef = args?.ref || null;
|
|
3704
|
+
|
|
3047
3705
|
if (mode === 'headless') {
|
|
3048
3706
|
const _guard = await guardHeadlessConflict(postId, client, name);
|
|
3049
3707
|
if (_guard) return _guard;
|
|
@@ -3052,8 +3710,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3052
3710
|
}
|
|
3053
3711
|
try {
|
|
3054
3712
|
const result = await client.headlessDuplicate(postId, index);
|
|
3055
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3056
|
-
|
|
3713
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3714
|
+
const _msg = `✅ ブロック複製完了 (index ${result.duplicated.index} → ${result.duplicated.newIndex})${_mt_dup}`;
|
|
3715
|
+
if (_inputRef) {
|
|
3716
|
+
const changeInfo = buildChangeInfoFromResult('duplicated', _snap, result, _preState, { inputRef: _inputRef });
|
|
3717
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3718
|
+
}
|
|
3719
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
3057
3720
|
} catch (e) {
|
|
3058
3721
|
const formatted = formatHeadlessConflictError(e);
|
|
3059
3722
|
if (formatted) return formatted;
|
|
@@ -3066,8 +3729,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3066
3729
|
return timeoutResponse(name, client, args?.site);
|
|
3067
3730
|
if (!result.success)
|
|
3068
3731
|
return errorResponse(name, result.error, args?.site);
|
|
3069
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3070
|
-
|
|
3732
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3733
|
+
const _msg = `✅ ブロック複製完了${_mt_dup}`;
|
|
3734
|
+
if (_inputRef) {
|
|
3735
|
+
const changeInfo = buildChangeInfoFromResult('duplicated', _snap, result, _preState, { inputRef: _inputRef });
|
|
3736
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3737
|
+
}
|
|
3738
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
3071
3739
|
}
|
|
3072
3740
|
|
|
3073
3741
|
case "save_post": {
|
|
@@ -3421,7 +4089,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3421
4089
|
}
|
|
3422
4090
|
|
|
3423
4091
|
case "get_selection": {
|
|
3424
|
-
const { mode, postId: _postId, sessionId: _sessionId, message, client } = await resolveMode(args, name);
|
|
4092
|
+
const { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
|
|
3425
4093
|
if (mode === 'error') {
|
|
3426
4094
|
return errorResponse(name, message, args?.site);
|
|
3427
4095
|
}
|
|
@@ -3437,16 +4105,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3437
4105
|
content: [{ type: "text", text: `ブロックが選択されていません。${_mt_gs}` }],
|
|
3438
4106
|
};
|
|
3439
4107
|
}
|
|
4108
|
+
|
|
4109
|
+
// ref 対応: fresh state から snapshot を毎回新規作成
|
|
4110
|
+
// safetyCritical: true — stale Store へのフォールバックを防ぐ
|
|
4111
|
+
const _siteName_gs = siteName || args?.site || 'default';
|
|
4112
|
+
let _snapInfo = null;
|
|
4113
|
+
try {
|
|
4114
|
+
const freshState = await getCurrentStructure(mode, client, _postId, _sessionId, { safetyCritical: true });
|
|
4115
|
+
if (freshState?.allBlocks?.length > 0) {
|
|
4116
|
+
_snapInfo = buildSnapshotFromState(freshState, mode, _postId, _sessionId, _siteName_gs);
|
|
4117
|
+
}
|
|
4118
|
+
} catch (_e) {
|
|
4119
|
+
// fresh state 取得失敗 → ref なしで index-only にフォールバック
|
|
4120
|
+
}
|
|
4121
|
+
|
|
3440
4122
|
// 複数選択
|
|
3441
4123
|
if (sel.isMultiSelect && sel.blockIds) {
|
|
3442
4124
|
let text = `選択中: ${sel.blockIds.length}ブロック\n` +
|
|
3443
4125
|
`タイプ: ${sel.blockTypes?.join(", ")}\n` +
|
|
3444
|
-
`位置: ${sel.blockIndices?.join(", ")}
|
|
4126
|
+
`位置: ${sel.blockIndices?.join(", ")}`;
|
|
4127
|
+
if (_snapInfo && sel.blockIndices?.length > 0) {
|
|
4128
|
+
const refs = sel.blockIndices
|
|
4129
|
+
.map(idx => _snapInfo.blocks?.find(b => b.index === idx)?.ref)
|
|
4130
|
+
.filter(Boolean);
|
|
4131
|
+
// 全数一致のときだけ refs を返す(部分一致は AI を誤誘導するため)
|
|
4132
|
+
if (refs.length === sel.blockIndices.length) {
|
|
4133
|
+
text += `\nrefs: ${refs.join(", ")}`;
|
|
4134
|
+
text += `\n[snapshot:${_snapInfo.snapshotId} rev:${_snapInfo.revision}]`;
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
text += _mt_gs;
|
|
3445
4138
|
return { content: [{ type: "text", text }] };
|
|
3446
4139
|
}
|
|
3447
4140
|
// 単一選択
|
|
3448
4141
|
let text = `選択中: ${sel.blockType}\n` +
|
|
3449
4142
|
`位置: index ${sel.blockIndex}`;
|
|
4143
|
+
if (_snapInfo) {
|
|
4144
|
+
const refEntry = _snapInfo.blocks?.find(b => b.index === sel.blockIndex);
|
|
4145
|
+
if (refEntry) {
|
|
4146
|
+
text += `\nref: ${refEntry.ref}`;
|
|
4147
|
+
text += `\n[snapshot:${_snapInfo.snapshotId} rev:${_snapInfo.revision}]`;
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
3450
4150
|
if (sel.textSelection?.text) {
|
|
3451
4151
|
text += `\n\nカーソル選択テキスト: "${sel.textSelection.text}"`;
|
|
3452
4152
|
if (sel.textSelection.context) {
|
|
@@ -3474,6 +4174,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3474
4174
|
const _siteName = siteName || args?.site || 'default';
|
|
3475
4175
|
|
|
3476
4176
|
const { action } = (args || {});
|
|
4177
|
+
const _inputRef = args?.ref || null;
|
|
3477
4178
|
if (!action) {
|
|
3478
4179
|
return { content: [{ type: "text", text: "❌ action は必須です" }], isError: true };
|
|
3479
4180
|
}
|
|
@@ -3490,6 +4191,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3490
4191
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
3491
4192
|
});
|
|
3492
4193
|
if (resolved.error) return resolved.error;
|
|
4194
|
+
const _preState = resolved.currentState || null; // Phase 2: 差分計算用
|
|
3493
4195
|
|
|
3494
4196
|
const index = resolved.index !== undefined ? resolved.index : args?.index;
|
|
3495
4197
|
if (index === undefined) {
|
|
@@ -3536,8 +4238,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3536
4238
|
if (!_isWrite) {
|
|
3537
4239
|
return { content: [{ type: "text", text: formatStructure(result) + _mt_to }] };
|
|
3538
4240
|
}
|
|
3539
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3540
|
-
|
|
4241
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
4242
|
+
const _msg = `✅ ${action} 完了: ${JSON.stringify(result.detail || result)}${_mt_to}`;
|
|
4243
|
+
if (_inputRef) {
|
|
4244
|
+
const changedIndex = result.targetIndex ?? index;
|
|
4245
|
+
const syntheticResult = { changes: [{ oldIndex: changedIndex, newIndices: [changedIndex], success: true }] };
|
|
4246
|
+
const changeInfo = buildChangeInfoFromResult('updated', _snap, syntheticResult, _preState);
|
|
4247
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
4248
|
+
}
|
|
4249
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
3541
4250
|
} catch (e) {
|
|
3542
4251
|
const formatted = formatHeadlessConflictError(e);
|
|
3543
4252
|
if (formatted) return formatted;
|
|
@@ -3555,8 +4264,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3555
4264
|
if (!_isWrite) {
|
|
3556
4265
|
return { content: [{ type: "text", text: formatStructure(result.structure) + _mt_to }] };
|
|
3557
4266
|
}
|
|
3558
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3559
|
-
|
|
4267
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
4268
|
+
const _msg = `✅ ${action} 完了: ${JSON.stringify(result.detail || {})}${_mt_to}`;
|
|
4269
|
+
if (_inputRef) {
|
|
4270
|
+
const changedIndex = result.targetIndex ?? index;
|
|
4271
|
+
const syntheticResult = { changes: [{ oldIndex: changedIndex, newIndices: [changedIndex], success: true }] };
|
|
4272
|
+
const changeInfo = buildChangeInfoFromResult('updated', _snap, syntheticResult, _preState);
|
|
4273
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
4274
|
+
}
|
|
4275
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
3560
4276
|
}
|
|
3561
4277
|
|
|
3562
4278
|
case "open_in_browser": {
|