Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
bentwnghk committed Jan 3, 2024
2 parents 7854a62 + e801dbe commit 7f2b562
Show file tree
Hide file tree
Showing 78 changed files with 613 additions and 48 deletions.
1 change: 0 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const withPWA = nextPWA({
const nextConfig = {
compress: isProd,
experimental: {
forceSwcTransforms: true,
optimizePackageImports: [
'modern-screenshot',
'emoji-mart',
Expand Down
37 changes: 37 additions & 0 deletions src/app/api/openai/chat/createChatCompletion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ describe('createChatCompletion', () => {
expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(577); // Your custom error status code
});

it('should return an cause response when OpenAI.APIError is thrown with cause', async () => {
// Arrange
const errorInfo = {
Expand Down Expand Up @@ -101,6 +102,42 @@ describe('createChatCompletion', () => {

const content = await result.json();
expect(content.body).toHaveProperty('endpoint');
expect(content.body.endpoint).toEqual('https://api.openai.com/v1');
expect(content.body.error).toEqual(errorInfo);
});

it('should return an cause response with desensitize Url', async () => {
// Arrange
const errorInfo = {
stack: 'abc',
cause: { message: 'api is undefined' },
};
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});

openaiInstance = new OpenAI({
apiKey: 'test',
dangerouslyAllowBrowser: true,
baseURL: 'https://api.abc.com/v1',
});

vi.spyOn(openaiInstance.chat.completions, 'create').mockRejectedValue(apiError);

// Act
const result = await createChatCompletion({
openai: openaiInstance,
payload: {
messages: [{ content: 'Hello', role: 'user' }],
model: 'gpt-3.5-turbo',
temperature: 0,
},
});

// Assert
expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(577); // Your custom error status code

const content = await result.json();
expect(content.body.endpoint).toEqual('https://api.***.com/v1');
expect(content.body.error).toEqual(errorInfo);
});

Expand Down
12 changes: 10 additions & 2 deletions src/app/api/openai/chat/createChatCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChatErrorType } from '@/types/fetch';
import { OpenAIChatStreamPayload } from '@/types/openai/chat';

import { createErrorResponse } from '../errorResponse';
import { desensitizeUrl } from './desensitizeUrl';

interface CreateChatCompletionOptions {
openai: OpenAI;
Expand All @@ -29,6 +30,13 @@ export const createChatCompletion = async ({ payload, openai }: CreateChatComple
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
} catch (error) {
let desensitizedEndpoint = openai.baseURL;

// refs: https://github.com/lobehub/lobe-chat/issues/842
if (openai.baseURL !== 'https://api.openai.com/v1') {
desensitizedEndpoint = desensitizeUrl(openai.baseURL);
}

// Check if the error is an OpenAI APIError
if (error instanceof OpenAI.APIError) {
let errorResult: any;
Expand All @@ -51,7 +59,7 @@ export const createChatCompletion = async ({ payload, openai }: CreateChatComple
console.error(errorResult);

return createErrorResponse(ChatErrorType.OpenAIBizError, {
endpoint: openai.baseURL,
endpoint: desensitizedEndpoint,
error: errorResult,
});
}
Expand All @@ -61,7 +69,7 @@ export const createChatCompletion = async ({ payload, openai }: CreateChatComple

// return as a GatewayTimeout error
return createErrorResponse(ChatErrorType.InternalServerError, {
endpoint: openai.baseURL,
endpoint: desensitizedEndpoint,
error: JSON.stringify(error),
});
}
Expand Down
47 changes: 47 additions & 0 deletions src/app/api/openai/chat/desensitizeUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest';

import { desensitizeUrl } from './desensitizeUrl';

describe('desensitizeUrl', () => {
it('should desensitize a URL with a subdomain', () => {
const originalUrl = 'https://api.example.com/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://api.ex****le.com/v1');
});

it('should desensitize a URL without a subdomain', () => {
const originalUrl = 'https://example.com/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://ex****le.com/v1');
});

it('should desensitize a URL without a subdomain less then 5 chartarters', () => {
const originalUrl = 'https://abc.com/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://***.com/v1');
});

it('should desensitize a URL with multiple subdomains', () => {
const originalUrl = 'https://sub.api.example.com/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://sub.api.ex****le.com/v1');
});

it('should desensitize a URL with path and query parameters', () => {
const originalUrl = 'https://api.example.com/v1?query=123';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://api.ex****le.com/v1?query=123');
});

it('should return the original URL if it is invalid', () => {
const originalUrl = 'invalidurl';
const result = desensitizeUrl(originalUrl);
expect(result).toBe(originalUrl);
});

it('should desensitize a URL with a port number', () => {
const originalUrl = 'https://api.example.com:8080/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://api.ex****le.com:****/v1');
});
});
34 changes: 34 additions & 0 deletions src/app/api/openai/chat/desensitizeUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const desensitizeUrl = (url: string) => {
try {
const urlObj = new URL(url);
const hostnameParts = urlObj.hostname.split('.');
const port = urlObj.port;

// Desensitize domain only if there are at least two parts (example.com)
if (hostnameParts.length > 1) {
// Desensitize the second level domain (second part from the right)
// Special case for short domain names
const secondLevelDomainIndex = hostnameParts.length - 2;
if (hostnameParts[secondLevelDomainIndex].length < 5) {
hostnameParts[secondLevelDomainIndex] = '***';
} else {
hostnameParts[secondLevelDomainIndex] = hostnameParts[secondLevelDomainIndex].replace(
/^(.*?)(\w{2})(\w+)(\w{2})$/,
(_, prefix, start, middle, end) => `${prefix}${start}****${end}`,
);
}
}

// Join the hostname parts back together
const desensitizedHostname = hostnameParts.join('.');

// Desensitize port if present
const desensitizedPort = port ? ':****' : '';

// Reconstruct the URL with the desensitized parts
return `${urlObj.protocol}//${desensitizedHostname}${desensitizedPort}${urlObj.pathname}${urlObj.search}`;
} catch {
// If the URL is invalid, return the original URL
return url;
}
};
2 changes: 1 addition & 1 deletion src/app/chat/features/ChatHeader/ShareButton/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';

import pkg from '@/../package.json';
import ChatList from '@/features/Conversation/ChatList';
import ChatList from '@/features/Conversation/container';
import { useSessionStore } from '@/store/session';
import { agentSelectors, sessionSelectors } from '@/store/session/selectors';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ActionIconGroup, RenderAction, useChatListActionsBar } from '@bentwnghk/ui';
import { ActionIconGroup } from '@bentwnghk/ui';
import { memo } from 'react';

import { RenderAction } from '../components/ChatList';
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
import { ErrorActionsBar } from './Error';
import { useCustomActions } from './customAction';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ActionIconGroup, useChatListActionsBar } from '@bentwnghk/ui';
import { ActionIconGroup } from '@bentwnghk/ui';
import { ActionsBarProps } from '@bentwnghk/ui/es/ChatList/ActionsBar';
import { memo } from 'react';

import { useChatListActionsBar } from '../hooks/useChatListActionsBar';

export const ErrorActionsBar = memo<ActionsBarProps>(({ text, onActionClick }) => {
const { regenerate, del } = useChatListActionsBar(text);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ActionIconGroup, RenderAction, useChatListActionsBar } from '@bentwnghk/ui';
import { ActionIconGroup } from '@bentwnghk/ui';
import { memo } from 'react';

import { RenderAction } from '../components/ChatList';
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';

export const DefaultActionsBar: RenderAction = memo(({ text, onActionClick }) => {
const { del } = useChatListActionsBar(text);
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ActionIconGroup, RenderAction, useChatListActionsBar } from '@bentwnghk/ui';
import { ActionIconGroup } from '@bentwnghk/ui';
import { memo } from 'react';

import { RenderAction } from '../components/ChatList';
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';

export const FunctionActionsBar: RenderAction = memo(({ text, onActionClick }) => {
const { regenerate, divider, del } = useChatListActionsBar(text);
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ActionIconGroup, RenderAction, useChatListActionsBar } from '@bentwnghk/ui';
import { ActionIconGroup } from '@bentwnghk/ui';
import { memo } from 'react';

import { RenderAction } from '../components/ChatList';
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
import { useCustomActions } from './customAction';

export const UserActionsBar: RenderAction = memo(({ text, onActionClick }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ChatListProps } from '@bentwnghk/ui';

import { useChatStore } from '@/store/chat';

import { ChatListProps } from '../components/ChatList';
import { AssistantActionsBar } from './Assistant';
import { DefaultActionsBar } from './Fallback';
import { FunctionActionsBar } from './Function';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Icon, RenderErrorMessage } from '@bentwnghk/ui';
import { Icon } from '@bentwnghk/ui';
import { Button, Input, Segmented } from 'antd';
import { KeySquare, SquareAsterisk } from 'lucide-react';
import { memo, useState } from 'react';
Expand All @@ -8,6 +8,7 @@ import { Flexbox } from 'react-layout-kit';
import { useChatStore } from '@/store/chat';
import { useGlobalStore } from '@/store/global';

import { RenderErrorMessage } from '../components/ChatList';
import APIKeyForm from './ApiKeyForm';
import { ErrorActionContainer, FormAction } from './style';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RenderErrorMessage } from '@bentwnghk/ui';
import { memo } from 'react';

import { RenderErrorMessage } from '../components/ChatList';
import APIKeyForm from './ApiKeyForm';
import { ErrorActionContainer } from './style';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Highlighter, RenderErrorMessage } from '@bentwnghk/ui';
import { Highlighter } from '@bentwnghk/ui';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import { RenderErrorMessage } from '../components/ChatList';
import OpenAPIKey from './OpenAPIKey';

interface OpenAIError {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Highlighter, RenderErrorMessage } from '@bentwnghk/ui';
import { Highlighter } from '@bentwnghk/ui';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import { RenderErrorMessage } from '../../components/ChatList';

interface OpenAIError {
code: 'invalid_api_key' | string;
message: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Avatar, RenderErrorMessage } from '@bentwnghk/ui';
import { Avatar } from '@bentwnghk/ui';
import { Button, Divider } from 'antd';
import { useTheme } from 'antd-style';
import isEqual from 'fast-deep-equal';
Expand All @@ -11,6 +11,7 @@ import { useChatStore } from '@/store/chat';
import { pluginHelpers, useToolStore } from '@/store/tool';
import { pluginSelectors } from '@/store/tool/selectors';

import { RenderErrorMessage } from '../../components/ChatList';
import { ErrorActionContainer, useStyles } from '../style';

const PluginSettings: RenderErrorMessage['Render'] = memo(({ id, plugin }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
import { ChatListProps } from '@bentwnghk/ui';

import { ChatErrorType } from '@/types/fetch';

import { ChatListProps } from '../components/ChatList';
import InvalidAccess from './InvalidAccess';
import OpenAPIKey from './OpenAPIKey';
import OpenAiBizError from './OpenAiBizError';
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Flexbox } from 'react-layout-kit';
import { useChatStore } from '@/store/chat';
import { ChatTranslate } from '@/types/message';

import BubblesLoading from '../Loading';
import BubblesLoading from '../components/BubblesLoading';

const useStyles = createStyles(({ stylish }) => ({
markdown: stylish.markdownInChat,
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ChatListProps, RenderMessageExtra } from '@bentwnghk/ui';

import { ChatListProps, RenderMessageExtra } from '../components/ChatList';
import { AssistantMessageExtra } from './Assistant';
import { UserMessageExtra } from './User';

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ReactNode, memo } from 'react';
import { LOADING_FLAT } from '@/const/message';
import { ChatMessage } from '@/types/message';

import BubblesLoading from '../Loading';
import BubblesLoading from '../components/BubblesLoading';

export const DefaultMessage = memo<
ChatMessage & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import FileList from '@/components/FileList';
import { LOADING_FLAT } from '@/const/message';
import { ChatMessage } from '@/types/message';

import BubblesLoading from '../Loading';
import BubblesLoading from '../components/BubblesLoading';

export const UserMessage = memo<
ChatMessage & {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ChatListProps } from '@bentwnghk/ui';
import { useResponsive } from 'antd-style';
import { useRouter } from 'next/navigation';

Expand All @@ -7,16 +6,17 @@ import { useSessionStore } from '@/store/session';
import { sessionSelectors } from '@/store/session/selectors';
import { pathString } from '@/utils/url';

import { ChatListProps } from '../components/ChatList';
import { AssistantMessage } from './Assistant';
import { DefaultMessage } from './Default';
import { FunctionMessage } from './Function';
import { UserMessage } from './User';

export const renderMessages: ChatListProps['renderMessages'] = {
assistant: AssistantMessage as any,
default: DefaultMessage as any,
function: FunctionMessage as any,
user: UserMessage as any,
assistant: AssistantMessage,
default: DefaultMessage,
function: FunctionMessage,
user: UserMessage,
};

export const useAvatarsClick = (): ChatListProps['onAvatarsClick'] => {
Expand Down
Loading

0 comments on commit 7f2b562

Please sign in to comment.