fix(mcp-server): recover sessions in one place, and more fixes from the third review

- A session whose state expired was recovered from its auto-save file only
  for the preview page; the tools built on their older copy and then
  overwrote the file. They now recover it first (restoreSavedSession).
- A preview tab that missed the last AI write pushed its older diagram
  over the recovered one after a restart. It now shows the recovered
  diagram and keeps its own copy in History.
- An empty record of what the model has seen (after load_diagram or a
  page tool on unseen changes) no longer lets one page of a multi-page
  document count for all, and get_diagram counts a page only once found.
- History: a thumbnail goes only to the entry it shows, the cached image
  never belongs to an older diagram, a re-serialized copy adds no entry,
  and a cleared document with its own pages is kept before a restore.
- The root cell id check reads attributes one by one: rack-id="1" or id
  text inside a label no longer counts.
- A compressed page counts as having cells; a saved file that could not
  be read is never written over.
This commit is contained in:
dayuan.jiang
2026-10-05 13:04:32 +09:00
parent 49468f9478
commit cfbf17f97e
12 changed files with 288 additions and 53 deletions
+24 -5
View File
@@ -144,7 +144,14 @@ function showNotice(text) {
noticeTimer = setTimeout(() => el.classList.remove('open'), 8000);
}
// source is 'sync' for replies to a server sync request, else 'edit'
// Same rule as hasCells in pages.ts: a cell besides the root cells, or a
// compressed page
function hasCells(xml) {
return /<(mxCell\b[^>]*\bid\s*=\s*["'](?![01]["'])|UserObject\b|object\b)|<diagram\b[^>]*>\s*[^\s<]/.test(xml || '');
}
// source is 'sync' for replies to a server sync request, 'recover' for the
// tab's copy after the server recovered the session, else 'edit'
async function pushState(xml, svg = '', baseVersion = currentVersion, source = 'edit') {
if (!sessionId) return;
try {
@@ -157,7 +164,11 @@ async function pushState(xml, svg = '', baseVersion = currentVersion, source = '
// 409: the AI wrote a newer version; load it now
else if (r.status === 409) {
const d = await r.json().catch(() => ({}));
if (d.savedToHistory) showNotice('The AI changed the diagram while you were editing. Your last change was saved in History.');
if (d.savedToHistory) {
showNotice(source === 'recover'
? 'The diagram was restored from its saved file. What this tab showed before is in History.'
: 'The AI changed the diagram while you were editing. Your last change was saved in History.');
}
poll();
}
} catch (e) { console.error('Push failed:', e); }
@@ -174,10 +185,18 @@ async function poll() {
const r = await fetch('/api/state?sessionId=' + encodeURIComponent(sessionId));
if (!r.ok) return;
const s = await r.json();
// The server lost this session (e.g. it expired) and rebuilt it
// with a blank diagram: push back what the browser shows.
// The server lost this session (it expired, or the MCP process
// restarted) and rebuilt it. Blank: push back what the browser
// shows. From the auto-save file, which can hold an AI write this
// tab never loaded: show that, and keep this tab's copy in History
// (a push based on version 0 is refused and saved there).
if (s.version < knownVersion && lastXml) {
pushState(lastXml);
if (!hasCells(s.xml)) {
pushState(lastXml);
} else {
currentVersion = 0;
if (s.xml !== lastXml) pushState(lastXml, '', 0, 'recover');
}
}
// Load new diagram from server (before export, so we export latest).
// While a page-targeted projection is on screen, only the restore