friday-mcp-v2 3.0.1 → 3.0.3
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 +800 -91
- package/dist/ws-server.js +1 -1
- 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 展開チェック(単体操作時のみ)
|
|
@@ -1115,16 +1166,494 @@ function appendSnapshotToText(text, snapshot, refInfo) {
|
|
|
1115
1166
|
}
|
|
1116
1167
|
|
|
1117
1168
|
let out = text + `\n\n[snapshot:${snapshot.snapshotId} rev:${snapshot.revision}]${expandedLine}`;
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1169
|
+
return out;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Phase 5: update_blocks / insert_block の差分/Legacy 自動分岐。
|
|
1174
|
+
* inputRef が null なら即 Legacy。差分構築失敗時も Legacy フォールバック。
|
|
1175
|
+
* @param {string} text - メッセージ本文
|
|
1176
|
+
* @param {object|null} snap - buildResponseSnapshot の結果
|
|
1177
|
+
* @param {object} result - 下流レスポンス
|
|
1178
|
+
* @param {object|null} preState - _preState
|
|
1179
|
+
* @param {string|null} inputRef - 入力 ref(null = Legacy 固定)
|
|
1180
|
+
* @param {boolean} isInsert - insertOnly 操作か
|
|
1181
|
+
* @param {object|null} refInfo - Legacy フォールバック用 _refInfo
|
|
1182
|
+
* @returns {string}
|
|
1183
|
+
*/
|
|
1184
|
+
function buildUpdateDiffResponse(text, snap, result, preState, inputRef, isInsert, refInfo) {
|
|
1185
|
+
if (!inputRef || !snap) {
|
|
1186
|
+
return appendSnapshotToTextLegacy(text, snap, refInfo);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (isInsert) {
|
|
1190
|
+
const changeInfo = buildChangeInfoFromResult('inserted', snap, result, preState);
|
|
1191
|
+
if (changeInfo) return appendRefChangesToText(text, changeInfo);
|
|
1192
|
+
return appendSnapshotToTextLegacy(text, snap, refInfo);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// update / expand 判定
|
|
1196
|
+
// success:false の change を除外(invalid regex, parse failure 等)
|
|
1197
|
+
const changes = result?.changes?.filter(c => c.success !== false);
|
|
1198
|
+
if (!changes || changes.length === 0) {
|
|
1199
|
+
return appendSnapshotToTextLegacy(text, snap, refInfo);
|
|
1200
|
+
}
|
|
1201
|
+
// filtered 済み changes で result を差し替え(buildChangeInfoFromResult が result.changes を直接参照するため)
|
|
1202
|
+
const filteredResult = { ...result, changes };
|
|
1203
|
+
if (changes.length === 1 && changes[0]?.newIndices?.length > 1) {
|
|
1204
|
+
// 1→N 展開(単体 target のみ)
|
|
1205
|
+
const changeInfo = buildChangeInfoFromResult('expanded', snap, filteredResult, preState, { inputRef });
|
|
1206
|
+
if (changeInfo) return appendRefChangesToText(text, changeInfo);
|
|
1207
|
+
} else if (changes.length === 1) {
|
|
1208
|
+
// 1→1 更新(単体 ref で成功 1 件のときだけ差分化)
|
|
1209
|
+
const changeInfo = buildChangeInfoFromResult('updated', snap, filteredResult, preState);
|
|
1210
|
+
if (changeInfo) return appendRefChangesToText(text, changeInfo);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return appendSnapshotToTextLegacy(text, snap, refInfo);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// ============================================================
|
|
1217
|
+
// Phase 2: ref 変化分類ロジック(差分通知方式の基盤)
|
|
1218
|
+
// ============================================================
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* @typedef {Object} DeletedChange
|
|
1222
|
+
* @property {string} snapshotId
|
|
1223
|
+
* @property {string} revision
|
|
1224
|
+
* @property {'deleted'} type
|
|
1225
|
+
* @property {string[]} deletedRefs - 削除された ref 配列 ['r5', 'r8']
|
|
1226
|
+
* @property {boolean} [refetchRequired]
|
|
1227
|
+
*/
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* @typedef {Object} InsertedChange
|
|
1231
|
+
* @property {string} snapshotId
|
|
1232
|
+
* @property {string} revision
|
|
1233
|
+
* @property {'inserted'} type
|
|
1234
|
+
* @property {Array<{index: number, ref: string, blockType: string, depth: number}>} inserted
|
|
1235
|
+
* @property {boolean} [refetchRequired]
|
|
1236
|
+
*/
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* @typedef {Object} MovedChange
|
|
1240
|
+
* @property {string} snapshotId
|
|
1241
|
+
* @property {string} revision
|
|
1242
|
+
* @property {'moved'} type
|
|
1243
|
+
* @property {Array<{oldRef: string, newRef: string}>} moved
|
|
1244
|
+
* @property {boolean} [refetchRequired]
|
|
1245
|
+
*/
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* @typedef {Object} UpdatedChange
|
|
1249
|
+
* @property {string} snapshotId
|
|
1250
|
+
* @property {string} revision
|
|
1251
|
+
* @property {'updated'} type
|
|
1252
|
+
* @property {string[]} updatedRefs - fingerprint 変化した ref 配列
|
|
1253
|
+
* @property {boolean} [refetchRequired]
|
|
1254
|
+
*/
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* @typedef {Object} ExpandedChange
|
|
1258
|
+
* @property {string} snapshotId
|
|
1259
|
+
* @property {string} revision
|
|
1260
|
+
* @property {'expanded'} type
|
|
1261
|
+
* @property {{oldRef: string, newRefs: string[]}} expanded
|
|
1262
|
+
* @property {boolean} [refetchRequired]
|
|
1263
|
+
*/
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* @typedef {Object} DuplicatedChange
|
|
1267
|
+
* @property {string} snapshotId
|
|
1268
|
+
* @property {string} revision
|
|
1269
|
+
* @property {'duplicated'} type
|
|
1270
|
+
* @property {string} sourceRef
|
|
1271
|
+
* @property {string} newRef
|
|
1272
|
+
* @property {number} sourceIndex
|
|
1273
|
+
* @property {number} newIndex
|
|
1274
|
+
* @property {boolean} [refetchRequired]
|
|
1275
|
+
*/
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* @typedef {DeletedChange|InsertedChange|MovedChange|UpdatedChange|ExpandedChange|DuplicatedChange} ChangeInfo
|
|
1279
|
+
*/
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* ref 変化情報をテキストに付加する��差分モード)��
|
|
1283
|
+
* blocks 一��は出力しない。
|
|
1284
|
+
* @param {string} text
|
|
1285
|
+
* @param {ChangeInfo|null} changeInfo
|
|
1286
|
+
* @returns {string}
|
|
1287
|
+
*/
|
|
1288
|
+
function appendRefChangesToText(text, changeInfo) {
|
|
1289
|
+
if (!changeInfo) return text;
|
|
1290
|
+
|
|
1291
|
+
let out = text + `\n\n[snapshot:${changeInfo.snapshotId} rev:${changeInfo.revision}]`;
|
|
1292
|
+
|
|
1293
|
+
switch (changeInfo.type) {
|
|
1294
|
+
case 'deleted':
|
|
1295
|
+
out += `\ndeleted: [${changeInfo.deletedRefs.join(', ')}] (invalid in new snapshot)`;
|
|
1296
|
+
break;
|
|
1297
|
+
case 'inserted':
|
|
1298
|
+
out += '\ninserted: ' + changeInfo.inserted
|
|
1299
|
+
.map(b => `[${b.index}|${b.ref}] ${b.blockType} (d:${b.depth})`)
|
|
1300
|
+
.join(', ');
|
|
1301
|
+
break;
|
|
1302
|
+
case 'moved':
|
|
1303
|
+
out += '\nmoved: ' + changeInfo.moved
|
|
1304
|
+
.map(m => `${m.oldRef}\u2192${m.newRef}`)
|
|
1305
|
+
.join(', ');
|
|
1306
|
+
break;
|
|
1307
|
+
case 'updated':
|
|
1308
|
+
out += `\nupdated: [${changeInfo.updatedRefs.join(', ')}] (use new snapshot)`;
|
|
1309
|
+
break;
|
|
1310
|
+
case 'expanded':
|
|
1311
|
+
out += `\nexpanded: ${changeInfo.expanded.oldRef} \u2192 [${changeInfo.expanded.newRefs.join(', ')}]`;
|
|
1312
|
+
break;
|
|
1313
|
+
case 'duplicated':
|
|
1314
|
+
out += `\nduplicated: ${changeInfo.sourceRef} \u2192 ${changeInfo.newRef} (source:[${changeInfo.sourceIndex}], new:[${changeInfo.newIndex}]) (use new snapshot)`;
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (changeInfo.refetchRequired) {
|
|
1319
|
+
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';
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return out;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* @typedef {Object} BatchDiffEntry
|
|
1327
|
+
* @property {number} opIndex
|
|
1328
|
+
* @property {'updated'|'expanded'|'failed'|'skipped'} type
|
|
1329
|
+
* @property {string} [ref] - updated/failed/skipped 時
|
|
1330
|
+
* @property {string} [oldRef] - expanded 時
|
|
1331
|
+
* @property {string[]} [newRefs] - expanded 時
|
|
1332
|
+
* @property {string} [error] - failed 時
|
|
1333
|
+
* @property {boolean} [refetchRequired] - この entry 単体の refetch 判定
|
|
1334
|
+
*/
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* @typedef {Object} BatchDiffInfo
|
|
1338
|
+
* @property {string} snapshotId
|
|
1339
|
+
* @property {string} revision
|
|
1340
|
+
* @property {'batch'} type
|
|
1341
|
+
* @property {BatchDiffEntry[]} entries
|
|
1342
|
+
* @property {boolean} [refetchRequired] - batch 全体の OR
|
|
1343
|
+
*/
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* BatchDiffInfo をテキストに付加する(batch 差分モード)。
|
|
1347
|
+
* @param {string} text
|
|
1348
|
+
* @param {BatchDiffInfo|null} batchInfo
|
|
1349
|
+
* @returns {string}
|
|
1350
|
+
*/
|
|
1351
|
+
function appendBatchRefChangesToText(text, batchInfo) {
|
|
1352
|
+
if (!batchInfo) return text;
|
|
1353
|
+
|
|
1354
|
+
let out = text + `\n\n[snapshot:${batchInfo.snapshotId} rev:${batchInfo.revision}]`;
|
|
1355
|
+
out += '\nchanges:';
|
|
1356
|
+
|
|
1357
|
+
for (const entry of batchInfo.entries) {
|
|
1358
|
+
switch (entry.type) {
|
|
1359
|
+
case 'updated':
|
|
1360
|
+
out += `\n [${entry.opIndex}] updated: [${entry.ref}]`;
|
|
1361
|
+
break;
|
|
1362
|
+
case 'expanded':
|
|
1363
|
+
out += `\n [${entry.opIndex}] expanded: ${entry.oldRef} \u2192 [${entry.newRefs.join(', ')}]`;
|
|
1364
|
+
break;
|
|
1365
|
+
case 'failed':
|
|
1366
|
+
out += `\n [${entry.opIndex}] failed: ${entry.ref}`;
|
|
1367
|
+
if (entry.error) out += ` \u2014 ${entry.error}`;
|
|
1368
|
+
break;
|
|
1369
|
+
case 'skipped':
|
|
1370
|
+
out += `\n [${entry.opIndex}] skipped: ${entry.ref}`;
|
|
1371
|
+
break;
|
|
1123
1372
|
}
|
|
1373
|
+
if (entry.refetchRequired) out += ' (refetch)';
|
|
1124
1374
|
}
|
|
1375
|
+
|
|
1376
|
+
if (batchInfo.refetchRequired) {
|
|
1377
|
+
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';
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1125
1380
|
return out;
|
|
1126
1381
|
}
|
|
1127
1382
|
|
|
1383
|
+
// ============================================================
|
|
1384
|
+
// Phase 7: batch 差分化 helper
|
|
1385
|
+
// ============================================================
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* allBlocks の parentIndex チェーンを辿り、resolvedOps 間に祖先/子孫関係があるか検出。
|
|
1389
|
+
* @param {Array<{resolvedIndex: number, ref: string}>} resolvedOps
|
|
1390
|
+
* @param {Array<{index: number, parentIndex: number|null}>} allBlocks
|
|
1391
|
+
* @returns {{childRef: string, childIndex: number, ancestorRef: string, ancestorIndex: number}|null}
|
|
1392
|
+
*/
|
|
1393
|
+
function checkAncestorOverlap(resolvedOps, allBlocks) {
|
|
1394
|
+
const targetMap = new Map(resolvedOps.map(o => [o.resolvedIndex, o.ref]));
|
|
1395
|
+
const parentMap = new Map(allBlocks.map(b => [b.index, b.parentIndex ?? null]));
|
|
1396
|
+
|
|
1397
|
+
for (const op of resolvedOps) {
|
|
1398
|
+
let current = parentMap.get(op.resolvedIndex);
|
|
1399
|
+
while (current != null) {
|
|
1400
|
+
if (targetMap.has(current)) {
|
|
1401
|
+
return {
|
|
1402
|
+
childRef: op.ref,
|
|
1403
|
+
childIndex: op.resolvedIndex,
|
|
1404
|
+
ancestorRef: targetMap.get(current),
|
|
1405
|
+
ancestorIndex: current,
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
current = parentMap.get(current) ?? null;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* allBlocks 内の rootIndex を含む subtree の flat size を返す。
|
|
1416
|
+
* @param {Array<{index: number, parentIndex: number|null}>} allBlocks
|
|
1417
|
+
* @param {number} rootIndex
|
|
1418
|
+
* @returns {number}
|
|
1419
|
+
*/
|
|
1420
|
+
function countFlatSubtreeSize(allBlocks, rootIndex) {
|
|
1421
|
+
const childrenOf = new Map();
|
|
1422
|
+
for (const b of allBlocks) {
|
|
1423
|
+
if (b.parentIndex != null) {
|
|
1424
|
+
if (!childrenOf.has(b.parentIndex)) childrenOf.set(b.parentIndex, []);
|
|
1425
|
+
childrenOf.get(b.parentIndex).push(b.index);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
let count = 1;
|
|
1429
|
+
const stack = childrenOf.get(rootIndex) ? [...childrenOf.get(rootIndex)] : [];
|
|
1430
|
+
while (stack.length > 0) {
|
|
1431
|
+
const idx = stack.pop();
|
|
1432
|
+
count++;
|
|
1433
|
+
const children = childrenOf.get(idx);
|
|
1434
|
+
if (children) stack.push(...children);
|
|
1435
|
+
}
|
|
1436
|
+
return count;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* opResult.blocks から新 subtree の flat size を計算する。
|
|
1441
|
+
* rawNewIndices のうち集合内に親を持たないものを root とみなし、各 root の subtree size を合算。
|
|
1442
|
+
* editor(top-level のみ)/ headless(flat 全体)両対応。
|
|
1443
|
+
* @param {Array<{index: number, parentIndex?: number|null}>} opResultBlocks
|
|
1444
|
+
* @param {number[]} rawNewIndices
|
|
1445
|
+
* @returns {number}
|
|
1446
|
+
*/
|
|
1447
|
+
function computeNewFlatSize(opResultBlocks, rawNewIndices) {
|
|
1448
|
+
if (!rawNewIndices || rawNewIndices.length === 0) return 0;
|
|
1449
|
+
const indexSet = new Set(rawNewIndices);
|
|
1450
|
+
const newRoots = rawNewIndices.filter(idx => {
|
|
1451
|
+
const b = opResultBlocks.find(bl => bl.index === idx);
|
|
1452
|
+
return !b || b.parentIndex == null || !indexSet.has(b.parentIndex);
|
|
1453
|
+
});
|
|
1454
|
+
let total = 0;
|
|
1455
|
+
const blocksForSize = opResultBlocks.map(b => ({ index: b.index, parentIndex: b.parentIndex ?? null }));
|
|
1456
|
+
for (const root of newRoots) {
|
|
1457
|
+
total += countFlatSubtreeSize(blocksForSize, root);
|
|
1458
|
+
}
|
|
1459
|
+
return total;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* [pre-state 戦略] 操作前の状態で対象ブロックにネスト子があるか判定。
|
|
1464
|
+
* delete, move, duplicate 用。操作後は対象が消える/移動するため pre-state で見る。
|
|
1465
|
+
* 下流 hasChildren フラグがあればそちらを優先(duplicate, move が返す)。
|
|
1466
|
+
* @param {object|null} preState - 操作前の state
|
|
1467
|
+
* @param {number[]} targetIndices - 操作対象 index 配列
|
|
1468
|
+
* @param {object} [downstream] - 下流 result(hasChildren があれば優先)
|
|
1469
|
+
* @returns {boolean}
|
|
1470
|
+
*/
|
|
1471
|
+
function checkRefetchFromPreState(preState, targetIndices, downstream) {
|
|
1472
|
+
if (downstream?.hasChildren === true) return true;
|
|
1473
|
+
if (downstream?.hasChildren === false) return false;
|
|
1474
|
+
|
|
1475
|
+
if (!preState?.allBlocks) return true;
|
|
1476
|
+
|
|
1477
|
+
for (const idx of targetIndices) {
|
|
1478
|
+
const hasChild = preState.allBlocks.some(b => b.parentIndex === idx);
|
|
1479
|
+
if (hasChild) return true;
|
|
1480
|
+
}
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* [post-result 戦略] 操作結果にネストブロックが含ま���るか判定。
|
|
1486
|
+
* insert, update 用��非ネストだったブロックを nested HTML に更新したケースを検知。
|
|
1487
|
+
* 新 snapshot の blocks から、対象 index を parentIndex に持つブロックがあるかで判定。
|
|
1488
|
+
* @param {object} snapshot - buildResponseSnapshot() の結果(新 snapshot)
|
|
1489
|
+
* @param {number[]} resultIndices - 操作結果の index 配列(挿入/更新された位置)
|
|
1490
|
+
* @returns {boolean}
|
|
1491
|
+
*/
|
|
1492
|
+
function checkRefetchFromResult(snapshot, resultIndices) {
|
|
1493
|
+
if (!snapshot?.blocks || resultIndices.length === 0) return false;
|
|
1494
|
+
|
|
1495
|
+
for (const idx of resultIndices) {
|
|
1496
|
+
const hasNested = snapshot.blocks.some(b => b.parentIndex === idx);
|
|
1497
|
+
if (hasNested) return true;
|
|
1498
|
+
}
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* 下流 result + 新 snapshot → ChangeInfo を構築する。
|
|
1504
|
+
* 構築不能な場合は null を返す(呼び出し側は Legacy フォールバック必須)。
|
|
1505
|
+
* @param {'deleted'|'inserted'|'moved'|'updated'|'expanded'|'duplicated'} type
|
|
1506
|
+
* @param {object} snapshot - buildResponseSnapshot() の結果
|
|
1507
|
+
* @param {object} result - 下流レスポンス
|
|
1508
|
+
* @param {object|null} preState - _preState
|
|
1509
|
+
* @param {object} [opts] - 追加オプション
|
|
1510
|
+
* @returns {ChangeInfo|null} null = Legacy フォールバック
|
|
1511
|
+
*/
|
|
1512
|
+
function buildChangeInfoFromResult(type, snapshot, result, preState, opts) {
|
|
1513
|
+
if (!snapshot?.snapshotId || !result) return null;
|
|
1514
|
+
|
|
1515
|
+
const base = { snapshotId: snapshot.snapshotId, revision: snapshot.revision };
|
|
1516
|
+
|
|
1517
|
+
switch (type) {
|
|
1518
|
+
case 'deleted': {
|
|
1519
|
+
const deleted = result.deleted;
|
|
1520
|
+
if (!deleted || deleted.length === 0) return null;
|
|
1521
|
+
const deletedRefs = opts?.inputRefs || deleted.map(d => generateBlockRef(d.index));
|
|
1522
|
+
const refetch = checkRefetchFromPreState(preState, deleted.map(d => d.index), result);
|
|
1523
|
+
return { ...base, type: 'deleted', deletedRefs, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1524
|
+
}
|
|
1525
|
+
case 'inserted': {
|
|
1526
|
+
let indices = result.insertedIndices;
|
|
1527
|
+
if (!indices && result.changes) {
|
|
1528
|
+
indices = result.changes.flatMap(c => c.newIndices || []).filter(i => i != null);
|
|
1529
|
+
}
|
|
1530
|
+
if (!indices || indices.length === 0) return null;
|
|
1531
|
+
// refetchRequired は全 indices で判定(子孫含む)
|
|
1532
|
+
const refetch = checkRefetchFromResult(snapshot, indices);
|
|
1533
|
+
// top-level のみ: 他の inserted index を parentIndex に持つものを除外
|
|
1534
|
+
// (headless は subtree 全体の flat index を返すが、editor は top-level のみ。差分レスポンスは top-level に統一)
|
|
1535
|
+
const indexSet = new Set(indices);
|
|
1536
|
+
const topLevelIndices = indices.filter(idx => {
|
|
1537
|
+
const b = snapshot.blocks?.find(bl => bl.index === idx);
|
|
1538
|
+
return !b || b.parentIndex == null || !indexSet.has(b.parentIndex);
|
|
1539
|
+
});
|
|
1540
|
+
const inserted = topLevelIndices.map(idx => {
|
|
1541
|
+
const b = snapshot.blocks?.find(bl => bl.index === idx);
|
|
1542
|
+
return b
|
|
1543
|
+
? { index: b.index, ref: b.ref, blockType: b.type, depth: b.depth }
|
|
1544
|
+
: { index: idx, ref: generateBlockRef(idx), blockType: 'unknown', depth: 0 };
|
|
1545
|
+
});
|
|
1546
|
+
if (inserted.length === 0) return null;
|
|
1547
|
+
return { ...base, type: 'inserted', inserted, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1548
|
+
}
|
|
1549
|
+
case 'moved': {
|
|
1550
|
+
const mapping = result.movedMapping;
|
|
1551
|
+
if (!mapping || mapping.length === 0) return null;
|
|
1552
|
+
// inputRefs 長さ不一致 → Legacy フォールバック(count > 1 安全弁)
|
|
1553
|
+
if (opts?.inputRefs && opts.inputRefs.length !== mapping.length) return null;
|
|
1554
|
+
const moved = mapping.map((m, i) => ({
|
|
1555
|
+
oldRef: opts?.inputRefs ? opts.inputRefs[i] : generateBlockRef(m.oldIndex),
|
|
1556
|
+
newRef: generateBlockRef(m.newIndex),
|
|
1557
|
+
}));
|
|
1558
|
+
const refetch = checkRefetchFromPreState(preState, mapping.map(m => m.oldIndex), result);
|
|
1559
|
+
return { ...base, type: 'moved', moved, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1560
|
+
}
|
|
1561
|
+
case 'updated': {
|
|
1562
|
+
const changes = result.changes;
|
|
1563
|
+
if (!changes || changes.length === 0) return null;
|
|
1564
|
+
const updatedRefs = changes.map(c => generateBlockRef(c.newIndices?.[0] ?? c.oldIndex));
|
|
1565
|
+
const resultIndices = changes.flatMap(c => c.newIndices || []).filter(i => i != null);
|
|
1566
|
+
const refetch = checkRefetchFromResult(snapshot, resultIndices);
|
|
1567
|
+
return { ...base, type: 'updated', updatedRefs, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1568
|
+
}
|
|
1569
|
+
case 'expanded': {
|
|
1570
|
+
const changes = result.changes || [];
|
|
1571
|
+
if (changes.length !== 1) return null;
|
|
1572
|
+
const change = changes[0];
|
|
1573
|
+
if (!change || !change.newIndices || change.newIndices.length <= 1) return null;
|
|
1574
|
+
const oldRef = opts?.inputRef || generateBlockRef(change.oldIndex);
|
|
1575
|
+
const newRefs = change.newIndices.map(idx => generateBlockRef(idx));
|
|
1576
|
+
const refetch = checkRefetchFromResult(snapshot, change.newIndices);
|
|
1577
|
+
return { ...base, type: 'expanded', expanded: { oldRef, newRefs }, ...(refetch ? { refetchRequired: true } : {}) };
|
|
1578
|
+
}
|
|
1579
|
+
case 'duplicated': {
|
|
1580
|
+
const srcIdx = result.sourceIndex ?? result.duplicated?.index;
|
|
1581
|
+
const newIdx = result.newIndex ?? result.duplicated?.newIndex;
|
|
1582
|
+
if (srcIdx == null || newIdx == null) return null;
|
|
1583
|
+
const refetch = checkRefetchFromPreState(preState, [srcIdx], result);
|
|
1584
|
+
return {
|
|
1585
|
+
...base, type: 'duplicated',
|
|
1586
|
+
sourceRef: opts?.inputRef || generateBlockRef(srcIdx),
|
|
1587
|
+
newRef: generateBlockRef(newIdx),
|
|
1588
|
+
sourceIndex: srcIdx, newIndex: newIdx,
|
|
1589
|
+
...(refetch ? { refetchRequired: true } : {}),
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
default:
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* 実行完了した resolvedOps + snapshot から BatchDiffInfo を構築する。
|
|
1599
|
+
* @param {Array} resolvedOps - rebasing 済みの op 配列
|
|
1600
|
+
* @param {object|null} snapshot - 最終 snapshot
|
|
1601
|
+
* @returns {BatchDiffInfo|null}
|
|
1602
|
+
*/
|
|
1603
|
+
function buildBatchDiffInfo(resolvedOps, snapshot) {
|
|
1604
|
+
if (!snapshot?.snapshotId) return null;
|
|
1605
|
+
|
|
1606
|
+
let refetchRequired = false;
|
|
1607
|
+
const entries = [];
|
|
1608
|
+
|
|
1609
|
+
// opIndex 順(入力順)でソート
|
|
1610
|
+
const opsByInput = [...resolvedOps].sort((a, b) => a.opIndex - b.opIndex);
|
|
1611
|
+
|
|
1612
|
+
for (const op of opsByInput) {
|
|
1613
|
+
if (op.status === 'failed') {
|
|
1614
|
+
entries.push({ opIndex: op.opIndex, type: 'failed', ref: op.ref, error: op.error || null });
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
if (op.status === 'skipped') {
|
|
1618
|
+
entries.push({ opIndex: op.opIndex, type: 'skipped', ref: op.ref });
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// rebased な新 ref を snapshot から取得
|
|
1623
|
+
const newRefs = (op.rebasedNewIndices || []).map(idx => {
|
|
1624
|
+
const b = snapshot.blocks?.find(bl => bl.index === idx);
|
|
1625
|
+
return b ? b.ref : generateBlockRef(idx);
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
// refetch 判定
|
|
1629
|
+
const opRefetch = checkRefetchFromResult(snapshot, op.rebasedNewIndices || []);
|
|
1630
|
+
if (opRefetch) refetchRequired = true;
|
|
1631
|
+
|
|
1632
|
+
if (op.status === 'expanded') {
|
|
1633
|
+
entries.push({
|
|
1634
|
+
opIndex: op.opIndex, type: 'expanded',
|
|
1635
|
+
oldRef: op.ref, newRefs,
|
|
1636
|
+
...(opRefetch ? { refetchRequired: true } : {}),
|
|
1637
|
+
});
|
|
1638
|
+
} else {
|
|
1639
|
+
// updated
|
|
1640
|
+
entries.push({
|
|
1641
|
+
opIndex: op.opIndex, type: 'updated',
|
|
1642
|
+
ref: newRefs[0] || op.ref,
|
|
1643
|
+
...(opRefetch ? { refetchRequired: true } : {}),
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
return {
|
|
1649
|
+
snapshotId: snapshot.snapshotId,
|
|
1650
|
+
revision: snapshot.revision,
|
|
1651
|
+
type: 'batch',
|
|
1652
|
+
entries,
|
|
1653
|
+
...(refetchRequired ? { refetchRequired: true } : {}),
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1128
1657
|
/**
|
|
1129
1658
|
* batch operations の実行
|
|
1130
1659
|
* @param {Array} operations - 各 operation: { target, newHTML?, replacements?, attributeUpdates? }
|
|
@@ -1221,17 +1750,30 @@ async function handleBatchOperations(operations, snapshotId, mode, client, postI
|
|
|
1221
1750
|
indexSet.add(op.resolvedIndex);
|
|
1222
1751
|
}
|
|
1223
1752
|
|
|
1224
|
-
//
|
|
1753
|
+
// ancestor/descendant overlap チェック(親子関係の target は batch 不可)
|
|
1754
|
+
const ancestorOverlap = checkAncestorOverlap(resolvedOps, currentState.allBlocks);
|
|
1755
|
+
if (ancestorOverlap) {
|
|
1756
|
+
return {
|
|
1757
|
+
content: [{ type: "text", text: `❌ operations に親子関係の target が含まれるため batch 不可です。ref ${ancestorOverlap.childRef} (index ${ancestorOverlap.childIndex}) は ref ${ancestorOverlap.ancestorRef} (index ${ancestorOverlap.ancestorIndex}) の子孫です。` }],
|
|
1758
|
+
isError: true,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// 2. oldFlatSize を計算(rebasing 用、降順ソート前に実行)
|
|
1763
|
+
for (const op of resolvedOps) {
|
|
1764
|
+
op.oldFlatSize = countFlatSubtreeSize(currentState.allBlocks, op.resolvedIndex);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// 3. 降順ソート(index が大きい方から処理 — 1 op = 1 index なので安全)
|
|
1225
1768
|
resolvedOps.sort((a, b) => b.resolvedIndex - a.resolvedIndex);
|
|
1226
1769
|
|
|
1227
|
-
//
|
|
1228
|
-
|
|
1770
|
+
// 4. 各 operation を順次実行(結果情報を保持)
|
|
1771
|
+
let lastSuccessfulResult = null;
|
|
1229
1772
|
for (const op of resolvedOps) {
|
|
1230
1773
|
const index = op.resolvedIndex;
|
|
1231
1774
|
|
|
1232
1775
|
let opResult;
|
|
1233
1776
|
try {
|
|
1234
|
-
// batch は ref → index 解決済み。1 op = 1 index で下流に渡す
|
|
1235
1777
|
const downstreamParams = {
|
|
1236
1778
|
index,
|
|
1237
1779
|
replacements: op.replacements,
|
|
@@ -1246,39 +1788,95 @@ async function handleBatchOperations(operations, snapshotId, mode, client, postI
|
|
|
1246
1788
|
} else {
|
|
1247
1789
|
opResult = await client.sendEditorCommand("update_blocks", downstreamParams, 10000, postId, sessionId);
|
|
1248
1790
|
}
|
|
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
1791
|
} catch (e) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1792
|
+
op.status = 'failed';
|
|
1793
|
+
op.error = e.message;
|
|
1794
|
+
op.rawNewIndices = [];
|
|
1795
|
+
op.newFlatSize = op.oldFlatSize;
|
|
1796
|
+
op.opResult = null;
|
|
1797
|
+
continue;
|
|
1263
1798
|
}
|
|
1799
|
+
|
|
1800
|
+
// op 成功判定
|
|
1801
|
+
const topLevelFail = !opResult || opResult.success === false || opResult.ok === false;
|
|
1802
|
+
if (topLevelFail) {
|
|
1803
|
+
op.status = 'failed';
|
|
1804
|
+
op.error = opResult?.error || 'unknown error';
|
|
1805
|
+
op.rawNewIndices = [];
|
|
1806
|
+
op.newFlatSize = op.oldFlatSize;
|
|
1807
|
+
op.opResult = opResult;
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// skipped 判定(attributeUpdates の filter mismatch)
|
|
1812
|
+
const successfulChanges = (opResult.changes || []).filter(c => c.success !== false);
|
|
1813
|
+
const hasSkippedResult = opResult.results?.some(r => r.skipped === true);
|
|
1814
|
+
if (successfulChanges.length === 0 && !topLevelFail && !opResult.error) {
|
|
1815
|
+
if (hasSkippedResult || op.attributeUpdates != null) {
|
|
1816
|
+
op.status = 'skipped';
|
|
1817
|
+
op.rawNewIndices = [];
|
|
1818
|
+
op.newFlatSize = op.oldFlatSize;
|
|
1819
|
+
op.opResult = opResult;
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
// changes 空で skipped でもない → failed
|
|
1823
|
+
op.status = 'failed';
|
|
1824
|
+
op.error = 'no successful changes';
|
|
1825
|
+
op.rawNewIndices = [];
|
|
1826
|
+
op.newFlatSize = op.oldFlatSize;
|
|
1827
|
+
op.opResult = opResult;
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// 成功: rawNewIndices を取得
|
|
1832
|
+
op.rawNewIndices = successfulChanges.flatMap(c => c.newIndices || []);
|
|
1833
|
+
op.newFlatSize = (opResult.blocks && opResult.blocks.length > 0)
|
|
1834
|
+
? computeNewFlatSize(opResult.blocks, op.rawNewIndices)
|
|
1835
|
+
: op.rawNewIndices.length || op.oldFlatSize;
|
|
1836
|
+
op.status = (op.rawNewIndices.length > 1) ? 'expanded' : 'updated';
|
|
1837
|
+
op.opResult = opResult;
|
|
1838
|
+
lastSuccessfulResult = opResult;
|
|
1264
1839
|
}
|
|
1265
1840
|
|
|
1266
|
-
//
|
|
1267
|
-
const
|
|
1841
|
+
// 5. rebasing(resolvedIndex 昇順で走査、低 index の展開が高 index をずらす)
|
|
1842
|
+
const opsForRebase = [...resolvedOps].sort((a, b) => a.resolvedIndex - b.resolvedIndex);
|
|
1843
|
+
let cumulativeDelta = 0;
|
|
1844
|
+
for (const op of opsForRebase) {
|
|
1845
|
+
if (op.status === 'failed' || op.status === 'skipped') {
|
|
1846
|
+
op.rebasedNewIndices = [];
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
op.rebasedNewIndices = op.rawNewIndices.map(idx => idx + cumulativeDelta);
|
|
1850
|
+
cumulativeDelta += (op.newFlatSize - op.oldFlatSize);
|
|
1851
|
+
}
|
|
1268
1852
|
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1853
|
+
// 6. 最終 snapshot 構築
|
|
1854
|
+
let snapshot;
|
|
1855
|
+
if (lastSuccessfulResult) {
|
|
1856
|
+
snapshot = buildResponseSnapshotFromResult(lastSuccessfulResult, mode, postId, sessionId, siteName);
|
|
1857
|
+
}
|
|
1858
|
+
if (!snapshot) {
|
|
1859
|
+
// 全 op 失敗/skipped or result.blocks なし → currentState から構築
|
|
1860
|
+
snapshot = buildSnapshotFromState(currentState, mode, postId, sessionId, siteName);
|
|
1861
|
+
}
|
|
1862
|
+
if (!snapshot) {
|
|
1863
|
+
// 最終手段: 再取得
|
|
1864
|
+
snapshot = await buildResponseSnapshot(mode, client, postId, sessionId, siteName);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// 7. BatchDiffInfo 構築
|
|
1868
|
+
const batchDiff = buildBatchDiffInfo(resolvedOps, snapshot);
|
|
1869
|
+
|
|
1870
|
+
// 8. レスポンス組み立て
|
|
1871
|
+
const successCount = resolvedOps.filter(r => r.status !== 'failed' && r.status !== 'skipped').length;
|
|
1872
|
+
const totalCount = resolvedOps.length;
|
|
1873
|
+
let text = `\u2705 batch \u5b8c\u4e86 (${successCount}/${totalCount} \u6210\u529f)`;
|
|
1874
|
+
|
|
1875
|
+
if (batchDiff) {
|
|
1876
|
+
text = appendBatchRefChangesToText(text + _modeTag, batchDiff);
|
|
1877
|
+
} else {
|
|
1878
|
+
text = appendSnapshotToTextLegacy(text + _modeTag, snapshot);
|
|
1280
1879
|
}
|
|
1281
|
-
text = appendSnapshotToText(text + _modeTag, snapshot);
|
|
1282
1880
|
|
|
1283
1881
|
return { content: [{ type: "text", text }] };
|
|
1284
1882
|
}
|
|
@@ -1901,6 +2499,7 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1901
2499
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
1902
2500
|
});
|
|
1903
2501
|
if (resolved.error) return resolved.error;
|
|
2502
|
+
const _preState = resolved.currentState || null; // Phase 2: 差分計算用
|
|
1904
2503
|
if (resolved.index !== undefined) {
|
|
1905
2504
|
tp.index = resolved.index;
|
|
1906
2505
|
delete tp._ref;
|
|
@@ -1924,6 +2523,12 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1924
2523
|
_fromInsertBlock, appendToEnd } = (args || {});
|
|
1925
2524
|
let { newHTML } = (args || {});
|
|
1926
2525
|
|
|
2526
|
+
// Phase 5: 差分化オプション
|
|
2527
|
+
// - 単体 target.ref かつ snapshotId ありのみ差分化
|
|
2528
|
+
// - appendToEnd は Legacy 固定(早期リターンで自然に除外)
|
|
2529
|
+
const _inputRef = (snapshotId && args?.target?.ref) ? args.target.ref : null;
|
|
2530
|
+
const _isRefInsert = !!(insertOnly && _inputRef);
|
|
2531
|
+
|
|
1927
2532
|
// [INSERT_OBSERVE] Step 5: insert_block 経由時の送信パラメータログ
|
|
1928
2533
|
if (process.env.FRIDAY_DEBUG === '1' && _fromInsertBlock) {
|
|
1929
2534
|
console.log(`[INSERT_OBSERVE] mcp-send: index=${index}, postId=${postId}, sessionId=${_sessionId || 'none'}, mode=${mode}, appendToEnd=${appendToEnd}, htmlLen=${(newHTML || '').length}`);
|
|
@@ -2011,8 +2616,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2011
2616
|
_fromInsertBlock: _fromInsertBlock || false,
|
|
2012
2617
|
});
|
|
2013
2618
|
const count = result.results?.[0]?.count || 1;
|
|
2014
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2015
|
-
return { content: [{ type: "text", text:
|
|
2619
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2620
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
2016
2621
|
}
|
|
2017
2622
|
|
|
2018
2623
|
const result = await client.headlessUpdate(postId, {
|
|
@@ -2037,14 +2642,14 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2037
2642
|
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText}${deprecationWarning}` }] };
|
|
2038
2643
|
}
|
|
2039
2644
|
|
|
2040
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2645
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2041
2646
|
if (result.results) {
|
|
2042
2647
|
const successCount = result.results.filter(r => r.success).length;
|
|
2043
2648
|
const failCount = result.results.filter(r => !r.success).length;
|
|
2044
|
-
return { content: [{ type: "text", text:
|
|
2649
|
+
return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}${deprecationWarning}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
|
|
2045
2650
|
}
|
|
2046
2651
|
|
|
2047
|
-
return { content: [{ type: "text", text:
|
|
2652
|
+
return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 更新完了${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
|
|
2048
2653
|
} catch (e) {
|
|
2049
2654
|
const formatted = formatHeadlessConflictError(e);
|
|
2050
2655
|
if (formatted) return formatted;
|
|
@@ -2066,8 +2671,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2066
2671
|
if (!result || result.timeout) return timeoutResponse(name, client, args?.site);
|
|
2067
2672
|
if (!result.success) return errorResponse(name, result.error, args?.site);
|
|
2068
2673
|
const count = result.inserted || 1;
|
|
2069
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2070
|
-
return { content: [{ type: "text", text:
|
|
2674
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2675
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
2071
2676
|
}
|
|
2072
2677
|
|
|
2073
2678
|
// target: "selected" の場合 → エディタ状態から選択情報を取得してMCP側で処理
|
|
@@ -2121,8 +2726,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2121
2726
|
if (!result.success)
|
|
2122
2727
|
return errorResponse(name, result.error, args?.site);
|
|
2123
2728
|
{
|
|
2124
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2125
|
-
return { content: [{ type: "text", text:
|
|
2729
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2730
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${hasTextSelection ? "(カーソル選択モード)" : "(差分)"}${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2126
2731
|
}
|
|
2127
2732
|
}
|
|
2128
2733
|
|
|
@@ -2140,8 +2745,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2140
2745
|
if (!result.success)
|
|
2141
2746
|
return errorResponse(name, result.error, args?.site);
|
|
2142
2747
|
{
|
|
2143
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2144
|
-
return { content: [{ type: "text", text:
|
|
2748
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2749
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2145
2750
|
}
|
|
2146
2751
|
}
|
|
2147
2752
|
|
|
@@ -2157,8 +2762,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2157
2762
|
if (!result.success)
|
|
2158
2763
|
return errorResponse(name, result.error, args?.site);
|
|
2159
2764
|
{
|
|
2160
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2161
|
-
return { content: [{ type: "text", text:
|
|
2765
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2766
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ 属性更新完了${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2162
2767
|
}
|
|
2163
2768
|
}
|
|
2164
2769
|
|
|
@@ -2198,16 +2803,16 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2198
2803
|
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText || "(対象なし)"}${deprecationWarning}` }] };
|
|
2199
2804
|
}
|
|
2200
2805
|
|
|
2201
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2806
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
2202
2807
|
if (result.results) {
|
|
2203
2808
|
const successCount = result.results.filter(r => r.success && !r.skipped).length;
|
|
2204
2809
|
const skipCount = result.results.filter(r => r.skipped).length;
|
|
2205
2810
|
const failCount = result.results.filter(r => !r.success).length;
|
|
2206
2811
|
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:
|
|
2812
|
+
return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
|
|
2208
2813
|
}
|
|
2209
2814
|
|
|
2210
|
-
return { content: [{ type: "text", text:
|
|
2815
|
+
return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
|
|
2211
2816
|
}
|
|
2212
2817
|
|
|
2213
2818
|
// ツール実行のハンドラ
|
|
@@ -2763,11 +3368,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2763
3368
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2764
3369
|
});
|
|
2765
3370
|
if (resolved.error) return resolved.error;
|
|
3371
|
+
const _preState = resolved.currentState || null; // Phase 2: 差分計算用
|
|
2766
3372
|
|
|
2767
3373
|
let { index, count, indices } = args;
|
|
2768
3374
|
if (resolved.index !== undefined) { index = resolved.index; count = count || 1; }
|
|
2769
3375
|
if (resolved.indices !== undefined) { indices = resolved.indices; }
|
|
2770
3376
|
|
|
3377
|
+
// Phase 3: 入力 ref を保持(ref + count > 1 は差分化できないので Legacy)
|
|
3378
|
+
const _inputRefs = args?.refs ? [...new Set(args.refs)]
|
|
3379
|
+
: (args?.ref && (count || 1) <= 1) ? [args.ref]
|
|
3380
|
+
: null;
|
|
3381
|
+
|
|
2771
3382
|
// indices と index/count の排他バリデーション
|
|
2772
3383
|
if (indices !== undefined && (index !== undefined || count !== undefined)) {
|
|
2773
3384
|
return { content: [{ type: "text", text: "❌ indices と index/count は同時に指定できません。" }], isError: true };
|
|
@@ -2788,8 +3399,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2788
3399
|
try {
|
|
2789
3400
|
const result = await client.headlessDeleteMultiple(postId, uniqueIndices);
|
|
2790
3401
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
2791
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2792
|
-
|
|
3402
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3403
|
+
const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
|
|
3404
|
+
if (_inputRefs) {
|
|
3405
|
+
const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3406
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3407
|
+
}
|
|
3408
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2793
3409
|
} catch (e) {
|
|
2794
3410
|
const formatted = formatHeadlessConflictError(e);
|
|
2795
3411
|
if (formatted) return formatted;
|
|
@@ -2803,8 +3419,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2803
3419
|
if (!result.success)
|
|
2804
3420
|
return errorResponse(name, result.error, args?.site);
|
|
2805
3421
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
2806
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2807
|
-
|
|
3422
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3423
|
+
const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
|
|
3424
|
+
if (_inputRefs) {
|
|
3425
|
+
const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3426
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3427
|
+
}
|
|
3428
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2808
3429
|
}
|
|
2809
3430
|
|
|
2810
3431
|
// 既存モード(index + count)
|
|
@@ -2815,8 +3436,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2815
3436
|
try {
|
|
2816
3437
|
const result = await client.headlessDelete(postId, index, count || 1);
|
|
2817
3438
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
2818
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2819
|
-
|
|
3439
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3440
|
+
const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
|
|
3441
|
+
if (_inputRefs) {
|
|
3442
|
+
const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3443
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3444
|
+
}
|
|
3445
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2820
3446
|
} catch (e) {
|
|
2821
3447
|
const formatted = formatHeadlessConflictError(e);
|
|
2822
3448
|
if (formatted) return formatted;
|
|
@@ -2830,8 +3456,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2830
3456
|
if (!result.success)
|
|
2831
3457
|
return errorResponse(name, result.error, args?.site);
|
|
2832
3458
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
2833
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2834
|
-
|
|
3459
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3460
|
+
const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
|
|
3461
|
+
if (_inputRefs) {
|
|
3462
|
+
const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3463
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3464
|
+
}
|
|
3465
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2835
3466
|
}
|
|
2836
3467
|
|
|
2837
3468
|
case "move_block": {
|
|
@@ -2875,12 +3506,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2875
3506
|
|
|
2876
3507
|
// expectedRevision チェック(全モード共通)
|
|
2877
3508
|
// ref モード以外でも expectedRevision が指定されていれば revision チェックを実施
|
|
3509
|
+
let _preState = null; // Phase 2: 差分計算用
|
|
2878
3510
|
if (args?.expectedRevision && !hasRef) {
|
|
2879
|
-
const { error: _revError } = await acquireFreshState({
|
|
3511
|
+
const { currentState, error: _revError } = await acquireFreshState({
|
|
2880
3512
|
expectedRevision: args.expectedRevision,
|
|
2881
3513
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2882
3514
|
});
|
|
2883
3515
|
if (_revError) return _revError;
|
|
3516
|
+
_preState = currentState || null;
|
|
2884
3517
|
}
|
|
2885
3518
|
|
|
2886
3519
|
// ref モード: fromRef + beforeRef/afterRef
|
|
@@ -2904,6 +3537,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2904
3537
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2905
3538
|
});
|
|
2906
3539
|
if (_stateError) return _stateError;
|
|
3540
|
+
_preState = currentState || null; // Phase 2: 差分計算用
|
|
2907
3541
|
|
|
2908
3542
|
// ref 解決(同じ currentState で 2 つの ref を解決)
|
|
2909
3543
|
try {
|
|
@@ -2919,6 +3553,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2919
3553
|
// ref → flat に変換完了。以降 flat モードに合流
|
|
2920
3554
|
}
|
|
2921
3555
|
|
|
3556
|
+
// Phase 4: 入力 ref 保持(ref モード + count <= 1 のみ差分化)
|
|
3557
|
+
const _inputRefs = (hasRef && count <= 1) ? [args.fromRef] : null;
|
|
3558
|
+
|
|
2922
3559
|
// 移動結果のレスポンス生成ヘルパー
|
|
2923
3560
|
const moveMsg = (moved) => {
|
|
2924
3561
|
if (moved?.noop) return `✅ 移動不要(同じ位置)${_mt_mb}`;
|
|
@@ -2941,8 +3578,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2941
3578
|
if (mode === 'headless') {
|
|
2942
3579
|
try {
|
|
2943
3580
|
const result = await client.headlessMoveFlat(postId, fromFlat, toFlat, count);
|
|
2944
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2945
|
-
|
|
3581
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3582
|
+
const _msg = moveMsg(result.moved);
|
|
3583
|
+
if (_inputRefs) {
|
|
3584
|
+
const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3585
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3586
|
+
}
|
|
3587
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2946
3588
|
} catch (e) {
|
|
2947
3589
|
const formatted = formatHeadlessConflictError(e);
|
|
2948
3590
|
if (formatted) return formatted;
|
|
@@ -2955,8 +3597,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2955
3597
|
return timeoutResponse(name, client, args?.site);
|
|
2956
3598
|
if (!result.success)
|
|
2957
3599
|
return errorResponse(name, result.error, args?.site);
|
|
2958
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2959
|
-
|
|
3600
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3601
|
+
const _msg = moveMsg(result.moved);
|
|
3602
|
+
if (_inputRefs) {
|
|
3603
|
+
const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
|
|
3604
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3605
|
+
}
|
|
3606
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
2960
3607
|
}
|
|
2961
3608
|
|
|
2962
3609
|
// 既存モード(from/to トップレベル)
|
|
@@ -2967,8 +3614,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2967
3614
|
if (mode === 'headless') {
|
|
2968
3615
|
try {
|
|
2969
3616
|
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:
|
|
3617
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3618
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(moveMsg(result.moved), _snap) }] };
|
|
2972
3619
|
} catch (e) {
|
|
2973
3620
|
const formatted = formatHeadlessConflictError(e);
|
|
2974
3621
|
if (formatted) return formatted;
|
|
@@ -2981,8 +3628,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2981
3628
|
return timeoutResponse(name, client, args?.site);
|
|
2982
3629
|
if (!result.success)
|
|
2983
3630
|
return errorResponse(name, result.error, args?.site);
|
|
2984
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2985
|
-
return { content: [{ type: "text", text:
|
|
3631
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3632
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(moveMsg(result.moved), _snap) }] };
|
|
2986
3633
|
}
|
|
2987
3634
|
|
|
2988
3635
|
case "undo": {
|
|
@@ -3041,9 +3688,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3041
3688
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
3042
3689
|
});
|
|
3043
3690
|
if (resolved.error) return resolved.error;
|
|
3691
|
+
const _preState = resolved.currentState || null; // Phase 2: 差分計算用
|
|
3044
3692
|
|
|
3045
3693
|
const index = resolved.index !== undefined ? resolved.index : args?.index;
|
|
3046
3694
|
|
|
3695
|
+
// Phase 3: 入力 ref を保持(index 経路は差分化しない)
|
|
3696
|
+
const _inputRef = args?.ref || null;
|
|
3697
|
+
|
|
3047
3698
|
if (mode === 'headless') {
|
|
3048
3699
|
const _guard = await guardHeadlessConflict(postId, client, name);
|
|
3049
3700
|
if (_guard) return _guard;
|
|
@@ -3052,8 +3703,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3052
3703
|
}
|
|
3053
3704
|
try {
|
|
3054
3705
|
const result = await client.headlessDuplicate(postId, index);
|
|
3055
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3056
|
-
|
|
3706
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3707
|
+
const _msg = `✅ ブロック複製完了 (index ${result.duplicated.index} → ${result.duplicated.newIndex})${_mt_dup}`;
|
|
3708
|
+
if (_inputRef) {
|
|
3709
|
+
const changeInfo = buildChangeInfoFromResult('duplicated', _snap, result, _preState, { inputRef: _inputRef });
|
|
3710
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3711
|
+
}
|
|
3712
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
3057
3713
|
} catch (e) {
|
|
3058
3714
|
const formatted = formatHeadlessConflictError(e);
|
|
3059
3715
|
if (formatted) return formatted;
|
|
@@ -3066,8 +3722,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3066
3722
|
return timeoutResponse(name, client, args?.site);
|
|
3067
3723
|
if (!result.success)
|
|
3068
3724
|
return errorResponse(name, result.error, args?.site);
|
|
3069
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3070
|
-
|
|
3725
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
3726
|
+
const _msg = `✅ ブロック複製完了${_mt_dup}`;
|
|
3727
|
+
if (_inputRef) {
|
|
3728
|
+
const changeInfo = buildChangeInfoFromResult('duplicated', _snap, result, _preState, { inputRef: _inputRef });
|
|
3729
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
3730
|
+
}
|
|
3731
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
3071
3732
|
}
|
|
3072
3733
|
|
|
3073
3734
|
case "save_post": {
|
|
@@ -3421,7 +4082,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3421
4082
|
}
|
|
3422
4083
|
|
|
3423
4084
|
case "get_selection": {
|
|
3424
|
-
const { mode, postId: _postId, sessionId: _sessionId, message, client } = await resolveMode(args, name);
|
|
4085
|
+
const { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
|
|
3425
4086
|
if (mode === 'error') {
|
|
3426
4087
|
return errorResponse(name, message, args?.site);
|
|
3427
4088
|
}
|
|
@@ -3437,16 +4098,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3437
4098
|
content: [{ type: "text", text: `ブロックが選択されていません。${_mt_gs}` }],
|
|
3438
4099
|
};
|
|
3439
4100
|
}
|
|
4101
|
+
|
|
4102
|
+
// ref 対応: fresh state から snapshot を毎回新規作成
|
|
4103
|
+
// safetyCritical: true — stale Store へのフォールバックを防ぐ
|
|
4104
|
+
const _siteName_gs = siteName || args?.site || 'default';
|
|
4105
|
+
let _snapInfo = null;
|
|
4106
|
+
try {
|
|
4107
|
+
const freshState = await getCurrentStructure(mode, client, _postId, _sessionId, { safetyCritical: true });
|
|
4108
|
+
if (freshState?.allBlocks?.length > 0) {
|
|
4109
|
+
_snapInfo = buildSnapshotFromState(freshState, mode, _postId, _sessionId, _siteName_gs);
|
|
4110
|
+
}
|
|
4111
|
+
} catch (_e) {
|
|
4112
|
+
// fresh state 取得失敗 → ref なしで index-only にフォールバック
|
|
4113
|
+
}
|
|
4114
|
+
|
|
3440
4115
|
// 複数選択
|
|
3441
4116
|
if (sel.isMultiSelect && sel.blockIds) {
|
|
3442
4117
|
let text = `選択中: ${sel.blockIds.length}ブロック\n` +
|
|
3443
4118
|
`タイプ: ${sel.blockTypes?.join(", ")}\n` +
|
|
3444
|
-
`位置: ${sel.blockIndices?.join(", ")}
|
|
4119
|
+
`位置: ${sel.blockIndices?.join(", ")}`;
|
|
4120
|
+
if (_snapInfo && sel.blockIndices?.length > 0) {
|
|
4121
|
+
const refs = sel.blockIndices
|
|
4122
|
+
.map(idx => _snapInfo.blocks?.find(b => b.index === idx)?.ref)
|
|
4123
|
+
.filter(Boolean);
|
|
4124
|
+
// 全数一致のときだけ refs を返す(部分一致は AI を誤誘導するため)
|
|
4125
|
+
if (refs.length === sel.blockIndices.length) {
|
|
4126
|
+
text += `\nrefs: ${refs.join(", ")}`;
|
|
4127
|
+
text += `\n[snapshot:${_snapInfo.snapshotId} rev:${_snapInfo.revision}]`;
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
text += _mt_gs;
|
|
3445
4131
|
return { content: [{ type: "text", text }] };
|
|
3446
4132
|
}
|
|
3447
4133
|
// 単一選択
|
|
3448
4134
|
let text = `選択中: ${sel.blockType}\n` +
|
|
3449
4135
|
`位置: index ${sel.blockIndex}`;
|
|
4136
|
+
if (_snapInfo) {
|
|
4137
|
+
const refEntry = _snapInfo.blocks?.find(b => b.index === sel.blockIndex);
|
|
4138
|
+
if (refEntry) {
|
|
4139
|
+
text += `\nref: ${refEntry.ref}`;
|
|
4140
|
+
text += `\n[snapshot:${_snapInfo.snapshotId} rev:${_snapInfo.revision}]`;
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
3450
4143
|
if (sel.textSelection?.text) {
|
|
3451
4144
|
text += `\n\nカーソル選択テキスト: "${sel.textSelection.text}"`;
|
|
3452
4145
|
if (sel.textSelection.context) {
|
|
@@ -3474,6 +4167,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3474
4167
|
const _siteName = siteName || args?.site || 'default';
|
|
3475
4168
|
|
|
3476
4169
|
const { action } = (args || {});
|
|
4170
|
+
const _inputRef = args?.ref || null;
|
|
3477
4171
|
if (!action) {
|
|
3478
4172
|
return { content: [{ type: "text", text: "❌ action は必須です" }], isError: true };
|
|
3479
4173
|
}
|
|
@@ -3490,6 +4184,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3490
4184
|
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
3491
4185
|
});
|
|
3492
4186
|
if (resolved.error) return resolved.error;
|
|
4187
|
+
const _preState = resolved.currentState || null; // Phase 2: 差分計算用
|
|
3493
4188
|
|
|
3494
4189
|
const index = resolved.index !== undefined ? resolved.index : args?.index;
|
|
3495
4190
|
if (index === undefined) {
|
|
@@ -3536,8 +4231,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3536
4231
|
if (!_isWrite) {
|
|
3537
4232
|
return { content: [{ type: "text", text: formatStructure(result) + _mt_to }] };
|
|
3538
4233
|
}
|
|
3539
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3540
|
-
|
|
4234
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
4235
|
+
const _msg = `✅ ${action} 完了: ${JSON.stringify(result.detail || result)}${_mt_to}`;
|
|
4236
|
+
if (_inputRef) {
|
|
4237
|
+
const changedIndex = result.targetIndex ?? index;
|
|
4238
|
+
const syntheticResult = { changes: [{ oldIndex: changedIndex, newIndices: [changedIndex], success: true }] };
|
|
4239
|
+
const changeInfo = buildChangeInfoFromResult('updated', _snap, syntheticResult, _preState);
|
|
4240
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
4241
|
+
}
|
|
4242
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
3541
4243
|
} catch (e) {
|
|
3542
4244
|
const formatted = formatHeadlessConflictError(e);
|
|
3543
4245
|
if (formatted) return formatted;
|
|
@@ -3555,8 +4257,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3555
4257
|
if (!_isWrite) {
|
|
3556
4258
|
return { content: [{ type: "text", text: formatStructure(result.structure) + _mt_to }] };
|
|
3557
4259
|
}
|
|
3558
|
-
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3559
|
-
|
|
4260
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
|
|
4261
|
+
const _msg = `✅ ${action} 完了: ${JSON.stringify(result.detail || {})}${_mt_to}`;
|
|
4262
|
+
if (_inputRef) {
|
|
4263
|
+
const changedIndex = result.targetIndex ?? index;
|
|
4264
|
+
const syntheticResult = { changes: [{ oldIndex: changedIndex, newIndices: [changedIndex], success: true }] };
|
|
4265
|
+
const changeInfo = buildChangeInfoFromResult('updated', _snap, syntheticResult, _preState);
|
|
4266
|
+
if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
|
|
4267
|
+
}
|
|
4268
|
+
return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
|
|
3560
4269
|
}
|
|
3561
4270
|
|
|
3562
4271
|
case "open_in_browser": {
|
package/dist/ws-server.js
CHANGED