([])
+
+ // Link editor state
+ const [linkEditorState, setLinkEditorState] = useState<{
+ show: boolean
+ position: { x: number; y: number }
+ link: { href: string; text: string; title?: string }
+ linkRange?: { from: number; to: number }
+ }>({
+ show: false,
+ position: { x: 0, y: 0 },
+ link: { href: '', text: '' }
+ })
+
+ // Link hover handlers
+ const handleLinkHover = useCallback(
+ (
+ attrs: { href: string; text: string; title?: string },
+ position: DOMRect,
+ _element: HTMLElement,
+ linkRange?: { from: number; to: number }
+ ) => {
+ if (!editable) return
+
+ const linkPosition = { x: position.left, y: position.top }
+
+ // For empty href, use the text content as initial href suggestion
+ const effectiveHref = attrs.href || attrs.text || ''
+
+ setLinkEditorState({
+ show: true,
+ position: linkPosition,
+ link: { ...attrs, href: effectiveHref },
+ linkRange
+ })
+ },
+ [editable]
+ )
+
+ const handleLinkHoverEnd = useCallback(() => {}, [])
+
+ // TipTap editor extensions
+ const extensions = useMemo(
+ () => [
+ StarterKit.configure({
+ heading: {
+ levels: [1, 2, 3, 4, 5, 6]
+ },
+ codeBlock: false,
+ link: false
+ }),
+ EnhancedLink.configure({
+ onLinkHover: handleLinkHover,
+ onLinkHoverEnd: handleLinkHoverEnd,
+ editable: editable
+ }),
+ TableOfContents.configure({
+ getIndex: getHierarchicalIndexes,
+ onUpdate(content) {
+ const resolveParent = (): HTMLElement | null => {
+ if (!scrollParent) return null
+ return typeof scrollParent === 'function' ? (scrollParent as () => HTMLElement)() : scrollParent
+ }
+
+ const parent = resolveParent()
+ if (!parent) return
+ const parentTop = parent.getBoundingClientRect().top
+
+ let closestIndex = -1
+ let minDelta = Number.POSITIVE_INFINITY
+ for (let i = 0; i < content.length; i++) {
+ const rect = content[i].dom.getBoundingClientRect()
+ const delta = rect.top - parentTop
+ const inThreshold = delta >= -50 && delta < minDelta
+
+ if (inThreshold) {
+ minDelta = delta
+ closestIndex = i
+ }
+ }
+ if (closestIndex === -1) {
+ // If all are above the viewport, pick the last one above
+ for (let i = 0; i < content.length; i++) {
+ const rect = content[i].dom.getBoundingClientRect()
+ if (rect.top < parentTop) closestIndex = i
+ }
+ if (closestIndex === -1) closestIndex = 0
+ }
+
+ const normalized = content.map((item, idx) => {
+ const rect = item.dom.getBoundingClientRect()
+ const isScrolledOver = rect.top < parentTop
+ const isActive = idx === closestIndex
+ return { ...item, isActive, isScrolledOver }
+ })
+
+ setTableOfContentsItems(normalized)
+ },
+ scrollParent: (scrollParent as any) ?? window
+ }),
+ CodeBlockShiki.configure({
+ theme: activeShikiTheme,
+ defaultLanguage: 'text'
+ }),
+ EnhancedMath.configure({
+ blockOptions: {
+ onClick: (node, pos) => {
+ // Get position from the clicked element
+ let position: { x: number; y: number; top: number } | undefined
+ if (event?.target instanceof HTMLElement) {
+ const rect =
+ event.target.closest('.math-display')?.getBoundingClientRect() || event.target.getBoundingClientRect()
+ position = {
+ x: rect.left + rect.width / 2,
+ y: rect.bottom,
+ top: rect.top
+ }
+ }
+
+ const customEvent = new CustomEvent('openMathDialog', {
+ detail: {
+ defaultValue: node.attrs.latex || '',
+ position: position,
+ onSubmit: () => {
+ editor.commands.focus()
+ },
+ onFormulaChange: (formula: string) => {
+ editor.chain().setNodeSelection(pos).updateBlockMath({ latex: formula }).run()
+ }
+ }
+ })
+ window.dispatchEvent(customEvent)
+ return true
+ }
+ },
+ inlineOptions: {
+ onClick: (node, pos) => {
+ let position: { x: number; y: number; top: number } | undefined
+ if (event?.target instanceof HTMLElement) {
+ const rect =
+ event.target.closest('.math-inline')?.getBoundingClientRect() || event.target.getBoundingClientRect()
+ position = {
+ x: rect.left + rect.width / 2,
+ y: rect.bottom,
+ top: rect.top
+ }
+ }
+
+ const customEvent = new CustomEvent('openMathDialog', {
+ detail: {
+ defaultValue: node.attrs.latex || '',
+ position: position,
+ onSubmit: () => {
+ editor.commands.focus()
+ },
+ onFormulaChange: (formula: string) => {
+ editor.chain().setNodeSelection(pos).updateInlineMath({ latex: formula }).run()
+ }
+ }
+ })
+ window.dispatchEvent(customEvent)
+ return true
+ }
+ }
+ }),
+ EnhancedImage,
+ Placeholder.configure({
+ placeholder,
+ showOnlyWhenEditable: true,
+ showOnlyCurrent: true,
+ includeChildren: false
+ }),
+ Mention.configure({
+ HTMLAttributes: {
+ class: 'mention'
+ },
+ suggestion: commandSuggestion
+ }),
+ Typography,
+ TableKit.configure({
+ table: {
+ resizable: true,
+ allowTableNodeSelection: true,
+ onRowActionClick: ({ rowIndex, position }) => {
+ showTableActionMenu('row', rowIndex, position)
+ },
+ onColumnActionClick: ({ colIndex, position }) => {
+ showTableActionMenu('column', colIndex, position)
+ }
+ },
+ tableRow: {},
+ tableHeader: {},
+ tableCell: {
+ allowNestedNodes: false
+ }
+ }),
+ TaskList,
+ TaskItem.configure({
+ nested: true
+ })
+ ],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [placeholder, activeShikiTheme, handleLinkHover, handleLinkHoverEnd]
+ )
+
+ const editor = useEditor({
+ shouldRerenderOnTransaction: true,
+ extensions,
+ content: html || '',
+ editable: editable,
+ editorProps: {
+ handlePaste: (view, event) => {
+ // First check if we're inside a code block - if so, insert plain text
+ const { selection } = view.state
+ const { $from } = selection
+ if ($from.parent.type.name === 'codeBlock') {
+ const text = event.clipboardData?.getData('text/plain') || ''
+ if (text) {
+ const tr = view.state.tr.insertText(text, selection.from, selection.to)
+ view.dispatch(tr)
+ return true
+ }
+ }
+
+ // Handle image paste
+ const items = Array.from(event.clipboardData?.items || [])
+ const imageItem = items.find((item) => item.type.startsWith('image/'))
+
+ if (imageItem) {
+ const file = imageItem.getAsFile()
+ if (file) {
+ // Handle image paste by saving to local storage
+ handleImagePaste(file)
+ return true
+ }
+ }
+
+ // Default behavior for non-code blocks
+ const text = event.clipboardData?.getData('text/plain') ?? ''
+ if (text) {
+ const html = markdownToHtml(text)
+ const { $from } = selection
+ const atStartOfLine = $from.parentOffset === 0
+ const inEmptyParagraph = $from.parent.type.name === 'paragraph' && $from.parent.textContent === ''
+
+ if (!atStartOfLine && !inEmptyParagraph) {
+ const cleanHtml = html.replace(/^(.*?)<\/p>/s, '$1')
+ editor.commands.insertContent(cleanHtml)
+ } else {
+ editor.commands.insertContent(html)
+ }
+ onPaste?.(html)
+ return true
+ }
+ return false
+ },
+ attributes: {
+ // Allow text selection even when not editable
+ style: editable
+ ? ''
+ : 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;'
+ }
+ },
+ onUpdate: ({ editor }) => {
+ const content = editor.getText()
+ const htmlContent = editor.getHTML()
+ try {
+ const convertedMarkdown = htmlToMarkdown(htmlContent)
+ setMarkdownState(convertedMarkdown)
+ onChange?.(convertedMarkdown)
+
+ onContentChange?.(content)
+ if (onHtmlChange) {
+ onHtmlChange(htmlContent)
+ }
+ } catch (error) {
+ logger.error('Error converting HTML to markdown:', error as Error)
+ }
+ },
+ onBlur: () => {
+ onBlur?.()
+ },
+ onCreate: ({ editor: currentEditor }) => {
+ migrateMathStrings(currentEditor)
+ try {
+ currentEditor.commands.focus('end')
+ } catch (error) {
+ logger.warn('Could not set cursor to end:', error as Error)
+ }
+ }
+ })
+
+ // Handle image paste function
+ const handleImagePaste = useCallback(
+ async (file: File) => {
+ try {
+ let processedFile: File | Blob = file
+ let extension = file.type.split('/')[1] ? `.${file.type.split('/')[1]}` : '.png'
+
+ // 如果图片需要压缩,先进行压缩
+ if (shouldCompressImage(file)) {
+ logger.info('Image needs compression, compressing...', {
+ originalSize: file.size,
+ fileName: file.name
+ })
+
+ processedFile = await compressImage(file, {
+ maxWidth: 1200,
+ maxHeight: 1200,
+ quality: 0.8,
+ outputFormat: file.type.includes('png') ? 'png' : 'jpeg'
+ })
+
+ // 更新扩展名
+ extension = file.type.includes('png') ? '.png' : '.jpg'
+
+ logger.info('Image compressed successfully', {
+ originalSize: file.size,
+ compressedSize: processedFile.size,
+ compressionRatio: (((file.size - processedFile.size) / file.size) * 100).toFixed(1) + '%'
+ })
+ }
+
+ // Convert file to buffer
+ const arrayBuffer = await blobToArrayBuffer(processedFile)
+ const buffer = new Uint8Array(arrayBuffer)
+
+ // Save image to local storage
+ const fileMetadata = await window.api.file.savePastedImage(buffer, extension)
+
+ // Insert image into editor using local file path
+ if (editor && !editor.isDestroyed) {
+ const imageUrl = `file://${fileMetadata.path}`
+ editor.chain().focus().setImage({ src: imageUrl, alt: fileMetadata.origin_name }).run()
+ }
+
+ logger.info('Image pasted and saved:', fileMetadata)
+ } catch (error) {
+ logger.error('Failed to handle image paste:', error as Error)
+ }
+ },
+ [editor]
+ )
+
+ useEffect(() => {
+ if (editor && !editor.isDestroyed) {
+ editor.setEditable(editable)
+ if (editable) {
+ try {
+ setTimeout(() => {
+ if (editor && !editor.isDestroyed) {
+ const isLong = editor.getText().length > 2000
+ if (!isLong) {
+ editor.commands.focus('end')
+ }
+ }
+ }, 0)
+ } catch (error) {
+ logger.warn('Could not set cursor to end after enabling editable:', error as Error)
+ }
+ }
+ }
+ }, [editor, editable])
+
+ // Link editor callbacks (after editor is defined)
+ const handleLinkSave = useCallback(
+ (href: string, text: string) => {
+ if (!editor || editor.isDestroyed) return
+
+ const { linkRange } = linkEditorState
+
+ if (linkRange) {
+ // We have explicit link range - use it
+ editor
+ .chain()
+ .focus()
+ .setTextSelection({ from: linkRange.from, to: linkRange.to })
+ .insertContent(text)
+ .setTextSelection({ from: linkRange.from, to: linkRange.from + text.length })
+ .setEnhancedLink({ href })
+ .run()
+ }
+ setLinkEditorState({
+ show: false,
+ position: { x: 0, y: 0 },
+ link: { href: '', text: '' }
+ })
+ },
+ [editor, linkEditorState]
+ )
+
+ const handleLinkRemove = useCallback(() => {
+ if (!editor || editor.isDestroyed) return
+
+ const { linkRange } = linkEditorState
+
+ if (linkRange) {
+ // Use a more reliable method - directly remove the mark from the range
+ const tr = editor.state.tr
+ tr.removeMark(linkRange.from, linkRange.to, editor.schema.marks.enhancedLink || editor.schema.marks.link)
+ editor.view.dispatch(tr)
+ } else {
+ // No explicit range - try to extend current mark range and remove
+ editor.chain().focus().extendMarkRange('enhancedLink').unsetEnhancedLink().run()
+ }
+
+ // Close link editor
+ setLinkEditorState({
+ show: false,
+ position: { x: 0, y: 0 },
+ link: { href: '', text: '' }
+ })
+ }, [editor, linkEditorState])
+
+ const handleLinkCancel = useCallback(() => {
+ setLinkEditorState({
+ show: false,
+ position: { x: 0, y: 0 },
+ link: { href: '', text: '' }
+ })
+ }, [])
+
+ // Show action menu for table rows/columns
+ const showTableActionMenu = useCallback(
+ (type: 'row' | 'column', index: number, position?: { x: number; y: number }) => {
+ if (!editor) return
+
+ const actions = [
+ {
+ id: type === 'row' ? 'insertRowBefore' : 'insertColumnBefore',
+ label:
+ type === 'row'
+ ? t('richEditor.action.table.insertRowBefore')
+ : t('richEditor.action.table.insertColumnBefore'),
+ action: () => {
+ if (type === 'row') {
+ editor.chain().focus().addRowBefore().run()
+ } else {
+ editor.chain().focus().addColumnBefore().run()
+ }
+ }
+ },
+ {
+ id: type === 'row' ? 'insertRowAfter' : 'insertColumnAfter',
+ label:
+ type === 'row'
+ ? t('richEditor.action.table.insertRowAfter')
+ : t('richEditor.action.table.insertColumnAfter'),
+ action: () => {
+ if (type === 'row') {
+ editor.chain().focus().addRowAfter().run()
+ } else {
+ editor.chain().focus().addColumnAfter().run()
+ }
+ }
+ },
+ {
+ id: type === 'row' ? 'deleteRow' : 'deleteColumn',
+ label: type === 'row' ? t('richEditor.action.table.deleteRow') : t('richEditor.action.table.deleteColumn'),
+ action: () => {
+ if (type === 'row') {
+ editor.chain().focus().deleteRow().run()
+ } else {
+ editor.chain().focus().deleteColumn().run()
+ }
+ }
+ }
+ ]
+
+ // Compute fallback position if not provided
+ let finalPosition = position
+ if (!finalPosition) {
+ const rect = editor.view.dom.getBoundingClientRect()
+ finalPosition = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
+ }
+
+ onShowTableActionMenu?.({ type, index, position: finalPosition!, actions })
+ },
+ [editor, onShowTableActionMenu]
+ )
+
+ useEffect(() => {
+ return () => {
+ if (editor && !editor.isDestroyed) {
+ editor.destroy()
+ }
+ }
+ }, [editor])
+
+ const formattingState = useEditorState({
+ editor,
+ selector: ({ editor }) => {
+ if (!editor || editor.isDestroyed) {
+ return {
+ isBold: false,
+ canBold: false,
+ isItalic: false,
+ canItalic: false,
+ isUnderline: false,
+ canUnderline: false,
+ isStrike: false,
+ canStrike: false,
+ isCode: false,
+ canCode: false,
+ canClearMarks: false,
+ isParagraph: false,
+ isHeading1: false,
+ isHeading2: false,
+ isHeading3: false,
+ isHeading4: false,
+ isHeading5: false,
+ isHeading6: false,
+ isBulletList: false,
+ isOrderedList: false,
+ isCodeBlock: false,
+ isBlockquote: false,
+ isLink: false,
+ canLink: false,
+ canUnlink: false,
+ canUndo: false,
+ canRedo: false,
+ isTable: false,
+ canTable: false,
+ canImage: false,
+ isMath: false,
+ isInlineMath: false,
+ canMath: false,
+ isTaskList: false
+ }
+ }
+
+ return {
+ isBold: editor.isActive('bold') ?? false,
+ canBold: editor.can().chain().toggleBold().run() ?? false,
+ isItalic: editor.isActive('italic') ?? false,
+ canItalic: editor.can().chain().toggleItalic().run() ?? false,
+ isUnderline: editor.isActive('underline') ?? false,
+ canUnderline: editor.can().chain().toggleUnderline().run() ?? false,
+ isStrike: editor.isActive('strike') ?? false,
+ canStrike: editor.can().chain().toggleStrike().run() ?? false,
+ isCode: editor.isActive('code') ?? false,
+ canCode: editor.can().chain().toggleCode().run() ?? false,
+ canClearMarks: editor.can().chain().unsetAllMarks().run() ?? false,
+ isParagraph: editor.isActive('paragraph') ?? false,
+ isHeading1: editor.isActive('heading', { level: 1 }) ?? false,
+ isHeading2: editor.isActive('heading', { level: 2 }) ?? false,
+ isHeading3: editor.isActive('heading', { level: 3 }) ?? false,
+ isHeading4: editor.isActive('heading', { level: 4 }) ?? false,
+ isHeading5: editor.isActive('heading', { level: 5 }) ?? false,
+ isHeading6: editor.isActive('heading', { level: 6 }) ?? false,
+ isBulletList: editor.isActive('bulletList') ?? false,
+ isOrderedList: editor.isActive('orderedList') ?? false,
+ isCodeBlock: editor.isActive('codeBlock') ?? false,
+ isBlockquote: editor.isActive('blockquote') ?? false,
+ isLink: (editor.isActive('enhancedLink') || editor.isActive('link')) ?? false,
+ canLink: editor.can().chain().setEnhancedLink({ href: '' }).run() ?? false,
+ canUnlink: editor.can().chain().unsetEnhancedLink().run() ?? false,
+ canUndo: editor.can().chain().undo().run() ?? false,
+ canRedo: editor.can().chain().redo().run() ?? false,
+ isTable: editor.isActive('table') ?? false,
+ canTable: editor.can().chain().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() ?? false,
+ canImage: editor.can().chain().setImage({ src: '' }).run() ?? false,
+ isMath: editor.isActive('blockMath') ?? false,
+ isInlineMath: editor.isActive('inlineMath') ?? false,
+ canMath: true,
+ isTaskList: editor.isActive('taskList') ?? false
+ }
+ }
+ })
+
+ const setMarkdown = useCallback(
+ (content: string) => {
+ try {
+ setMarkdownState(content)
+ onChange?.(content)
+
+ const convertedHtml = markdownToHtml(content)
+
+ editor.commands.setContent(convertedHtml)
+
+ onHtmlChange?.(convertedHtml)
+ } catch (error) {
+ logger.error('Error setting markdown content:', error as Error)
+ }
+ },
+ [editor.commands, onChange, onHtmlChange]
+ )
+
+ const setHtml = useCallback(
+ (htmlContent: string) => {
+ try {
+ const convertedMarkdown = htmlToMarkdown(htmlContent)
+ setMarkdownState(convertedMarkdown)
+ onChange?.(convertedMarkdown)
+
+ editor.commands.setContent(htmlContent)
+
+ onHtmlChange?.(htmlContent)
+ } catch (error) {
+ logger.error('Error setting HTML content:', error as Error)
+ }
+ },
+ [editor.commands, onChange, onHtmlChange]
+ )
+
+ const clear = useCallback(() => {
+ setMarkdownState('')
+ onChange?.('')
+ onHtmlChange?.('')
+ }, [onChange, onHtmlChange])
+
+ // Utility methods
+ const toHtml = useCallback((content: string): string => {
+ try {
+ return markdownToHtml(content)
+ } catch (error) {
+ logger.error('Error converting markdown to HTML:', error as Error)
+ return ''
+ }
+ }, [])
+
+ const toSafeHtml = useCallback((content: string): string => {
+ try {
+ return markdownToHtml(content)
+ } catch (error) {
+ logger.error('Error converting markdown to safe HTML:', error as Error)
+ return ''
+ }
+ }, [])
+
+ const toMarkdown = useCallback((htmlContent: string): string => {
+ try {
+ return htmlToMarkdown(htmlContent)
+ } catch (error) {
+ logger.error('Error converting HTML to markdown:', error as Error)
+ return ''
+ }
+ }, [])
+
+ const getPreviewText = useCallback(
+ (content: string, maxLength?: number): string => {
+ try {
+ return markdownToPreviewText(content, maxLength || previewLength)
+ } catch (error) {
+ logger.error('Error generating preview text:', error as Error)
+ return ''
+ }
+ },
+ [previewLength]
+ )
+
+ return {
+ // Editor instance
+ editor,
+
+ // State
+ markdown,
+ html,
+ previewText,
+ isMarkdown,
+ disabled: !editable,
+ formattingState,
+ tableOfContentsItems,
+ linkEditor: {
+ show: linkEditorState.show,
+ position: linkEditorState.position,
+ link: linkEditorState.link,
+ onSave: handleLinkSave,
+ onRemove: handleLinkRemove,
+ onCancel: handleLinkCancel
+ },
+
+ // Actions
+ setMarkdown,
+ setHtml,
+ clear,
+
+ // Utilities
+ toHtml,
+ toSafeHtml,
+ toMarkdown,
+ getPreviewText
+ }
+}
diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx
index b8065ed40e..66e62dca6f 100644
--- a/src/renderer/src/components/Tab/TabContainer.tsx
+++ b/src/renderer/src/components/Tab/TabContainer.tsx
@@ -20,6 +20,7 @@ import {
LayoutGrid,
Monitor,
Moon,
+ NotepadText,
Palette,
Settings,
Sparkle,
@@ -50,6 +51,8 @@ const getTabIcon = (tabId: string): React.ReactNode | undefined => {
return
case 'apps':
return
+ case 'notes':
+ return
case 'knowledge':
return
case 'mcp':
diff --git a/src/renderer/src/components/Tags/CustomTag.tsx b/src/renderer/src/components/Tags/CustomTag.tsx
index c7abd15a62..561b3edf41 100644
--- a/src/renderer/src/components/Tags/CustomTag.tsx
+++ b/src/renderer/src/components/Tags/CustomTag.tsx
@@ -1,6 +1,6 @@
import { CloseOutlined } from '@ant-design/icons'
import { Tooltip } from 'antd'
-import { CSSProperties, FC, memo, useMemo } from 'react'
+import { CSSProperties, FC, memo, MouseEventHandler, useMemo } from 'react'
import styled from 'styled-components'
export interface CustomTagProps {
@@ -12,7 +12,7 @@ export interface CustomTagProps {
tooltip?: string
closable?: boolean
onClose?: () => void
- onClick?: () => void
+ onClick?: MouseEventHandler
disabled?: boolean
inactive?: boolean
}
@@ -37,8 +37,12 @@ const CustomTag: FC = ({
$color={actualColor}
$size={size}
$closable={closable}
+ $clickable={!disabled && !!onClick}
onClick={disabled ? undefined : onClick}
- style={{ cursor: disabled ? 'not-allowed' : onClick ? 'pointer' : 'auto', ...style }}>
+ style={{
+ ...(disabled && { cursor: 'not-allowed' }),
+ ...style
+ }}>
{icon && icon} {children}
{closable && (
= ({
export default memo(CustomTag)
-const Tag = styled.div<{ $color: string; $size: number; $closable: boolean }>`
+const Tag = styled.div<{ $color: string; $size: number; $closable: boolean; $clickable: boolean }>`
display: inline-flex;
align-items: center;
gap: 4px;
@@ -79,10 +83,16 @@ const Tag = styled.div<{ $color: string; $size: number; $closable: boolean }>`
line-height: 1;
white-space: nowrap;
position: relative;
+ cursor: ${({ $clickable }) => ($clickable ? 'pointer' : 'auto')};
.iconfont {
font-size: ${({ $size }) => $size}px;
color: ${({ $color }) => $color};
}
+
+ transition: opacity 0.2s ease;
+ &:hover {
+ opacity: ${({ $clickable }) => ($clickable ? 0.8 : 1)};
+ }
`
const CloseIcon = styled(CloseOutlined)<{ $size: number; $color: string }>`
diff --git a/src/renderer/src/components/Tags/Model/FreeTag.tsx b/src/renderer/src/components/Tags/Model/FreeTag.tsx
index a046449dcf..44b19012e0 100644
--- a/src/renderer/src/components/Tags/Model/FreeTag.tsx
+++ b/src/renderer/src/components/Tags/Model/FreeTag.tsx
@@ -15,6 +15,7 @@ export const FreeTag = ({ size, showTooltip, ...restProps }: Props) => {
color="#7cb305"
icon={t('models.type.free')}
tooltip={showTooltip ? t('models.type.free') : undefined}
- {...restProps}>
+ {...restProps}
+ />
)
}
diff --git a/src/renderer/src/components/__tests__/CustomTag.test.tsx b/src/renderer/src/components/__tests__/CustomTag.test.tsx
index 12a4620b3d..55359ea5cc 100644
--- a/src/renderer/src/components/__tests__/CustomTag.test.tsx
+++ b/src/renderer/src/components/__tests__/CustomTag.test.tsx
@@ -41,4 +41,15 @@ describe('CustomTag', () => {
expect(document.querySelector('.ant-tooltip')).toBeNull()
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
})
+
+ it('should not allow click when disabled', async () => {
+ render(
+
+ custom-tag
+
+ )
+ const tag = screen.getByText('custom-tag')
+ expect(tag).toBeInTheDocument()
+ expect(tag).toHaveStyle({ cursor: 'not-allowed' })
+ })
})
diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx
index e41b02f08a..7b0069a025 100644
--- a/src/renderer/src/components/app/Sidebar.tsx
+++ b/src/renderer/src/components/app/Sidebar.tsx
@@ -22,6 +22,7 @@ import {
MessageSquare,
Monitor,
Moon,
+ NotepadText,
Palette,
Settings,
Sparkle,
@@ -136,7 +137,8 @@ const MainMenus: FC = () => {
translate: ,
minapp: ,
knowledge: ,
- files: ,
+ files: ,
+ notes: ,
code_tools:
}
@@ -148,7 +150,8 @@ const MainMenus: FC = () => {
minapp: '/apps',
knowledge: '/knowledge',
files: '/files',
- code_tools: '/code'
+ code_tools: '/code',
+ notes: '/notes'
}
return sidebarIcons.visible.map((icon) => {
diff --git a/src/renderer/src/config/minapps.ts b/src/renderer/src/config/minapps.ts
index aa2cee372c..e11718bc62 100644
--- a/src/renderer/src/config/minapps.ts
+++ b/src/renderer/src/config/minapps.ts
@@ -27,6 +27,7 @@ import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.webp?url'
import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url'
import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url'
+import LongCatAppLogo from '@renderer/assets/images/apps/longcat.svg?url'
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
import MonicaLogo from '@renderer/assets/images/apps/monica.webp?url'
import n8nLogo from '@renderer/assets/images/apps/n8n.svg?url'
@@ -35,7 +36,6 @@ import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
-import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.webp?url'
@@ -56,6 +56,7 @@ import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
+import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png?url'
import i18n from '@renderer/i18n'
import { MinAppType } from '@renderer/types'
@@ -126,7 +127,8 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
id: 'zhipu',
name: i18n.t('minapps.chatglm'),
url: 'https://chatglm.cn/main/alltoolsdetail',
- logo: ZhipuProviderLogo
+ logo: ZhipuProviderLogo,
+ bodered: true
},
{
id: 'moonshot',
@@ -475,6 +477,13 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
style: {
padding: 5
}
+ },
+ {
+ id: 'longcat',
+ name: 'LongCat',
+ logo: LongCatAppLogo,
+ url: 'https://longcat.chat/',
+ bodered: true
}
]
diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts
index afee5463a2..5324f7004e 100644
--- a/src/renderer/src/config/models.ts
+++ b/src/renderer/src/config/models.ts
@@ -146,8 +146,11 @@ import XirangModelLogo from '@renderer/assets/images/models/xirang.png'
import XirangModelLogoDark from '@renderer/assets/images/models/xirang_dark.png'
import YiModelLogo from '@renderer/assets/images/models/yi.png'
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
+import ZhipuModelLogo from '@renderer/assets/images/models/zhipu.png'
+import ZhipuModelLogoDark from '@renderer/assets/images/models/zhipu_dark.png'
import YoudaoLogo from '@renderer/assets/images/providers/netease-youdao.svg'
import NomicLogo from '@renderer/assets/images/providers/nomic.png'
+import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import { getProviderByModel } from '@renderer/services/AssistantService'
import {
isSystemProviderId,
@@ -163,247 +166,6 @@ import OpenAI from 'openai'
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from './prompts'
import { getWebSearchTools } from './tools'
-// Vision models
-const visionAllowedModels = [
- 'llava',
- 'moondream',
- 'minicpm',
- 'gemini-1\\.5',
- 'gemini-2\\.0',
- 'gemini-2\\.5',
- 'gemini-exp',
- 'claude-3',
- 'claude-sonnet-4',
- 'claude-opus-4',
- 'vision',
- 'glm-4(?:\\.\\d+)?v(?:-[\\w-]+)?',
- 'qwen-vl',
- 'qwen2-vl',
- 'qwen2.5-vl',
- 'qwen2.5-omni',
- 'qvq',
- 'internvl2',
- 'grok-vision-beta',
- 'grok-4(?:-[\\w-]+)?',
- 'pixtral',
- 'gpt-4(?:-[\\w-]+)',
- 'gpt-4.1(?:-[\\w-]+)?',
- 'gpt-4o(?:-[\\w-]+)?',
- 'gpt-4.5(?:-[\\w-]+)',
- 'gpt-5(?:-[\\w-]+)?',
- 'chatgpt-4o(?:-[\\w-]+)?',
- 'o1(?:-[\\w-]+)?',
- 'o3(?:-[\\w-]+)?',
- 'o4(?:-[\\w-]+)?',
- 'deepseek-vl(?:[\\w-]+)?',
- 'kimi-latest',
- 'gemma-3(?:-[\\w-]+)',
- 'doubao-seed-1[.-]6(?:-[\\w-]+)?',
- 'kimi-thinking-preview',
- `gemma3(?:[-:\\w]+)?`,
- 'kimi-vl-a3b-thinking(?:-[\\w-]+)?',
- 'llama-guard-4(?:-[\\w-]+)?',
- 'llama-4(?:-[\\w-]+)?',
- 'step-1o(?:.*vision)?',
- 'step-1v(?:-[\\w-]+)?'
-]
-
-const visionExcludedModels = [
- 'gpt-4-\\d+-preview',
- 'gpt-4-turbo-preview',
- 'gpt-4-32k',
- 'gpt-4-\\d+',
- 'o1-mini',
- 'o3-mini',
- 'o1-preview',
- 'AIDC-AI/Marco-o1'
-]
-export const VISION_REGEX = new RegExp(
- `\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
- 'i'
-)
-
-// For middleware to identify models that must use the dedicated Image API
-export const DEDICATED_IMAGE_MODELS = ['grok-2-image', 'dall-e-3', 'dall-e-2', 'gpt-image-1']
-export const isDedicatedImageGenerationModel = (model: Model): boolean => {
- const modelId = getLowerBaseModelName(model.id)
- return DEDICATED_IMAGE_MODELS.filter((m) => modelId.includes(m)).length > 0
-}
-
-// Text to image models
-export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus|midjourney|mj-|image|gpt-image/i
-
-// Reasoning models
-export const REASONING_REGEX =
- /^(o\d+(?:-[\w-]+)?|.*\b(?:reasoning|reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-(?:3-mini|4)(?:-[\w-]+)?\b.*)$/i
-
-// Embedding models
-export const EMBEDDING_REGEX =
- /(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings|voyage-)/i
-
-// Rerank models
-export const RERANKING_REGEX = /(?:rerank|re-rank|re-ranker|re-ranking|retrieval|retriever)/i
-
-export const NOT_SUPPORTED_REGEX = /(?:^tts|whisper|speech)/i
-
-// Tool calling models
-export const FUNCTION_CALLING_MODELS = [
- 'gpt-4o',
- 'gpt-4o-mini',
- 'gpt-4',
- 'gpt-4.5',
- 'gpt-oss(?:-[\\w-]+)',
- 'gpt-5(?:-[0-9-]+)?',
- 'o(1|3|4)(?:-[\\w-]+)?',
- 'claude',
- 'qwen',
- 'qwen3',
- 'hunyuan',
- 'deepseek',
- 'glm-4(?:-[\\w-]+)?',
- 'glm-4.5(?:-[\\w-]+)?',
- 'learnlm(?:-[\\w-]+)?',
- 'gemini(?:-[\\w-]+)?', // 提前排除了gemini的嵌入模型
- 'grok-3(?:-[\\w-]+)?',
- 'doubao-seed-1[.-]6(?:-[\\w-]+)?',
- 'kimi-k2(?:-[\\w-]+)?'
-]
-
-const FUNCTION_CALLING_EXCLUDED_MODELS = [
- 'aqa(?:-[\\w-]+)?',
- 'imagen(?:-[\\w-]+)?',
- 'o1-mini',
- 'o1-preview',
- 'AIDC-AI/Marco-o1',
- 'gemini-1(?:\\.[\\w-]+)?',
- 'qwen-mt(?:-[\\w-]+)?',
- 'gpt-5-chat(?:-[\\w-]+)?'
-]
-
-export const FUNCTION_CALLING_REGEX = new RegExp(
- `\\b(?!(?:${FUNCTION_CALLING_EXCLUDED_MODELS.join('|')})\\b)(?:${FUNCTION_CALLING_MODELS.join('|')})\\b`,
- 'i'
-)
-
-export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp(
- `\\b(?:claude-3(-|\\.)(7|5)-sonnet(?:-[\\w-]+)|claude-3(-|\\.)5-haiku(?:-[\\w-]+)|claude-sonnet-4(?:-[\\w-]+)?|claude-opus-4(?:-[\\w-]+)?)\\b`,
- 'i'
-)
-
-// 模型类型到支持的reasoning_effort的映射表
-// TODO: refactor this. too many identical options
-export const MODEL_SUPPORTED_REASONING_EFFORT: ReasoningEffortConfig = {
- default: ['low', 'medium', 'high'] as const,
- o: ['low', 'medium', 'high'] as const,
- gpt5: ['minimal', 'low', 'medium', 'high'] as const,
- grok: ['low', 'high'] as const,
- gemini: ['low', 'medium', 'high', 'auto'] as const,
- gemini_pro: ['low', 'medium', 'high', 'auto'] as const,
- qwen: ['low', 'medium', 'high'] as const,
- qwen_thinking: ['low', 'medium', 'high'] as const,
- doubao: ['auto', 'high'] as const,
- doubao_no_auto: ['high'] as const,
- hunyuan: ['auto'] as const,
- zhipu: ['auto'] as const,
- perplexity: ['low', 'medium', 'high'] as const,
- deepseek_hybrid: ['auto'] as const
-} as const
-
-// 模型类型到支持选项的映射表
-export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
- default: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.default] as const,
- o: MODEL_SUPPORTED_REASONING_EFFORT.o,
- gpt5: [...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const,
- grok: MODEL_SUPPORTED_REASONING_EFFORT.grok,
- gemini: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
- gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
- qwen: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
- qwen_thinking: MODEL_SUPPORTED_REASONING_EFFORT.qwen_thinking,
- doubao: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.doubao] as const,
- doubao_no_auto: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.doubao_no_auto] as const,
- hunyuan: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.hunyuan] as const,
- zhipu: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.zhipu] as const,
- perplexity: MODEL_SUPPORTED_REASONING_EFFORT.perplexity,
- deepseek_hybrid: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.deepseek_hybrid] as const
-} as const
-
-export const getThinkModelType = (model: Model): ThinkingModelType => {
- let thinkingModelType: ThinkingModelType = 'default'
- if (isGPT5SeriesModel(model)) {
- thinkingModelType = 'gpt5'
- } else if (isSupportedReasoningEffortOpenAIModel(model)) {
- thinkingModelType = 'o'
- } else if (isSupportedThinkingTokenGeminiModel(model)) {
- if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
- thinkingModelType = 'gemini'
- } else {
- thinkingModelType = 'gemini_pro'
- }
- } else if (isSupportedReasoningEffortGrokModel(model)) thinkingModelType = 'grok'
- else if (isSupportedThinkingTokenQwenModel(model)) {
- if (isQwenAlwaysThinkModel(model)) {
- thinkingModelType = 'qwen_thinking'
- }
- thinkingModelType = 'qwen'
- } else if (isSupportedThinkingTokenDoubaoModel(model)) {
- if (isDoubaoThinkingAutoModel(model)) {
- thinkingModelType = 'doubao'
- } else {
- thinkingModelType = 'doubao_no_auto'
- }
- } else if (isSupportedThinkingTokenHunyuanModel(model)) thinkingModelType = 'hunyuan'
- else if (isSupportedReasoningEffortPerplexityModel(model)) thinkingModelType = 'perplexity'
- else if (isSupportedThinkingTokenZhipuModel(model)) thinkingModelType = 'zhipu'
- else if (isDeepSeekHybridInferenceModel(model)) thinkingModelType = 'deepseek_hybrid'
- return thinkingModelType
-}
-
-export function isFunctionCallingModel(model?: Model): boolean {
- if (
- !model ||
- isEmbeddingModel(model) ||
- isRerankModel(model) ||
- isTextToImageModel(model) ||
- isGenerateImageModel(model)
- ) {
- return false
- }
-
- const modelId = getLowerBaseModelName(model.id)
-
- if (isUserSelectedModelType(model, 'function_calling') !== undefined) {
- return isUserSelectedModelType(model, 'function_calling')!
- }
-
- if (model.provider === 'qiniu') {
- return ['deepseek-v3-tool', 'deepseek-v3-0324', 'qwq-32b', 'qwen2.5-72b-instruct'].includes(modelId)
- }
-
- if (model.provider === 'doubao' || modelId.includes('doubao')) {
- return FUNCTION_CALLING_REGEX.test(modelId) || FUNCTION_CALLING_REGEX.test(model.name)
- }
-
- if (['deepseek', 'anthropic', 'kimi', 'moonshot'].includes(model.provider)) {
- return true
- }
-
- // 2025/08/26 百炼与火山引擎均不支持 v3.1 函数调用
- // 先默认支持
- if (isDeepSeekHybridInferenceModel(model)) {
- if (isSystemProviderId(model.provider)) {
- switch (model.provider) {
- case 'dashscope':
- case 'doubao':
- // case 'nvidia': // nvidia api 太烂了 测不了能不能用 先假设能用
- return false
- }
- }
- return true
- }
-
- return FUNCTION_CALLING_REGEX.test(modelId)
-}
-
export function getModelLogo(modelId: string) {
const isLight = true
@@ -521,6 +283,7 @@ export function getModelLogo(modelId: string) {
xirang: isLight ? XirangModelLogo : XirangModelLogoDark,
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,
youdao: YoudaoLogo,
+ 'embedding-3': ZhipuProviderLogo,
embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
@@ -528,7 +291,9 @@ export function getModelLogo(modelId: string) {
'voyage-': VoyageModelLogo,
tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark,
'nomic-': NomicLogo,
- 'pangu-': PanguModelLogo
+ 'pangu-': PanguModelLogo,
+ cogview: isLight ? ZhipuModelLogo : ZhipuModelLogoDark,
+ zhipu: isLight ? ZhipuModelLogo : ZhipuModelLogoDark
}
for (const key in logoMap) {
@@ -541,35 +306,43 @@ export function getModelLogo(modelId: string) {
return undefined
}
+export const glm45FlashModel: Model = {
+ id: 'glm-4.5-flash',
+ name: 'GLM-4.5-Flash',
+ provider: 'cherryin',
+ group: 'GLM-4.5'
+}
+
+export const qwen38bModel: Model = {
+ id: 'Qwen/Qwen3-8B',
+ name: 'Qwen3-8B',
+ provider: 'cherryin',
+ group: 'Qwen'
+}
+
export const SYSTEM_MODELS: Record = {
defaultModel: [
+ // Default assistant model
+ glm45FlashModel,
+ // Default topic naming model
+ qwen38bModel,
+ // Default translation model
+ glm45FlashModel,
+ // Default quick assistant model
+ glm45FlashModel
+ ],
+ cherryin: [
{
- // 默认助手模型
- id: 'deepseek-ai/DeepSeek-V3',
- name: 'deepseek-ai/DeepSeek-V3',
- provider: 'silicon',
- group: 'deepseek-ai'
+ id: 'glm-4.5-flash',
+ name: 'GLM-4.5-Flash',
+ provider: 'cherryin',
+ group: 'GLM-4.5'
},
{
- // 默认话题命名模型
id: 'Qwen/Qwen3-8B',
- name: 'Qwen/Qwen3-8B',
- provider: 'silicon',
+ name: 'Qwen3-8B',
+ provider: 'cherryin',
group: 'Qwen'
- },
- {
- // 默认翻译模型
- id: 'deepseek-ai/DeepSeek-V3',
- name: 'deepseek-ai/DeepSeek-V3',
- provider: 'silicon',
- group: 'deepseek-ai'
- },
- {
- // 默认快捷助手模型
- id: 'deepseek-ai/DeepSeek-V3',
- name: 'deepseek-ai/DeepSeek-V3',
- provider: 'silicon',
- group: 'deepseek-ai'
}
],
vertexai: [],
@@ -966,6 +739,12 @@ export const SYSTEM_MODELS: Record =
provider: 'gemini',
name: 'Gemini 2.0 Flash',
group: 'Gemini 2.0'
+ },
+ {
+ id: 'gemini-2.5-flash-image-preview',
+ provider: 'gemini',
+ name: 'Gemini 2.5 Flash Image',
+ group: 'Gemini 2.5'
}
],
anthropic: [
@@ -1231,113 +1010,35 @@ export const SYSTEM_MODELS: Record =
{ id: 'yi-vision-v2', name: 'Yi Vision v2', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
],
zhipu: [
- {
- id: 'glm-4.5',
- provider: 'zhipu',
- name: 'GLM-4.5',
- group: 'GLM-4.5'
- },
{
id: 'glm-4.5-flash',
provider: 'zhipu',
name: 'GLM-4.5-Flash',
group: 'GLM-4.5'
},
+ {
+ id: 'glm-4.5',
+ provider: 'zhipu',
+ name: 'GLM-4.5',
+ group: 'GLM-4.5'
+ },
{
id: 'glm-4.5-air',
provider: 'zhipu',
- name: 'GLM-4.5-AIR',
+ name: 'GLM-4.5-Air',
group: 'GLM-4.5'
},
{
id: 'glm-4.5-airx',
provider: 'zhipu',
- name: 'GLM-4.5-AIRX',
+ name: 'GLM-4.5-AirX',
group: 'GLM-4.5'
},
{
- id: 'glm-4.5-x',
+ id: 'glm-4.5v',
provider: 'zhipu',
- name: 'GLM-4.5-X',
- group: 'GLM-4.5'
- },
- {
- id: 'glm-z1-air',
- provider: 'zhipu',
- name: 'GLM-Z1-AIR',
- group: 'GLM-Z1'
- },
- {
- id: 'glm-z1-airx',
- provider: 'zhipu',
- name: 'GLM-Z1-AIRX',
- group: 'GLM-Z1'
- },
- {
- id: 'glm-z1-flash',
- provider: 'zhipu',
- name: 'GLM-Z1-FLASH',
- group: 'GLM-Z1'
- },
- {
- id: 'glm-4-long',
- provider: 'zhipu',
- name: 'GLM-4-Long',
- group: 'GLM-4'
- },
- {
- id: 'glm-4-plus',
- provider: 'zhipu',
- name: 'GLM-4-Plus',
- group: 'GLM-4'
- },
- {
- id: 'glm-4-air-250414',
- provider: 'zhipu',
- name: 'GLM-4-Air-250414',
- group: 'GLM-4'
- },
- {
- id: 'glm-4-airx',
- provider: 'zhipu',
- name: 'GLM-4-AirX',
- group: 'GLM-4'
- },
- {
- id: 'glm-4-flash-250414',
- provider: 'zhipu',
- name: 'GLM-4-Flash-250414',
- group: 'GLM-4'
- },
- {
- id: 'glm-4-flashx',
- provider: 'zhipu',
- name: 'GLM-4-FlashX',
- group: 'GLM-4'
- },
- {
- id: 'glm-4v',
- provider: 'zhipu',
- name: 'GLM 4V',
- group: 'GLM-4v'
- },
- {
- id: 'glm-4v-flash',
- provider: 'zhipu',
- name: 'GLM-4V-Flash',
- group: 'GLM-4v'
- },
- {
- id: 'glm-4v-plus-0111',
- provider: 'zhipu',
- name: 'GLM-4V-Plus-0111',
- group: 'GLM-4v'
- },
- {
- id: 'glm-4-alltools',
- provider: 'zhipu',
- name: 'GLM-4-AllTools',
- group: 'GLM-4-AllTools'
+ name: 'GLM-4.5V',
+ group: 'GLM-4.5V'
},
{
id: 'embedding-3',
@@ -1869,6 +1570,12 @@ export const SYSTEM_MODELS: Record =
}
],
openrouter: [
+ {
+ id: 'google/gemini-2.5-flash-image-preview',
+ provider: 'openrouter',
+ name: 'Google: Gemini 2.5 Flash Image',
+ group: 'google'
+ },
{
id: 'google/gemini-2.5-flash-preview',
provider: 'openrouter',
@@ -2386,107 +2093,305 @@ export const SYSTEM_MODELS: Record =
]
}
-export const TEXT_TO_IMAGES_MODELS = [
- {
- id: 'Kwai-Kolors/Kolors',
- provider: 'silicon',
- name: 'Kolors',
- group: 'Kwai-Kolors'
+// Vision models
+const visionAllowedModels = [
+ 'llava',
+ 'moondream',
+ 'minicpm',
+ 'gemini-1\\.5',
+ 'gemini-2\\.0',
+ 'gemini-2\\.5',
+ 'gemini-exp',
+ 'claude-3',
+ 'claude-sonnet-4',
+ 'claude-opus-4',
+ 'vision',
+ 'glm-4(?:\\.\\d+)?v(?:-[\\w-]+)?',
+ 'qwen-vl',
+ 'qwen2-vl',
+ 'qwen2.5-vl',
+ 'qwen2.5-omni',
+ 'qvq',
+ 'internvl2',
+ 'grok-vision-beta',
+ 'grok-4(?:-[\\w-]+)?',
+ 'pixtral',
+ 'gpt-4(?:-[\\w-]+)',
+ 'gpt-4.1(?:-[\\w-]+)?',
+ 'gpt-4o(?:-[\\w-]+)?',
+ 'gpt-4.5(?:-[\\w-]+)',
+ 'gpt-5(?:-[\\w-]+)?',
+ 'chatgpt-4o(?:-[\\w-]+)?',
+ 'o1(?:-[\\w-]+)?',
+ 'o3(?:-[\\w-]+)?',
+ 'o4(?:-[\\w-]+)?',
+ 'deepseek-vl(?:[\\w-]+)?',
+ 'kimi-latest',
+ 'gemma-3(?:-[\\w-]+)',
+ 'doubao-seed-1[.-]6(?:-[\\w-]+)?',
+ 'kimi-thinking-preview',
+ `gemma3(?:[-:\\w]+)?`,
+ 'kimi-vl-a3b-thinking(?:-[\\w-]+)?',
+ 'llama-guard-4(?:-[\\w-]+)?',
+ 'llama-4(?:-[\\w-]+)?',
+ 'step-1o(?:.*vision)?',
+ 'step-1v(?:-[\\w-]+)?'
+]
+
+const visionExcludedModels = [
+ 'gpt-4-\\d+-preview',
+ 'gpt-4-turbo-preview',
+ 'gpt-4-32k',
+ 'gpt-4-\\d+',
+ 'o1-mini',
+ 'o3-mini',
+ 'o1-preview',
+ 'AIDC-AI/Marco-o1'
+]
+export const VISION_REGEX = new RegExp(
+ `\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
+ 'i'
+)
+
+// Text to image models
+export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus|midjourney|mj-|image|gpt-image/i
+
+// Reasoning models
+export const REASONING_REGEX =
+ /^(o\d+(?:-[\w-]+)?|.*\b(?:reasoning|reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-(?:3-mini|4)(?:-[\w-]+)?\b.*)$/i
+
+// Embedding models
+export const EMBEDDING_REGEX =
+ /(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings|voyage-)/i
+
+// Rerank models
+export const RERANKING_REGEX = /(?:rerank|re-rank|re-ranker|re-ranking|retrieval|retriever)/i
+
+export const NOT_SUPPORTED_REGEX = /(?:^tts|whisper|speech)/i
+
+// Tool calling models
+export const FUNCTION_CALLING_MODELS = [
+ 'gpt-4o',
+ 'gpt-4o-mini',
+ 'gpt-4',
+ 'gpt-4.5',
+ 'gpt-oss(?:-[\\w-]+)',
+ 'gpt-5(?:-[0-9-]+)?',
+ 'o(1|3|4)(?:-[\\w-]+)?',
+ 'claude',
+ 'qwen',
+ 'qwen3',
+ 'hunyuan',
+ 'deepseek',
+ 'glm-4(?:-[\\w-]+)?',
+ 'glm-4.5(?:-[\\w-]+)?',
+ 'learnlm(?:-[\\w-]+)?',
+ 'gemini(?:-[\\w-]+)?', // 提前排除了gemini的嵌入模型
+ 'grok-3(?:-[\\w-]+)?',
+ 'doubao-seed-1[.-]6(?:-[\\w-]+)?',
+ 'kimi-k2(?:-[\\w-]+)?'
+]
+
+const FUNCTION_CALLING_EXCLUDED_MODELS = [
+ 'aqa(?:-[\\w-]+)?',
+ 'imagen(?:-[\\w-]+)?',
+ 'o1-mini',
+ 'o1-preview',
+ 'AIDC-AI/Marco-o1',
+ 'gemini-1(?:\\.[\\w-]+)?',
+ 'qwen-mt(?:-[\\w-]+)?',
+ 'gpt-5-chat(?:-[\\w-]+)?',
+ 'glm-4\\.5v'
+]
+
+export const FUNCTION_CALLING_REGEX = new RegExp(
+ `\\b(?!(?:${FUNCTION_CALLING_EXCLUDED_MODELS.join('|')})\\b)(?:${FUNCTION_CALLING_MODELS.join('|')})\\b`,
+ 'i'
+)
+
+export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp(
+ `\\b(?:claude-3(-|\\.)(7|5)-sonnet(?:-[\\w-]+)|claude-3(-|\\.)5-haiku(?:-[\\w-]+)|claude-sonnet-4(?:-[\\w-]+)?|claude-opus-4(?:-[\\w-]+)?)\\b`,
+ 'i'
+)
+
+// 模型类型到支持的reasoning_effort的映射表
+// TODO: refactor this. too many identical options
+export const MODEL_SUPPORTED_REASONING_EFFORT: ReasoningEffortConfig = {
+ default: ['low', 'medium', 'high'] as const,
+ o: ['low', 'medium', 'high'] as const,
+ gpt5: ['minimal', 'low', 'medium', 'high'] as const,
+ grok: ['low', 'high'] as const,
+ gemini: ['low', 'medium', 'high', 'auto'] as const,
+ gemini_pro: ['low', 'medium', 'high', 'auto'] as const,
+ qwen: ['low', 'medium', 'high'] as const,
+ qwen_thinking: ['low', 'medium', 'high'] as const,
+ doubao: ['auto', 'high'] as const,
+ doubao_no_auto: ['high'] as const,
+ hunyuan: ['auto'] as const,
+ zhipu: ['auto'] as const,
+ perplexity: ['low', 'medium', 'high'] as const,
+ deepseek_hybrid: ['auto'] as const
+} as const
+
+// 模型类型到支持选项的映射表
+export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
+ default: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.default] as const,
+ o: MODEL_SUPPORTED_REASONING_EFFORT.o,
+ gpt5: [...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const,
+ grok: MODEL_SUPPORTED_REASONING_EFFORT.grok,
+ gemini: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
+ gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
+ qwen: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
+ qwen_thinking: MODEL_SUPPORTED_REASONING_EFFORT.qwen_thinking,
+ doubao: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.doubao] as const,
+ doubao_no_auto: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.doubao_no_auto] as const,
+ hunyuan: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.hunyuan] as const,
+ zhipu: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.zhipu] as const,
+ perplexity: MODEL_SUPPORTED_REASONING_EFFORT.perplexity,
+ deepseek_hybrid: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.deepseek_hybrid] as const
+} as const
+
+export const getThinkModelType = (model: Model): ThinkingModelType => {
+ let thinkingModelType: ThinkingModelType = 'default'
+ if (isGPT5SeriesModel(model)) {
+ thinkingModelType = 'gpt5'
+ } else if (isSupportedReasoningEffortOpenAIModel(model)) {
+ thinkingModelType = 'o'
+ } else if (isSupportedThinkingTokenGeminiModel(model)) {
+ if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
+ thinkingModelType = 'gemini'
+ } else {
+ thinkingModelType = 'gemini_pro'
+ }
+ } else if (isSupportedReasoningEffortGrokModel(model)) thinkingModelType = 'grok'
+ else if (isSupportedThinkingTokenQwenModel(model)) {
+ if (isQwenAlwaysThinkModel(model)) {
+ thinkingModelType = 'qwen_thinking'
+ }
+ thinkingModelType = 'qwen'
+ } else if (isSupportedThinkingTokenDoubaoModel(model)) {
+ if (isDoubaoThinkingAutoModel(model)) {
+ thinkingModelType = 'doubao'
+ } else {
+ thinkingModelType = 'doubao_no_auto'
+ }
+ } else if (isSupportedThinkingTokenHunyuanModel(model)) thinkingModelType = 'hunyuan'
+ else if (isSupportedReasoningEffortPerplexityModel(model)) thinkingModelType = 'perplexity'
+ else if (isSupportedThinkingTokenZhipuModel(model)) thinkingModelType = 'zhipu'
+ else if (isDeepSeekHybridInferenceModel(model)) thinkingModelType = 'deepseek_hybrid'
+ return thinkingModelType
+}
+
+export function isFunctionCallingModel(model?: Model): boolean {
+ if (!model || isEmbeddingModel(model) || isRerankModel(model) || isTextToImageModel(model)) {
+ return false
}
- // {
- // id: 'black-forest-labs/FLUX.1-schnell',
- // provider: 'silicon',
- // name: 'FLUX.1 Schnell',
- // group: 'FLUX'
- // },
- // {
- // id: 'black-forest-labs/FLUX.1-dev',
- // provider: 'silicon',
- // name: 'FLUX.1 Dev',
- // group: 'FLUX'
- // },
- // {
- // id: 'black-forest-labs/FLUX.1-pro',
- // provider: 'silicon',
- // name: 'FLUX.1 Pro',
- // group: 'FLUX'
- // },
- // {
- // id: 'Pro/black-forest-labs/FLUX.1-schnell',
- // provider: 'silicon',
- // name: 'FLUX.1 Schnell Pro',
- // group: 'FLUX'
- // },
- // {
- // id: 'LoRA/black-forest-labs/FLUX.1-dev',
- // provider: 'silicon',
- // name: 'FLUX.1 Dev LoRA',
- // group: 'FLUX'
- // },
- // {
- // id: 'deepseek-ai/Janus-Pro-7B',
- // provider: 'silicon',
- // name: 'Janus-Pro-7B',
- // group: 'deepseek-ai'
- // },
- // {
- // id: 'stabilityai/stable-diffusion-3-5-large',
- // provider: 'silicon',
- // name: 'Stable Diffusion 3.5 Large',
- // group: 'Stable Diffusion'
- // },
- // {
- // id: 'stabilityai/stable-diffusion-3-5-large-turbo',
- // provider: 'silicon',
- // name: 'Stable Diffusion 3.5 Large Turbo',
- // group: 'Stable Diffusion'
- // },
- // {
- // id: 'stabilityai/stable-diffusion-3-medium',
- // provider: 'silicon',
- // name: 'Stable Diffusion 3 Medium',
- // group: 'Stable Diffusion'
- // },
- // {
- // id: 'stabilityai/stable-diffusion-2-1',
- // provider: 'silicon',
- // name: 'Stable Diffusion 2.1',
- // group: 'Stable Diffusion'
- // },
- // {
- // id: 'stabilityai/stable-diffusion-xl-base-1.0',
- // provider: 'silicon',
- // name: 'Stable Diffusion XL Base 1.0',
- // group: 'Stable Diffusion'
- // }
+
+ const modelId = getLowerBaseModelName(model.id)
+
+ if (isUserSelectedModelType(model, 'function_calling') !== undefined) {
+ return isUserSelectedModelType(model, 'function_calling')!
+ }
+
+ if (model.provider === 'qiniu') {
+ return ['deepseek-v3-tool', 'deepseek-v3-0324', 'qwq-32b', 'qwen2.5-72b-instruct'].includes(modelId)
+ }
+
+ if (model.provider === 'doubao' || modelId.includes('doubao')) {
+ return FUNCTION_CALLING_REGEX.test(modelId) || FUNCTION_CALLING_REGEX.test(model.name)
+ }
+
+ if (['deepseek', 'anthropic', 'kimi', 'moonshot'].includes(model.provider)) {
+ return true
+ }
+
+ // 2025/08/26 百炼与火山引擎均不支持 v3.1 函数调用
+ // 先默认支持
+ if (isDeepSeekHybridInferenceModel(model)) {
+ if (isSystemProviderId(model.provider)) {
+ switch (model.provider) {
+ case 'dashscope':
+ case 'doubao':
+ // case 'nvidia': // nvidia api 太烂了 测不了能不能用 先假设能用
+ return false
+ }
+ }
+ return true
+ }
+
+ return FUNCTION_CALLING_REGEX.test(modelId)
+}
+
+// For middleware to identify models that must use the dedicated Image API
+export const DEDICATED_IMAGE_MODELS = [
+ 'grok-2-image',
+ 'grok-2-image-1212',
+ 'grok-2-image-latest',
+ 'dall-e-3',
+ 'dall-e-2',
+ 'gpt-image-1'
]
-export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
- 'stabilityai/stable-diffusion-2-1',
- 'stabilityai/stable-diffusion-xl-base-1.0'
-]
+// Models that should auto-enable image generation button when selected
+export const AUTO_ENABLE_IMAGE_MODELS = ['gemini-2.5-flash-image-preview', ...DEDICATED_IMAGE_MODELS]
-export const SUPPORTED_DISABLE_GENERATION_MODELS = [
- 'gemini-2.0-flash-exp',
+export const OPENAI_IMAGE_GENERATION_MODELS = [
+ 'o3',
'gpt-4o',
'gpt-4o-mini',
'gpt-4.1',
'gpt-4.1-mini',
'gpt-4.1-nano',
- 'o3'
+ 'gpt-5',
+ 'gpt-image-1'
]
export const GENERATE_IMAGE_MODELS = [
+ 'gemini-2.0-flash-exp',
'gemini-2.0-flash-exp-image-generation',
'gemini-2.0-flash-preview-image-generation',
'gemini-2.5-flash-image-preview',
- 'grok-2-image-1212',
- 'grok-2-image',
- 'grok-2-image-latest',
- 'gpt-image-1',
- ...SUPPORTED_DISABLE_GENERATION_MODELS
+ ...DEDICATED_IMAGE_MODELS
]
+export const isDedicatedImageGenerationModel = (model: Model): boolean => {
+ if (!model) return false
+
+ const modelId = getLowerBaseModelName(model.id)
+ return DEDICATED_IMAGE_MODELS.some((m) => modelId.includes(m))
+}
+
+export const isAutoEnableImageGenerationModel = (model: Model): boolean => {
+ if (!model) return false
+
+ const modelId = getLowerBaseModelName(model.id)
+ return AUTO_ENABLE_IMAGE_MODELS.some((m) => modelId.includes(m))
+}
+
+export function isGenerateImageModel(model: Model): boolean {
+ if (!model) {
+ return false
+ }
+
+ const provider = getProviderByModel(model)
+
+ if (!provider) {
+ return false
+ }
+
+ if (isEmbeddingModel(model)) {
+ return false
+ }
+
+ const modelId = getLowerBaseModelName(model.id, '/')
+
+ if (provider && provider.type === 'openai-response') {
+ return OPENAI_IMAGE_GENERATION_MODELS.some((imageModel) => modelId.includes(imageModel))
+ }
+
+ return GENERATE_IMAGE_MODELS.some((imageModel) => modelId.includes(imageModel))
+}
+
export const GEMINI_SEARCH_REGEX = new RegExp('gemini-2\\..*', 'i')
export const OPENAI_NO_SUPPORT_DEV_ROLE_MODELS = ['o1-preview', 'o1-mini']
@@ -2656,9 +2561,9 @@ export function isSupportedThinkingTokenModel(model?: Model): boolean {
// Specifically for DeepSeek V3.1. White list for now
if (isDeepSeekHybridInferenceModel(model)) {
- return (['openrouter', 'dashscope', 'doubao', 'silicon', 'nvidia'] satisfies SystemProviderId[]).some(
- (id) => id === model.provider
- )
+ return (
+ ['openrouter', 'dashscope', 'modelscope', 'doubao', 'silicon', 'nvidia', 'ppio'] satisfies SystemProviderId[]
+ ).some((id) => id === model.provider)
}
return (
@@ -2885,7 +2790,7 @@ export const isDeepSeekHybridInferenceModel = (model: Model) => {
const modelId = getLowerBaseModelName(model.id)
// deepseek官方使用chat和reasoner做推理控制,其他provider需要单独判断,id可能会有所差别
// openrouter: deepseek/deepseek-chat-v3.1 不知道会不会有其他provider仿照ds官方分出一个同id的作为非思考模式的模型,这里有风险
- return /deepseek-v3(?:\.1|-1-\d+)?/.test(modelId) || modelId === 'deepseek-chat-v3.1'
+ return /deepseek-v3(?:\.1|-1-\d+)?/.test(modelId) || modelId.includes('deepseek-chat-v3.1')
}
export const isSupportedThinkingTokenDeepSeekModel = isDeepSeekHybridInferenceModel
@@ -3099,30 +3004,6 @@ export function isOpenRouterBuiltInWebSearchModel(model: Model): boolean {
return isOpenAIWebSearchChatCompletionOnlyModel(model) || modelId.includes('sonar')
}
-export function isGenerateImageModel(model: Model): boolean {
- if (!model) {
- return false
- }
-
- const provider = getProviderByModel(model)
-
- if (!provider) {
- return false
- }
-
- const isEmbedding = isEmbeddingModel(model)
-
- if (isEmbedding) {
- return false
- }
-
- const modelId = getLowerBaseModelName(model.id, '/')
- if (GENERATE_IMAGE_MODELS.includes(modelId)) {
- return true
- }
- return false
-}
-
export function isNotSupportedImageSizeModel(model?: Model): boolean {
if (!model) {
return false
@@ -3133,14 +3014,6 @@ export function isNotSupportedImageSizeModel(model?: Model): boolean {
return baseName.includes('grok-2-image')
}
-export function isSupportedDisableGenerationModel(model: Model): boolean {
- if (!model) {
- return false
- }
-
- return SUPPORTED_DISABLE_GENERATION_MODELS.includes(getLowerBaseModelName(model.id))
-}
-
export function getOpenAIWebSearchParams(model: Model, isEnableWebSearch?: boolean): Record {
if (!isEnableWebSearch) {
return {}
diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts
index 798df72d70..e1037c52de 100644
--- a/src/renderer/src/config/providers.ts
+++ b/src/renderer/src/config/providers.ts
@@ -11,6 +11,7 @@ import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-clou
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
import CephalonProviderLogo from '@renderer/assets/images/providers/cephalon.jpeg'
+import CherryInProviderLogo from '@renderer/assets/images/providers/cherryin.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
@@ -65,6 +66,16 @@ import { TOKENFLUX_HOST } from './constant'
import { SYSTEM_MODELS } from './models'
export const SYSTEM_PROVIDERS_CONFIG: Record = {
+ cherryin: {
+ id: 'cherryin',
+ name: 'CherryIN',
+ type: 'openai',
+ apiKey: '',
+ apiHost: 'https://api.cherry-ai.com/',
+ models: SYSTEM_MODELS.cherryin,
+ isSystem: true,
+ enabled: true
+ },
silicon: {
id: 'silicon',
name: 'Silicon',
@@ -73,7 +84,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record =
apiHost: 'https://api.siliconflow.cn',
models: SYSTEM_MODELS.silicon,
isSystem: true,
- enabled: true
+ enabled: false
},
aihubmix: {
id: 'aihubmix',
@@ -95,6 +106,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record =
isSystem: true,
enabled: false
},
+ zhipu: {
+ id: 'zhipu',
+ name: 'ZhiPu',
+ type: 'openai',
+ apiKey: '',
+ apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
+ models: SYSTEM_MODELS.zhipu,
+ isSystem: true,
+ enabled: false
+ },
deepseek: {
id: 'deepseek',
name: 'deepseek',
@@ -320,16 +341,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record =
enabled: false,
isAuthed: false
},
- zhipu: {
- id: 'zhipu',
- name: 'ZhiPu',
- type: 'openai',
- apiKey: '',
- apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
- models: SYSTEM_MODELS.zhipu,
- isSystem: true,
- enabled: false
- },
yi: {
id: 'yi',
name: 'Yi',
@@ -595,6 +606,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record =
export const SYSTEM_PROVIDERS: SystemProvider[] = Object.values(SYSTEM_PROVIDERS_CONFIG)
export const PROVIDER_LOGO_MAP: AtLeast = {
+ cherryin: CherryInProviderLogo,
ph8: Ph8ProviderLogo,
'302ai': Ai302ProviderLogo,
openai: OpenAiProviderLogo,
@@ -673,6 +685,16 @@ type ProviderUrls = {
}
export const PROVIDER_URLS: Record = {
+ cherryin: {
+ api: {
+ url: 'https://api.cherry-ai.com'
+ },
+ websites: {
+ official: 'https://cherry-ai.com',
+ docs: 'https://docs.cherry-ai.com',
+ models: 'https://docs.cherry-ai.com/pre-basic/providers/cherryin'
+ }
+ },
ph8: {
api: {
url: 'https://ph8.co'
@@ -1240,7 +1262,8 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
'deepseek',
'baichuan',
'minimax',
- 'xirang'
+ 'xirang',
+ 'poe'
] as const satisfies SystemProviderId[]
/**
diff --git a/src/renderer/src/config/sidebar.ts b/src/renderer/src/config/sidebar.ts
index 90a83cf42d..80c8b863ba 100644
--- a/src/renderer/src/config/sidebar.ts
+++ b/src/renderer/src/config/sidebar.ts
@@ -12,7 +12,8 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'minapp',
'knowledge',
'files',
- 'code_tools'
+ 'code_tools',
+ 'notes'
]
/**
diff --git a/src/renderer/src/config/webSearchProviders.ts b/src/renderer/src/config/webSearchProviders.ts
index 62c0536f4d..abeac52938 100644
--- a/src/renderer/src/config/webSearchProviders.ts
+++ b/src/renderer/src/config/webSearchProviders.ts
@@ -8,6 +8,12 @@ type WebSearchProviderConfig = {
}
export const WEB_SEARCH_PROVIDER_CONFIG: Record = {
+ zhipu: {
+ websites: {
+ official: 'https://docs.bigmodel.cn/cn/guide/tools/web-search',
+ apiKey: 'https://zhipuaishengchan.datasink.sensorsdata.cn/t/yv'
+ }
+ },
tavily: {
websites: {
official: 'https://tavily.com',
@@ -49,6 +55,12 @@ export const WEB_SEARCH_PROVIDER_CONFIG: Record
message_blocks: EntityTable