friday-mcp-v2 3.0.0 → 3.0.1
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 +1070 -98
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
11
11
|
import { FridayWPClient, ConnectionRegistry } from "./wordpress-api.js";
|
|
12
12
|
import { BridgeClient } from "./bridge-client.js";
|
|
13
13
|
import fetch from "node-fetch";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
14
15
|
import { readFileSync, statSync, realpathSync } from "node:fs";
|
|
15
16
|
import { execFile } from "node:child_process";
|
|
16
17
|
import { resolve as resolvePath, sep, dirname } from "node:path";
|
|
@@ -102,6 +103,205 @@ const statusCache = {
|
|
|
102
103
|
}
|
|
103
104
|
};
|
|
104
105
|
|
|
106
|
+
// ========================================
|
|
107
|
+
// Snapshot キャッシュ(Phase 1: インデックスズレ対処)
|
|
108
|
+
// ========================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* djb2 ハッシュ → base36 文字列
|
|
112
|
+
* @param {string} str
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function simpleHash(str) {
|
|
116
|
+
let hash = 5381;
|
|
117
|
+
for (let i = 0; i < str.length; i++) {
|
|
118
|
+
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
|
119
|
+
hash = hash & hash;
|
|
120
|
+
}
|
|
121
|
+
return (hash >>> 0).toString(36);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function generateSnapshotId() {
|
|
125
|
+
return 'snap_' + randomUUID().replace(/-/g, '').slice(0, 12);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function generateBlockRef(index) {
|
|
129
|
+
return 'r' + index;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* ブロックの fingerprint を生成
|
|
134
|
+
* @param {object} block - state.allBlocks の要素
|
|
135
|
+
* @param {string} mode - "editor" | "headless"
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
function computeFingerprint(block, mode) {
|
|
139
|
+
const type = block.type || 'unknown';
|
|
140
|
+
let contentSource;
|
|
141
|
+
if (mode === 'headless' && block.html) {
|
|
142
|
+
contentSource = block.html.slice(0, 200);
|
|
143
|
+
} else if (block.attributes) {
|
|
144
|
+
contentSource = JSON.stringify(block.attributes).slice(0, 200);
|
|
145
|
+
} else {
|
|
146
|
+
contentSource = '';
|
|
147
|
+
}
|
|
148
|
+
return type + ':' + simpleHash(contentSource);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* revision を計算(内容ベースハッシュ)
|
|
153
|
+
* serialize_blocks 相当の粒度で内容変更を検知する
|
|
154
|
+
* @param {{ allBlocks: Array }} state
|
|
155
|
+
* @param {string} mode
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
function computeRevision(state, mode) {
|
|
159
|
+
const source = state.allBlocks.map(b => {
|
|
160
|
+
const content = (mode === 'headless' && b.html)
|
|
161
|
+
? b.html.slice(0, 200)
|
|
162
|
+
: JSON.stringify(b.attributes || {}).slice(0, 200);
|
|
163
|
+
return `${b.type}:${b.index}:${content}`;
|
|
164
|
+
}).join('|');
|
|
165
|
+
return 'rev_' + simpleHash(source);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 既に取得済みの currentState から snapshot を組み立て、キャッシュに登録して返す。
|
|
170
|
+
* buildResponseSnapshot() の「state を受け取る版」— getCurrentStructure() を呼ばない。
|
|
171
|
+
*/
|
|
172
|
+
function buildSnapshotFromState(currentState, mode, postId, sessionId, siteName) {
|
|
173
|
+
if (!currentState?.allBlocks || currentState.allBlocks.length === 0) return null;
|
|
174
|
+
|
|
175
|
+
const newSnapshotId = generateSnapshotId();
|
|
176
|
+
const newSnapshotBlocks = buildSnapshotBlocks(currentState, mode);
|
|
177
|
+
const newRevision = computeRevision(currentState, mode);
|
|
178
|
+
|
|
179
|
+
const snapshotRecord = {
|
|
180
|
+
snapshotId: newSnapshotId, postId, mode,
|
|
181
|
+
sessionId: mode === 'editor' ? sessionId : null,
|
|
182
|
+
siteName: siteName || 'default',
|
|
183
|
+
createdAt: Date.now(),
|
|
184
|
+
revision: newRevision,
|
|
185
|
+
blocks: newSnapshotBlocks,
|
|
186
|
+
displayMode: 'full',
|
|
187
|
+
};
|
|
188
|
+
snapshotCache.set(snapshotRecord);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
snapshotId: newSnapshotId,
|
|
192
|
+
revision: newRevision,
|
|
193
|
+
blocks: newSnapshotBlocks.map(b => ({
|
|
194
|
+
ref: b.ref, index: b.index, type: b.type,
|
|
195
|
+
depth: b.depth, parentIndex: b.parentIndex,
|
|
196
|
+
})),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* expectedRevision と現在の revision を比較する。
|
|
202
|
+
* 一致すれば null を返す。不一致の場合は最新 snapshot 付きのエラーレスポンスを返す。
|
|
203
|
+
* currentState から直接 snapshot を組み立てるため、追加の通信コストなし。
|
|
204
|
+
* @returns {object|null} null=一致(続行可)、object=不一致エラーレスポンス
|
|
205
|
+
*/
|
|
206
|
+
function checkRevisionMismatch(expectedRevision, currentState, mode, postId, sessionId, siteName) {
|
|
207
|
+
const currentRevision = computeRevision(currentState, mode);
|
|
208
|
+
if (expectedRevision === currentRevision) return null;
|
|
209
|
+
|
|
210
|
+
// 不一致 — 更新は拒否するが、最新 snapshot を返して再取得の往復を省く
|
|
211
|
+
const latestSnapshot = buildSnapshotFromState(currentState, mode, postId, sessionId, siteName);
|
|
212
|
+
let text = `❌ REVISION_MISMATCH: 記事の構造が変更されています(expected: ${expectedRevision}, current: ${currentRevision})。\n` +
|
|
213
|
+
`更新はスキップしました。以下の最新 snapshot で再試行してください。`;
|
|
214
|
+
if (latestSnapshot) {
|
|
215
|
+
text = appendSnapshotToText(text, latestSnapshot);
|
|
216
|
+
}
|
|
217
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* state.allBlocks から SnapshotBlock[] を構築
|
|
222
|
+
* @param {{ allBlocks: Array }} state
|
|
223
|
+
* @param {string} mode
|
|
224
|
+
* @returns {Array}
|
|
225
|
+
*/
|
|
226
|
+
function buildSnapshotBlocks(state, mode) {
|
|
227
|
+
return state.allBlocks.map(b => ({
|
|
228
|
+
ref: generateBlockRef(b.index),
|
|
229
|
+
index: b.index,
|
|
230
|
+
type: b.type,
|
|
231
|
+
depth: b.depth || 0,
|
|
232
|
+
parentIndex: b.parentIndex ?? null,
|
|
233
|
+
fingerprint: computeFingerprint(b, mode),
|
|
234
|
+
section: b.section || null,
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const snapshotCache = {
|
|
239
|
+
/** @type {Map<string, object>} snapshotId → SnapshotRecord */
|
|
240
|
+
_entries: new Map(),
|
|
241
|
+
/** @type {Map<string, string[]>} "postId:siteName" → snapshotId[] (FIFO) */
|
|
242
|
+
_byArticle: new Map(),
|
|
243
|
+
TTL: 300_000,
|
|
244
|
+
MAX_PER_ARTICLE: 3,
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* snapshotId で取得(TTL チェック付き)
|
|
248
|
+
* @param {string} snapshotId
|
|
249
|
+
* @returns {object|null}
|
|
250
|
+
*/
|
|
251
|
+
get(snapshotId) {
|
|
252
|
+
const entry = this._entries.get(snapshotId);
|
|
253
|
+
if (!entry) return null;
|
|
254
|
+
if ((Date.now() - entry.createdAt) >= this.TTL) {
|
|
255
|
+
this._remove(snapshotId);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return entry;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* snapshot を登録(記事単位 eviction 付き)
|
|
263
|
+
* @param {object} record - SnapshotRecord
|
|
264
|
+
*/
|
|
265
|
+
set(record) {
|
|
266
|
+
const articleKey = `${record.postId}:${record.siteName}`;
|
|
267
|
+
let articleSnaps = this._byArticle.get(articleKey) || [];
|
|
268
|
+
articleSnaps.push(record.snapshotId);
|
|
269
|
+
while (articleSnaps.length > this.MAX_PER_ARTICLE) {
|
|
270
|
+
const oldest = articleSnaps.shift();
|
|
271
|
+
this._entries.delete(oldest);
|
|
272
|
+
}
|
|
273
|
+
this._byArticle.set(articleKey, articleSnaps);
|
|
274
|
+
this._entries.set(record.snapshotId, record);
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/** @param {string} snapshotId */
|
|
278
|
+
_remove(snapshotId) {
|
|
279
|
+
const entry = this._entries.get(snapshotId);
|
|
280
|
+
if (entry) {
|
|
281
|
+
const articleKey = `${entry.postId}:${entry.siteName}`;
|
|
282
|
+
const articleSnaps = this._byArticle.get(articleKey);
|
|
283
|
+
if (articleSnaps) {
|
|
284
|
+
const idx = articleSnaps.indexOf(snapshotId);
|
|
285
|
+
if (idx >= 0) articleSnaps.splice(idx, 1);
|
|
286
|
+
if (articleSnaps.length === 0) this._byArticle.delete(articleKey);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this._entries.delete(snapshotId);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
/** @param {string} [siteName] 指定時はそのサイトのみ。省略時は全破棄。 */
|
|
293
|
+
clear(siteName) {
|
|
294
|
+
if (siteName) {
|
|
295
|
+
for (const [id, record] of this._entries) {
|
|
296
|
+
if (record.siteName === siteName) this._remove(id);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
this._entries.clear();
|
|
300
|
+
this._byArticle.clear();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
105
305
|
// ヘルパー関数: 正規表現用に文字列をエスケープ
|
|
106
306
|
function escapeRegExp(string) {
|
|
107
307
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -536,8 +736,17 @@ const targetSchema = {
|
|
|
536
736
|
required: ["level", "contains"],
|
|
537
737
|
description: "Select from heading to next same-level heading.",
|
|
538
738
|
},
|
|
739
|
+
ref: {
|
|
740
|
+
type: "string",
|
|
741
|
+
description: "Block ref from snapshot (e.g. 'r5'). Requires snapshotId.",
|
|
742
|
+
},
|
|
743
|
+
refs: {
|
|
744
|
+
type: "array",
|
|
745
|
+
items: { type: "string" },
|
|
746
|
+
description: "Multiple block refs from snapshot. Requires snapshotId.",
|
|
747
|
+
},
|
|
539
748
|
},
|
|
540
|
-
description: "Target: one of selected/index/indices/range/heading + optional filters.",
|
|
749
|
+
description: "Target: one of selected/index/indices/range/heading/ref/refs + optional filters.",
|
|
541
750
|
};
|
|
542
751
|
|
|
543
752
|
const targetSchemaNoSelected = {
|
|
@@ -574,12 +783,16 @@ function normalizeTarget(target) {
|
|
|
574
783
|
target.indices && 'indices',
|
|
575
784
|
target.range && 'range',
|
|
576
785
|
target.heading && 'heading',
|
|
786
|
+
target.ref && 'ref',
|
|
787
|
+
target.refs && 'refs',
|
|
577
788
|
].filter(Boolean);
|
|
578
789
|
if (primaries.length > 1) {
|
|
579
790
|
throw new Error(`target に複数の primary selector が指定されています: ${primaries.join(', ')}。1つだけ指定してください。`);
|
|
580
791
|
}
|
|
581
792
|
|
|
582
793
|
const result = {};
|
|
794
|
+
if (target.ref) result._ref = target.ref;
|
|
795
|
+
if (target.refs) result._refs = target.refs;
|
|
583
796
|
if (target.selected) result.target = "selected";
|
|
584
797
|
if (target.index !== undefined) result.index = target.index;
|
|
585
798
|
if (target.indices) result.indices = target.indices;
|
|
@@ -628,6 +841,448 @@ function normalizeInsert(insert) {
|
|
|
628
841
|
return { insertOnly: true, insertPosition: pos };
|
|
629
842
|
}
|
|
630
843
|
|
|
844
|
+
// ========================================
|
|
845
|
+
// ref 解決ヘルパー(Phase 2a)
|
|
846
|
+
// ========================================
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* 現在のブロック構造を取得する(ref 解決・レスポンス snapshot 用)
|
|
850
|
+
* @param {string} mode - "editor" | "headless"
|
|
851
|
+
* @param {object} client - WordPressAPI インスタンス
|
|
852
|
+
* @param {number} postId
|
|
853
|
+
* @param {string|null} sessionId
|
|
854
|
+
* @returns {Promise<object>} { allBlocks: Block[] }
|
|
855
|
+
*/
|
|
856
|
+
async function getCurrentStructure(mode, client, postId, sessionId, { safetyCritical = false } = {}) {
|
|
857
|
+
if (mode === 'editor') {
|
|
858
|
+
// Phase 3.1: editor からライブ状態を即時取得(session store キャッシュをバイパス)
|
|
859
|
+
try {
|
|
860
|
+
const result = await client.sendEditorCommand(
|
|
861
|
+
'get_fresh_state', {}, 10000, postId, sessionId
|
|
862
|
+
);
|
|
863
|
+
if (result?.success && result.allBlocks) {
|
|
864
|
+
return result;
|
|
865
|
+
}
|
|
866
|
+
} catch (_e) {
|
|
867
|
+
// get_fresh_state 失敗
|
|
868
|
+
}
|
|
869
|
+
// safety-critical 経路: stale キャッシュにフォールバックしない
|
|
870
|
+
if (safetyCritical) {
|
|
871
|
+
throw new Error('editor のライブ状態を取得できませんでした。拡張機能の接続を確認してください。');
|
|
872
|
+
}
|
|
873
|
+
// 非 safety-critical: 従来キャッシュにフォールバック
|
|
874
|
+
return await client.getEditorState(postId, sessionId);
|
|
875
|
+
} else {
|
|
876
|
+
const structure = await client.headlessGetStructure(postId);
|
|
877
|
+
const blocksData = await client.headlessGetBlocks(postId);
|
|
878
|
+
structure.allBlocks = blocksData.blocks;
|
|
879
|
+
return structure;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* snapshot の ref を現在構造上の index に解決する
|
|
885
|
+
* @param {string} snapshotId
|
|
886
|
+
* @param {string} ref
|
|
887
|
+
* @param {string} mode - 現在の mode
|
|
888
|
+
* @param {string|null} sessionId - 現在の sessionId
|
|
889
|
+
* @param {number} postId
|
|
890
|
+
* @param {object} currentState - getCurrentStructure() の結果(キャッシュ用)
|
|
891
|
+
* @returns {number} 解決された現在の index
|
|
892
|
+
* @throws {Error} 解決不能な場合
|
|
893
|
+
*/
|
|
894
|
+
function resolveRefFromState(snapshotId, ref, mode, sessionId, postId, currentState) {
|
|
895
|
+
// 1. snapshot 取得
|
|
896
|
+
const snap = snapshotCache.get(snapshotId);
|
|
897
|
+
if (!snap) {
|
|
898
|
+
throw new Error(`snapshot "${snapshotId}" が見つかりません(期限切れまたは無効)。get_article_structure を再取得してください。`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// 2. 状態ソース束縛チェック
|
|
902
|
+
if (snap.mode !== mode) {
|
|
903
|
+
throw new Error(`snapshot は ${snap.mode} モードで取得されましたが、現在は ${mode} モードです。get_article_structure を再取得してください。`);
|
|
904
|
+
}
|
|
905
|
+
if (mode === 'editor' && snap.sessionId && sessionId && snap.sessionId !== sessionId) {
|
|
906
|
+
throw new Error(`snapshot は sessionId "${snap.sessionId}" で取得されましたが、現在の sessionId は "${sessionId}" です。get_article_structure を再取得してください。`);
|
|
907
|
+
}
|
|
908
|
+
if (snap.postId !== postId) {
|
|
909
|
+
throw new Error(`snapshot は postId ${snap.postId} 用ですが、postId ${postId} に対して使用されています。`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 3. snapshot 内の ref 情報を取得
|
|
913
|
+
const snapBlock = snap.blocks.find(b => b.ref === ref);
|
|
914
|
+
if (!snapBlock) {
|
|
915
|
+
throw new Error(`ref "${ref}" は snapshot "${snapshotId}" 内に存在しません。`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// 4. 現在構造の検証
|
|
919
|
+
const currentBlocks = currentState?.allBlocks;
|
|
920
|
+
if (!currentBlocks || currentBlocks.length === 0) {
|
|
921
|
+
throw new Error('現在のブロック構造を取得できませんでした。');
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// 5. fingerprint 照合(2段、曖昧ならエラー)
|
|
925
|
+
const candidates = [];
|
|
926
|
+
for (const cb of currentBlocks) {
|
|
927
|
+
const fp = computeFingerprint(cb, mode);
|
|
928
|
+
if (fp === snapBlock.fingerprint) {
|
|
929
|
+
candidates.push(cb);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// 5a. 完全一致 1件 → 確定
|
|
934
|
+
if (candidates.length === 1) return candidates[0].index;
|
|
935
|
+
|
|
936
|
+
// 5b. 完全一致 複数件 → depth + parentIndex で絞り込み
|
|
937
|
+
if (candidates.length > 1) {
|
|
938
|
+
const refined = candidates.filter(c =>
|
|
939
|
+
(c.depth || 0) === snapBlock.depth &&
|
|
940
|
+
(c.parentIndex ?? null) === snapBlock.parentIndex
|
|
941
|
+
);
|
|
942
|
+
if (refined.length === 1) return refined[0].index;
|
|
943
|
+
throw new Error(
|
|
944
|
+
`ref "${ref}" (type: ${snapBlock.type}) の解決先が ${refined.length > 0 ? refined.length : candidates.length} 件見つかりました。` +
|
|
945
|
+
`一意に特定できません。get_article_structure を再取得してください。`
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// 5c. 一致 0件 → エラー
|
|
950
|
+
throw new Error(
|
|
951
|
+
`ref "${ref}" (${snapBlock.type}, fingerprint: ${snapBlock.fingerprint}) に一致するブロックが見つかりません。` +
|
|
952
|
+
`構造が大幅に変更された可能性があります。get_article_structure を再取得してください。`
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* safety-critical な fresh state 取得 + expectedRevision チェック。
|
|
958
|
+
* state 取得と revision 判定の入口を統一し、分岐ごとのズレを防ぐ。
|
|
959
|
+
* @returns {{ currentState, error? }}
|
|
960
|
+
*/
|
|
961
|
+
async function acquireFreshState({
|
|
962
|
+
expectedRevision, mode, client, postId, sessionId, siteName,
|
|
963
|
+
}) {
|
|
964
|
+
let currentState;
|
|
965
|
+
try {
|
|
966
|
+
currentState = await getCurrentStructure(mode, client, postId, sessionId, { safetyCritical: true });
|
|
967
|
+
} catch (e) {
|
|
968
|
+
return { error: {
|
|
969
|
+
content: [{ type: "text", text: `❌ 構造取得に失敗しました: ${e.message}${expectedRevision ? '\nexpectedRevision が指定されているため、更新を中止します。' : ''}` }],
|
|
970
|
+
isError: true,
|
|
971
|
+
}};
|
|
972
|
+
}
|
|
973
|
+
if (expectedRevision) {
|
|
974
|
+
const mismatch = checkRevisionMismatch(expectedRevision, currentState, mode, postId, sessionId, siteName);
|
|
975
|
+
if (mismatch) return { error: mismatch };
|
|
976
|
+
}
|
|
977
|
+
return { currentState };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* ref/refs → index/indices 解決 + expectedRevision チェックの共通処理。
|
|
982
|
+
* delete_block / duplicate_block / update_blocks / table_operations で共通利用する。
|
|
983
|
+
* @returns {{ index?, indices?, error? }}
|
|
984
|
+
* - 成功: { index } or { indices } or {}(ref なしで revision OK の場合)
|
|
985
|
+
* - 失敗: { error: MCP エラーレスポンス }(呼び出し元は return error で即返却)
|
|
986
|
+
*/
|
|
987
|
+
async function resolveRefsAndCheckRevision({
|
|
988
|
+
snapshotId, ref, refs, expectedRevision,
|
|
989
|
+
mode, client, postId, sessionId, siteName,
|
|
990
|
+
}) {
|
|
991
|
+
const hasRef = ref || refs;
|
|
992
|
+
|
|
993
|
+
// Case 1: ref/refs 指定あり
|
|
994
|
+
if (hasRef) {
|
|
995
|
+
if (!snapshotId) {
|
|
996
|
+
return { error: {
|
|
997
|
+
content: [{ type: "text", text: "❌ ref/refs を使用するには snapshotId が必要です。get_article_structure で取得してください。" }],
|
|
998
|
+
isError: true,
|
|
999
|
+
}};
|
|
1000
|
+
}
|
|
1001
|
+
const { currentState, error } = await acquireFreshState({
|
|
1002
|
+
expectedRevision, mode, client, postId, sessionId, siteName,
|
|
1003
|
+
});
|
|
1004
|
+
if (error) return { error };
|
|
1005
|
+
try {
|
|
1006
|
+
if (ref) {
|
|
1007
|
+
const index = resolveRefFromState(snapshotId, ref, mode, sessionId, postId, currentState);
|
|
1008
|
+
return { index };
|
|
1009
|
+
}
|
|
1010
|
+
if (refs) {
|
|
1011
|
+
const indices = refs.map(r => resolveRefFromState(snapshotId, r, mode, sessionId, postId, currentState));
|
|
1012
|
+
return { indices };
|
|
1013
|
+
}
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
return { error: {
|
|
1016
|
+
content: [{ type: "text", text: `❌ ref 解決エラー: ${e.message}` }],
|
|
1017
|
+
isError: true,
|
|
1018
|
+
}};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Case 2: ref なし + expectedRevision のみ
|
|
1023
|
+
if (expectedRevision) {
|
|
1024
|
+
const { error } = await acquireFreshState({
|
|
1025
|
+
expectedRevision, mode, client, postId, sessionId, siteName,
|
|
1026
|
+
});
|
|
1027
|
+
if (error) return { error };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Case 3: どちらもなし
|
|
1031
|
+
return {};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* 更新後のレスポンス snapshot を構築する
|
|
1036
|
+
* @param {string} mode
|
|
1037
|
+
* @param {object} client
|
|
1038
|
+
* @param {number} postId
|
|
1039
|
+
* @param {string|null} sessionId
|
|
1040
|
+
* @param {string} siteName
|
|
1041
|
+
* @returns {Promise<object|null>} { snapshotId, revision, blocks[] } or null
|
|
1042
|
+
*/
|
|
1043
|
+
async function buildResponseSnapshot(mode, client, postId, sessionId, siteName) {
|
|
1044
|
+
let newState;
|
|
1045
|
+
try {
|
|
1046
|
+
newState = await getCurrentStructure(mode, client, postId, sessionId);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!newState?.allBlocks || newState.allBlocks.length === 0) {
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const newSnapshotId = generateSnapshotId();
|
|
1056
|
+
const newSnapshotBlocks = buildSnapshotBlocks(newState, mode);
|
|
1057
|
+
const newRevision = computeRevision(newState, mode);
|
|
1058
|
+
|
|
1059
|
+
const snapshotRecord = {
|
|
1060
|
+
snapshotId: newSnapshotId,
|
|
1061
|
+
postId,
|
|
1062
|
+
mode,
|
|
1063
|
+
sessionId: mode === 'editor' ? sessionId : null,
|
|
1064
|
+
siteName: siteName || 'default',
|
|
1065
|
+
createdAt: Date.now(),
|
|
1066
|
+
revision: newRevision,
|
|
1067
|
+
blocks: newSnapshotBlocks,
|
|
1068
|
+
displayMode: 'full',
|
|
1069
|
+
};
|
|
1070
|
+
snapshotCache.set(snapshotRecord);
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
snapshotId: newSnapshotId,
|
|
1074
|
+
revision: newRevision,
|
|
1075
|
+
blocks: newSnapshotBlocks.map(b => ({
|
|
1076
|
+
ref: b.ref,
|
|
1077
|
+
index: b.index,
|
|
1078
|
+
type: b.type,
|
|
1079
|
+
depth: b.depth,
|
|
1080
|
+
parentIndex: b.parentIndex,
|
|
1081
|
+
})),
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* snapshot 情報をテキストレスポンスに付加する
|
|
1087
|
+
* blocks[] を含めて AI が新しい ref を取得できるようにする
|
|
1088
|
+
* @param {string} text - 元のレスポンステキスト
|
|
1089
|
+
* @param {object|null} snapshot - buildResponseSnapshot の結果
|
|
1090
|
+
* @param {object} [refInfo] - 単体操作の ref 解決情報(1→N 展開追跡用)
|
|
1091
|
+
* @param {string} [refInfo.oldSnapshotId] - 元の snapshotId
|
|
1092
|
+
* @param {string} [refInfo.usedRef] - 使用した ref
|
|
1093
|
+
* @param {number} [refInfo.resolvedIndex] - 解決された元の index
|
|
1094
|
+
* @returns {string} snapshot 行と blocks 一覧が付加されたテキスト
|
|
1095
|
+
*/
|
|
1096
|
+
function appendSnapshotToText(text, snapshot, refInfo) {
|
|
1097
|
+
if (!snapshot) return text;
|
|
1098
|
+
|
|
1099
|
+
// 1→N 展開チェック(単体操作時のみ)
|
|
1100
|
+
let expandedLine = '';
|
|
1101
|
+
if (refInfo?.oldSnapshotId && refInfo?.usedRef) {
|
|
1102
|
+
const oldSnap = snapshotCache.get(refInfo.oldSnapshotId);
|
|
1103
|
+
if (oldSnap) {
|
|
1104
|
+
const oldCount = oldSnap.blocks.length;
|
|
1105
|
+
const newCount = snapshot.blocks.length;
|
|
1106
|
+
const delta = newCount - oldCount;
|
|
1107
|
+
if (delta > 0) {
|
|
1108
|
+
// 単体操作での展開: 元 index 位置から delta+1 個が新ブロック
|
|
1109
|
+
const newRefs = snapshot.blocks
|
|
1110
|
+
.filter(b => b.index >= refInfo.resolvedIndex && b.index < refInfo.resolvedIndex + 1 + delta)
|
|
1111
|
+
.map(b => b.ref);
|
|
1112
|
+
expandedLine = `\nexpanded: ${refInfo.usedRef} → [${newRefs.join(', ')}]`;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
let out = text + `\n\n[snapshot:${snapshot.snapshotId} rev:${snapshot.revision}]${expandedLine}`;
|
|
1118
|
+
if (snapshot.blocks && snapshot.blocks.length > 0) {
|
|
1119
|
+
out += '\nblocks:';
|
|
1120
|
+
for (const b of snapshot.blocks) {
|
|
1121
|
+
const parent = b.parentIndex !== null ? `,p:${b.parentIndex}` : '';
|
|
1122
|
+
out += `\n [${b.index}|${b.ref}] ${b.type} (d:${b.depth}${parent})`;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return out;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* batch operations の実行
|
|
1130
|
+
* @param {Array} operations - 各 operation: { target, newHTML?, replacements?, attributeUpdates? }
|
|
1131
|
+
* @param {string} snapshotId
|
|
1132
|
+
* @param {string} mode
|
|
1133
|
+
* @param {object} client
|
|
1134
|
+
* @param {number} postId
|
|
1135
|
+
* @param {string|null} sessionId
|
|
1136
|
+
* @param {string} siteName
|
|
1137
|
+
* @param {string} _modeTag
|
|
1138
|
+
* @param {string} toolName
|
|
1139
|
+
*/
|
|
1140
|
+
async function handleBatchOperations(operations, snapshotId, mode, client, postId, sessionId, siteName, _modeTag, toolName, expectedRevision) {
|
|
1141
|
+
// 1. 全 operation の target を正規化 & ref を一括解決
|
|
1142
|
+
let currentState;
|
|
1143
|
+
try {
|
|
1144
|
+
currentState = await getCurrentStructure(mode, client, postId, sessionId, { safetyCritical: true });
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
return { content: [{ type: "text", text: `❌ 現在のブロック構造を取得できませんでした: ${e.message}` }], isError: true };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// revision 楽観的ロック(expectedRevision が指定されている場合のみ)
|
|
1150
|
+
if (expectedRevision) {
|
|
1151
|
+
const mismatch = checkRevisionMismatch(
|
|
1152
|
+
expectedRevision, currentState, mode, postId, sessionId, siteName
|
|
1153
|
+
);
|
|
1154
|
+
if (mismatch) return mismatch;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// batch は ref のみ許可(1 operation = 1 ref = 1 block)
|
|
1158
|
+
const resolvedOps = [];
|
|
1159
|
+
try {
|
|
1160
|
+
for (let i = 0; i < operations.length; i++) {
|
|
1161
|
+
const op = operations[i];
|
|
1162
|
+
const ref = op.target?.ref;
|
|
1163
|
+
if (!ref || typeof ref !== 'string') {
|
|
1164
|
+
return {
|
|
1165
|
+
content: [{ type: "text", text: `❌ operations[${i}]: batch の target には ref(文字列)が必須です。index/section/range 等は batch では使用できません。` }],
|
|
1166
|
+
isError: true,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
// ref 以外の selector が指定されていないか確認
|
|
1170
|
+
const extraKeys = Object.keys(op.target).filter(k => k !== 'ref');
|
|
1171
|
+
if (extraKeys.length > 0) {
|
|
1172
|
+
return {
|
|
1173
|
+
content: [{ type: "text", text: `❌ operations[${i}]: batch の target は ref のみ指定可能です。不正なキー: ${extraKeys.join(', ')}` }],
|
|
1174
|
+
isError: true,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// 変更種別がちょうど1つか検証(1 op = 1 ref = 1 change-kind)
|
|
1179
|
+
const changeKinds = [
|
|
1180
|
+
op.replacements && op.replacements.length > 0 && 'replacements',
|
|
1181
|
+
op.newHTML && 'newHTML',
|
|
1182
|
+
op.attributeUpdates && 'attributeUpdates',
|
|
1183
|
+
].filter(Boolean);
|
|
1184
|
+
if (changeKinds.length === 0) {
|
|
1185
|
+
return {
|
|
1186
|
+
content: [{ type: "text", text: `❌ operations[${i}]: 変更内容が指定されていません。replacements, newHTML, attributeUpdates のいずれか1つを指定してください。` }],
|
|
1187
|
+
isError: true,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
if (changeKinds.length > 1) {
|
|
1191
|
+
return {
|
|
1192
|
+
content: [{ type: "text", text: `❌ operations[${i}]: 複数の変更種別が指定されています: ${changeKinds.join(', ')}。1つだけ指定してください。` }],
|
|
1193
|
+
isError: true,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const resolvedIndex = resolveRefFromState(snapshotId, ref, mode, sessionId, postId, currentState);
|
|
1198
|
+
|
|
1199
|
+
resolvedOps.push({
|
|
1200
|
+
opIndex: i,
|
|
1201
|
+
ref,
|
|
1202
|
+
resolvedIndex,
|
|
1203
|
+
newHTML: op.newHTML,
|
|
1204
|
+
replacements: op.replacements,
|
|
1205
|
+
attributeUpdates: op.attributeUpdates,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
return { content: [{ type: "text", text: `❌ batch ref 解決エラー: ${e.message}` }], isError: true };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// 同一 index への複数 operation を排他チェック
|
|
1213
|
+
const indexSet = new Set();
|
|
1214
|
+
for (const op of resolvedOps) {
|
|
1215
|
+
if (indexSet.has(op.resolvedIndex)) {
|
|
1216
|
+
return {
|
|
1217
|
+
content: [{ type: "text", text: `❌ index ${op.resolvedIndex} (ref: ${op.ref}) に対して複数の operation が指定されています。同一ブロックへの複数操作は許可されていません。` }],
|
|
1218
|
+
isError: true,
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
indexSet.add(op.resolvedIndex);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// 2. 降順ソート(index が大きい方から処理 — 1 op = 1 index なので安全)
|
|
1225
|
+
resolvedOps.sort((a, b) => b.resolvedIndex - a.resolvedIndex);
|
|
1226
|
+
|
|
1227
|
+
// 3. 各 operation を順次実行
|
|
1228
|
+
const results = [];
|
|
1229
|
+
for (const op of resolvedOps) {
|
|
1230
|
+
const index = op.resolvedIndex;
|
|
1231
|
+
|
|
1232
|
+
let opResult;
|
|
1233
|
+
try {
|
|
1234
|
+
// batch は ref → index 解決済み。1 op = 1 index で下流に渡す
|
|
1235
|
+
const downstreamParams = {
|
|
1236
|
+
index,
|
|
1237
|
+
replacements: op.replacements,
|
|
1238
|
+
newHTML: op.newHTML,
|
|
1239
|
+
attributeUpdates: op.attributeUpdates,
|
|
1240
|
+
insertOnly: false,
|
|
1241
|
+
dryRun: false,
|
|
1242
|
+
_fromInsertBlock: false,
|
|
1243
|
+
};
|
|
1244
|
+
if (mode === 'headless') {
|
|
1245
|
+
opResult = await client.headlessUpdate(postId, downstreamParams);
|
|
1246
|
+
} else {
|
|
1247
|
+
opResult = await client.sendEditorCommand("update_blocks", downstreamParams, 10000, postId, sessionId);
|
|
1248
|
+
}
|
|
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
|
+
} catch (e) {
|
|
1257
|
+
results.push({
|
|
1258
|
+
ref: op.ref,
|
|
1259
|
+
resolvedIndex: op.resolvedIndex,
|
|
1260
|
+
status: 'failed',
|
|
1261
|
+
error: e.message,
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// 4. レスポンス snapshot 構築
|
|
1267
|
+
const snapshot = await buildResponseSnapshot(mode, client, postId, sessionId, siteName);
|
|
1268
|
+
|
|
1269
|
+
// レスポンス組み立て
|
|
1270
|
+
// 注: 1→N 展開の newRefs[] は snapshot.blocks[] 全体を返しているため、
|
|
1271
|
+
// AI は新 snapshot から直接新しい ref を特定できる。
|
|
1272
|
+
// per-operation の newRefs 追跡は複数変更時に不正確になるため省略。
|
|
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}`;
|
|
1280
|
+
}
|
|
1281
|
+
text = appendSnapshotToText(text + _modeTag, snapshot);
|
|
1282
|
+
|
|
1283
|
+
return { content: [{ type: "text", text }] };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
631
1286
|
// フィードバック送信ヘルパー
|
|
632
1287
|
const FEEDBACK_URL = process.env.FRIDAY_FEEDBACK_URL || '';
|
|
633
1288
|
async function sendFeedback(data) {
|
|
@@ -776,7 +1431,7 @@ const tools = [
|
|
|
776
1431
|
},
|
|
777
1432
|
{
|
|
778
1433
|
name: "delete_block",
|
|
779
|
-
description: "Delete block(s) by index or selection.",
|
|
1434
|
+
description: "Delete block(s) by index, ref, or selection.",
|
|
780
1435
|
inputSchema: {
|
|
781
1436
|
type: "object",
|
|
782
1437
|
properties: {
|
|
@@ -785,12 +1440,16 @@ const tools = [
|
|
|
785
1440
|
index: { type: "number", description: "Index (0-based). Omit for selected." },
|
|
786
1441
|
count: { type: "number", description: "Consecutive count (default: 1)" },
|
|
787
1442
|
indices: { type: "array", items: { type: "number" }, description: "Multiple indices (exclusive with index/count)" },
|
|
1443
|
+
snapshotId: { type: "string", description: "Snapshot ID from get_article_structure. Required when using ref/refs." },
|
|
1444
|
+
ref: { type: "string", description: "Block ref from snapshot (exclusive with index/indices)." },
|
|
1445
|
+
refs: { type: "array", items: { type: "string" }, description: "Multiple block refs (exclusive with index/indices)." },
|
|
1446
|
+
expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
|
|
788
1447
|
},
|
|
789
1448
|
},
|
|
790
1449
|
},
|
|
791
1450
|
{
|
|
792
1451
|
name: "move_block",
|
|
793
|
-
description: "Move block(s). Use from/to (top-level)
|
|
1452
|
+
description: "Move block(s). Use from/to (top-level), fromFlat/toFlat (nested), or fromRef+beforeRef/afterRef (ref-based).",
|
|
794
1453
|
inputSchema: {
|
|
795
1454
|
type: "object",
|
|
796
1455
|
properties: {
|
|
@@ -801,6 +1460,11 @@ const tools = [
|
|
|
801
1460
|
fromFlat: { type: "number", description: "Source flattened index" },
|
|
802
1461
|
toFlat: { type: "number", description: "Target flattened position" },
|
|
803
1462
|
count: { type: "integer", description: "Consecutive count (default: 1)" },
|
|
1463
|
+
snapshotId: { type: "string", description: "Snapshot ID from get_article_structure. Required when using ref." },
|
|
1464
|
+
expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
|
|
1465
|
+
fromRef: { type: "string", description: "Source block ref (exclusive with from/fromFlat)." },
|
|
1466
|
+
beforeRef: { type: "string", description: "Move before this ref (exclusive with to/toFlat/afterRef)." },
|
|
1467
|
+
afterRef: { type: "string", description: "Move after this ref (exclusive with to/toFlat/beforeRef)." },
|
|
804
1468
|
},
|
|
805
1469
|
},
|
|
806
1470
|
},
|
|
@@ -830,13 +1494,16 @@ const tools = [
|
|
|
830
1494
|
},
|
|
831
1495
|
{
|
|
832
1496
|
name: "duplicate_block",
|
|
833
|
-
description: "Duplicate block by index or selection.",
|
|
1497
|
+
description: "Duplicate block by index, ref, or selection.",
|
|
834
1498
|
inputSchema: {
|
|
835
1499
|
type: "object",
|
|
836
1500
|
properties: {
|
|
837
1501
|
postId: postIdParam,
|
|
838
1502
|
site: siteParam,
|
|
839
1503
|
index: { type: "number", description: "Index (0-based). Omit for selected." },
|
|
1504
|
+
snapshotId: { type: "string", description: "Snapshot ID from get_article_structure. Required when using ref." },
|
|
1505
|
+
ref: { type: "string", description: "Block ref from snapshot (exclusive with index)." },
|
|
1506
|
+
expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
|
|
840
1507
|
},
|
|
841
1508
|
},
|
|
842
1509
|
},
|
|
@@ -919,7 +1586,7 @@ const tools = [
|
|
|
919
1586
|
},
|
|
920
1587
|
{
|
|
921
1588
|
name: "insert_block",
|
|
922
|
-
description: "Insert Gutenberg HTML. Omit index to append.",
|
|
1589
|
+
description: "Insert Gutenberg HTML. Omit index to append. Use beforeRef/afterRef for ref-based positioning.",
|
|
923
1590
|
inputSchema: {
|
|
924
1591
|
type: "object",
|
|
925
1592
|
properties: {
|
|
@@ -928,6 +1595,11 @@ const tools = [
|
|
|
928
1595
|
rawHTML: { type: "string", description: "HTML to insert (exclusive with filePath)" },
|
|
929
1596
|
filePath: { type: "string", description: "Local file path (exclusive with rawHTML)" },
|
|
930
1597
|
index: { type: "number", description: "Position (0-based). Omit to append." },
|
|
1598
|
+
position: { type: "string", enum: ["before", "after"], description: "Insert relative to index: 'before' (default) or 'after'. Requires index." },
|
|
1599
|
+
snapshotId: { type: "string", description: "Snapshot ID. Required when using beforeRef/afterRef." },
|
|
1600
|
+
expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
|
|
1601
|
+
beforeRef: { type: "string", description: "Insert before this ref (exclusive with index)." },
|
|
1602
|
+
afterRef: { type: "string", description: "Insert after this ref (exclusive with index)." },
|
|
931
1603
|
},
|
|
932
1604
|
},
|
|
933
1605
|
},
|
|
@@ -957,13 +1629,59 @@ const tools = [
|
|
|
957
1629
|
},
|
|
958
1630
|
{
|
|
959
1631
|
name: "update_blocks",
|
|
960
|
-
description: "Update block(s). Supports replacements, newHTML, or attributeUpdates.",
|
|
1632
|
+
description: "Update block(s). Supports replacements, newHTML, or attributeUpdates. Use ref+snapshotId for safe index resolution.",
|
|
961
1633
|
inputSchema: {
|
|
962
1634
|
type: "object",
|
|
963
1635
|
properties: {
|
|
964
1636
|
postId: postIdParam,
|
|
965
1637
|
site: siteParam,
|
|
1638
|
+
snapshotId: {
|
|
1639
|
+
type: "string",
|
|
1640
|
+
description: "Snapshot ID from get_article_structure. Required when using ref/refs in target.",
|
|
1641
|
+
},
|
|
1642
|
+
expectedRevision: {
|
|
1643
|
+
type: "string",
|
|
1644
|
+
description: "Revision from snapshot. If provided, rejects update when structure has changed.",
|
|
1645
|
+
},
|
|
966
1646
|
target: targetSchema,
|
|
1647
|
+
operations: {
|
|
1648
|
+
type: "array",
|
|
1649
|
+
items: {
|
|
1650
|
+
type: "object",
|
|
1651
|
+
properties: {
|
|
1652
|
+
target: {
|
|
1653
|
+
type: "object",
|
|
1654
|
+
properties: {
|
|
1655
|
+
ref: { type: "string", description: "Block ref from snapshot (required)." },
|
|
1656
|
+
},
|
|
1657
|
+
required: ["ref"],
|
|
1658
|
+
description: "Batch target: ref only (1 block per operation).",
|
|
1659
|
+
},
|
|
1660
|
+
replacements: {
|
|
1661
|
+
type: "array",
|
|
1662
|
+
items: {
|
|
1663
|
+
type: "object",
|
|
1664
|
+
properties: {
|
|
1665
|
+
old: { type: "string" },
|
|
1666
|
+
new: { type: "string" },
|
|
1667
|
+
regex: { type: "boolean" },
|
|
1668
|
+
},
|
|
1669
|
+
required: ["old", "new"],
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
newHTML: { type: "string" },
|
|
1673
|
+
attributeUpdates: {
|
|
1674
|
+
type: "object",
|
|
1675
|
+
properties: {
|
|
1676
|
+
filter: { type: "object" },
|
|
1677
|
+
set: { type: "object" },
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
required: ["target"],
|
|
1682
|
+
},
|
|
1683
|
+
description: "Batch operations: each targets a single block by ref. Requires snapshotId.",
|
|
1684
|
+
},
|
|
967
1685
|
replacements: {
|
|
968
1686
|
type: "array",
|
|
969
1687
|
items: {
|
|
@@ -999,12 +1717,12 @@ const tools = [
|
|
|
999
1717
|
description: "Preview only",
|
|
1000
1718
|
},
|
|
1001
1719
|
},
|
|
1002
|
-
|
|
1720
|
+
// target は operations[] 使用時は不要
|
|
1003
1721
|
},
|
|
1004
1722
|
},
|
|
1005
1723
|
{
|
|
1006
1724
|
name: "table_operations",
|
|
1007
|
-
description: "Table operations (get/update/add/delete rows/columns/cells).",
|
|
1725
|
+
description: "Table operations (get/update/add/delete rows/columns/cells). Use ref+snapshotId for safe index resolution.",
|
|
1008
1726
|
inputSchema: {
|
|
1009
1727
|
type: "object",
|
|
1010
1728
|
properties: {
|
|
@@ -1040,8 +1758,11 @@ const tools = [
|
|
|
1040
1758
|
items: { type: "string" },
|
|
1041
1759
|
description: "add_row: new cells, add_column: init values, update_row/column: replacements. Omit for empty.",
|
|
1042
1760
|
},
|
|
1761
|
+
snapshotId: { type: "string", description: "Snapshot ID from get_article_structure. Required when using ref." },
|
|
1762
|
+
ref: { type: "string", description: "Block ref from snapshot (exclusive with index)." },
|
|
1763
|
+
expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
|
|
1043
1764
|
},
|
|
1044
|
-
required: ["
|
|
1765
|
+
required: ["action"],
|
|
1045
1766
|
},
|
|
1046
1767
|
},
|
|
1047
1768
|
{
|
|
@@ -1114,11 +1835,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1114
1835
|
*/
|
|
1115
1836
|
async function handleUpdateBlocksTool(args, toolName) {
|
|
1116
1837
|
const name = toolName || "update_blocks";
|
|
1117
|
-
let { mode, postId: _postId, sessionId: _sessionId, message, client } = await resolveMode(args, name);
|
|
1838
|
+
let { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
|
|
1118
1839
|
if (mode === 'error') {
|
|
1119
1840
|
return errorResponse(name, message, args?.site);
|
|
1120
1841
|
}
|
|
1121
1842
|
const _modeTag = process.env.FRIDAY_DEBUG === '1' ? `\n[DEBUG] mode=${mode}` : '';
|
|
1843
|
+
const _siteName = siteName || args?.site || 'default';
|
|
1122
1844
|
|
|
1123
1845
|
// slug → postId 解決(headless モード時のみ)
|
|
1124
1846
|
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
@@ -1140,6 +1862,27 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1140
1862
|
};
|
|
1141
1863
|
}
|
|
1142
1864
|
|
|
1865
|
+
// --- operations[] 排他チェック ---
|
|
1866
|
+
const snapshotId = args?.snapshotId;
|
|
1867
|
+
if (args?.operations && args.operations.length > 0) {
|
|
1868
|
+
const conflicting = ['target', 'replacements', 'newHTML', 'attributeUpdates', 'insert', 'filePath']
|
|
1869
|
+
.filter(k => args[k] !== undefined);
|
|
1870
|
+
if (conflicting.length > 0) {
|
|
1871
|
+
return {
|
|
1872
|
+
content: [{ type: "text", text: `❌ operations と ${conflicting.join(', ')} は同時に指定できません。operations を使う場合、各操作は operations 内に記述してください。` }],
|
|
1873
|
+
isError: true,
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
if (!snapshotId) {
|
|
1877
|
+
return {
|
|
1878
|
+
content: [{ type: "text", text: `❌ operations には snapshotId が必要です。get_article_structure で取得してください。` }],
|
|
1879
|
+
isError: true,
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
// batch 実行に委譲
|
|
1883
|
+
return await handleBatchOperations(args.operations, snapshotId, mode, client, postId, _sessionId, _siteName, _modeTag, name, args?.expectedRevision);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1143
1886
|
// --- normalizeTarget / normalizeInsert ---
|
|
1144
1887
|
let tp, ins;
|
|
1145
1888
|
try {
|
|
@@ -1149,6 +1892,31 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1149
1892
|
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
1150
1893
|
}
|
|
1151
1894
|
|
|
1895
|
+
// --- ref 解決 + revision チェック(共通ヘルパー) ---
|
|
1896
|
+
const resolved = await resolveRefsAndCheckRevision({
|
|
1897
|
+
snapshotId,
|
|
1898
|
+
ref: tp._ref,
|
|
1899
|
+
refs: tp._refs,
|
|
1900
|
+
expectedRevision: args?.expectedRevision,
|
|
1901
|
+
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
1902
|
+
});
|
|
1903
|
+
if (resolved.error) return resolved.error;
|
|
1904
|
+
if (resolved.index !== undefined) {
|
|
1905
|
+
tp.index = resolved.index;
|
|
1906
|
+
delete tp._ref;
|
|
1907
|
+
}
|
|
1908
|
+
if (resolved.indices !== undefined) {
|
|
1909
|
+
tp.indices = resolved.indices;
|
|
1910
|
+
delete tp._refs;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// 1→N 展開追跡用の refInfo(単体 ref + newHTML 置換時のみ。insert 系は対象外)
|
|
1914
|
+
const _refInfo = (snapshotId && args?.target?.ref && args?.newHTML && !args?.insert) ? {
|
|
1915
|
+
oldSnapshotId: snapshotId,
|
|
1916
|
+
usedRef: args.target.ref,
|
|
1917
|
+
resolvedIndex: tp.index,
|
|
1918
|
+
} : null;
|
|
1919
|
+
|
|
1152
1920
|
const { target, index, indices, startIndex, endIndex, section, blockType, typeIndex,
|
|
1153
1921
|
contains, headingLevel, headingContains } = tp;
|
|
1154
1922
|
const { insertOnly, insertPosition } = ins;
|
|
@@ -1156,6 +1924,11 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1156
1924
|
_fromInsertBlock, appendToEnd } = (args || {});
|
|
1157
1925
|
let { newHTML } = (args || {});
|
|
1158
1926
|
|
|
1927
|
+
// [INSERT_OBSERVE] Step 5: insert_block 経由時の送信パラメータログ
|
|
1928
|
+
if (process.env.FRIDAY_DEBUG === '1' && _fromInsertBlock) {
|
|
1929
|
+
console.log(`[INSERT_OBSERVE] mcp-send: index=${index}, postId=${postId}, sessionId=${_sessionId || 'none'}, mode=${mode}, appendToEnd=${appendToEnd}, htmlLen=${(newHTML || '').length}`);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1159
1932
|
// --- filePath → newHTML 解決(update_blocks 直接呼び出し時) ---
|
|
1160
1933
|
if (args?.filePath) {
|
|
1161
1934
|
if (newHTML) {
|
|
@@ -1196,7 +1969,7 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1196
1969
|
};
|
|
1197
1970
|
}
|
|
1198
1971
|
|
|
1199
|
-
// --- ターゲットチェック(appendToEnd 時は不要) ---
|
|
1972
|
+
// --- ターゲットチェック(appendToEnd / _parentIndex 時は不要) ---
|
|
1200
1973
|
const hasTarget = target || index !== undefined || indices ||
|
|
1201
1974
|
startIndex !== undefined || section || blockType || contains ||
|
|
1202
1975
|
(headingLevel && headingContains);
|
|
@@ -1238,7 +2011,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1238
2011
|
_fromInsertBlock: _fromInsertBlock || false,
|
|
1239
2012
|
});
|
|
1240
2013
|
const count = result.results?.[0]?.count || 1;
|
|
1241
|
-
|
|
2014
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2015
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
1242
2016
|
}
|
|
1243
2017
|
|
|
1244
2018
|
const result = await client.headlessUpdate(postId, {
|
|
@@ -1263,13 +2037,14 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1263
2037
|
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText}${deprecationWarning}` }] };
|
|
1264
2038
|
}
|
|
1265
2039
|
|
|
2040
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
1266
2041
|
if (result.results) {
|
|
1267
2042
|
const successCount = result.results.filter(r => r.success).length;
|
|
1268
2043
|
const failCount = result.results.filter(r => !r.success).length;
|
|
1269
|
-
return { content: [{ type: "text", text: `✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}${deprecationWarning}
|
|
2044
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}${deprecationWarning}`, _snap, _refInfo) }] };
|
|
1270
2045
|
}
|
|
1271
2046
|
|
|
1272
|
-
return { content: [{ type: "text", text: `✅ 更新完了${deprecationWarning}${_modeTag}
|
|
2047
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ 更新完了${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
1273
2048
|
} catch (e) {
|
|
1274
2049
|
const formatted = formatHeadlessConflictError(e);
|
|
1275
2050
|
if (formatted) return formatted;
|
|
@@ -1291,7 +2066,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1291
2066
|
if (!result || result.timeout) return timeoutResponse(name, client, args?.site);
|
|
1292
2067
|
if (!result.success) return errorResponse(name, result.error, args?.site);
|
|
1293
2068
|
const count = result.inserted || 1;
|
|
1294
|
-
|
|
2069
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2070
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
1295
2071
|
}
|
|
1296
2072
|
|
|
1297
2073
|
// target: "selected" の場合 → エディタ状態から選択情報を取得してMCP側で処理
|
|
@@ -1344,7 +2120,10 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1344
2120
|
return timeoutResponse(name, client, args?.site);
|
|
1345
2121
|
if (!result.success)
|
|
1346
2122
|
return errorResponse(name, result.error, args?.site);
|
|
1347
|
-
|
|
2123
|
+
{
|
|
2124
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2125
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${hasTextSelection ? "(カーソル選択モード)" : "(差分)"}${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2126
|
+
}
|
|
1348
2127
|
}
|
|
1349
2128
|
|
|
1350
2129
|
// newHTMLの場合
|
|
@@ -1360,7 +2139,10 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1360
2139
|
return timeoutResponse(name, client, args?.site);
|
|
1361
2140
|
if (!result.success)
|
|
1362
2141
|
return errorResponse(name, result.error, args?.site);
|
|
1363
|
-
|
|
2142
|
+
{
|
|
2143
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2144
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2145
|
+
}
|
|
1364
2146
|
}
|
|
1365
2147
|
|
|
1366
2148
|
// attributeUpdatesの場合
|
|
@@ -1374,12 +2156,15 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1374
2156
|
return timeoutResponse(name, client, args?.site);
|
|
1375
2157
|
if (!result.success)
|
|
1376
2158
|
return errorResponse(name, result.error, args?.site);
|
|
1377
|
-
|
|
2159
|
+
{
|
|
2160
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2161
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ 属性更新完了${deprecationWarning}`, _snap, _refInfo) }] };
|
|
2162
|
+
}
|
|
1378
2163
|
}
|
|
1379
2164
|
|
|
1380
2165
|
// その他のターゲット → 全てWP側で解決&実行
|
|
1381
2166
|
const maxWait = (section || blockType || contains) ? 15000 : 10000;
|
|
1382
|
-
const
|
|
2167
|
+
const editorParams = {
|
|
1383
2168
|
index, indices, startIndex, endIndex,
|
|
1384
2169
|
section, blockType, typeIndex, contains,
|
|
1385
2170
|
headingLevel, headingContains,
|
|
@@ -1388,7 +2173,8 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1388
2173
|
insertPosition,
|
|
1389
2174
|
dryRun: dryRun || false,
|
|
1390
2175
|
_fromInsertBlock: _fromInsertBlock || false,
|
|
1391
|
-
}
|
|
2176
|
+
};
|
|
2177
|
+
const result = await client.sendEditorCommand("update_blocks", editorParams, maxWait, _postId, _sessionId);
|
|
1392
2178
|
if (!result || result.timeout)
|
|
1393
2179
|
return timeoutResponse(name, client, args?.site);
|
|
1394
2180
|
if (!result.success)
|
|
@@ -1412,15 +2198,16 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
1412
2198
|
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText || "(対象なし)"}${deprecationWarning}` }] };
|
|
1413
2199
|
}
|
|
1414
2200
|
|
|
2201
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
1415
2202
|
if (result.results) {
|
|
1416
2203
|
const successCount = result.results.filter(r => r.success && !r.skipped).length;
|
|
1417
2204
|
const skipCount = result.results.filter(r => r.skipped).length;
|
|
1418
2205
|
const failCount = result.results.filter(r => !r.success).length;
|
|
1419
2206
|
const detailText = result.results.filter(r => r.success && r.replaceCount > 0).map(r => ` [${r.index}]: ${r.replaceCount}箇所`).join('\n');
|
|
1420
|
-
return { content: [{ type: "text", text: `✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}${deprecationWarning}${_modeTag}
|
|
2207
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
1421
2208
|
}
|
|
1422
2209
|
|
|
1423
|
-
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}${_modeTag}
|
|
2210
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
|
|
1424
2211
|
}
|
|
1425
2212
|
|
|
1426
2213
|
// ツール実行のハンドラ
|
|
@@ -1475,6 +2262,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1475
2262
|
state = await client.getEditorState(_postId, _sessionId);
|
|
1476
2263
|
}
|
|
1477
2264
|
|
|
2265
|
+
// --- snapshot 作成(allBlocks がある場合のみ) ---
|
|
2266
|
+
let _refTag = (_idx, _prefix) => '';
|
|
2267
|
+
let _snapshotLine = '';
|
|
2268
|
+
|
|
2269
|
+
if (state.allBlocks && state.allBlocks.length > 0) {
|
|
2270
|
+
const snapshotId = generateSnapshotId();
|
|
2271
|
+
const snapshotBlocks = buildSnapshotBlocks(state, mode);
|
|
2272
|
+
const revision = computeRevision(state, mode);
|
|
2273
|
+
const snapshotRecord = {
|
|
2274
|
+
snapshotId, postId, mode,
|
|
2275
|
+
sessionId: mode === 'editor' ? _sessionId : null,
|
|
2276
|
+
siteName: siteName || 'default',
|
|
2277
|
+
createdAt: Date.now(),
|
|
2278
|
+
revision,
|
|
2279
|
+
blocks: snapshotBlocks,
|
|
2280
|
+
displayMode: full ? 'full' : contains ? 'contains' : section ? (blockType ? 'section+blockType' : 'section') : blockType ? 'blockType' : headingLevel ? 'headingLevel' : 'default',
|
|
2281
|
+
};
|
|
2282
|
+
snapshotCache.set(snapshotRecord);
|
|
2283
|
+
|
|
2284
|
+
const _refMap = new Map(snapshotBlocks.map(b => [b.index, b.ref]));
|
|
2285
|
+
_refTag = (idx, prefix = '|') => { const r = _refMap.get(idx); return r ? `${prefix}${r}` : ''; };
|
|
2286
|
+
_snapshotLine = `\n[snapshot:${snapshotId} rev:${revision}]`;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// ツリー構造情報のフォーマットヘルパー
|
|
2290
|
+
function formatTreeLine(b) {
|
|
2291
|
+
const depth = b.depth || 0;
|
|
2292
|
+
const indent = ' '.repeat(depth + 1);
|
|
2293
|
+
let marker = '';
|
|
2294
|
+
let childInfo = '';
|
|
2295
|
+
if (b.isContainer) {
|
|
2296
|
+
marker = '▼ ';
|
|
2297
|
+
if (b.columnCount) childInfo = ` [${b.columnCount}カラム]`;
|
|
2298
|
+
else if (b.tabNames && b.tabNames.length > 0) childInfo = ` [${b.tabNames.join(", ")}]`;
|
|
2299
|
+
else if (b.childCount) childInfo = ` [${b.childCount}子]`;
|
|
2300
|
+
}
|
|
2301
|
+
const parentInfo = (depth > 0 && b.parentIndex != null) ? `, p:${b.parentIndex}` : '';
|
|
2302
|
+
return { indent, marker, childInfo, depthInfo: `(d:${depth}${parentInfo})` };
|
|
2303
|
+
}
|
|
2304
|
+
|
|
1478
2305
|
// ========================================
|
|
1479
2306
|
// 全ブロック情報取得(full=true)
|
|
1480
2307
|
// ========================================
|
|
@@ -1505,7 +2332,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1505
2332
|
|
|
1506
2333
|
const blockList = blocks.map(b => {
|
|
1507
2334
|
const attrPreview = JSON.stringify(b.attributes || {}).slice(0, 200);
|
|
1508
|
-
|
|
2335
|
+
const t = formatTreeLine(b);
|
|
2336
|
+
return `${t.indent}[${b.index}${_refTag(b.index)}] ${t.marker}${b.type}${t.childInfo} - ${b.section || "(記事冒頭)"} ${t.depthInfo}\n${t.indent} ${attrPreview}${attrPreview.length >= 200 ? '...' : ''}`;
|
|
1509
2337
|
}).join("\n\n");
|
|
1510
2338
|
|
|
1511
2339
|
let paginationInfo = '';
|
|
@@ -1519,7 +2347,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1519
2347
|
return {
|
|
1520
2348
|
content: [{
|
|
1521
2349
|
type: "text",
|
|
1522
|
-
text: `📊 get_article_structure(${paramLabel})\n\n全ブロック情報 (${blocks.length}件):\n\n${blockList}${paginationInfo}${_modeTag_gas}`,
|
|
2350
|
+
text: `📊 get_article_structure(${paramLabel})\n\n全ブロック情報 (${blocks.length}件):\n\n${blockList}${paginationInfo}${_snapshotLine}${_modeTag_gas}`,
|
|
1523
2351
|
}],
|
|
1524
2352
|
};
|
|
1525
2353
|
}
|
|
@@ -1556,6 +2384,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1556
2384
|
type: block.type,
|
|
1557
2385
|
section: block.section,
|
|
1558
2386
|
depth: block.depth,
|
|
2387
|
+
parentIndex: block.parentIndex,
|
|
2388
|
+
isContainer: block.isContainer,
|
|
2389
|
+
childCount: block.childCount,
|
|
1559
2390
|
preview: preview
|
|
1560
2391
|
});
|
|
1561
2392
|
}
|
|
@@ -1572,9 +2403,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1572
2403
|
|
|
1573
2404
|
const matchList = matches
|
|
1574
2405
|
.map(m => {
|
|
1575
|
-
|
|
2406
|
+
const t = formatTreeLine(m);
|
|
2407
|
+
let line = `${t.indent}index ${m.index}${_refTag(m.index, ' ')}: ${t.marker}${m.type}${t.childInfo} - ${m.section || "(記事冒頭)"} ${t.depthInfo}`;
|
|
1576
2408
|
if (m.preview) {
|
|
1577
|
-
line += `\n
|
|
2409
|
+
line += `\n${t.indent} "${m.preview}"`;
|
|
1578
2410
|
}
|
|
1579
2411
|
return line;
|
|
1580
2412
|
})
|
|
@@ -1583,7 +2415,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1583
2415
|
return {
|
|
1584
2416
|
content: [{
|
|
1585
2417
|
type: "text",
|
|
1586
|
-
text: `📊 get_article_structure(${paramLabel})\n\n"${contains}" を含むブロック (${matches.length}個):\n\n${matchList}${_modeTag_gas}`,
|
|
2418
|
+
text: `📊 get_article_structure(${paramLabel})\n\n"${contains}" を含むブロック (${matches.length}個):\n\n${matchList}${_snapshotLine}${_modeTag_gas}`,
|
|
1587
2419
|
}],
|
|
1588
2420
|
};
|
|
1589
2421
|
}
|
|
@@ -1613,9 +2445,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1613
2445
|
|
|
1614
2446
|
const matchList = (searchResult.matches || [])
|
|
1615
2447
|
.map(m => {
|
|
1616
|
-
|
|
2448
|
+
const t = formatTreeLine(m);
|
|
2449
|
+
let line = `${t.indent}index ${m.index}${_refTag(m.index, ' ')}: ${t.marker}${m.type}${t.childInfo} - ${m.section || "(記事冒頭)"} ${t.depthInfo}`;
|
|
1617
2450
|
if (m.preview) {
|
|
1618
|
-
line += `\n
|
|
2451
|
+
line += `\n${t.indent} "${m.preview}"`;
|
|
1619
2452
|
}
|
|
1620
2453
|
return line;
|
|
1621
2454
|
})
|
|
@@ -1624,7 +2457,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1624
2457
|
return {
|
|
1625
2458
|
content: [{
|
|
1626
2459
|
type: "text",
|
|
1627
|
-
text: `📊 get_article_structure(${paramLabel})\n\n"${contains}" を含むブロック (${(searchResult.matches || []).length}個):\n\n${matchList}${_modeTag_gas}`,
|
|
2460
|
+
text: `📊 get_article_structure(${paramLabel})\n\n"${contains}" を含むブロック (${(searchResult.matches || []).length}個):\n\n${matchList}${_snapshotLine}${_modeTag_gas}`,
|
|
1628
2461
|
}],
|
|
1629
2462
|
};
|
|
1630
2463
|
}
|
|
@@ -1698,12 +2531,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1698
2531
|
}
|
|
1699
2532
|
|
|
1700
2533
|
const blockList = filteredBlocks
|
|
1701
|
-
.map(b =>
|
|
2534
|
+
.map(b => {
|
|
2535
|
+
const t = formatTreeLine(b);
|
|
2536
|
+
return `${t.indent}index ${b.index}${_refTag(b.index, ' ')}: ${t.marker}${blockType}${t.childInfo} ${t.depthInfo}`;
|
|
2537
|
+
})
|
|
1702
2538
|
.join("\n");
|
|
1703
2539
|
return {
|
|
1704
2540
|
content: [{
|
|
1705
2541
|
type: "text",
|
|
1706
|
-
text: `📊 get_article_structure(${paramLabel})\n\n${sectionHeading.text} セクション内の ${blockType} (${filteredBlocks.length}個):\n\n${blockList}${_modeTag_gas}`,
|
|
2542
|
+
text: `📊 get_article_structure(${paramLabel})\n\n${sectionHeading.text} セクション内の ${blockType} (${filteredBlocks.length}個):\n\n${blockList}${_snapshotLine}${_modeTag_gas}`,
|
|
1707
2543
|
}],
|
|
1708
2544
|
};
|
|
1709
2545
|
}
|
|
@@ -1723,19 +2559,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1723
2559
|
const typeData = state.blockSummary[blockType];
|
|
1724
2560
|
const blockList = typeData.blocks
|
|
1725
2561
|
.map(b => {
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
info += ` [${b.tabNames.join(", ")}]`;
|
|
1729
|
-
} else if (b.columnCount) {
|
|
1730
|
-
info += ` [${b.columnCount}カラム]`;
|
|
1731
|
-
}
|
|
1732
|
-
return info;
|
|
2562
|
+
const t = formatTreeLine(b);
|
|
2563
|
+
return `${t.indent}index ${b.index}${_refTag(b.index, ' ')}: ${t.marker}${b.section || "(記事冒頭)"} (H${b.sectionLevel || "-"})${t.childInfo} ${t.depthInfo}`;
|
|
1733
2564
|
})
|
|
1734
2565
|
.join("\n");
|
|
1735
2566
|
return {
|
|
1736
2567
|
content: [{
|
|
1737
2568
|
type: "text",
|
|
1738
|
-
text: `📊 get_article_structure(${paramLabel})\n\n${blockType} の一覧 (${typeData.count}個):\n\n${blockList}${_modeTag_gas}`,
|
|
2569
|
+
text: `📊 get_article_structure(${paramLabel})\n\n${blockType} の一覧 (${typeData.count}個):\n\n${blockList}${_snapshotLine}${_modeTag_gas}`,
|
|
1739
2570
|
}],
|
|
1740
2571
|
};
|
|
1741
2572
|
}
|
|
@@ -1793,20 +2624,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1793
2624
|
|
|
1794
2625
|
const blockList = sectionBlocks
|
|
1795
2626
|
.map(b => {
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
let info = '';
|
|
1799
|
-
if (b.isContainer) {
|
|
1800
|
-
marker = '▼ ';
|
|
1801
|
-
if (b.columnCount) {
|
|
1802
|
-
info = ` [${b.columnCount}カラム]`;
|
|
1803
|
-
} else if (b.tabNames && b.tabNames.length > 0) {
|
|
1804
|
-
info = ` [${b.tabNames.join(", ")}]`;
|
|
1805
|
-
} else if (b.childCount) {
|
|
1806
|
-
info = ` [${b.childCount}子]`;
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
return `${indent}${b.index}: ${marker}${b.type}${info} (d:${b.depth || 0})`;
|
|
2627
|
+
const t = formatTreeLine(b);
|
|
2628
|
+
return `${t.indent}${b.index}${_refTag(b.index, ' ')}: ${t.marker}${b.type}${t.childInfo} ${t.depthInfo}`;
|
|
1810
2629
|
})
|
|
1811
2630
|
.join("\n");
|
|
1812
2631
|
|
|
@@ -1817,7 +2636,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1817
2636
|
return {
|
|
1818
2637
|
content: [{
|
|
1819
2638
|
type: "text",
|
|
1820
|
-
text: `📊 get_article_structure(${paramLabel})\n\n${sectionHeading.text} セクション (index:${sectionHeading.index}):\n\n${blockList}${nextInfo}${_modeTag_gas}`,
|
|
2639
|
+
text: `📊 get_article_structure(${paramLabel})\n\n${sectionHeading.text} セクション (index:${sectionHeading.index}):\n\n${blockList}${nextInfo}${_snapshotLine}${_modeTag_gas}`,
|
|
1821
2640
|
}],
|
|
1822
2641
|
};
|
|
1823
2642
|
}
|
|
@@ -1836,12 +2655,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1836
2655
|
};
|
|
1837
2656
|
}
|
|
1838
2657
|
const headingList = filteredHeadings
|
|
1839
|
-
.map(h => ` ${"#".repeat(h.level)} ${h.text} (index:${h.index})`)
|
|
2658
|
+
.map(h => ` ${"#".repeat(h.level)} ${h.text} (index:${h.index}${_refTag(h.index)})`)
|
|
1840
2659
|
.join("\n");
|
|
1841
2660
|
return {
|
|
1842
2661
|
content: [{
|
|
1843
2662
|
type: "text",
|
|
1844
|
-
text: `📊 get_article_structure(${paramLabel})\n\nH${headingLevel}見出し一覧 (${filteredHeadings.length}個):\n\n${headingList}${_modeTag_gas}`,
|
|
2663
|
+
text: `📊 get_article_structure(${paramLabel})\n\nH${headingLevel}見出し一覧 (${filteredHeadings.length}個):\n\n${headingList}${_snapshotLine}${_modeTag_gas}`,
|
|
1845
2664
|
}],
|
|
1846
2665
|
};
|
|
1847
2666
|
}
|
|
@@ -1854,11 +2673,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1854
2673
|
for (const heading of state.headings) {
|
|
1855
2674
|
const indent = heading.level === 2 ? "" : " ";
|
|
1856
2675
|
const prefix = "#".repeat(heading.level);
|
|
1857
|
-
output += `${indent}${prefix} ${heading.text} (H${heading.level}, index:${heading.index})\n`;
|
|
2676
|
+
output += `${indent}${prefix} ${heading.text} (H${heading.level}, index:${heading.index}${_refTag(heading.index)})\n`;
|
|
1858
2677
|
}
|
|
1859
2678
|
|
|
1860
2679
|
return {
|
|
1861
|
-
content: [{ type: "text", text: output + _modeTag_gas }],
|
|
2680
|
+
content: [{ type: "text", text: output + _snapshotLine + _modeTag_gas }],
|
|
1862
2681
|
};
|
|
1863
2682
|
}
|
|
1864
2683
|
|
|
@@ -1916,7 +2735,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1916
2735
|
}
|
|
1917
2736
|
|
|
1918
2737
|
case "delete_block": {
|
|
1919
|
-
let { mode, postId: _postId, sessionId: _sessionId, message, client } = await resolveMode(args, name);
|
|
2738
|
+
let { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
|
|
1920
2739
|
if (mode === 'error') {
|
|
1921
2740
|
return errorResponse(name, message, args?.site);
|
|
1922
2741
|
}
|
|
@@ -1925,13 +2744,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1925
2744
|
if (_resolved.editorConnected) mode = 'editor';
|
|
1926
2745
|
const postId = _resolved.postId ?? _postId;
|
|
1927
2746
|
const _mt_db = process.env.FRIDAY_DEBUG === '1' ? `\n[DEBUG] mode=${mode}` : '';
|
|
2747
|
+
const _siteName = siteName || args?.site || 'default';
|
|
1928
2748
|
|
|
1929
2749
|
if (mode === 'headless') {
|
|
1930
2750
|
const _guard = await guardHeadlessConflict(postId, client, name);
|
|
1931
2751
|
if (_guard) return _guard;
|
|
1932
2752
|
}
|
|
1933
2753
|
|
|
1934
|
-
|
|
2754
|
+
// ref/refs と index/indices 排他チェック
|
|
2755
|
+
if ((args?.ref || args?.refs) && (args?.index !== undefined || args?.indices !== undefined)) {
|
|
2756
|
+
return { content: [{ type: "text", text: "❌ ref/refs と index/indices は同時に指定できません。" }], isError: true };
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// ref 解決 + revision チェック
|
|
2760
|
+
const resolved = await resolveRefsAndCheckRevision({
|
|
2761
|
+
snapshotId: args?.snapshotId, ref: args?.ref, refs: args?.refs,
|
|
2762
|
+
expectedRevision: args?.expectedRevision,
|
|
2763
|
+
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2764
|
+
});
|
|
2765
|
+
if (resolved.error) return resolved.error;
|
|
2766
|
+
|
|
2767
|
+
let { index, count, indices } = args;
|
|
2768
|
+
if (resolved.index !== undefined) { index = resolved.index; count = count || 1; }
|
|
2769
|
+
if (resolved.indices !== undefined) { indices = resolved.indices; }
|
|
1935
2770
|
|
|
1936
2771
|
// indices と index/count の排他バリデーション
|
|
1937
2772
|
if (indices !== undefined && (index !== undefined || count !== undefined)) {
|
|
@@ -1953,7 +2788,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1953
2788
|
try {
|
|
1954
2789
|
const result = await client.headlessDeleteMultiple(postId, uniqueIndices);
|
|
1955
2790
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
1956
|
-
|
|
2791
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2792
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`, _snap) }] };
|
|
1957
2793
|
} catch (e) {
|
|
1958
2794
|
const formatted = formatHeadlessConflictError(e);
|
|
1959
2795
|
if (formatted) return formatted;
|
|
@@ -1967,7 +2803,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1967
2803
|
if (!result.success)
|
|
1968
2804
|
return errorResponse(name, result.error, args?.site);
|
|
1969
2805
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
1970
|
-
|
|
2806
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2807
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`, _snap) }] };
|
|
1971
2808
|
}
|
|
1972
2809
|
|
|
1973
2810
|
// 既存モード(index + count)
|
|
@@ -1978,7 +2815,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1978
2815
|
try {
|
|
1979
2816
|
const result = await client.headlessDelete(postId, index, count || 1);
|
|
1980
2817
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
1981
|
-
|
|
2818
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2819
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`, _snap) }] };
|
|
1982
2820
|
} catch (e) {
|
|
1983
2821
|
const formatted = formatHeadlessConflictError(e);
|
|
1984
2822
|
if (formatted) return formatted;
|
|
@@ -1992,11 +2830,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1992
2830
|
if (!result.success)
|
|
1993
2831
|
return errorResponse(name, result.error, args?.site);
|
|
1994
2832
|
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
1995
|
-
|
|
2833
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2834
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`, _snap) }] };
|
|
1996
2835
|
}
|
|
1997
2836
|
|
|
1998
2837
|
case "move_block": {
|
|
1999
|
-
let { mode, postId: _postId, sessionId: _sessionId, message, client } = await resolveMode(args, name);
|
|
2838
|
+
let { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
|
|
2000
2839
|
if (mode === 'error') {
|
|
2001
2840
|
return errorResponse(name, message, args?.site);
|
|
2002
2841
|
}
|
|
@@ -2005,13 +2844,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2005
2844
|
if (_resolved.editorConnected) mode = 'editor';
|
|
2006
2845
|
const postId = _resolved.postId ?? _postId;
|
|
2007
2846
|
const _mt_mb = process.env.FRIDAY_DEBUG === '1' ? `\n[DEBUG] mode=${mode}` : '';
|
|
2847
|
+
const _siteName = siteName || args?.site || 'default';
|
|
2008
2848
|
|
|
2009
2849
|
if (mode === 'headless') {
|
|
2010
2850
|
const _guard = await guardHeadlessConflict(postId, client, name);
|
|
2011
2851
|
if (_guard) return _guard;
|
|
2012
2852
|
}
|
|
2013
2853
|
|
|
2014
|
-
const { from, to
|
|
2854
|
+
const { from, to } = args;
|
|
2855
|
+
let { fromFlat, toFlat } = args;
|
|
2015
2856
|
const count = args.count ?? 1;
|
|
2016
2857
|
|
|
2017
2858
|
// count バリデーション
|
|
@@ -2019,14 +2860,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2019
2860
|
return { content: [{ type: "text", text: "❌ count は1以上の整数を指定してください。" }], isError: true };
|
|
2020
2861
|
}
|
|
2021
2862
|
|
|
2022
|
-
//
|
|
2863
|
+
// 3 モード排他バリデーション
|
|
2023
2864
|
const hasTopLevel = from !== undefined || to !== undefined;
|
|
2024
2865
|
const hasFlat = fromFlat !== undefined || toFlat !== undefined;
|
|
2025
|
-
|
|
2026
|
-
|
|
2866
|
+
const hasRef = args?.fromRef !== undefined || args?.beforeRef !== undefined || args?.afterRef !== undefined;
|
|
2867
|
+
const _modeCount = [hasTopLevel, hasFlat, hasRef].filter(Boolean).length;
|
|
2868
|
+
|
|
2869
|
+
if (_modeCount > 1) {
|
|
2870
|
+
return { content: [{ type: "text", text: "❌ from/to, fromFlat/toFlat, ref(fromRef/beforeRef/afterRef) は同時に指定できません。" }], isError: true };
|
|
2871
|
+
}
|
|
2872
|
+
if (_modeCount === 0) {
|
|
2873
|
+
return { content: [{ type: "text", text: "❌ from/to, fromFlat/toFlat, または fromRef+beforeRef/afterRef を指定してください。" }], isError: true };
|
|
2027
2874
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2875
|
+
|
|
2876
|
+
// expectedRevision チェック(全モード共通)
|
|
2877
|
+
// ref モード以外でも expectedRevision が指定されていれば revision チェックを実施
|
|
2878
|
+
if (args?.expectedRevision && !hasRef) {
|
|
2879
|
+
const { error: _revError } = await acquireFreshState({
|
|
2880
|
+
expectedRevision: args.expectedRevision,
|
|
2881
|
+
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2882
|
+
});
|
|
2883
|
+
if (_revError) return _revError;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
// ref モード: fromRef + beforeRef/afterRef
|
|
2887
|
+
if (hasRef) {
|
|
2888
|
+
if (!args.fromRef) {
|
|
2889
|
+
return { content: [{ type: "text", text: "❌ ref モードでは fromRef が必須です。" }], isError: true };
|
|
2890
|
+
}
|
|
2891
|
+
if (!args.beforeRef && !args.afterRef) {
|
|
2892
|
+
return { content: [{ type: "text", text: "❌ ref モードでは beforeRef または afterRef が必須です。" }], isError: true };
|
|
2893
|
+
}
|
|
2894
|
+
if (args.beforeRef && args.afterRef) {
|
|
2895
|
+
return { content: [{ type: "text", text: "❌ beforeRef と afterRef は同時に指定できません。" }], isError: true };
|
|
2896
|
+
}
|
|
2897
|
+
if (!args.snapshotId) {
|
|
2898
|
+
return { content: [{ type: "text", text: "❌ ref を使用するには snapshotId が必要です。get_article_structure で取得してください。" }], isError: true };
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
// acquireFreshState で 1 回だけ state 取得 + revision チェック
|
|
2902
|
+
const { currentState, error: _stateError } = await acquireFreshState({
|
|
2903
|
+
expectedRevision: args.expectedRevision,
|
|
2904
|
+
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
2905
|
+
});
|
|
2906
|
+
if (_stateError) return _stateError;
|
|
2907
|
+
|
|
2908
|
+
// ref 解決(同じ currentState で 2 つの ref を解決)
|
|
2909
|
+
try {
|
|
2910
|
+
fromFlat = resolveRefFromState(args.snapshotId, args.fromRef, mode, _sessionId, postId, currentState);
|
|
2911
|
+
const destRef = args.beforeRef || args.afterRef;
|
|
2912
|
+
const resolvedDest = resolveRefFromState(args.snapshotId, destRef, mode, _sessionId, postId, currentState);
|
|
2913
|
+
// 位置計算: beforeRef → そのまま, afterRef → +1
|
|
2914
|
+
toFlat = args.beforeRef ? resolvedDest : resolvedDest + 1;
|
|
2915
|
+
} catch (e) {
|
|
2916
|
+
return { content: [{ type: "text", text: `❌ ref 解決エラー: ${e.message}` }], isError: true };
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
// ref → flat に変換完了。以降 flat モードに合流
|
|
2030
2920
|
}
|
|
2031
2921
|
|
|
2032
2922
|
// 移動結果のレスポンス生成ヘルパー
|
|
@@ -2039,8 +2929,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2039
2929
|
: `✅ ブロック移動 (${typeLabel})${_mt_mb}`;
|
|
2040
2930
|
};
|
|
2041
2931
|
|
|
2042
|
-
// fromFlat/toFlat
|
|
2043
|
-
if (hasFlat) {
|
|
2932
|
+
// fromFlat/toFlat モード(フラットインデックス移動)— ref モードもここに合流
|
|
2933
|
+
if (hasFlat || hasRef) {
|
|
2044
2934
|
if (fromFlat === undefined || toFlat === undefined) {
|
|
2045
2935
|
return { content: [{ type: "text", text: "❌ fromFlat と toFlat の両方を指定してください。" }], isError: true };
|
|
2046
2936
|
}
|
|
@@ -2051,7 +2941,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2051
2941
|
if (mode === 'headless') {
|
|
2052
2942
|
try {
|
|
2053
2943
|
const result = await client.headlessMoveFlat(postId, fromFlat, toFlat, count);
|
|
2054
|
-
|
|
2944
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2945
|
+
return { content: [{ type: "text", text: appendSnapshotToText(moveMsg(result.moved), _snap) }] };
|
|
2055
2946
|
} catch (e) {
|
|
2056
2947
|
const formatted = formatHeadlessConflictError(e);
|
|
2057
2948
|
if (formatted) return formatted;
|
|
@@ -2064,7 +2955,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2064
2955
|
return timeoutResponse(name, client, args?.site);
|
|
2065
2956
|
if (!result.success)
|
|
2066
2957
|
return errorResponse(name, result.error, args?.site);
|
|
2067
|
-
|
|
2958
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2959
|
+
return { content: [{ type: "text", text: appendSnapshotToText(moveMsg(result.moved), _snap) }] };
|
|
2068
2960
|
}
|
|
2069
2961
|
|
|
2070
2962
|
// 既存モード(from/to トップレベル)
|
|
@@ -2075,7 +2967,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2075
2967
|
if (mode === 'headless') {
|
|
2076
2968
|
try {
|
|
2077
2969
|
const result = await client.headlessMove(postId, from, to, count);
|
|
2078
|
-
|
|
2970
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2971
|
+
return { content: [{ type: "text", text: appendSnapshotToText(moveMsg(result.moved), _snap) }] };
|
|
2079
2972
|
} catch (e) {
|
|
2080
2973
|
const formatted = formatHeadlessConflictError(e);
|
|
2081
2974
|
if (formatted) return formatted;
|
|
@@ -2088,7 +2981,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2088
2981
|
return timeoutResponse(name, client, args?.site);
|
|
2089
2982
|
if (!result.success)
|
|
2090
2983
|
return errorResponse(name, result.error, args?.site);
|
|
2091
|
-
|
|
2984
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
2985
|
+
return { content: [{ type: "text", text: appendSnapshotToText(moveMsg(result.moved), _snap) }] };
|
|
2092
2986
|
}
|
|
2093
2987
|
|
|
2094
2988
|
case "undo": {
|
|
@@ -2124,7 +3018,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2124
3018
|
}
|
|
2125
3019
|
|
|
2126
3020
|
case "duplicate_block": {
|
|
2127
|
-
let { mode, postId: _postId, sessionId: _sessionId, message, client } = await resolveMode(args, name);
|
|
3021
|
+
let { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
|
|
2128
3022
|
if (mode === 'error') {
|
|
2129
3023
|
return errorResponse(name, message, args?.site);
|
|
2130
3024
|
}
|
|
@@ -2133,8 +3027,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2133
3027
|
if (_resolved.editorConnected) mode = 'editor';
|
|
2134
3028
|
const postId = _resolved.postId ?? _postId;
|
|
2135
3029
|
const _mt_dup = process.env.FRIDAY_DEBUG === '1' ? `\n[DEBUG] mode=${mode}` : '';
|
|
3030
|
+
const _siteName = siteName || args?.site || 'default';
|
|
3031
|
+
|
|
3032
|
+
// ref と index 排他チェック
|
|
3033
|
+
if (args?.ref && args?.index !== undefined) {
|
|
3034
|
+
return { content: [{ type: "text", text: "❌ ref と index は同時に指定できません。" }], isError: true };
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// ref 解決 + revision チェック
|
|
3038
|
+
const resolved = await resolveRefsAndCheckRevision({
|
|
3039
|
+
snapshotId: args?.snapshotId, ref: args?.ref,
|
|
3040
|
+
expectedRevision: args?.expectedRevision,
|
|
3041
|
+
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
3042
|
+
});
|
|
3043
|
+
if (resolved.error) return resolved.error;
|
|
2136
3044
|
|
|
2137
|
-
const
|
|
3045
|
+
const index = resolved.index !== undefined ? resolved.index : args?.index;
|
|
2138
3046
|
|
|
2139
3047
|
if (mode === 'headless') {
|
|
2140
3048
|
const _guard = await guardHeadlessConflict(postId, client, name);
|
|
@@ -2144,7 +3052,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2144
3052
|
}
|
|
2145
3053
|
try {
|
|
2146
3054
|
const result = await client.headlessDuplicate(postId, index);
|
|
2147
|
-
|
|
3055
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3056
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ブロック複製完了 (index ${result.duplicated.index} → ${result.duplicated.newIndex})${_mt_dup}`, _snap) }] };
|
|
2148
3057
|
} catch (e) {
|
|
2149
3058
|
const formatted = formatHeadlessConflictError(e);
|
|
2150
3059
|
if (formatted) return formatted;
|
|
@@ -2157,7 +3066,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2157
3066
|
return timeoutResponse(name, client, args?.site);
|
|
2158
3067
|
if (!result.success)
|
|
2159
3068
|
return errorResponse(name, result.error, args?.site);
|
|
2160
|
-
|
|
3069
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3070
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ブロック複製完了${_mt_dup}`, _snap) }] };
|
|
2161
3071
|
}
|
|
2162
3072
|
|
|
2163
3073
|
case "save_post": {
|
|
@@ -2362,13 +3272,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2362
3272
|
}
|
|
2363
3273
|
|
|
2364
3274
|
case "insert_block": {
|
|
2365
|
-
let { rawHTML, filePath, index } = (args || {});
|
|
3275
|
+
let { rawHTML, filePath, index, position } = (args || {});
|
|
3276
|
+
|
|
3277
|
+
// parentIndex は廃止済み(v3.0.0 Phase 5)— position を使用
|
|
3278
|
+
if (args?.parentIndex !== undefined) {
|
|
3279
|
+
return { content: [{ type: "text", text: "❌ parentIndex は廃止されました。代わりに index + position ('before'/'after') を使用してください。" }], isError: true };
|
|
3280
|
+
}
|
|
2366
3281
|
|
|
2367
3282
|
// 排他チェック
|
|
2368
3283
|
if (rawHTML && filePath) {
|
|
2369
3284
|
return { content: [{ type: "text", text: "❌ rawHTML と filePath は同時に指定できません。どちらか一方を指定してください。" }], isError: true };
|
|
2370
3285
|
}
|
|
2371
3286
|
|
|
3287
|
+
// Phase 4b: beforeRef/afterRef と index/position の排他チェック
|
|
3288
|
+
const _hasRefPosition = args?.beforeRef !== undefined || args?.afterRef !== undefined;
|
|
3289
|
+
const _hasIndexPosition = index !== undefined || position !== undefined;
|
|
3290
|
+
if (_hasRefPosition && _hasIndexPosition) {
|
|
3291
|
+
return { content: [{ type: "text", text: "❌ beforeRef/afterRef と index/position は同時に指定できません。" }], isError: true };
|
|
3292
|
+
}
|
|
3293
|
+
if (args?.beforeRef && args?.afterRef) {
|
|
3294
|
+
return { content: [{ type: "text", text: "❌ beforeRef と afterRef は同時に指定できません。" }], isError: true };
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// position 指定時は index 必須(ref モードでないとき)
|
|
3298
|
+
if (!_hasRefPosition && position !== undefined && index === undefined) {
|
|
3299
|
+
return { content: [{ type: "text", text: "❌ position を指定する場合は index も指定してください。末尾追加は index を省略してください。" }], isError: true };
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
// position バリデーション
|
|
3303
|
+
if (position !== undefined && position !== "before" && position !== "after") {
|
|
3304
|
+
return { content: [{ type: "text", text: `❌ position は 'before' または 'after' を指定してください: ${position}` }], isError: true };
|
|
3305
|
+
}
|
|
3306
|
+
|
|
2372
3307
|
// filePath → rawHTML 解決
|
|
2373
3308
|
if (filePath) {
|
|
2374
3309
|
try { rawHTML = readHTMLFromFile(filePath).html; }
|
|
@@ -2378,21 +3313,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2378
3313
|
return { content: [{ type: "text", text: "❌ rawHTML または filePath を指定してください。Gutenberg HTML 形式のブロックマークアップが必要です。" }], isError: true };
|
|
2379
3314
|
}
|
|
2380
3315
|
|
|
2381
|
-
// index バリデーション(現行 Editor 互換: 整数 >= 0
|
|
2382
|
-
if (index !== undefined && (!Number.isInteger(index) || index < 0)) {
|
|
3316
|
+
// index バリデーション(現行 Editor 互換: 整数 >= 0)— ref モードでないとき
|
|
3317
|
+
if (!_hasRefPosition && index !== undefined && (!Number.isInteger(index) || index < 0)) {
|
|
2383
3318
|
return { content: [{ type: "text", text: `❌ Invalid index: ${index}` }], isError: true };
|
|
2384
3319
|
}
|
|
2385
3320
|
|
|
2386
|
-
// update_blocks
|
|
3321
|
+
// update_blocks コードパスに委譲
|
|
3322
|
+
// expectedRevision は全モードで委譲(ref/index/append 問わず)
|
|
2387
3323
|
const delegatedArgs = {
|
|
2388
3324
|
postId: args.postId,
|
|
2389
3325
|
site: args.site,
|
|
2390
3326
|
newHTML: rawHTML,
|
|
2391
|
-
insert: { position: 'before' },
|
|
2392
3327
|
_fromInsertBlock: true,
|
|
2393
|
-
|
|
3328
|
+
expectedRevision: args.expectedRevision,
|
|
2394
3329
|
};
|
|
2395
|
-
|
|
3330
|
+
|
|
3331
|
+
if (_hasRefPosition) {
|
|
3332
|
+
// ref モード: target.ref + insert.position で委譲
|
|
3333
|
+
const _ref = args.beforeRef || args.afterRef;
|
|
3334
|
+
delegatedArgs.target = { ref: _ref };
|
|
3335
|
+
delegatedArgs.insert = { position: args.beforeRef ? 'before' : 'after' };
|
|
3336
|
+
delegatedArgs.snapshotId = args.snapshotId;
|
|
3337
|
+
delegatedArgs.appendToEnd = false;
|
|
3338
|
+
} else if (index !== undefined) {
|
|
3339
|
+
// 既存: index モード
|
|
3340
|
+
delegatedArgs.target = { index };
|
|
3341
|
+
delegatedArgs.insert = { position: position ?? 'before' };
|
|
3342
|
+
delegatedArgs.appendToEnd = false;
|
|
3343
|
+
} else {
|
|
3344
|
+
// 末尾追加
|
|
3345
|
+
delegatedArgs.insert = { position: 'before' };
|
|
3346
|
+
delegatedArgs.appendToEnd = true;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
2396
3349
|
return await handleUpdateBlocksTool(delegatedArgs, name);
|
|
2397
3350
|
}
|
|
2398
3351
|
|
|
@@ -2510,7 +3463,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2510
3463
|
}
|
|
2511
3464
|
|
|
2512
3465
|
case "table_operations": {
|
|
2513
|
-
let { mode, postId: _postId, sessionId: _sessionId, message, client } = await resolveMode(args, name);
|
|
3466
|
+
let { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
|
|
2514
3467
|
if (mode === 'error') {
|
|
2515
3468
|
return errorResponse(name, message, args?.site);
|
|
2516
3469
|
}
|
|
@@ -2518,15 +3471,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2518
3471
|
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
2519
3472
|
if (_resolved.editorConnected) mode = 'editor';
|
|
2520
3473
|
const postId = _resolved.postId ?? _postId;
|
|
3474
|
+
const _siteName = siteName || args?.site || 'default';
|
|
2521
3475
|
|
|
2522
|
-
const {
|
|
2523
|
-
if (index === undefined) {
|
|
2524
|
-
return { content: [{ type: "text", text: "❌ index は必須です" }], isError: true };
|
|
2525
|
-
}
|
|
3476
|
+
const { action } = (args || {});
|
|
2526
3477
|
if (!action) {
|
|
2527
3478
|
return { content: [{ type: "text", text: "❌ action は必須です" }], isError: true };
|
|
2528
3479
|
}
|
|
2529
3480
|
|
|
3481
|
+
// ref と index 排他チェック
|
|
3482
|
+
if (args?.ref && args?.index !== undefined) {
|
|
3483
|
+
return { content: [{ type: "text", text: "❌ ref と index は同時に指定できません。" }], isError: true };
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
// ref 解決 + revision チェック
|
|
3487
|
+
const resolved = await resolveRefsAndCheckRevision({
|
|
3488
|
+
snapshotId: args?.snapshotId, ref: args?.ref,
|
|
3489
|
+
expectedRevision: args?.expectedRevision,
|
|
3490
|
+
mode, client, postId, sessionId: _sessionId, siteName: _siteName,
|
|
3491
|
+
});
|
|
3492
|
+
if (resolved.error) return resolved.error;
|
|
3493
|
+
|
|
3494
|
+
const index = resolved.index !== undefined ? resolved.index : args?.index;
|
|
3495
|
+
if (index === undefined) {
|
|
3496
|
+
return { content: [{ type: "text", text: "❌ table_operations では index または ref のいずれかが必要です。" }], isError: true };
|
|
3497
|
+
}
|
|
3498
|
+
|
|
2530
3499
|
const tableParams = {
|
|
2531
3500
|
index, action,
|
|
2532
3501
|
row: args.row, col: args.col,
|
|
@@ -2557,16 +3526,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2557
3526
|
return text;
|
|
2558
3527
|
};
|
|
2559
3528
|
|
|
3529
|
+
const _isWrite = action !== 'get_structure';
|
|
2560
3530
|
const _mt_to = process.env.FRIDAY_DEBUG === '1' ? `\n[DEBUG] mode=${mode}` : '';
|
|
2561
3531
|
if (mode === 'headless') {
|
|
2562
3532
|
const _guard = await guardHeadlessConflict(postId, client, name);
|
|
2563
3533
|
if (_guard) return _guard;
|
|
2564
3534
|
try {
|
|
2565
3535
|
const result = await client.headlessTableOperation(postId, tableParams);
|
|
2566
|
-
if (
|
|
3536
|
+
if (!_isWrite) {
|
|
2567
3537
|
return { content: [{ type: "text", text: formatStructure(result) + _mt_to }] };
|
|
2568
3538
|
}
|
|
2569
|
-
|
|
3539
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3540
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${action} 完了: ${JSON.stringify(result.detail || result)}${_mt_to}`, _snap) }] };
|
|
2570
3541
|
} catch (e) {
|
|
2571
3542
|
const formatted = formatHeadlessConflictError(e);
|
|
2572
3543
|
if (formatted) return formatted;
|
|
@@ -2581,10 +3552,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2581
3552
|
if (!result.success)
|
|
2582
3553
|
return errorResponse(name, result.error, args?.site);
|
|
2583
3554
|
|
|
2584
|
-
if (
|
|
3555
|
+
if (!_isWrite) {
|
|
2585
3556
|
return { content: [{ type: "text", text: formatStructure(result.structure) + _mt_to }] };
|
|
2586
3557
|
}
|
|
2587
|
-
|
|
3558
|
+
const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
|
|
3559
|
+
return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${action} 完了: ${JSON.stringify(result.detail || {})}${_mt_to}`, _snap) }] };
|
|
2588
3560
|
}
|
|
2589
3561
|
|
|
2590
3562
|
case "open_in_browser": {
|