mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
🐛 fix: restore file URLs in context prompts (#15549)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user