mirror of
https://github.com/langgenius/dify.git
synced 2026-04-30 15:38:08 +08:00
Merge branch 'main' into feat/summary-index
This commit is contained in:
@ -31,6 +31,8 @@ NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false
|
||||
|
||||
# The timeout for the text generation in millisecond
|
||||
NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
|
||||
# Used by web/docker/entrypoint.sh to overwrite/export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS at container startup (Docker only)
|
||||
TEXT_GENERATION_TIMEOUT_MS=60000
|
||||
|
||||
# CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
||||
NEXT_PUBLIC_CSP_WHITELIST=
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# base image
|
||||
FROM node:22-alpine3.21 AS base
|
||||
FROM node:22.21.1-alpine3.23 AS base
|
||||
LABEL maintainer="takatost@gmail.com"
|
||||
|
||||
# if you located in China, you can use aliyun mirror to speed up
|
||||
|
||||
@ -54,7 +54,7 @@ const pageNameEnrichmentPlugin = (): amplitude.Types.EnrichmentPlugin => {
|
||||
}
|
||||
|
||||
const AmplitudeProvider: FC<IAmplitudeProps> = ({
|
||||
sessionReplaySampleRate = 1,
|
||||
sessionReplaySampleRate = 0.5,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
// Only enable in Saas edition with valid API key
|
||||
|
||||
@ -106,12 +106,12 @@ const ConfigPrompt: FC<Props> = ({
|
||||
const handleAddPrompt = useCallback(() => {
|
||||
const newPrompt = produce(payload as PromptItem[], (draft) => {
|
||||
if (draft.length === 0) {
|
||||
draft.push({ role: PromptRole.system, text: '' })
|
||||
draft.push({ role: PromptRole.system, text: '', id: uuid4() })
|
||||
|
||||
return
|
||||
}
|
||||
const isLastItemUser = draft[draft.length - 1].role === PromptRole.user
|
||||
draft.push({ role: isLastItemUser ? PromptRole.assistant : PromptRole.user, text: '' })
|
||||
draft.push({ role: isLastItemUser ? PromptRole.assistant : PromptRole.user, text: '', id: uuid4() })
|
||||
})
|
||||
onChange(newPrompt)
|
||||
}, [onChange, payload])
|
||||
|
||||
@ -236,7 +236,8 @@
|
||||
"brace-expansion@<2.0.2": "2.0.2",
|
||||
"devalue@<5.3.2": "5.3.2",
|
||||
"es-iterator-helpers": "npm:@nolyfill/es-iterator-helpers@^1",
|
||||
"esbuild@<0.25.0": "0.25.0",
|
||||
"esbuild@<0.27.2": "0.27.2",
|
||||
"glob@>=10.2.0,<10.5.0": "11.1.0",
|
||||
"hasown": "npm:@nolyfill/hasown@^1",
|
||||
"is-arguments": "npm:@nolyfill/is-arguments@^1",
|
||||
"is-core-module": "npm:@nolyfill/is-core-module@^1",
|
||||
@ -278,7 +279,6 @@
|
||||
"@types/react-dom": "~19.2.3",
|
||||
"brace-expansion": "~2.0",
|
||||
"canvas": "^3.2.0",
|
||||
"esbuild": "~0.25.0",
|
||||
"pbkdf2": "~3.1.3",
|
||||
"prismjs": "~1.30",
|
||||
"string-width": "~4.2.3"
|
||||
|
||||
626
web/pnpm-lock.yaml
generated
626
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
231
web/service/base.spec.ts
Normal file
231
web/service/base.spec.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { handleStream } from './base'
|
||||
|
||||
describe('handleStream', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Invalid response data handling', () => {
|
||||
it('should handle null bufferObj from JSON.parse gracefully', async () => {
|
||||
// Arrange
|
||||
const onData = vi.fn()
|
||||
const onCompleted = vi.fn()
|
||||
|
||||
// Create a mock response that returns 'data: null'
|
||||
const mockReader = {
|
||||
read: vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
done: false,
|
||||
value: new TextEncoder().encode('data: null\n'),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
done: true,
|
||||
value: undefined,
|
||||
}),
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => mockReader,
|
||||
},
|
||||
} as unknown as Response
|
||||
|
||||
// Act
|
||||
handleStream(mockResponse, onData, onCompleted)
|
||||
|
||||
// Wait for the stream to be processed
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
// Assert
|
||||
expect(onData).toHaveBeenCalledWith('', true, {
|
||||
conversationId: undefined,
|
||||
messageId: '',
|
||||
errorMessage: 'Invalid response data',
|
||||
errorCode: 'invalid_data',
|
||||
})
|
||||
expect(onCompleted).toHaveBeenCalledWith(true, 'Invalid response data')
|
||||
})
|
||||
|
||||
it('should handle non-object bufferObj from JSON.parse gracefully', async () => {
|
||||
// Arrange
|
||||
const onData = vi.fn()
|
||||
const onCompleted = vi.fn()
|
||||
|
||||
// Create a mock response that returns a primitive value
|
||||
const mockReader = {
|
||||
read: vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
done: false,
|
||||
value: new TextEncoder().encode('data: "string"\n'),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
done: true,
|
||||
value: undefined,
|
||||
}),
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => mockReader,
|
||||
},
|
||||
} as unknown as Response
|
||||
|
||||
// Act
|
||||
handleStream(mockResponse, onData, onCompleted)
|
||||
|
||||
// Wait for the stream to be processed
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
// Assert
|
||||
expect(onData).toHaveBeenCalledWith('', true, {
|
||||
conversationId: undefined,
|
||||
messageId: '',
|
||||
errorMessage: 'Invalid response data',
|
||||
errorCode: 'invalid_data',
|
||||
})
|
||||
expect(onCompleted).toHaveBeenCalledWith(true, 'Invalid response data')
|
||||
})
|
||||
|
||||
it('should handle valid message event correctly', async () => {
|
||||
// Arrange
|
||||
const onData = vi.fn()
|
||||
const onCompleted = vi.fn()
|
||||
|
||||
const validMessage = {
|
||||
event: 'message',
|
||||
answer: 'Hello world',
|
||||
conversation_id: 'conv-123',
|
||||
task_id: 'task-456',
|
||||
id: 'msg-789',
|
||||
}
|
||||
|
||||
const mockReader = {
|
||||
read: vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
done: false,
|
||||
value: new TextEncoder().encode(`data: ${JSON.stringify(validMessage)}\n`),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
done: true,
|
||||
value: undefined,
|
||||
}),
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => mockReader,
|
||||
},
|
||||
} as unknown as Response
|
||||
|
||||
// Act
|
||||
handleStream(mockResponse, onData, onCompleted)
|
||||
|
||||
// Wait for the stream to be processed
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
// Assert
|
||||
expect(onData).toHaveBeenCalledWith('Hello world', true, {
|
||||
conversationId: 'conv-123',
|
||||
taskId: 'task-456',
|
||||
messageId: 'msg-789',
|
||||
})
|
||||
expect(onCompleted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle error status 400 correctly', async () => {
|
||||
// Arrange
|
||||
const onData = vi.fn()
|
||||
const onCompleted = vi.fn()
|
||||
|
||||
const errorMessage = {
|
||||
status: 400,
|
||||
message: 'Bad request',
|
||||
code: 'bad_request',
|
||||
}
|
||||
|
||||
const mockReader = {
|
||||
read: vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
done: false,
|
||||
value: new TextEncoder().encode(`data: ${JSON.stringify(errorMessage)}\n`),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
done: true,
|
||||
value: undefined,
|
||||
}),
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => mockReader,
|
||||
},
|
||||
} as unknown as Response
|
||||
|
||||
// Act
|
||||
handleStream(mockResponse, onData, onCompleted)
|
||||
|
||||
// Wait for the stream to be processed
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
// Assert
|
||||
expect(onData).toHaveBeenCalledWith('', false, {
|
||||
conversationId: undefined,
|
||||
messageId: '',
|
||||
errorMessage: 'Bad request',
|
||||
errorCode: 'bad_request',
|
||||
})
|
||||
expect(onCompleted).toHaveBeenCalledWith(true, 'Bad request')
|
||||
})
|
||||
|
||||
it('should handle malformed JSON gracefully', async () => {
|
||||
// Arrange
|
||||
const onData = vi.fn()
|
||||
const onCompleted = vi.fn()
|
||||
|
||||
const mockReader = {
|
||||
read: vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
done: false,
|
||||
value: new TextEncoder().encode('data: {invalid json}\n'),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
done: true,
|
||||
value: undefined,
|
||||
}),
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => mockReader,
|
||||
},
|
||||
} as unknown as Response
|
||||
|
||||
// Act
|
||||
handleStream(mockResponse, onData, onCompleted)
|
||||
|
||||
// Wait for the stream to be processed
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
// Assert - malformed JSON triggers the catch block which calls onData and returns
|
||||
expect(onData).toHaveBeenCalled()
|
||||
expect(onCompleted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should throw error when response is not ok', () => {
|
||||
// Arrange
|
||||
const onData = vi.fn()
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
} as unknown as Response
|
||||
|
||||
// Act & Assert
|
||||
expect(() => handleStream(mockResponse, onData)).toThrow('Network response was not ok')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -217,6 +217,17 @@ export const handleStream = (
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!bufferObj || typeof bufferObj !== 'object') {
|
||||
onData('', isFirstMessage, {
|
||||
conversationId: undefined,
|
||||
messageId: '',
|
||||
errorMessage: 'Invalid response data',
|
||||
errorCode: 'invalid_data',
|
||||
})
|
||||
hasError = true
|
||||
onCompleted?.(true, 'Invalid response data')
|
||||
return
|
||||
}
|
||||
if (bufferObj.status === 400 || !bufferObj.event) {
|
||||
onData('', false, {
|
||||
conversationId: undefined,
|
||||
|
||||
@ -19,6 +19,28 @@ describe('formatNumber', () => {
|
||||
it('should correctly handle empty input', () => {
|
||||
expect(formatNumber('')).toBe('')
|
||||
})
|
||||
it('should format very small numbers without scientific notation', () => {
|
||||
expect(formatNumber(0.0000008)).toBe('0.0000008')
|
||||
expect(formatNumber(0.0000001)).toBe('0.0000001')
|
||||
expect(formatNumber(0.000001)).toBe('0.000001')
|
||||
expect(formatNumber(0.00001)).toBe('0.00001')
|
||||
})
|
||||
it('should format negative small numbers without scientific notation', () => {
|
||||
expect(formatNumber(-0.0000008)).toBe('-0.0000008')
|
||||
expect(formatNumber(-0.0000001)).toBe('-0.0000001')
|
||||
})
|
||||
it('should handle small numbers from string input', () => {
|
||||
expect(formatNumber('0.0000008')).toBe('0.0000008')
|
||||
expect(formatNumber('8E-7')).toBe('0.0000008')
|
||||
expect(formatNumber('1e-7')).toBe('0.0000001')
|
||||
})
|
||||
it('should handle small numbers with multi-digit mantissa in scientific notation', () => {
|
||||
expect(formatNumber(1.23e-7)).toBe('0.000000123')
|
||||
expect(formatNumber(1.234e-7)).toBe('0.0000001234')
|
||||
expect(formatNumber(12.34e-7)).toBe('0.000001234')
|
||||
expect(formatNumber(0.0001234)).toBe('0.0001234')
|
||||
expect(formatNumber('1.23e-7')).toBe('0.000000123')
|
||||
})
|
||||
})
|
||||
describe('formatFileSize', () => {
|
||||
it('should return the input if it is falsy', () => {
|
||||
|
||||
@ -26,11 +26,39 @@ import 'dayjs/locale/zh-tw'
|
||||
* Formats a number with comma separators.
|
||||
* @example formatNumber(1234567) will return '1,234,567'
|
||||
* @example formatNumber(1234567.89) will return '1,234,567.89'
|
||||
* @example formatNumber(0.0000008) will return '0.0000008'
|
||||
*/
|
||||
export const formatNumber = (num: number | string) => {
|
||||
if (!num)
|
||||
return num
|
||||
const parts = num.toString().split('.')
|
||||
const n = typeof num === 'string' ? Number(num) : num
|
||||
|
||||
let numStr: string
|
||||
|
||||
// Force fixed decimal for small numbers to avoid scientific notation
|
||||
if (Math.abs(n) < 0.001 && n !== 0) {
|
||||
const str = n.toString()
|
||||
const match = str.match(/e-(\d+)$/)
|
||||
let precision: number
|
||||
if (match) {
|
||||
// Scientific notation: precision is exponent + decimal digits in mantissa
|
||||
const exponent = Number.parseInt(match[1], 10)
|
||||
const mantissa = str.split('e')[0]
|
||||
const mantissaDecimalPart = mantissa.split('.')[1]
|
||||
precision = exponent + (mantissaDecimalPart?.length || 0)
|
||||
}
|
||||
else {
|
||||
// Decimal notation: count decimal places
|
||||
const decimalPart = str.split('.')[1]
|
||||
precision = decimalPart?.length || 0
|
||||
}
|
||||
numStr = n.toFixed(precision)
|
||||
}
|
||||
else {
|
||||
numStr = n.toString()
|
||||
}
|
||||
|
||||
const parts = numStr.split('.')
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return parts.join('.')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user