mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Fix sync fallback, hide recovery, and planner roles
This commit is contained in:
306
bme-sync.js
306
bme-sync.js
@@ -49,6 +49,23 @@ function normalizeRemoteFilenameCandidate(fileName, fallbackValue = "ST-BME_sync
|
||||
return sanitized || fallbackValue;
|
||||
}
|
||||
|
||||
function normalizeLegacyRemoteFilenameCandidate(
|
||||
fileName,
|
||||
fallbackValue = "ST-BME_sync_unknown.json",
|
||||
) {
|
||||
const raw = String(fileName ?? "");
|
||||
const normalized =
|
||||
typeof raw.normalize === "function" ? raw.normalize("NFKD") : raw;
|
||||
const sanitized = normalized
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^A-Za-z0-9._~-]+/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^\.+/g, "")
|
||||
.slice(0, BME_SYNC_FILENAME_MAX_LENGTH)
|
||||
.trim();
|
||||
return sanitized || fallbackValue;
|
||||
}
|
||||
|
||||
function buildSyncFilename(chatId) {
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
const legacyName = `${BME_SYNC_FILE_PREFIX}${normalizedChatId}${BME_SYNC_FILE_SUFFIX}`;
|
||||
@@ -74,6 +91,18 @@ function buildSyncFilename(chatId) {
|
||||
return `${core}${BME_SYNC_FILE_SUFFIX}`;
|
||||
}
|
||||
|
||||
function buildLegacyRawSyncFilename(chatId) {
|
||||
return `${BME_SYNC_FILE_PREFIX}${normalizeChatId(chatId)}${BME_SYNC_FILE_SUFFIX}`;
|
||||
}
|
||||
|
||||
function rememberResolvedSyncFilename(chatId, filename) {
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
const normalizedFilename = String(filename || "").trim();
|
||||
if (!normalizedChatId || !normalizedFilename) return "";
|
||||
sanitizedFilenameByChatId.set(normalizedChatId, normalizedFilename);
|
||||
return normalizedFilename;
|
||||
}
|
||||
|
||||
function normalizeRevision(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
||||
@@ -933,28 +962,56 @@ async function sanitizeFilename(fileName, options = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchImpl = getFetch(options);
|
||||
const response = await fetchImpl("/api/files/sanitize-filename", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...getRequestHeadersSafe(options),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ fileName }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return finalFallback;
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
const sanitized = String(payload?.fileName || "").trim();
|
||||
const sanitized = await requestSanitizedFilename(fileName, options);
|
||||
return normalizeRemoteFilenameCandidate(sanitized, finalFallback);
|
||||
} catch {
|
||||
return finalFallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestSanitizedFilename(fileName, options = {}) {
|
||||
if (options.disableRemoteSanitize) {
|
||||
return String(fileName || "");
|
||||
}
|
||||
|
||||
const fetchImpl = getFetch(options);
|
||||
const response = await fetchImpl("/api/files/sanitize-filename", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...getRequestHeadersSafe(options),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ fileName }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
return String(payload?.fileName || "").trim();
|
||||
}
|
||||
|
||||
async function resolveLegacySyncFilename(chatId, options = {}) {
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
const rawFileName = buildLegacyRawSyncFilename(normalizedChatId);
|
||||
const legacyFallback = normalizeLegacyRemoteFilenameCandidate(
|
||||
rawFileName,
|
||||
"ST-BME_sync_unknown.json",
|
||||
);
|
||||
|
||||
if (options.disableRemoteSanitize) {
|
||||
return legacyFallback;
|
||||
}
|
||||
|
||||
try {
|
||||
const sanitized = await requestSanitizedFilename(rawFileName, options);
|
||||
return normalizeLegacyRemoteFilenameCandidate(sanitized, legacyFallback);
|
||||
} catch {
|
||||
return legacyFallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSyncFilename(chatId, options = {}) {
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
if (!normalizedChatId) {
|
||||
@@ -968,10 +1025,41 @@ async function resolveSyncFilename(chatId, options = {}) {
|
||||
const rawFileName = buildSyncFilename(normalizedChatId);
|
||||
const sanitized = await sanitizeFilename(rawFileName, options);
|
||||
const finalName = normalizeRemoteFilenameCandidate(sanitized, rawFileName);
|
||||
sanitizedFilenameByChatId.set(normalizedChatId, finalName);
|
||||
rememberResolvedSyncFilename(normalizedChatId, finalName);
|
||||
return finalName;
|
||||
}
|
||||
|
||||
async function resolveSyncFilenameCandidates(chatId, options = {}) {
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
if (!normalizedChatId) {
|
||||
throw new Error("chatId 不能为空");
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
const pushCandidate = (value) => {
|
||||
const normalizedValue = String(value || "").trim();
|
||||
if (!normalizedValue || candidates.includes(normalizedValue)) return;
|
||||
candidates.push(normalizedValue);
|
||||
};
|
||||
|
||||
if (sanitizedFilenameByChatId.has(normalizedChatId)) {
|
||||
pushCandidate(sanitizedFilenameByChatId.get(normalizedChatId));
|
||||
}
|
||||
|
||||
const primaryRawFileName = buildSyncFilename(normalizedChatId);
|
||||
const primarySanitized = await sanitizeFilename(primaryRawFileName, options);
|
||||
pushCandidate(
|
||||
normalizeRemoteFilenameCandidate(primarySanitized, primaryRawFileName),
|
||||
);
|
||||
|
||||
const legacyRawFileName = buildLegacyRawSyncFilename(normalizedChatId);
|
||||
if (legacyRawFileName !== primaryRawFileName) {
|
||||
pushCandidate(await resolveLegacySyncFilename(normalizedChatId, options));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function readRemoteSnapshot(chatId, options = {}) {
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
if (!normalizedChatId) {
|
||||
@@ -983,70 +1071,81 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const filename = await resolveSyncFilename(normalizedChatId, options);
|
||||
const fetchImpl = getFetch(options);
|
||||
const cacheBust = `t=${Date.now()}`;
|
||||
const url = `/user/files/${encodeURIComponent(filename)}?${cacheBust}`;
|
||||
const candidateFilenames = await resolveSyncFilenameCandidates(
|
||||
normalizedChatId,
|
||||
options,
|
||||
);
|
||||
let lastNotFoundFilename = candidateFilenames[0] || "";
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImpl(url, {
|
||||
method: "GET",
|
||||
cache: "no-store",
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 读取远端同步文件失败:", error);
|
||||
return {
|
||||
exists: false,
|
||||
status: "network-error",
|
||||
filename,
|
||||
snapshot: null,
|
||||
error,
|
||||
};
|
||||
for (const filename of candidateFilenames) {
|
||||
const cacheBust = `t=${Date.now()}`;
|
||||
const url = `/user/files/${encodeURIComponent(filename)}?${cacheBust}`;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImpl(url, {
|
||||
method: "GET",
|
||||
cache: "no-store",
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 读取远端同步文件失败:", error);
|
||||
return {
|
||||
exists: false,
|
||||
status: "network-error",
|
||||
filename,
|
||||
snapshot: null,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
lastNotFoundFilename = filename;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
const error = new Error(errorText || `HTTP ${response.status}`);
|
||||
console.warn("[ST-BME] 读取远端同步文件失败:", error);
|
||||
return {
|
||||
exists: false,
|
||||
status: "http-error",
|
||||
filename,
|
||||
snapshot: null,
|
||||
error,
|
||||
statusCode: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const remotePayload = await response.json();
|
||||
const snapshot = normalizeSyncSnapshot(remotePayload, normalizedChatId);
|
||||
rememberResolvedSyncFilename(normalizedChatId, filename);
|
||||
return {
|
||||
exists: true,
|
||||
status: "ok",
|
||||
filename,
|
||||
snapshot,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 解析远端同步文件失败:", error);
|
||||
return {
|
||||
exists: false,
|
||||
status: "invalid-json",
|
||||
filename,
|
||||
snapshot: null,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
return {
|
||||
exists: false,
|
||||
status: "not-found",
|
||||
filename,
|
||||
snapshot: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
const error = new Error(errorText || `HTTP ${response.status}`);
|
||||
console.warn("[ST-BME] 读取远端同步文件失败:", error);
|
||||
return {
|
||||
exists: false,
|
||||
status: "http-error",
|
||||
filename,
|
||||
snapshot: null,
|
||||
error,
|
||||
statusCode: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const remotePayload = await response.json();
|
||||
const snapshot = normalizeSyncSnapshot(remotePayload, normalizedChatId);
|
||||
return {
|
||||
exists: true,
|
||||
status: "ok",
|
||||
filename,
|
||||
snapshot,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 解析远端同步文件失败:", error);
|
||||
return {
|
||||
exists: false,
|
||||
status: "invalid-json",
|
||||
filename,
|
||||
snapshot: null,
|
||||
error,
|
||||
};
|
||||
}
|
||||
return {
|
||||
exists: false,
|
||||
status: "not-found",
|
||||
filename: lastNotFoundFilename,
|
||||
snapshot: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeSnapshotToRemote(snapshot, chatId, options = {}) {
|
||||
@@ -1671,37 +1770,48 @@ export async function deleteRemoteSyncFile(chatId, options = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const filename = await resolveSyncFilename(normalizedChatId, options);
|
||||
const fetchImpl = getFetch(options);
|
||||
const response = await fetchImpl("/api/files/delete", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...getRequestHeadersSafe(options),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: `/user/files/${filename}`,
|
||||
}),
|
||||
});
|
||||
const filenames = await resolveSyncFilenameCandidates(
|
||||
normalizedChatId,
|
||||
options,
|
||||
);
|
||||
let lastNotFoundFilename = filenames[0] || "";
|
||||
|
||||
if (response.status === 404) {
|
||||
for (const filename of filenames) {
|
||||
const response = await fetchImpl("/api/files/delete", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...getRequestHeadersSafe(options),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: `/user/files/${filename}`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
lastNotFoundFilename = filename;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
throw new Error(errorText || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
sanitizedFilenameByChatId.delete(normalizedChatId);
|
||||
return {
|
||||
deleted: false,
|
||||
deleted: true,
|
||||
chatId: normalizedChatId,
|
||||
filename,
|
||||
reason: "not-found",
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
throw new Error(errorText || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
deleted: true,
|
||||
deleted: false,
|
||||
chatId: normalizedChatId,
|
||||
filename,
|
||||
filename: lastNotFoundFilename,
|
||||
reason: "not-found",
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 删除远端同步文件失败:", error);
|
||||
|
||||
Reference in New Issue
Block a user