mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 20:49:53 +08:00
feat(mcp-server): saved sessions, restore_version and History on disk (#981)
* feat(mcp-server): list and resume saved diagram sessions start_session accepts an optional session_id to continue a saved diagram under the same preview URL and auto-save file, reporting the restored pages. New read-only tool list_saved_diagrams prints every auto-saved file newest first with its pages. Autosaver gains list() and dataDir(). INSTRUCTIONS and both READMEs mention the new tool. * feat(mcp-server): add restore_version tool for undo and redo from History Add the restore_version write tool so the model can put an earlier version from History back on the canvas (steps_back, default 1). The current canvas is kept in History unless it is the blank page, so a redo is possible. http-server.ts exports restoreHistoryEntry, shared by the HTTP restore endpoint and the tool; history.ts exports HistoryEntry and otherVersions (distinct versions other than the canvas, newest first). The drawing guide tells the model to call restore_version when the user asks to undo, and both READMEs document the tool. * feat(mcp-server): save the diagram History next to the auto-saved file The Autosaver writes each session's History (the XML of its last 20 versions, without thumbnails) to <session-id>.history.json beside the .drawio file, after the same 1 second delay and only when the entries changed. When a saved session is resumed after an idle timeout or a process restart, the saved History is loaded back before the diagram, so the History panel and restore_version keep working. Removing the oldest files also removes their History files. * fix(mcp-server): review fixes for saved sessions, undo tool, history on disk * fix(mcp-server): Codex review fixes for saved sessions, undo and History on disk - start_session resumes every saved file, also a cleared one or one of empty named pages, and decompresses pages draw.io compressed - History kept by a lost user edit or a recovering tab is saved too - A diagram cleared before its first save keeps its History on disk - The History file is written again after the 50-file cap removed it - restore_version compares versions like the edit gate (a bare mxGraphModel from the browser has no page name), checks the restore result, and says "none" when no version is left - The preview closes an open History list when the server state was recreated, since the entries got new ids - start_session's description mentions session_id * fix(mcp-server): compare History versions pairwise and tie restores to the server state Second Codex review of the History fixes: - One bare mxGraphModel in History no longer hides page renames in every comparison: names are left out only when one of the two is bare - A History list asked for before the server recreated the session is dropped, and a restore names the state its list belongs to; the server refuses one from a lost state (History ids change after a restart) * fix(mcp-server): restore from a History list with the state it was taken in Third Codex review: a restore sent the tab's current state, so an old list shown after a failed refresh passed the server's check. The list now keeps the state it came from, and the server refuses a missing (null) state too; only a tab of an older version, which sends none, is let through.
This commit is contained in:
@@ -569,6 +569,8 @@ function handleStateApi(
|
||||
data.xml !== current.xml
|
||||
if (saved) {
|
||||
addHistory(sessionId, data.xml, data.svg || "")
|
||||
// Saved with the History, as after any change
|
||||
stateListener?.(sessionId, current.xml)
|
||||
}
|
||||
res.writeHead(409, {
|
||||
"Content-Type": "application/json",
|
||||
@@ -605,6 +607,7 @@ function handleStateApi(
|
||||
// it in history so the user can restore it.
|
||||
addHistory(sessionId, data.xml, data.svg || "")
|
||||
savedToHistory = true
|
||||
if (current) stateListener?.(sessionId, current.xml)
|
||||
}
|
||||
res.writeHead(409, { "Content-Type": "application/json" })
|
||||
res.end(
|
||||
@@ -670,6 +673,36 @@ function handleHistoryApi(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a History entry back on the canvas (the preview page's Restore button
|
||||
* and the restore_version tool). Returns the new version, or null when the
|
||||
* entry is unknown.
|
||||
*/
|
||||
export function restoreHistoryEntry(
|
||||
sessionId: string,
|
||||
entryId: number,
|
||||
): number | null {
|
||||
const entry = getHistoryEntry(sessionId, entryId)
|
||||
if (!entry) return null
|
||||
|
||||
// Edits in the browser since the last entry are not in history
|
||||
// yet: keep them, so the restore can be undone
|
||||
// (any state besides a blank page; a cleared document with its
|
||||
// own pages counts)
|
||||
const current = stateStore.get(sessionId)
|
||||
if (
|
||||
current &&
|
||||
contentFingerprint(current.xml) !== contentFingerprint(BLANK_MXFILE)
|
||||
) {
|
||||
keepInHistory(sessionId, current.xml, current.svg)
|
||||
}
|
||||
const newVersion = setState(sessionId, entry.xml)
|
||||
addHistory(sessionId, entry.xml, entry.svg)
|
||||
|
||||
log.info(`Restored session ${sessionId} to history entry ${entryId}`)
|
||||
return newVersion
|
||||
}
|
||||
|
||||
function handleRestoreApi(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
@@ -682,37 +715,34 @@ function handleRestoreApi(
|
||||
|
||||
readBody(req, res, (body) => {
|
||||
try {
|
||||
const { sessionId, id } = JSON.parse(body)
|
||||
const data = JSON.parse(body)
|
||||
const { sessionId, id } = data
|
||||
if (!sessionId || typeof id !== "number") {
|
||||
res.writeHead(400, { "Content-Type": "application/json" })
|
||||
res.end(JSON.stringify({ error: "sessionId and id required" }))
|
||||
return
|
||||
}
|
||||
// Picked from the list of a state the server has since lost
|
||||
// (a restart reloads History under new ids), or before the tab
|
||||
// knew the state. A tab of an older version sends none.
|
||||
const current = stateStore.get(sessionId)
|
||||
if (
|
||||
current &&
|
||||
"stateId" in data &&
|
||||
data.stateId !== current.stateId
|
||||
) {
|
||||
res.writeHead(409, { "Content-Type": "application/json" })
|
||||
res.end(JSON.stringify({ error: "Session was recreated" }))
|
||||
return
|
||||
}
|
||||
|
||||
const entry = getHistoryEntry(sessionId, id)
|
||||
if (!entry) {
|
||||
const newVersion = restoreHistoryEntry(sessionId, id)
|
||||
if (newVersion === null) {
|
||||
res.writeHead(404, { "Content-Type": "application/json" })
|
||||
res.end(JSON.stringify({ error: "Entry not found" }))
|
||||
return
|
||||
}
|
||||
|
||||
// Edits in the browser since the last entry are not in history
|
||||
// yet: keep them, so the restore can be undone
|
||||
// (any state besides a blank page; a cleared document with its
|
||||
// own pages counts)
|
||||
const current = stateStore.get(sessionId)
|
||||
if (
|
||||
current &&
|
||||
contentFingerprint(current.xml) !==
|
||||
contentFingerprint(BLANK_MXFILE)
|
||||
) {
|
||||
keepInHistory(sessionId, current.xml, current.svg)
|
||||
}
|
||||
const newVersion = setState(sessionId, entry.xml)
|
||||
addHistory(sessionId, entry.xml, entry.svg)
|
||||
|
||||
log.info(`Restored session ${sessionId} to history entry ${id}`)
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" })
|
||||
res.end(JSON.stringify({ success: true, newVersion }))
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user