refactor: remove legacy data compatibility

This commit is contained in:
2026-06-07 16:56:23 +08:00
parent 5e0c16f8b2
commit 1ed7e56f35
6 changed files with 46 additions and 412 deletions
+19 -80
View File
@@ -1,10 +1,11 @@
import { atomicWriteJson, readJsonFile } from "../utils/fileStore.js";
import { readJsonFile } from "../utils/fileStore.js";
import {
type ResultReferenceKind,
type ResultReferenceRecord,
type ResultReferenceSource,
type RetrievalContext,
RESULT_REFERENCE_KIND,
RESULT_REFERENCE_SOURCE,
type ResultReferenceStore,
} from "./store.js";
@@ -34,6 +35,7 @@ export type RenderJunctionPayload = {
export class ResultReferenceResolver {
constructor(private readonly store: ResultReferenceStore) {}
// Resolver 负责按结果类型做结构校验,Store 只关心授权和落盘。
async register(input: RegisterResultReferenceInput) {
const normalizedData = normalizeDataForKind(
input.kind,
@@ -66,15 +68,12 @@ export class ResultReferenceResolver {
throw new Error(`render payload file not found: ${filePath}`);
}
const payloadCandidate = normalizeRenderPayloadFile(raw, {
filePath,
projectId: input.projectId,
});
if (payloadCandidate.repaired) {
await atomicWriteJson(filePath, payloadCandidate.file);
const wrapper = normalizeRenderPayloadFile(raw, filePath);
if (!wrapper) {
throw new Error("render payload file must use the wrapped { metadata, location, data } format");
}
const payload = extractRenderJunctionPayload(payloadCandidate.data);
const payload = extractRenderJunctionPayload(wrapper.data);
if (!payload) {
throw new Error("render payload file does not contain a valid junction render payload");
}
@@ -84,6 +83,7 @@ export class ResultReferenceResolver {
data: payload,
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
schemaVersion: 1,
source: RESULT_REFERENCE_SOURCE.agentGenerated,
});
}
@@ -144,6 +144,7 @@ export const extractRenderJunctionPayload = (
return null;
}
// 节点渲染结果只保留前端真正需要的映射字段,剔除空值并统一转为字符串。
const nodeAreaMap = normalizeStringRecord(candidate.node_area_map);
if (Object.keys(nodeAreaMap).length === 0) {
return null;
@@ -182,35 +183,18 @@ const normalizeDataForKind = (
const normalizeRenderPayloadFile = (
value: unknown,
context: { filePath: string; projectId?: string },
): { data: unknown; file: Record<string, unknown>; repaired: boolean } => {
filePath: string,
): { data: unknown } | null => {
if (!isRecord(value) || !("data" in value)) {
return {
data: value,
file: {
metadata: buildWrapperMetadata({}, value, context.projectId),
location: buildWrapperLocation(undefined, context.filePath),
data: value,
},
repaired: false,
};
return null;
}
const metadata = buildWrapperMetadata(value.metadata, value, context.projectId);
const location = buildWrapperLocation(value.location, context.filePath);
const next: Record<string, unknown> = {
...value,
metadata,
location,
};
return {
data: next.data,
file: next,
repaired:
JSON.stringify(metadata) !== JSON.stringify(value.metadata ?? null) ||
JSON.stringify(location) !== JSON.stringify(value.location ?? null),
};
if (!isRecord(value.metadata) || !isRecord(value.location)) {
return null;
}
if (value.location.file_path !== filePath) {
return null;
}
return { data: value.data };
};
const unwrapReferencePayload = (value: unknown): Record<string, unknown> | null => {
@@ -232,48 +216,3 @@ const normalizeStringRecord = (value: Record<string, unknown>) =>
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const buildWrapperMetadata = (
value: unknown,
root: unknown,
fallbackProjectId?: string,
) => {
const metadata = isRecord(value) ? { ...value } : {};
const source = isRecord(root) ? root : {};
if (typeof metadata.createdAt !== "string" || metadata.createdAt.trim().length === 0) {
const createdAt =
typeof source.createdAt === "string" && source.createdAt.trim().length > 0
? source.createdAt.trim()
: new Date().toISOString();
metadata.createdAt = createdAt;
}
if (
typeof metadata.projectId !== "string" ||
metadata.projectId.trim().length === 0
) {
const projectId =
typeof source.projectId === "string" && source.projectId.trim().length > 0
? source.projectId.trim()
: fallbackProjectId;
if (projectId) {
metadata.projectId = projectId;
}
}
return metadata;
};
const buildWrapperLocation = (value: unknown, filePath: string) => {
if (isRecord(value)) {
return {
...value,
file_path: filePath,
};
}
return {
file_path: filePath,
};
};
+6 -61
View File
@@ -10,7 +10,6 @@ import {
listJsonFiles,
readJsonFile,
removeFileIfExists,
toProjectKey,
} from "../utils/fileStore.js";
export const RESULT_REF_PATTERN = /^res-[a-f0-9-]{8,64}$/;
@@ -22,8 +21,6 @@ export const RESULT_REFERENCE_KIND = {
export const RESULT_REFERENCE_SOURCE = {
agentGenerated: "agent_generated",
legacy: "legacy",
migration: "migration",
} as const;
export type ResultReferenceKind =
@@ -133,6 +130,7 @@ export class ResultReferenceStore {
source: input.source,
traceId: input.traceId,
};
// result_ref 对外暴露短引用,完整数据落盘;这样可以避免大结果直接塞进模型上下文。
await atomicWriteJson(this.filePath(resultRef), record);
return record;
}
@@ -143,13 +141,13 @@ export class ResultReferenceStore {
return null;
}
const rawRecord = await readJsonFile<unknown>(this.filePath(normalizedResultRef));
const record =
normalizeResultReferenceRecord(rawRecord) ??
normalizeLegacyRenderReferenceRecord(rawRecord, normalizedResultRef, context);
const record = normalizeResultReferenceRecord(
await readJsonFile<unknown>(this.filePath(normalizedResultRef)),
);
if (!record) {
return null;
}
// 读取 result_ref 时按用户、项目和可选会话三层校验,防止跨项目/跨用户取数。
if (record.actorKey !== context.actorKey) {
return null;
}
@@ -202,6 +200,7 @@ export class ResultReferenceStore {
if (!stats) {
continue;
}
// TTL 以文件修改时间为准,清理长期无人访问的 result_ref 文件。
if (now - stats.mtimeMs > this.ttlMs) {
await removeFileIfExists(filePath);
}
@@ -303,60 +302,6 @@ const normalizeResultRef = (value: string) => {
return match?.[1] ?? null;
};
const normalizeLegacyRenderReferenceRecord = (
value: unknown,
resultRef: string,
context: RetrievalContext,
): ResultReferenceRecord | null => {
const data = extractLegacyRenderPayload(value);
if (!data) {
return null;
}
const root = isRecord(value) ? value : {};
const metadata = isRecord(root.metadata) ? root.metadata : {};
const projectId = firstNonEmptyString(root.projectId, metadata.projectId);
const createdAt =
firstNonEmptyString(root.createdAt, metadata.createdAt) ?? new Date().toISOString();
return {
resultRef,
actorKey: context.actorKey,
clientSessionId: context.clientSessionId ?? "",
createdAt,
data,
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
preview: buildPreview(data),
projectId,
projectKey: toProjectKey(projectId),
schemaVersion: 1,
sessionId: context.clientSessionId ?? resultRef,
sizeBytes: estimateBytes(data),
source: RESULT_REFERENCE_SOURCE.legacy,
traceId: "legacy-render-ref",
};
};
const extractLegacyRenderPayload = (value: unknown) => {
if (!isRecord(value)) {
return null;
}
const candidate = isRecord(value.data) ? value.data : value;
if (!isRecord(candidate.node_area_map)) {
return null;
}
return candidate;
};
const firstNonEmptyString = (...values: unknown[]) => {
for (const value of values) {
if (typeof value === "string" && value.trim().length > 0) {
return value.trim();
}
}
return undefined;
};
const isResultPreview = (value: unknown): value is ResultPreview =>
isRecord(value) &&
typeof value.count === "number" &&