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"; type ResolveOptions = { expectedKind?: ResultReferenceKind; }; type RegisterResultReferenceInput = { actorKey: string; clientSessionId: string; data: unknown; kind: ResultReferenceKind; projectId?: string; projectKey: string; schemaVersion: number; sessionId: string; source: ResultReferenceSource; traceId: string; }; export type RenderJunctionPayload = { node_area_map: Record; area_ids?: string[]; area_colors?: Record; }; export class ResultReferenceResolver { constructor(private readonly store: ResultReferenceStore) {} // Resolver 负责按结果类型做结构校验,Store 只关心授权和落盘。 async register(input: RegisterResultReferenceInput) { const normalizedData = normalizeDataForKind( input.kind, input.data, input.schemaVersion, ); if (!normalizedData) { throw new Error(`invalid payload for result ref kind '${input.kind}'`); } return this.store.store({ actorKey: input.actorKey, clientSessionId: input.clientSessionId, data: normalizedData, kind: input.kind, projectId: input.projectId, projectKey: input.projectKey, schemaVersion: input.schemaVersion, sessionId: input.sessionId, source: input.source, traceId: input.traceId, }); } async registerRenderPayloadFile( filePath: string, input: Omit, ) { const raw = await readJsonFile(filePath); if (raw === null) { throw new Error(`render payload file not found: ${filePath}`); } const wrapper = normalizeRenderPayloadFile(raw, filePath); if (!wrapper) { throw new Error("render payload file must use the wrapped { metadata, location, data } format"); } const payload = extractRenderJunctionPayload(wrapper.data); if (!payload) { throw new Error("render payload file does not contain a valid junction render payload"); } return this.register({ ...input, data: payload, kind: RESULT_REFERENCE_KIND.renderJunctionsPayload, schemaVersion: 1, source: RESULT_REFERENCE_SOURCE.agentGenerated, }); } async getFullAuthorized( resultRef: string, context: RetrievalContext, options: ResolveOptions = {}, ) { const record = await this.getResolvedRecord(resultRef, context, options); if (!record) { return null; } return { ok: true, result_ref: record.resultRef, result_size_bytes: record.sizeBytes, stored_at: record.createdAt, data: record.data, preview: record.preview, kind: record.kind, schema_version: record.schemaVersion, source: record.source, }; } private async getResolvedRecord( resultRef: string, context: RetrievalContext, options: ResolveOptions, ): Promise { const record = await this.store.getAuthorizedRecord(resultRef, context); if (!record) { return null; } if (options.expectedKind && record.kind !== options.expectedKind) { return null; } const normalizedData = normalizeDataForKind( record.kind, record.data, record.schemaVersion, ); if (!normalizedData) { return null; } return { ...record, data: normalizedData, }; } } export const extractRenderJunctionPayload = ( value: unknown, ): RenderJunctionPayload | null => { const candidate = unwrapReferencePayload(value); if (!candidate || !isRecord(candidate.node_area_map)) { return null; } // 节点渲染结果只保留前端真正需要的映射字段,剔除空值并统一转为字符串。 const nodeAreaMap = normalizeStringRecord(candidate.node_area_map); if (Object.keys(nodeAreaMap).length === 0) { return null; } const areaIds = Array.isArray(candidate.area_ids) ? candidate.area_ids.map((entry) => String(entry).trim()).filter(Boolean) : undefined; const areaColors = isRecord(candidate.area_colors) ? normalizeStringRecord(candidate.area_colors) : undefined; return { node_area_map: nodeAreaMap, ...(areaIds && areaIds.length > 0 ? { area_ids: areaIds } : {}), ...(areaColors && Object.keys(areaColors).length > 0 ? { area_colors: areaColors } : {}), }; }; const normalizeDataForKind = ( kind: ResultReferenceKind, data: unknown, schemaVersion: number, ): unknown | null => { if (!Number.isInteger(schemaVersion) || schemaVersion < 1) { return null; } if (kind === RESULT_REFERENCE_KIND.renderJunctionsPayload) { return extractRenderJunctionPayload(data); } return data; }; const normalizeRenderPayloadFile = ( value: unknown, filePath: string, ): { data: unknown } | null => { if (!isRecord(value) || !("data" in value)) { return 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 | null => { if (!isRecord(value)) { return null; } if ("data" in value && value.data !== undefined && value.data !== null) { return isRecord(value.data) ? value.data : null; } return value; }; const normalizeStringRecord = (value: Record) => Object.fromEntries( Object.entries(value) .map(([key, entry]) => [String(key), String(entry ?? "").trim()]) .filter(([, entry]) => entry.length > 0), ); const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value);