mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
fix(server): empty mimetype attachments fallback (#11869)
This commit is contained in:
@@ -385,6 +385,45 @@ test('should create message correctly', async t => {
|
|||||||
t.truthy(messageId, 'should be able to create message with valid session');
|
t.truthy(messageId, 'should be able to create message with valid session');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// with attachment url
|
||||||
|
{
|
||||||
|
const { id } = await createWorkspace(app);
|
||||||
|
const sessionId = await createCopilotSession(
|
||||||
|
app,
|
||||||
|
id,
|
||||||
|
randomUUID(),
|
||||||
|
promptName
|
||||||
|
);
|
||||||
|
const messageId = await createCopilotMessage(app, sessionId, undefined, [
|
||||||
|
'http://example.com/cat.jpg',
|
||||||
|
]);
|
||||||
|
t.truthy(messageId, 'should be able to create message with url link');
|
||||||
|
}
|
||||||
|
|
||||||
|
// with attachment
|
||||||
|
{
|
||||||
|
const { id } = await createWorkspace(app);
|
||||||
|
const sessionId = await createCopilotSession(
|
||||||
|
app,
|
||||||
|
id,
|
||||||
|
randomUUID(),
|
||||||
|
promptName
|
||||||
|
);
|
||||||
|
const smallestPng =
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
|
||||||
|
const pngData = await fetch(smallestPng).then(res => res.arrayBuffer());
|
||||||
|
const messageId = await createCopilotMessage(
|
||||||
|
app,
|
||||||
|
sessionId,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
[new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })]
|
||||||
|
);
|
||||||
|
t.truthy(messageId, 'should be able to create message with blobs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
createCopilotMessage(app, randomUUID()),
|
createCopilotMessage(app, randomUUID()),
|
||||||
|
|||||||
@@ -492,19 +492,53 @@ export async function createCopilotMessage(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
content?: string,
|
content?: string,
|
||||||
attachments?: string[],
|
attachments?: string[],
|
||||||
blobs?: ArrayBuffer[],
|
blobs?: File[],
|
||||||
params?: Record<string, string>
|
params?: Record<string, string>
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const res = await app.gql(
|
let resp = app
|
||||||
`
|
.POST('/graphql')
|
||||||
mutation createCopilotMessage($options: CreateChatMessageInput!) {
|
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||||
createCopilotMessage(options: $options)
|
.field(
|
||||||
|
'operations',
|
||||||
|
JSON.stringify({
|
||||||
|
query: `
|
||||||
|
mutation createCopilotMessage($options: CreateChatMessageInput!) {
|
||||||
|
createCopilotMessage(options: $options)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
options: { sessionId, content, attachments, blobs: [], params },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.field(
|
||||||
|
'map',
|
||||||
|
JSON.stringify(
|
||||||
|
Array.from<any>({ length: blobs?.length ?? 0 }).reduce(
|
||||||
|
(acc, _, idx) => {
|
||||||
|
acc[idx.toString()] = [`variables.options.blobs.${idx}`];
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (blobs && blobs.length) {
|
||||||
|
for (const [idx, file] of blobs.entries()) {
|
||||||
|
resp = resp.attach(
|
||||||
|
idx.toString(),
|
||||||
|
Buffer.from(await file.arrayBuffer()),
|
||||||
|
{
|
||||||
|
filename: file.name || `file${idx}`,
|
||||||
|
contentType: file.type || 'application/octet-stream',
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
`,
|
}
|
||||||
{ options: { sessionId, content, attachments, blobs, params } }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.createCopilotMessage;
|
const res = await resp.expect(200);
|
||||||
|
|
||||||
|
return res.body.data.createCopilotMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function chatWithText(
|
export async function chatWithText(
|
||||||
|
|||||||
@@ -138,7 +138,15 @@ export class FalProvider
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
model_name: options.modelName || undefined,
|
model_name: options.modelName || undefined,
|
||||||
image_url: attachments?.[0],
|
image_url: attachments
|
||||||
|
?.map(v =>
|
||||||
|
typeof v === 'string'
|
||||||
|
? v
|
||||||
|
: v.mimeType.startsWith('image/')
|
||||||
|
? v.attachment
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
.filter(v => !!v)[0],
|
||||||
prompt: content.trim(),
|
prompt: content.trim(),
|
||||||
loras: lora.length ? lora : undefined,
|
loras: lora.length ? lora : undefined,
|
||||||
controlnets: controlnets.length ? controlnets : undefined,
|
controlnets: controlnets.length ? controlnets : undefined,
|
||||||
|
|||||||
@@ -52,7 +52,15 @@ export const ChatMessageRole = Object.values(AiPromptRole) as [
|
|||||||
|
|
||||||
export const PureMessageSchema = z.object({
|
export const PureMessageSchema = z.object({
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
attachments: z.array(z.string()).optional().nullable(),
|
attachments: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({ attachment: z.string(), mimeType: z.string() }),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
params: z.record(z.any()).optional().nullable(),
|
params: z.record(z.any()).optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,15 +35,26 @@ const FORMAT_INFER_MAP: Record<string, string> = {
|
|||||||
flv: 'video/flv',
|
flv: 'video/flv',
|
||||||
};
|
};
|
||||||
|
|
||||||
function inferMimeType(url: string) {
|
async function inferMimeType(url: string) {
|
||||||
if (url.startsWith('data:')) {
|
if (url.startsWith('data:')) {
|
||||||
return url.split(';')[0].split(':')[1];
|
return url.split(';')[0].split(':')[1];
|
||||||
}
|
}
|
||||||
const extension = url.split('.').pop();
|
const pathname = new URL(url).pathname;
|
||||||
|
const extension = pathname.split('.').pop();
|
||||||
if (extension) {
|
if (extension) {
|
||||||
return FORMAT_INFER_MAP[extension];
|
const ext = FORMAT_INFER_MAP[extension];
|
||||||
|
if (ext) {
|
||||||
|
return ext;
|
||||||
|
}
|
||||||
|
const mimeType = await fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
redirect: 'follow',
|
||||||
|
}).then(res => res.headers.get('Content-Type'));
|
||||||
|
if (mimeType) {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return 'application/octet-stream';
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function chatToGPTMessage(
|
export async function chatToGPTMessage(
|
||||||
@@ -66,19 +77,24 @@ export async function chatToGPTMessage(
|
|||||||
contents.push({ type: 'text', text: content });
|
contents.push({ type: 'text', text: content });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const url of attachments) {
|
for (let attachment of attachments) {
|
||||||
if (SIMPLE_IMAGE_URL_REGEX.test(url)) {
|
let mimeType: string;
|
||||||
const mimeType =
|
if (typeof attachment === 'string') {
|
||||||
typeof mimetype === 'string' ? mimetype : inferMimeType(url);
|
mimeType =
|
||||||
if (mimeType) {
|
typeof mimetype === 'string'
|
||||||
if (mimeType.startsWith('image/')) {
|
? mimetype
|
||||||
contents.push({ type: 'image', image: url, mimeType });
|
: await inferMimeType(attachment);
|
||||||
} else {
|
} else {
|
||||||
const data = url.startsWith('data:')
|
({ attachment, mimeType } = attachment);
|
||||||
? await fetch(url).then(r => r.arrayBuffer())
|
}
|
||||||
: new URL(url);
|
if (SIMPLE_IMAGE_URL_REGEX.test(attachment)) {
|
||||||
contents.push({ type: 'file' as const, data, mimeType });
|
if (mimeType.startsWith('image/')) {
|
||||||
}
|
contents.push({ type: 'image', image: attachment, mimeType });
|
||||||
|
} else {
|
||||||
|
const data = attachment.startsWith('data:')
|
||||||
|
? await fetch(attachment).then(r => r.arrayBuffer())
|
||||||
|
: new URL(attachment);
|
||||||
|
contents.push({ type: 'file' as const, data, mimeType });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { Admin } from '../../core/common';
|
|||||||
import { AccessController } from '../../core/permission';
|
import { AccessController } from '../../core/permission';
|
||||||
import { UserType } from '../../core/user';
|
import { UserType } from '../../core/user';
|
||||||
import { PromptService } from './prompt';
|
import { PromptService } from './prompt';
|
||||||
|
import { PromptMessage } from './providers';
|
||||||
import { ChatSessionService } from './session';
|
import { ChatSessionService } from './session';
|
||||||
import { CopilotStorage } from './storage';
|
import { CopilotStorage } from './storage';
|
||||||
import {
|
import {
|
||||||
@@ -113,7 +114,7 @@ class CreateChatMessageInput implements Omit<SubmittedMessage, 'content'> {
|
|||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
content!: string | undefined;
|
content!: string | undefined;
|
||||||
|
|
||||||
@Field(() => [String], { nullable: true })
|
@Field(() => [String], { nullable: true, deprecationReason: 'use blobs' })
|
||||||
attachments!: string[] | undefined;
|
attachments!: string[] | undefined;
|
||||||
|
|
||||||
@Field(() => [GraphQLUpload], { nullable: true })
|
@Field(() => [GraphQLUpload], { nullable: true })
|
||||||
@@ -527,8 +528,8 @@ export class CopilotResolver {
|
|||||||
throw new BadRequestException('Session not found');
|
throw new BadRequestException('Session not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachments: PromptMessage['attachments'] = options.attachments || [];
|
||||||
if (options.blobs) {
|
if (options.blobs) {
|
||||||
options.attachments = options.attachments || [];
|
|
||||||
const { workspaceId } = session.config;
|
const { workspaceId } = session.config;
|
||||||
|
|
||||||
const blobs = await Promise.all(options.blobs);
|
const blobs = await Promise.all(options.blobs);
|
||||||
@@ -539,18 +540,18 @@ export class CopilotResolver {
|
|||||||
const filename = createHash('sha256')
|
const filename = createHash('sha256')
|
||||||
.update(uploaded.buffer)
|
.update(uploaded.buffer)
|
||||||
.digest('base64url');
|
.digest('base64url');
|
||||||
const link = await this.storage.put(
|
const attachment = await this.storage.put(
|
||||||
user.id,
|
user.id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
filename,
|
filename,
|
||||||
uploaded.buffer
|
uploaded.buffer
|
||||||
);
|
);
|
||||||
options.attachments.push(link);
|
attachments.push({ attachment, mimeType: blob.mimetype });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.chatSession.createMessage(options);
|
return await this.chatSession.createMessage({ ...options, attachments });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new CopilotFailedToCreateMessage(e.message);
|
throw new CopilotFailedToCreateMessage(e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,11 @@ export class ChatSession implements AsyncDisposable {
|
|||||||
firstMessage.attachments || [],
|
firstMessage.attachments || [],
|
||||||
]
|
]
|
||||||
.flat()
|
.flat()
|
||||||
.filter(v => !!v?.trim());
|
.filter(v =>
|
||||||
|
typeof v === 'string'
|
||||||
|
? !!v.trim()
|
||||||
|
: v && v.attachment.trim() && v.mimeType
|
||||||
|
);
|
||||||
|
|
||||||
return finished;
|
return finished;
|
||||||
}
|
}
|
||||||
@@ -553,7 +557,12 @@ export class ChatSessionService {
|
|||||||
action: prompt.action || null,
|
action: prompt.action || null,
|
||||||
tokens: tokenCost,
|
tokens: tokenCost,
|
||||||
createdAt,
|
createdAt,
|
||||||
messages: preload.concat(ret.data),
|
messages: preload.concat(ret.data).map(m => ({
|
||||||
|
...m,
|
||||||
|
attachments: m.attachments
|
||||||
|
?.map(a => (typeof a === 'string' ? a : a.attachment))
|
||||||
|
.filter(a => !!a),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
|
|||||||
Reference in New Issue
Block a user