


























@@ -19,6 +19,7 @@ const resolveOpenAiCompatibleHttpOperatorScopesMock = vi.fn();
1919const resolveOpenAiCompatibleHttpSenderIsOwnerMock = vi.fn();
2020const loadSessionEntryMock = vi.fn();
2121const readSessionMessagesMock = vi.fn();
22+const resolveSessionHistoryTranscriptPathMock = vi.fn();
2223const getRuntimeConfigMock = vi.fn(() => ({}));
23242425vi.mock("../config/config.js", () => ({
@@ -34,6 +35,11 @@ vi.mock("./http-utils.js", () => ({
3435vi.mock("./session-utils.js", () => ({
3536loadSessionEntry: loadSessionEntryMock,
3637readSessionMessagesAsync: readSessionMessagesMock,
38+readSessionMessagesWithSourceAsync: async (...args: unknown[]) => ({
39+messages: await readSessionMessagesMock(...args),
40+transcriptPath: await resolveSessionHistoryTranscriptPathMock(...args),
41+}),
42+resolveSessionHistoryTranscriptPathAsync: resolveSessionHistoryTranscriptPathMock,
3743}));
38443945const {
@@ -144,6 +150,8 @@ async function requestManagedImage(params: {
144150headers?: Record<string, string>;
145151transcriptMessages?: Record<string, unknown>[];
146152sessionEntry?: { sessionId: string; sessionFile?: string };
153+resolvedTranscriptPath?: string | null;
154+onReadTranscriptMessages?: () => Promise<void> | void;
147155}) {
148156authorizeGatewayHttpRequestOrReplyMock.mockImplementation(async ({ res }) => {
149157if (params.denyAuth) {
@@ -167,21 +175,27 @@ async function requestManagedImage(params: {
167175storePath: path.join(params.stateDir, "gateway-sessions.json"),
168176entry: params.sessionEntry ?? { sessionId: "sess-1", sessionFile: "session.jsonl" },
169177});
170-readSessionMessagesMock.mockReturnValue(
171-params.transcriptMessages ?? [
172-{
173-role: "assistant",
174-content: [
175-{
176-type: "image",
177-url: params.pathName,
178-openUrl: params.pathName,
179-},
180-],
181-__openclaw: { id: "msg-1" },
182-},
183-],
178+resolveSessionHistoryTranscriptPathMock.mockResolvedValue(
179+params.resolvedTranscriptPath ?? params.sessionEntry?.sessionFile ?? "session.jsonl",
184180);
181+readSessionMessagesMock.mockImplementation(async () => {
182+await params.onReadTranscriptMessages?.();
183+return (
184+params.transcriptMessages ?? [
185+{
186+role: "assistant",
187+content: [
188+{
189+type: "image",
190+url: params.pathName,
191+openUrl: params.pathName,
192+},
193+],
194+__openclaw: { id: "msg-1" },
195+},
196+]
197+);
198+});
185199186200const auth = { mode: "test" } as never;
187201const server = http.createServer((req, res) => {
@@ -272,6 +286,12 @@ describe("handleManagedOutgoingImageHttpRequest", () => {
272286expect(result.headers["content-type"]).toBe("image/png");
273287expect(result.headers["content-disposition"]).toContain("inline");
274288expect(result.body.toString("utf-8")).toBe("original-image");
289+expect(readSessionMessagesMock).toHaveBeenCalledWith(
290+"sess-1",
291+path.join(stateDir, "gateway-sessions.json"),
292+"session.jsonl",
293+expect.objectContaining({ allowResetArchiveFallback: true }),
294+);
275295});
276296277297it("rejects unauthenticated requests before serving bytes", async () => {
@@ -404,6 +424,99 @@ describe("handleManagedOutgoingImageHttpRequest", () => {
404424expect(third.result.statusCode).toBe(200);
405425expect(readSessionMessagesMock).toHaveBeenCalledTimes(2);
406426});
427+428+it("reuses the session attachment index for archive-backed requests", async () => {
429+const { attachmentId, sessionKey } = await createFixture(stateDir);
430+const archiveFile = path.join(
431+stateDir,
432+"sessions",
433+"sess-main.jsonl.reset.2026-02-16T22-26-34.000Z",
434+);
435+await fs.mkdir(path.dirname(archiveFile), { recursive: true });
436+await fs.writeFile(archiveFile, '{"message":{}}\n', "utf-8");
437+438+const transcriptMessages = [
439+{
440+__openclaw: { id: "msg-1" },
441+content: [
442+{
443+type: "image",
444+url: `/api/chat/media/outgoing/${encodeURIComponent(sessionKey)}/${attachmentId}/full`,
445+openUrl: `/api/chat/media/outgoing/${encodeURIComponent(sessionKey)}/${attachmentId}/full`,
446+},
447+],
448+},
449+];
450+451+const pathName = `/api/chat/media/outgoing/${encodeURIComponent(sessionKey)}/${attachmentId}/full`;
452+const first = await requestManagedImage({
453+ stateDir,
454+ pathName,
455+authResponse: { authMethod: "token" },
456+sessionEntry: { sessionId: "sess-main" },
457+resolvedTranscriptPath: archiveFile,
458+ transcriptMessages,
459+});
460+const second = await requestManagedImage({
461+ stateDir,
462+ pathName,
463+authResponse: { authMethod: "token" },
464+sessionEntry: { sessionId: "sess-main" },
465+resolvedTranscriptPath: archiveFile,
466+ transcriptMessages,
467+});
468+469+expect(first.result.statusCode).toBe(200);
470+expect(second.result.statusCode).toBe(200);
471+expect(readSessionMessagesMock).toHaveBeenCalledTimes(1);
472+});
473+474+it("does not cache a session attachment index when the transcript changes during the read", async () => {
475+const { attachmentId, sessionKey } = await createFixture(stateDir);
476+const sessionFile = path.join(stateDir, "sessions", "sess-main.jsonl");
477+await fs.mkdir(path.dirname(sessionFile), { recursive: true });
478+await fs.writeFile(sessionFile, '{"message":{}}\n', "utf-8");
479+480+const transcriptMessages = [
481+{
482+__openclaw: { id: "msg-1" },
483+content: [
484+{
485+type: "image",
486+url: `/api/chat/media/outgoing/${encodeURIComponent(sessionKey)}/${attachmentId}/full`,
487+openUrl: `/api/chat/media/outgoing/${encodeURIComponent(sessionKey)}/${attachmentId}/full`,
488+},
489+],
490+},
491+];
492+493+let mutatedTranscript = false;
494+const pathName = `/api/chat/media/outgoing/${encodeURIComponent(sessionKey)}/${attachmentId}/full`;
495+const first = await requestManagedImage({
496+ stateDir,
497+ pathName,
498+authResponse: { authMethod: "token" },
499+sessionEntry: { sessionId: "sess-main", sessionFile },
500+ transcriptMessages,
501+onReadTranscriptMessages: async () => {
502+if (!mutatedTranscript) {
503+mutatedTranscript = true;
504+await fs.appendFile(sessionFile, '{"message":{"content":"updated"}}\n', "utf-8");
505+}
506+},
507+});
508+const second = await requestManagedImage({
509+ stateDir,
510+ pathName,
511+authResponse: { authMethod: "token" },
512+sessionEntry: { sessionId: "sess-main", sessionFile },
513+ transcriptMessages,
514+});
515+516+expect(first.result.statusCode).toBe(200);
517+expect(second.result.statusCode).toBe(200);
518+expect(readSessionMessagesMock).toHaveBeenCalledTimes(2);
519+});
407520});
408521409522describe("createManagedOutgoingImageBlocks", () => {
@@ -942,6 +1055,12 @@ describe("cleanupManagedOutgoingImageRecords", () => {
9421055expect(result.deletedFileCount).toBe(0);
9431056expect(result.retainedCount).toBe(1);
9441057await expect(fs.access(fixture.originalPath)).resolves.toBeUndefined();
1058+expect(readSessionMessagesMock).toHaveBeenCalledWith(
1059+"sess-main",
1060+path.join(stateDir, "gateway-sessions.json"),
1061+"/tmp/sess-main.jsonl",
1062+expect.objectContaining({ allowResetArchiveFallback: true }),
1063+);
9451064});
94610659471066it("reads each session transcript once while evaluating committed records", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。