🐛 fix: restore file URLs in context prompts (#15549)

This commit is contained in:
YuTengjing
2026-06-08 19:26:16 +08:00
committed by GitHub
parent 235a16fc11
commit a2fd98a2d1
9 changed files with 124 additions and 32 deletions
@@ -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,
@@ -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"');
});
});
@@ -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,
@@ -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('<image ref="msg_1cs5ql.image_1" name="test.png"></image>');
expect(content[0].text).toContain(
'<image ref="msg_1cs5ql.image_1" name="test.png" url="http://example.com/image.jpg"></image>',
);
expect(content[0].text).toContain(
'<file id="file1" name="test.txt" type="text/plain" size="100" url="http://example.com/test.txt"></file>',
);
});
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(
'<file id="file1" name="test.txt" type="text/plain" size="100"></file>',
);
expect(content[0].text).not.toContain('http://example.com/image.jpg');
expect(content[0].text).not.toContain('http://example.com/test.txt');
});
@@ -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: {
@@ -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,
+3 -3
View File
@@ -647,7 +647,7 @@ describe('ChatService', () => {
<files_info>
<images>
<images_docstring>here are user upload images you can refer to</images_docstring>
<image ref="image_1" name="abc.png"></image>
<image ref="image_1" name="abc.png" url="http://example.com/image.jpg"></image>
</images>
</files_info>
<!-- END SYSTEM CONTEXT -->`,
@@ -790,7 +790,7 @@ describe('ChatService', () => {
<files_info>
<images>
<images_docstring>here are user upload images you can refer to</images_docstring>
<image ref="${visualRef}" name="local-image.png"></image>
<image ref="${visualRef}" name="local-image.png" url="http://127.0.0.1:3000/uploads/image.png"></image>
</images>
</files_info>
<!-- END SYSTEM CONTEXT -->`,
@@ -891,7 +891,7 @@ describe('ChatService', () => {
<files_info>
<images>
<images_docstring>here are user upload images you can refer to</images_docstring>
<image ref="${visualRef}" name="remote-image.jpg"></image>
<image ref="${visualRef}" name="remote-image.jpg" url="https://example.com/remote-image.jpg"></image>
</images>
</files_info>
<!-- END SYSTEM CONTEXT -->`,
@@ -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', () => {
<files_info>
<images>
<images_docstring>here are user upload images you can refer to</images_docstring>
<image ref="image_1" name="ttt.png"></image>
<image ref="image_1" name="ttt.png" url="http://example.com/xxx0asd-dsd.png"></image>
</images>
<files>
<files_docstring>here are user upload files you can refer to</files_docstring>
<file id="file1" name="abc.png" type="plain/txt" size="100000"></file>
<file id="file_oKMve9qySLMI" name="2402.16667v1.pdf" type="undefined" size="11256078"></file>
<file id="file1" name="abc.png" type="plain/txt" size="100000" url="http://abc.com/abc.txt"></file>
<file id="file_oKMve9qySLMI" name="2402.16667v1.pdf" type="undefined" size="11256078" url="https://xxx.com/ppp/480497/5826c2b8-fde0-4de1-a54b-a224d5e3d898.pdf"></file>
</files>
</files_info>
<!-- END SYSTEM CONTEXT -->`,
@@ -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', () => {
<files_info>
<images>
<images_docstring>here are user upload images you can refer to</images_docstring>
<image ref="image_1" name="abc.png"></image>
<image ref="image_1" name="abc.png" url="http://example.com/image.jpg"></image>
</images>
</files_info>
<!-- END SYSTEM CONTEXT -->`,
@@ -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;
});
});
@@ -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: {