From a2fd98a2d18e1b4c47b40c3d086620448f578527 Mon Sep 17 00:00:00 2001 From: YuTengjing Date: Mon, 8 Jun 2026 19:26:16 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20restore=20file=20URLs=20i?= =?UTF-8?q?n=20context=20prompts=20(#15549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/engine/messages/MessagesEngine.ts | 2 +- .../messages/__tests__/MessagesEngine.test.ts | 29 +++++++++++-- .../src/processors/MessageContent.ts | 7 ++-- .../__tests__/MessageContent.test.ts | 42 ++++++++++++++++++- .../__tests__/serverMessagesEngine.test.ts | 30 +++++++++++++ .../modules/Mecha/ContextEngineering/index.ts | 4 +- src/services/chat/chat.test.ts | 6 +-- .../chat/mecha/contextEngineering.test.ts | 31 +++++++------- src/services/chat/mecha/contextEngineering.ts | 5 ++- 9 files changed, 124 insertions(+), 32 deletions(-) diff --git a/packages/context-engine/src/engine/messages/MessagesEngine.ts b/packages/context-engine/src/engine/messages/MessagesEngine.ts index c4d6cbf038..373d72aea9 100644 --- a/packages/context-engine/src/engine/messages/MessagesEngine.ts +++ b/packages/context-engine/src/engine/messages/MessagesEngine.ts @@ -452,7 +452,7 @@ export class MessagesEngine { new ReactionFeedbackProcessor({ enabled: true }), // Message content processing (image encoding, multimodal) new MessageContentProcessor({ - fileContext: fileContext || { enabled: true, includeFileUrl: false }, + fileContext: fileContext || { enabled: true, includeFileUrl: true }, isCanUseVideo: capabilities?.isCanUseVideo || (() => false), isCanUseVision: capabilities?.isCanUseVision || (() => true), model, diff --git a/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts b/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts index 71e6801835..e293b87552 100644 --- a/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +++ b/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts @@ -389,13 +389,34 @@ describe('MessagesEngine', () => { expect(result).toBeDefined(); }); - it('should default to enabled without file URLs', async () => { - const params = createBasicParams(); + it('should default to enabled with file URLs', async () => { + const params = createBasicParams({ + messages: [ + { + content: 'Read this', + createdAt: Date.now(), + fileList: [ + { + fileType: 'text/plain', + id: 'file1', + name: 'test.txt', + size: 100, + url: 'https://files.example.com/test.txt', + }, + ], + id: 'msg-1', + role: 'user', + updatedAt: Date.now(), + } as UIChatMessage, + ], + }); const engine = new MessagesEngine(params); - // Should not throw const result = await engine.process(); - expect(result).toBeDefined(); + const userMessage = result.messages.find((message) => message.role === 'user'); + const content = userMessage?.content as any[]; + + expect(content[0].text).toContain('url="https://files.example.com/test.txt"'); }); }); diff --git a/packages/context-engine/src/processors/MessageContent.ts b/packages/context-engine/src/processors/MessageContent.ts index 74f0bb368c..e7f0343f9b 100644 --- a/packages/context-engine/src/processors/MessageContent.ts +++ b/packages/context-engine/src/processors/MessageContent.ts @@ -212,10 +212,9 @@ export class MessageContentProcessor extends BaseProcessor { // Add file context (if file context is enabled and has files, images or videos) if ((hasFiles || hasImages || hasVideos) && this.config.fileContext?.enabled) { const filesContext = filesPrompts({ - // Signed file URLs are volatile and can break provider-side prefix cache reuse. - // Keep file refs stable by default; structured multimodal parts still carry - // the fetchable URL when the target model supports the media type. - addUrl: this.config.fileContext.includeFileUrl ?? false, + // File access URLs are needed by sandbox/code tools that fetch attachments from text. + // Call sites can still disable them for environments such as desktop local files. + addUrl: this.config.fileContext.includeFileUrl ?? true, fileList: message.fileList, imageList: message.imageList || [], messageId: message.id, diff --git a/packages/context-engine/src/processors/__tests__/MessageContent.test.ts b/packages/context-engine/src/processors/__tests__/MessageContent.test.ts index f7960f1220..490b42efd3 100644 --- a/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +++ b/packages/context-engine/src/processors/__tests__/MessageContent.test.ts @@ -357,11 +357,49 @@ describe('MessageContentProcessor', () => { expect(content[0].type).toBe('text'); expect(content[0].text).toContain('SYSTEM CONTEXT'); expect(content[0].text).toContain('Hello'); - expect(content[0].text).toContain(''); + expect(content[0].text).toContain( + '', + ); + expect(content[0].text).toContain( + '', + ); + }); + + it('should omit file URLs when includeFileUrl is disabled', async () => { + mockIsCanUseVision.mockReturnValue(false); + + const processor = new MessageContentProcessor({ + fileContext: { enabled: true, includeFileUrl: false }, + isCanUseVision: mockIsCanUseVision, + model: 'gpt-4', + provider: 'openai', + }); + + const messages: UIChatMessage[] = [ + { + content: 'Hello', + createdAt: Date.now(), + fileList: [ + { + fileType: 'text/plain', + id: 'file1', + name: 'test.txt', + size: 100, + url: 'http://example.com/test.txt', + }, + ], + id: 'test', + role: 'user', + updatedAt: Date.now(), + }, + ]; + + const result = await processor.process(createContext(messages)); + + const content = result.messages[0].content as any[]; expect(content[0].text).toContain( '', ); - expect(content[0].text).not.toContain('http://example.com/image.jpg'); expect(content[0].text).not.toContain('http://example.com/test.txt'); }); diff --git a/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts b/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts index fa57c7dd04..faf7fbd056 100644 --- a/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts +++ b/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts @@ -81,6 +81,36 @@ describe('serverMessagesEngine', () => { expect(result).toEqual([{ content: getCurrentDateContent(), role: 'system' }]); }); + it('should include file URLs in server-side file context', async () => { + const result = await serverMessagesEngine({ + messages: [ + { + content: 'Read this', + createdAt: Date.now(), + fileList: [ + { + fileType: 'text/plain', + id: 'file1', + name: 'test.txt', + size: 100, + url: 'https://app.example.com/f/file1', + }, + ], + id: 'msg-1', + role: 'user', + updatedAt: Date.now(), + } as UIChatMessage, + ], + model: 'gpt-4', + provider: 'openai', + }); + + const userMessage = result.find((message) => message.role === 'user'); + const content = userMessage?.content as any[]; + + expect(content[0].text).toContain('url="https://app.example.com/f/file1"'); + }); + it('should pass active topic document initial context into MessagesEngine', async () => { const result = await serverMessagesEngine({ initialContext: { diff --git a/src/server/modules/Mecha/ContextEngineering/index.ts b/src/server/modules/Mecha/ContextEngineering/index.ts index 6ca29b2835..ef7d14b7e3 100644 --- a/src/server/modules/Mecha/ContextEngineering/index.ts +++ b/src/server/modules/Mecha/ContextEngineering/index.ts @@ -91,8 +91,8 @@ export const serverMessagesEngine = async ({ enableAgentMode, enableHistoryCount, - // File context refs must stay stable; media URLs are sent through structured parts. - fileContext: { enabled: true, includeFileUrl: false }, + // Server-side file access URLs resolve to stable file-proxy URLs in production. + fileContext: { enabled: true, includeFileUrl: true }, // Force finish mode (inject summary prompt when maxSteps exceeded) forceFinish, diff --git a/src/services/chat/chat.test.ts b/src/services/chat/chat.test.ts index c841e90984..7e0b949a20 100644 --- a/src/services/chat/chat.test.ts +++ b/src/services/chat/chat.test.ts @@ -647,7 +647,7 @@ describe('ChatService', () => { here are user upload images you can refer to - + `, @@ -790,7 +790,7 @@ describe('ChatService', () => { here are user upload images you can refer to - + `, @@ -891,7 +891,7 @@ describe('ChatService', () => { here are user upload images you can refer to - + `, diff --git a/src/services/chat/mecha/contextEngineering.test.ts b/src/services/chat/mecha/contextEngineering.test.ts index afa9a35c23..eaeadcb7c5 100644 --- a/src/services/chat/mecha/contextEngineering.test.ts +++ b/src/services/chat/mecha/contextEngineering.test.ts @@ -56,18 +56,20 @@ vi.mock('@/services/agent', () => ({ }, })); -// 默认设置 isServerMode 为 false -let isServerMode = false; +// 默认设置运行环境为 browser/client +const runtimeFlags = vi.hoisted(() => ({ + isServerMode: false, +})); vi.mock('@lobechat/const', async (importOriginal) => { const actual = await importOriginal(); return { ...(actual as any), get isServerMode() { - return isServerMode; + return runtimeFlags.isServerMode; }, - isDeprecatedEdition: false, isDesktop: false, + isDeprecatedEdition: false, }; }); @@ -80,6 +82,7 @@ beforeEach(() => { }); afterEach(() => { + runtimeFlags.isServerMode = false; vi.resetModules(); vi.clearAllMocks(); }); @@ -186,7 +189,7 @@ describe('contextEngineering', () => { describe('handle with files content in server mode', () => { it('should includes files', async () => { - isServerMode = true; + runtimeFlags.isServerMode = true; // Mock isCanUseVision to return true for vision models vi.spyOn(helpers, 'isCanUseVision').mockReturnValue(true); @@ -243,12 +246,12 @@ describe('contextEngineering', () => { here are user upload images you can refer to - + here are user upload files you can refer to - - + + `, @@ -267,11 +270,11 @@ describe('contextEngineering', () => { }, ]); - isServerMode = false; + runtimeFlags.isServerMode = false; }); it('should include image files in server mode', async () => { - isServerMode = true; + runtimeFlags.isServerMode = true; vi.spyOn(helpers, 'isCanUseVision').mockReturnValue(false); @@ -316,7 +319,7 @@ describe('contextEngineering', () => { here are user upload images you can refer to - + `, @@ -331,7 +334,7 @@ describe('contextEngineering', () => { }, ]); - isServerMode = false; + runtimeFlags.isServerMode = false; }); }); @@ -756,7 +759,7 @@ describe('contextEngineering', () => { }); it('should process placeholder variables combined with other processors', async () => { - isServerMode = true; + runtimeFlags.isServerMode = true; vi.spyOn(helpers, 'isCanUseVision').mockReturnValue(true); const messages: UIChatMessage[] = [ @@ -799,7 +802,7 @@ describe('contextEngineering', () => { expect(content[1].type).toBe('image_url'); expect(content[1].image_url.url).toBe('http://example.com/test.jpg'); - isServerMode = false; + runtimeFlags.isServerMode = false; }); }); diff --git a/src/services/chat/mecha/contextEngineering.ts b/src/services/chat/mecha/contextEngineering.ts index 2018cafc60..6e405f7e72 100644 --- a/src/services/chat/mecha/contextEngineering.ts +++ b/src/services/chat/mecha/contextEngineering.ts @@ -14,6 +14,7 @@ import { PageAgentIdentifier } from '@lobechat/builtin-tool-page-agent'; import { WebOnboardingIdentifier } from '@lobechat/builtin-tool-web-onboarding'; import { AGENT_PLAN_FILE_TYPE, + isDesktop, KLAVIS_SERVER_TYPES, LOBEHUB_SKILL_PROVIDERS, } from '@lobechat/const'; @@ -658,8 +659,8 @@ export const contextEngineering = async ({ isCanUseVision, }, - // File context configuration - fileContext: { enabled: true, includeFileUrl: false }, + // Desktop local/static URLs are not fetchable by remote providers or cloud tools. + fileContext: { enabled: true, includeFileUrl: !isDesktop }, // Knowledge injection knowledge: {