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;
|
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) {
|
function buildSyncFilename(chatId) {
|
||||||
const normalizedChatId = normalizeChatId(chatId);
|
const normalizedChatId = normalizeChatId(chatId);
|
||||||
const legacyName = `${BME_SYNC_FILE_PREFIX}${normalizedChatId}${BME_SYNC_FILE_SUFFIX}`;
|
const legacyName = `${BME_SYNC_FILE_PREFIX}${normalizedChatId}${BME_SYNC_FILE_SUFFIX}`;
|
||||||
@@ -74,6 +91,18 @@ function buildSyncFilename(chatId) {
|
|||||||
return `${core}${BME_SYNC_FILE_SUFFIX}`;
|
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) {
|
function normalizeRevision(value) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
||||||
@@ -933,28 +962,56 @@ async function sanitizeFilename(fileName, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fetchImpl = getFetch(options);
|
const sanitized = await requestSanitizedFilename(fileName, 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();
|
|
||||||
return normalizeRemoteFilenameCandidate(sanitized, finalFallback);
|
return normalizeRemoteFilenameCandidate(sanitized, finalFallback);
|
||||||
} catch {
|
} catch {
|
||||||
return finalFallback;
|
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 = {}) {
|
async function resolveSyncFilename(chatId, options = {}) {
|
||||||
const normalizedChatId = normalizeChatId(chatId);
|
const normalizedChatId = normalizeChatId(chatId);
|
||||||
if (!normalizedChatId) {
|
if (!normalizedChatId) {
|
||||||
@@ -968,10 +1025,41 @@ async function resolveSyncFilename(chatId, options = {}) {
|
|||||||
const rawFileName = buildSyncFilename(normalizedChatId);
|
const rawFileName = buildSyncFilename(normalizedChatId);
|
||||||
const sanitized = await sanitizeFilename(rawFileName, options);
|
const sanitized = await sanitizeFilename(rawFileName, options);
|
||||||
const finalName = normalizeRemoteFilenameCandidate(sanitized, rawFileName);
|
const finalName = normalizeRemoteFilenameCandidate(sanitized, rawFileName);
|
||||||
sanitizedFilenameByChatId.set(normalizedChatId, finalName);
|
rememberResolvedSyncFilename(normalizedChatId, finalName);
|
||||||
return 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 = {}) {
|
async function readRemoteSnapshot(chatId, options = {}) {
|
||||||
const normalizedChatId = normalizeChatId(chatId);
|
const normalizedChatId = normalizeChatId(chatId);
|
||||||
if (!normalizedChatId) {
|
if (!normalizedChatId) {
|
||||||
@@ -983,70 +1071,81 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = await resolveSyncFilename(normalizedChatId, options);
|
|
||||||
const fetchImpl = getFetch(options);
|
const fetchImpl = getFetch(options);
|
||||||
const cacheBust = `t=${Date.now()}`;
|
const candidateFilenames = await resolveSyncFilenameCandidates(
|
||||||
const url = `/user/files/${encodeURIComponent(filename)}?${cacheBust}`;
|
normalizedChatId,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
let lastNotFoundFilename = candidateFilenames[0] || "";
|
||||||
|
|
||||||
let response;
|
for (const filename of candidateFilenames) {
|
||||||
try {
|
const cacheBust = `t=${Date.now()}`;
|
||||||
response = await fetchImpl(url, {
|
const url = `/user/files/${encodeURIComponent(filename)}?${cacheBust}`;
|
||||||
method: "GET",
|
|
||||||
cache: "no-store",
|
let response;
|
||||||
});
|
try {
|
||||||
} catch (error) {
|
response = await fetchImpl(url, {
|
||||||
console.warn("[ST-BME] 读取远端同步文件失败:", error);
|
method: "GET",
|
||||||
return {
|
cache: "no-store",
|
||||||
exists: false,
|
});
|
||||||
status: "network-error",
|
} catch (error) {
|
||||||
filename,
|
console.warn("[ST-BME] 读取远端同步文件失败:", error);
|
||||||
snapshot: null,
|
return {
|
||||||
error,
|
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 {
|
||||||
return {
|
exists: false,
|
||||||
exists: false,
|
status: "not-found",
|
||||||
status: "not-found",
|
filename: lastNotFoundFilename,
|
||||||
filename,
|
snapshot: null,
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeSnapshotToRemote(snapshot, chatId, options = {}) {
|
async function writeSnapshotToRemote(snapshot, chatId, options = {}) {
|
||||||
@@ -1671,37 +1770,48 @@ export async function deleteRemoteSyncFile(chatId, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filename = await resolveSyncFilename(normalizedChatId, options);
|
|
||||||
const fetchImpl = getFetch(options);
|
const fetchImpl = getFetch(options);
|
||||||
const response = await fetchImpl("/api/files/delete", {
|
const filenames = await resolveSyncFilenameCandidates(
|
||||||
method: "POST",
|
normalizedChatId,
|
||||||
headers: {
|
options,
|
||||||
...getRequestHeadersSafe(options),
|
);
|
||||||
"Content-Type": "application/json",
|
let lastNotFoundFilename = filenames[0] || "";
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
path: `/user/files/${filename}`,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
deleted: false,
|
deleted: true,
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
filename,
|
filename,
|
||||||
reason: "not-found",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text().catch(() => response.statusText);
|
|
||||||
throw new Error(errorText || `HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deleted: true,
|
deleted: false,
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
filename,
|
filename: lastNotFoundFilename,
|
||||||
|
reason: "not-found",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[ST-BME] 删除远端同步文件失败:", error);
|
console.warn("[ST-BME] 删除远端同步文件失败:", error);
|
||||||
|
|||||||
@@ -1203,7 +1203,7 @@ async function buildPlannerMessages(rawUserInput) {
|
|||||||
// Extra user blocks before user message
|
// Extra user blocks before user message
|
||||||
for (const b of enaUserBlocks) {
|
for (const b of enaUserBlocks) {
|
||||||
const content = await renderTemplateAll(b.content, env, messageVars);
|
const content = await renderTemplateAll(b.content, env, messageVars);
|
||||||
messages.splice(Math.max(0, messages.length - 1), 0, { role: 'system', content: `【extra-user-block】\n${content}` });
|
messages.splice(Math.max(0, messages.length - 1), 0, { role: 'user', content: `【extra-user-block】\n${content}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7) Assistant blocks
|
// 7) Assistant blocks
|
||||||
|
|||||||
@@ -170,9 +170,69 @@ function clearManagedState() {
|
|||||||
hideState.lastProcessedLength = 0;
|
hideState.lastProcessedLength = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreManagedSystemFlags(chat, runtime = {}) {
|
function isManagedSystemMessage(message) {
|
||||||
if (!Array.isArray(chat) || hideState.managedSystemIndices.size === 0) {
|
return Boolean(
|
||||||
|
message?.is_system === true &&
|
||||||
|
message?.extra &&
|
||||||
|
typeof message.extra === "object" &&
|
||||||
|
message.extra[BME_HIDE_HASH_MARKER] === true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectManagedSystemIndices(chat) {
|
||||||
|
if (!Array.isArray(chat) || chat.length === 0) return [];
|
||||||
|
const indices = [];
|
||||||
|
for (let index = 0; index < chat.length; index++) {
|
||||||
|
if (isManagedSystemMessage(chat[index])) {
|
||||||
|
indices.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateManagedStateFromChat(
|
||||||
|
chat,
|
||||||
|
chatKey = getCurrentChatKey(),
|
||||||
|
{ bootstrapLength = false } = {},
|
||||||
|
) {
|
||||||
|
if (!Array.isArray(chat)) {
|
||||||
hideState.managedSystemIndices.clear();
|
hideState.managedSystemIndices.clear();
|
||||||
|
hideState.hiddenRangeEnd = -1;
|
||||||
|
return { managedCount: 0, hiddenRangeEnd: -1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const managedIndices = collectManagedSystemIndices(chat);
|
||||||
|
hideState.managedSystemIndices.clear();
|
||||||
|
for (const index of managedIndices) {
|
||||||
|
hideState.managedSystemIndices.add(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideState.managedChatRef = chat;
|
||||||
|
hideState.managedChatKey = chatKey;
|
||||||
|
hideState.hiddenRangeEnd =
|
||||||
|
managedIndices.length > 0 ? managedIndices[managedIndices.length - 1] : -1;
|
||||||
|
if (managedIndices.length > 0 && bootstrapLength) {
|
||||||
|
hideState.lastProcessedLength = chat.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
managedCount: managedIndices.length,
|
||||||
|
hiddenRangeEnd: hideState.hiddenRangeEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreManagedSystemFlags(chat, runtime = {}) {
|
||||||
|
if (!Array.isArray(chat)) {
|
||||||
|
hideState.managedSystemIndices.clear();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hideState.managedSystemIndices.size === 0) {
|
||||||
|
hydrateManagedStateFromChat(chat, getCurrentChatKey(runtime), {
|
||||||
|
bootstrapLength: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (hideState.managedSystemIndices.size === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,11 +339,22 @@ async function runHideApply(settings = {}, runtime = {}, options = {}) {
|
|||||||
|
|
||||||
const chatKey = getCurrentChatKey(runtime);
|
const chatKey = getCurrentChatKey(runtime);
|
||||||
const previousChatKey = hideState.managedChatKey;
|
const previousChatKey = hideState.managedChatKey;
|
||||||
|
const hadTrackedState =
|
||||||
|
hideState.managedSystemIndices.size > 0 ||
|
||||||
|
hideState.hiddenRangeEnd >= 0 ||
|
||||||
|
(Number.isFinite(hideState.lastProcessedLength) &&
|
||||||
|
hideState.lastProcessedLength > 0);
|
||||||
|
adoptManagedChat(chat, chatKey, runtime);
|
||||||
|
hydrateManagedStateFromChat(chat, chatKey, {
|
||||||
|
bootstrapLength: !hadTrackedState,
|
||||||
|
});
|
||||||
const sameChat =
|
const sameChat =
|
||||||
previousChatKey !== null && chatKey !== null && previousChatKey === chatKey;
|
previousChatKey !== null && chatKey !== null && previousChatKey === chatKey;
|
||||||
const previousHiddenEnd = sameChat ? hideState.hiddenRangeEnd : -1;
|
const previousHiddenEnd = hideState.hiddenRangeEnd;
|
||||||
const previousLength = sameChat ? hideState.lastProcessedLength : 0;
|
const previousLength =
|
||||||
adoptManagedChat(chat, chatKey, runtime);
|
sameChat && Number.isFinite(hideState.lastProcessedLength)
|
||||||
|
? hideState.lastProcessedLength
|
||||||
|
: 0;
|
||||||
hideState.lastProcessedLength = chatLength;
|
hideState.lastProcessedLength = chatLength;
|
||||||
|
|
||||||
if (!normalized.enabled || normalized.hideLastN <= 0) {
|
if (!normalized.enabled || normalized.hideLastN <= 0) {
|
||||||
@@ -435,6 +506,9 @@ export async function unhideAll(runtime = {}) {
|
|||||||
return buildResult({ chatLength });
|
return buildResult({ chatLength });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hydrateManagedStateFromChat(chatInfo.chat, getCurrentChatKey(runtime), {
|
||||||
|
bootstrapLength: false,
|
||||||
|
});
|
||||||
const { shownCount } = await unhideCurrentRange(runtime, version, {
|
const { shownCount } = await unhideCurrentRange(runtime, version, {
|
||||||
full: true,
|
full: true,
|
||||||
});
|
});
|
||||||
@@ -454,6 +528,12 @@ export async function unhideAll(runtime = {}) {
|
|||||||
export function resetHideState(runtime = {}) {
|
export function resetHideState(runtime = {}) {
|
||||||
clearScheduledTimer(runtime);
|
clearScheduledTimer(runtime);
|
||||||
beginOperation();
|
beginOperation();
|
||||||
|
const chatInfo = getCurrentChatInfo(runtime);
|
||||||
|
if (Array.isArray(chatInfo.chat)) {
|
||||||
|
hydrateManagedStateFromChat(chatInfo.chat, chatInfo.chatId || null, {
|
||||||
|
bootstrapLength: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
restoreManagedSystemFlags(hideState.managedChatRef, runtime);
|
restoreManagedSystemFlags(hideState.managedChatRef, runtime);
|
||||||
clearManagedState();
|
clearManagedState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,9 +144,40 @@ async function testResetClearsStateWithoutIssuingCommands() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testUnhideAllRecoversPersistedManagedMarkersAfterStateLoss() {
|
||||||
|
resetHideState();
|
||||||
|
const chat = [
|
||||||
|
{
|
||||||
|
mes: "user-1",
|
||||||
|
is_user: true,
|
||||||
|
is_system: true,
|
||||||
|
extra: { __st_bme_hide_managed: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mes: "assistant-1",
|
||||||
|
is_user: false,
|
||||||
|
is_system: true,
|
||||||
|
extra: { __st_bme_hide_managed: true },
|
||||||
|
},
|
||||||
|
{ mes: "user-2", is_user: true, is_system: false },
|
||||||
|
];
|
||||||
|
const runtime = createRuntime(chat);
|
||||||
|
|
||||||
|
const result = await unhideAll(runtime);
|
||||||
|
|
||||||
|
assert.equal(result.active, false);
|
||||||
|
assert.equal(result.shownCount, 3);
|
||||||
|
assert.deepEqual(runtime.commands, ["/unhide 0-2"]);
|
||||||
|
assert.equal(chat[0].is_system, false);
|
||||||
|
assert.equal(chat[1].is_system, false);
|
||||||
|
assert.equal(chat[0].extra, undefined);
|
||||||
|
assert.equal(chat[1].extra, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
await testApplyUsesNativeHide();
|
await testApplyUsesNativeHide();
|
||||||
await testDisableUnhidesManagedRange();
|
await testDisableUnhidesManagedRange();
|
||||||
await testIncrementalOnlyHidesOverflowDelta();
|
await testIncrementalOnlyHidesOverflowDelta();
|
||||||
await testResetClearsStateWithoutIssuingCommands();
|
await testResetClearsStateWithoutIssuingCommands();
|
||||||
|
await testUnhideAllRecoversPersistedManagedMarkersAfterStateLoss();
|
||||||
|
|
||||||
console.log("hide-engine tests passed");
|
console.log("hide-engine tests passed");
|
||||||
|
|||||||
@@ -341,6 +341,49 @@ async function testDownloadImport() {
|
|||||||
assert.equal(db.lastImportPayload.nodes[0].id, "remote-node");
|
assert.equal(db.lastImportPayload.nodes[0].id, "remote-node");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testLegacyRemoteFilenameFallbackAndReuse() {
|
||||||
|
const { fetch, remoteFiles, logs } = createMockFetchEnvironment();
|
||||||
|
const dbByChatId = new Map();
|
||||||
|
const chatId = "chat~legacy name";
|
||||||
|
const db = new FakeDb(chatId);
|
||||||
|
dbByChatId.set(chatId, db);
|
||||||
|
|
||||||
|
remoteFiles.set("ST-BME_sync_chat~legacy_name.json", {
|
||||||
|
meta: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
chatId,
|
||||||
|
revision: 4,
|
||||||
|
deviceId: "remote-device",
|
||||||
|
lastModified: 400,
|
||||||
|
nodeCount: 1,
|
||||||
|
edgeCount: 0,
|
||||||
|
tombstoneCount: 0,
|
||||||
|
},
|
||||||
|
nodes: [{ id: "legacy-node", updatedAt: 300 }],
|
||||||
|
edges: [],
|
||||||
|
tombstones: [],
|
||||||
|
state: {
|
||||||
|
lastProcessedFloor: 3,
|
||||||
|
extractionCount: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtime = buildRuntimeOptions({ dbByChatId, fetch });
|
||||||
|
const status = await getRemoteStatus(chatId, runtime);
|
||||||
|
assert.equal(status.exists, true);
|
||||||
|
assert.equal(status.filename, "ST-BME_sync_chat~legacy_name.json");
|
||||||
|
|
||||||
|
const downloadResult = await download(chatId, runtime);
|
||||||
|
assert.equal(downloadResult.downloaded, true);
|
||||||
|
assert.equal(downloadResult.filename, "ST-BME_sync_chat~legacy_name.json");
|
||||||
|
assert.equal(db.lastImportPayload.nodes[0].id, "legacy-node");
|
||||||
|
|
||||||
|
const uploadResult = await upload(chatId, runtime);
|
||||||
|
assert.equal(uploadResult.uploaded, true);
|
||||||
|
assert.equal(uploadResult.filename, "ST-BME_sync_chat~legacy_name.json");
|
||||||
|
assert.equal(logs.uploadedPayloads.at(-1)?.name, "ST-BME_sync_chat~legacy_name.json");
|
||||||
|
}
|
||||||
|
|
||||||
async function testMergeRules() {
|
async function testMergeRules() {
|
||||||
const local = {
|
const local = {
|
||||||
meta: {
|
meta: {
|
||||||
@@ -595,6 +638,38 @@ async function testDeleteRemoteSyncFile() {
|
|||||||
assert.equal(logs.deleteCalls, 2);
|
assert.equal(logs.deleteCalls, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testDeleteRemoteSyncFileFallsBackToLegacyFilename() {
|
||||||
|
const { fetch, remoteFiles, logs } = createMockFetchEnvironment();
|
||||||
|
const dbByChatId = new Map();
|
||||||
|
const chatId = "chat~legacy delete";
|
||||||
|
dbByChatId.set(chatId, new FakeDb(chatId));
|
||||||
|
remoteFiles.set("ST-BME_sync_chat~legacy_delete.json", {
|
||||||
|
meta: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
chatId,
|
||||||
|
revision: 1,
|
||||||
|
lastModified: 10,
|
||||||
|
deviceId: "remote-device",
|
||||||
|
nodeCount: 0,
|
||||||
|
edgeCount: 0,
|
||||||
|
tombstoneCount: 0,
|
||||||
|
},
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
tombstones: [],
|
||||||
|
state: {
|
||||||
|
lastProcessedFloor: -1,
|
||||||
|
extractionCount: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtime = buildRuntimeOptions({ dbByChatId, fetch });
|
||||||
|
const deleteResult = await deleteRemoteSyncFile(chatId, runtime);
|
||||||
|
assert.equal(deleteResult.deleted, true);
|
||||||
|
assert.equal(deleteResult.filename, "ST-BME_sync_chat~legacy_delete.json");
|
||||||
|
assert.equal(logs.deleteCalls, 2, "应先尝试新文件名,再回退删除 legacy 文件名");
|
||||||
|
}
|
||||||
|
|
||||||
async function testAutoSyncOnVisibility() {
|
async function testAutoSyncOnVisibility() {
|
||||||
const { fetch, logs } = createMockFetchEnvironment();
|
const { fetch, logs } = createMockFetchEnvironment();
|
||||||
const dbByChatId = new Map();
|
const dbByChatId = new Map();
|
||||||
@@ -748,10 +823,12 @@ async function main() {
|
|||||||
await testUploadPayloadMetaFirstAndDebounce();
|
await testUploadPayloadMetaFirstAndDebounce();
|
||||||
await testUploadSanitizesIllegalChatIdFilename();
|
await testUploadSanitizesIllegalChatIdFilename();
|
||||||
await testDownloadImport();
|
await testDownloadImport();
|
||||||
|
await testLegacyRemoteFilenameFallbackAndReuse();
|
||||||
await testMergeRules();
|
await testMergeRules();
|
||||||
await testMergeRuntimeMetaPolicies();
|
await testMergeRuntimeMetaPolicies();
|
||||||
await testSyncNowLockAndAutoSync();
|
await testSyncNowLockAndAutoSync();
|
||||||
await testDeleteRemoteSyncFile();
|
await testDeleteRemoteSyncFile();
|
||||||
|
await testDeleteRemoteSyncFileFallsBackToLegacyFilename();
|
||||||
await testAutoSyncOnVisibility();
|
await testAutoSyncOnVisibility();
|
||||||
await testSyncNowRemoteReadErrorPath();
|
await testSyncNowRemoteReadErrorPath();
|
||||||
await testSyncAppliedHook();
|
await testSyncAppliedHook();
|
||||||
|
|||||||
Reference in New Issue
Block a user