feat: remove knowledge queue

This commit is contained in:
kangfenmao 2024-12-19 13:45:11 +08:00
parent c2462fd51c
commit ca6027dd83
26 changed files with 388 additions and 375 deletions

View File

@ -55,6 +55,8 @@ class FileStorage {
const storedFilePath = path.join(this.storageDir, file)
const storedStats = fs.statSync(storedFilePath)
console.debug('storedFilePath', storedFilePath)
if (storedStats.size === fileSize) {
const [originalHash, storedHash] = await Promise.all([
this.getFileHash(filePath),

View File

@ -11,7 +11,6 @@ import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
import { FileType, RagAppRequestParams } from '@types'
import { app } from 'electron'
import Logger from 'electron-log'
class KnowledgeService {
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
@ -27,7 +26,6 @@ class KnowledgeService {
}
private getRagApplication = async ({ id, model, apiKey, baseURL }: RagAppRequestParams): Promise<RAGApplication> => {
Logger.log('getRagApplication', { id, model, apiKey, baseURL })
return new RAGApplicationBuilder()
.setModel('NO_MODEL')
.setEmbeddingModel(
@ -82,7 +80,7 @@ class KnowledgeService {
return await ragApplication.addLoader(new DocxLoader({ filePathOrUrl: data.path }) as any)
}
if (data.ext === '.md') {
if (data.ext === '.md' || data.ext === '.mdx') {
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: data.path }) as any)
}

View File

@ -123,6 +123,10 @@ export class WindowService {
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
mainWindow.webContents.on('will-navigate', (event, url) => {
if (url.includes('localhost:5173')) {
return
}
event.preventDefault()
shell.openExternal(url)
})

View File

@ -1,4 +1,4 @@
import { BookOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
@ -91,7 +91,7 @@ const Sidebar: FC = () => {
<Tooltip title={t('knowledge_base.title')} mouseEnterDelay={0.5} placement="right">
<StyledLink onClick={() => to('/knowledge')}>
<Icon className={isRoute('/knowledge')}>
<BookOutlined />
<FileSearchOutlined />
</Icon>
</StyledLink>
</Tooltip>

View File

@ -1,20 +1,21 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { db } from '@renderer/databases/index'
import { RootState, useAppSelector } from '@renderer/store'
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
import FileManager from '@renderer/services/FileManager'
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
import { RootState } from '@renderer/store'
import {
addBase,
addItem,
addProcessingItem,
clearAllItems,
clearCompletedItems,
clearAllProcessing,
clearCompletedProcessing,
deleteBase,
removeItem as removeItemAction,
removeProcessingItem,
renameBase,
selectProcessingItemBySource,
selectProcessingItemsByType,
updateBase,
updateFiles as updateFilesAction,
updateNotes,
updateProcessingStatus
updateItemProcessingStatus,
updateNotes
} from '@renderer/store/knowledge'
import { FileType, KnowledgeBase, ProcessingStatus } from '@renderer/types'
import { KnowledgeItem } from '@renderer/types'
@ -26,7 +27,6 @@ import { v4 as uuidv4 } from 'uuid'
export const useKnowledge = (baseId: string) => {
const dispatch = useDispatch()
const base = useSelector((state: RootState) => state.knowledge.bases.find((b) => b.id === baseId))
const knowledgeState = useAppSelector((state: RootState) => state.knowledge)
// 重命名知识库
const renameKnowledgeBase = (name: string) => {
@ -41,27 +41,37 @@ export const useKnowledge = (baseId: string) => {
// 添加文件列表
const addFiles = (files: FileType[]) => {
for (const file of files) {
const newItem = {
const newItem: KnowledgeItem = {
id: uuidv4(),
type: 'file' as const,
content: file,
created_at: Date.now(),
updated_at: Date.now()
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newItem }))
}
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 添加URL
const addUrl = (url: string) => {
const newUrlItem = {
const newUrlItem: KnowledgeItem = {
id: uuidv4(),
type: 'url' as const,
content: url,
created_at: Date.now(),
updated_at: Date.now()
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newUrlItem }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 添加笔记
@ -85,10 +95,15 @@ export const useKnowledge = (baseId: string) => {
type: 'note',
content: '', // store中不需要存储实际内容
created_at: Date.now(),
updated_at: Date.now()
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(updateNotes({ baseId, item: noteRef }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 更新文件列表
@ -101,6 +116,7 @@ export const useKnowledge = (baseId: string) => {
updated_at: Date.now()
}))
dispatch(updateFilesAction({ baseId, items: newItems }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 更新笔记内容
@ -115,6 +131,7 @@ export const useKnowledge = (baseId: string) => {
await db.knowledge_notes.put(updatedNote)
dispatch(updateNotes({ baseId, item: updatedNote }))
}
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 获取笔记内容
@ -123,47 +140,23 @@ export const useKnowledge = (baseId: string) => {
}
// 移除项目
const removeItem = (item: KnowledgeItem) => {
const removeItem = async (item: KnowledgeItem) => {
dispatch(removeItemAction({ baseId, item }))
if (base) {
const config = getRagAppRequestParams(base)
if (item?.uniqueId) {
await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, config })
}
// 添加文件到处理队列
const addFileToQueue = (itemId: string) => {
dispatch(
addProcessingItem({
baseId,
type: 'file',
sourceId: itemId
})
)
if (item.type === 'file' && typeof item.content === 'object') {
await FileManager.deleteFile(item.content.id)
}
// 添加URL到处理队列
const addUrlToQueue = (itemId: string) => {
dispatch(
addProcessingItem({
baseId,
type: 'url',
sourceId: itemId
})
)
}
// 添加笔记到处理队列
const addNoteToQueue = (itemId: string) => {
dispatch(
addProcessingItem({
baseId,
type: 'note',
sourceId: itemId
})
)
}
// 更新处理状态
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
dispatch(
updateProcessingStatus({
updateItemProcessingStatus({
baseId,
itemId,
status,
@ -173,38 +166,46 @@ export const useKnowledge = (baseId: string) => {
)
}
// 获取特定的处理状态
const getProcessingStatus = (sourceId: string) => {
return selectProcessingItemBySource(knowledgeState, baseId, sourceId)
// 获取特定项目的处理状态
const getProcessingStatus = (itemId: string) => {
return base?.items.find((item) => item.id === itemId)?.processingStatus
}
// 获取特定类型的所有处理项
const getProcessingItemsByType = (type: 'file' | 'url' | 'note') => {
return selectProcessingItemsByType(knowledgeState, baseId, type)
}
// 从队列中移除项目
const removeFromQueue = (itemId: string) => {
dispatch(
removeProcessingItem({
baseId,
itemId
})
)
return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || []
}
// 清除已完成的项目
const clearCompleted = () => {
dispatch(clearCompletedItems({ baseId }))
dispatch(clearCompletedProcessing({ baseId }))
}
// 清除所有队列项目
// 清除所有处理状态
const clearAll = () => {
dispatch(clearAllItems({ baseId }))
dispatch(clearAllProcessing({ baseId }))
}
// 添加 Sitemap
const addSitemap = (url: string) => {
const newSitemapItem: KnowledgeItem = {
id: uuidv4(),
type: 'sitemap' as const,
content: url,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newSitemapItem }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
const fileItems = base?.items.filter((item) => item.type === 'file') || []
const urlItems = base?.items.filter((item) => item.type === 'url') || []
const sitemapItems = base?.items.filter((item) => item.type === 'sitemap') || []
const [noteItems, setNoteItems] = useState<KnowledgeItem[]>([])
useEffect(() => {
@ -224,24 +225,46 @@ export const useKnowledge = (baseId: string) => {
base,
fileItems,
urlItems,
sitemapItems,
noteItems,
renameKnowledgeBase,
updateKnowledgeBase,
addFiles,
addUrl,
addSitemap,
addNote,
updateFiles,
updateNoteContent,
getNoteContent,
addFileToQueue,
addUrlToQueue,
addNoteToQueue,
updateItemStatus,
getProcessingStatus,
getProcessingItemsByType,
removeFromQueue,
clearCompleted,
clearAll,
removeItem
}
}
export const useKnowledgeBases = () => {
const dispatch = useDispatch()
const bases = useSelector((state: RootState) => state.knowledge.bases)
const addKnowledgeBase = (base: KnowledgeBase) => {
dispatch(addBase(base))
}
const renameKnowledgeBase = (baseId: string, name: string) => {
dispatch(renameBase({ baseId, name }))
}
const deleteKnowledgeBase = (baseId: string) => {
dispatch(deleteBase({ baseId }))
}
return {
bases,
addKnowledgeBase,
renameKnowledgeBase,
deleteKnowledgeBase
}
}

View File

@ -555,11 +555,13 @@
"status_completed": "Completed",
"status_failed": "Failed",
"url_added": "URL added",
"query": "Query",
"search_placeholder": "Enter text to search",
"add_note": "Add Note",
"no_bases": "No knowledge bases available",
"clear_selection": "Clear selection"
"clear_selection": "Clear selection",
"delete_confirm": "Are you sure you want to delete this knowledge base?",
"sitemaps": "Site Maps",
"add_sitemap": "Add Site Map"
}
}
}

View File

@ -555,11 +555,13 @@
"status_completed": "Завершено",
"status_failed": "Ошибка",
"url_added": "URL добавлен",
"query": "Поиск",
"search_placeholder": "Введите текст для поиска",
"add_note": "Добавить запись",
"no_bases": "База знаний не найдена",
"clear_selection": "Очистить выбор"
"clear_selection": "Очистить выбор",
"delete_confirm": "Вы уверены, что хотите удалить эту базу знаний?",
"sitemaps": "Карта сайта",
"add_sitemap": "Добавить карту сайта"
}
}
}

View File

@ -529,7 +529,7 @@
"notes_placeholder": "输入此知识库的附加信息或上下文...",
"delete": "删除",
"rename": "重命名",
"urls": "网",
"urls": "网",
"add_url": "添加网址",
"url_placeholder": "请输入网址",
"invalid_url": "无效的网址",
@ -544,11 +544,13 @@
"status_completed": "已完成",
"status_failed": "失败",
"url_added": "网址已添加",
"query": "查询",
"search_placeholder": "输入查询内容",
"add_note": "添加笔记",
"no_bases": "暂无知识库",
"clear_selection": "清除选择"
"clear_selection": "清除选择",
"delete_confirm": "确定要删除此知识库吗?",
"sitemaps": "站点地图",
"add_sitemap": "添加站点地图"
}
}
}

View File

@ -543,11 +543,13 @@
"status_completed": "已完成",
"status_failed": "失敗",
"url_added": "網址已添加",
"query": "查詢",
"search_placeholder": "輸入查詢內容",
"add_note": "添加筆記",
"no_bases": "暫無知識庫",
"clear_selection": "清除選擇"
"clear_selection": "清除選擇",
"delete_confirm": "確定要刪除此知識庫嗎?",
"sitemaps": "站點地圖",
"add_sitemap": "添加站點地圖"
}
}
}

View File

@ -11,9 +11,10 @@ interface Props {
files: FileType[]
setFiles: (files: FileType[]) => void
ToolbarButton: any
disabled?: boolean
}
const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton }) => {
const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton, disabled }) => {
const { t } = useTranslation()
const extensions = isVisionModel(model)
? [...imageExts, ...documentExts, ...textExts]
@ -37,7 +38,7 @@ const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton })
return (
<Tooltip placement="top" title={t('chat.input.upload')} arrow>
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile}>
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile} disabled={disabled}>
<PaperClipOutlined style={{ rotate: '135deg' }} />
</ToolbarButton>
</Tooltip>

View File

@ -49,7 +49,7 @@ interface Props {
let _text = ''
let _files: FileType[] = []
let _base: KnowledgeBase
let _base: KnowledgeBase | undefined
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const [text, setText] = useState(_text)
@ -80,7 +80,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const [spaceClickCount, setSpaceClickCount] = useState(0)
const spaceClickTimer = useRef<NodeJS.Timeout>()
const [isTranslating, setIsTranslating] = useState(false)
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase>(_base)
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
@ -450,8 +450,19 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<KnowledgeBaseButton selectedBase={selectedKnowledgeBase} onSelect={handleKnowledgeBaseSelect} />
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
<KnowledgeBaseButton
selectedBase={selectedKnowledgeBase}
onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton}
disabled={files.length > 0}
/>
<AttachmentButton
model={model}
files={files}
setFiles={setFiles}
ToolbarButton={ToolbarButton}
disabled={!!selectedKnowledgeBase}
/>
<ToolbarButton type="text" onClick={onNewContext}>
<Tooltip placement="top" title={t('chat.input.new.context')}>
<PicCenterOutlined />

View File

@ -1,4 +1,4 @@
import { BookOutlined } from '@ant-design/icons'
import { FileSearchOutlined } from '@ant-design/icons'
import { useAppSelector } from '@renderer/store'
import { KnowledgeBase } from '@renderer/types'
import { Button, Popover, Tooltip } from 'antd'
@ -9,6 +9,8 @@ import styled from 'styled-components'
interface Props {
selectedBase?: KnowledgeBase
onSelect: (base?: KnowledgeBase) => void
disabled?: boolean
ToolbarButton?: any
}
const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => {
@ -42,14 +44,14 @@ const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => {
)
}
const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect }) => {
const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, ToolbarButton }) => {
const { t } = useTranslation()
if (selectedBase) {
return (
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
<ToolbarButton type="text" onClick={() => onSelect(undefined)}>
<BookOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
</ToolbarButton>
</Tooltip>
)
@ -61,8 +63,8 @@ const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect }) => {
placement="top"
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
trigger="click">
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)}>
<BookOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
</ToolbarButton>
</Popover>
</Tooltip>
@ -78,40 +80,4 @@ const EmptyMessage = styled.div`
padding: 8px;
`
const ToolbarButton = styled(Button)`
width: 30px;
height: 30px;
font-size: 17px;
border-radius: 50%;
transition: all 0.3s ease;
color: var(--color-icon);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0;
&.anticon,
&.iconfont {
transition: all 0.3s ease;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-soft);
.anticon,
.iconfont {
color: var(--color-text-1);
}
}
&.active {
background-color: var(--color-primary) !important;
.anticon,
.iconfont {
color: var(--color-white-soft);
}
&:hover {
background-color: var(--color-primary);
}
}
`
export default KnowledgeBaseButton

View File

@ -29,18 +29,20 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
if (message.role === 'assistant') {
let metrixs = ''
let hasMetrics = false
if (message?.metrics?.completion_tokens && message?.metrics?.time_completion_millsec) {
hasMetrics = true
metrixs = t('settings.messages.metrics', {
time_first_token_millsec: message?.metrics?.time_first_token_millsec,
token_speed: (message?.metrics?.completion_tokens / (message?.metrics?.time_completion_millsec / 1000)).toFixed(
2
0
)
})
}
return (
<MessageMetadata className="message-tokens" onClick={locateMessage}>
<MessageMetadata className={`message-tokens ${hasMetrics ? 'has-metrics' : ''}`} onClick={locateMessage}>
<span className="metrics">{metrixs}</span>
<span className="tokens">
Tokens: {message?.usage?.total_tokens} {message?.usage?.prompt_tokens} {message?.usage?.completion_tokens}
@ -68,7 +70,7 @@ const MessageMetadata = styled.div`
display: block;
}
&:hover {
&.has-metrics:hover {
.metrics {
display: block;
}

View File

@ -2,10 +2,10 @@ import {
DeleteOutlined,
EditOutlined,
FileTextOutlined,
GlobalOutlined,
LinkOutlined,
PlusOutlined,
SearchOutlined,
StopOutlined
SearchOutlined
} from '@ant-design/icons'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
@ -18,8 +18,8 @@ import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import KnowledgeSearchPopup from './KnowledgeSearchPopup'
import StatusIcon from './StatusIcon'
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
import StatusIcon from './components/StatusIcon'
const { Dragger } = Upload
const { Title } = Typography
@ -35,15 +35,13 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
noteItems,
fileItems,
urlItems,
sitemapItems,
addFiles,
updateNoteContent,
addUrl,
addSitemap,
removeItem,
getProcessingStatus,
addFileToQueue,
addNoteToQueue,
addUrlToQueue,
clearAll,
addNote
} = useKnowledge(selectedBase.id || '')
@ -109,23 +107,36 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
}
}
const handleAddSitemap = async () => {
const url = await PromptPopup.show({
title: t('knowledge_base.add_sitemap'),
message: '',
inputPlaceholder: t('knowledge_base.sitemap_placeholder'),
inputProps: {
maxLength: 1000,
rows: 1
}
})
if (url) {
try {
new URL(url)
if (sitemapItems.find((item) => item.content === url)) {
message.success(t('knowledge_base.sitemap_added'))
return
}
addSitemap(url)
} catch (e) {
console.error('Invalid Sitemap URL:', url)
}
}
}
const handleAddNote = async () => {
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
note && addNote(note)
}
const handleIndexAll = async () => {
fileItems.forEach((item) => !item.uniqueId && addFileToQueue(item.id))
urlItems.forEach((item) => !item.uniqueId && addUrlToQueue(item.id))
noteItems.forEach((note) => !note.uniqueId && addNoteToQueue(note.id))
message.success(t('knowledge_base.index_started'))
}
const handleCancelIndex = () => {
clearAll()
message.success(t('knowledge_base.index_cancelled'))
}
const handleEditNote = async (note: any) => {
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
editedText && updateNoteContent(note.id, editedText)
@ -198,6 +209,33 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</div>
</ContentSection>
<ContentSection>
<TitleWrapper>
<Title level={5}>{t('knowledge_base.sitemaps')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddSitemap}>
{t('knowledge_base.add_sitemap')}
</Button>
</TitleWrapper>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{sitemapItems.map((item) => (
<ItemCard key={item.id}>
<ItemContent>
<ItemInfo>
<GlobalOutlined style={{ fontSize: '16px' }} />
<a href={item.content as string} target="_blank" rel="noopener noreferrer">
{item.content as string}
</a>
</ItemInfo>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</div>
</ItemContent>
</ItemCard>
))}
</div>
</ContentSection>
<ContentSection>
<TitleWrapper>
<Title level={5}>{t('knowledge_base.notes')}</Title>
@ -222,19 +260,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
))}
</div>
</ContentSection>
<IndexSection>
<Button type="primary" onClick={handleIndexAll} icon={<FileTextOutlined />}>
{t('knowledge_base.index_all')}
</Button>
<Button onClick={handleCancelIndex} icon={<StopOutlined />} style={{ marginLeft: '10px' }}>
{t('knowledge_base.cancel_index')}
</Button>
<Button
onClick={() => KnowledgeSearchPopup.show({ base })}
icon={<SearchOutlined />}
style={{ marginLeft: '10px' }}>
{t('knowledge_base.query')}
<Button type="primary" onClick={() => KnowledgeSearchPopup.show({ base })} icon={<SearchOutlined />}>
{t('knowledge_base.search')}
</Button>
</IndexSection>
<div style={{ minHeight: '20px' }} />
@ -321,7 +349,7 @@ const ItemInfo = styled.div`
const IndexSection = styled.div`
margin-top: 20px;
display: flex;
justify-content: flex-end;
justify-content: center;
`
export default KnowledgeContent

View File

@ -3,35 +3,35 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import ListItem from '@renderer/components/ListItem'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import Scrollbar from '@renderer/components/Scrollbar'
import { RootState } from '@renderer/store'
import { deleteBase, renameBase } from '@renderer/store/knowledge'
import { useKnowledgeBases } from '@renderer/hooks/useknowledge'
import { KnowledgeBase } from '@renderer/types'
import { Dropdown, Empty, MenuProps } from 'antd'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import AddKnowledgePopup from './AddKnowledgePopup'
import AddKnowledgePopup from './components/AddKnowledgePopup'
import KnowledgeContent from './KnowledgeContent'
const KnowledgePage: FC = () => {
const { t } = useTranslation()
const { bases } = useSelector((state: RootState) => state.knowledge)
const { bases, renameKnowledgeBase, deleteKnowledgeBase } = useKnowledgeBases()
const [selectedBase, setSelectedBase] = useState<KnowledgeBase>()
const dispatch = useDispatch()
const handleAddKnowledge = async () => {
await AddKnowledgePopup.show({
title: t('knowledge_base.add.title')
})
await AddKnowledgePopup.show({ title: t('knowledge_base.add.title') })
}
useEffect(() => {
if (bases.length > 0) {
setSelectedBase(bases[0])
if (!selectedBase) {
return setSelectedBase(bases[0])
}
}, [bases])
if (selectedBase && !bases.includes(selectedBase)) {
return setSelectedBase(bases[0])
}
}
}, [bases, selectedBase])
const getMenuItems = useCallback(
(base: KnowledgeBase) => {
@ -47,7 +47,7 @@ const KnowledgePage: FC = () => {
defaultValue: base.name || ''
})
if (name && base.name !== name) {
dispatch(renameBase({ baseId: base.id, name }))
renameKnowledgeBase(base.id, name)
}
}
},
@ -58,14 +58,20 @@ const KnowledgePage: FC = () => {
key: 'delete',
icon: <DeleteOutlined />,
onClick: () => {
dispatch(deleteBase({ baseId: base.id }))
window.modal.confirm({
title: t('knowledge_base.delete_confirm'),
centered: true,
onOk: () => {
deleteKnowledgeBase(base.id)
}
})
}
}
]
return menus
},
[dispatch, t]
[deleteKnowledgeBase, renameKnowledgeBase, t]
)
return (

View File

@ -1,16 +1,15 @@
import { TopView } from '@renderer/components/TopView'
import { isEmbeddingModel } from '@renderer/config/models'
import { useKnowledgeBases } from '@renderer/hooks/useknowledge'
import { useProviders } from '@renderer/hooks/useProvider'
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
import { getModelUniqId } from '@renderer/services/ModelService'
import { addBase } from '@renderer/store/knowledge'
import { Model } from '@renderer/types'
import { Form, Input, Modal, Select } from 'antd'
import { find, sortBy } from 'lodash'
import { nanoid } from 'nanoid'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
interface ShowParams {
title: string
@ -29,8 +28,8 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true)
const [form] = Form.useForm<FormData>()
const { t } = useTranslation()
const dispatch = useDispatch()
const { providers } = useProviders()
const { addKnowledgeBase } = useKnowledgeBases()
const allModels = providers
.map((p) => p.models)
.flat()
@ -61,14 +60,13 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
name: values.name,
model: selectedModel,
items: [],
processingQueue: [],
created_at: Date.now(),
updated_at: Date.now()
}
await window.api.knowledgeBase.create(getRagAppRequestParams(newBase))
dispatch(addBase(newBase as any))
addKnowledgeBase(newBase as any)
setOpen(false)
resolve(newBase)
}

View File

@ -1,6 +1,6 @@
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
import { Center } from '@renderer/components/Layout'
import { KnowledgeBase, ProcessingItem } from '@renderer/types'
import { KnowledgeBase, ProcessingStatus } from '@renderer/types'
import { Tooltip } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -9,13 +9,14 @@ import styled from 'styled-components'
interface StatusIconProps {
sourceId: string
base: KnowledgeBase
getProcessingStatus: (sourceId: string) => ProcessingItem | undefined
getProcessingStatus: (sourceId: string) => ProcessingStatus | undefined
}
const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }) => {
const { t } = useTranslation()
const status = getProcessingStatus(sourceId)
const item = base.items.find((item) => item.id === sourceId)
const errorText = item?.processingError
if (!status) {
if (item?.uniqueId) {
@ -34,7 +35,7 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
)
}
switch (status.status) {
switch (status) {
case 'pending':
return (
<Tooltip title={t('knowledge_base.status_pending')} placement="left">
@ -55,7 +56,7 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
)
case 'failed':
return (
<Tooltip title={t('knowledge_base.status_failed')} placement="left">
<Tooltip title={errorText || t('knowledge_base.status_failed')} placement="left">
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
</Tooltip>
)

View File

@ -1,4 +1,4 @@
import { GithubOutlined, TwitterOutlined } from '@ant-design/icons'
import { GithubOutlined, XOutlined } from '@ant-design/icons'
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SendOutlined, SoundOutlined } from '@ant-design/icons'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { HStack } from '@renderer/components/Layout'
@ -208,7 +208,7 @@ const AboutSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>
<TwitterOutlined />X
<XOutlined />X
</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://x.com/kangfenmao')}>
{t('settings.about.website.button')}

View File

@ -25,7 +25,12 @@ export default class AnthropicProvider extends BaseProvider {
}
private async getMessageParam(message: Message): Promise<MessageParam> {
const parts: MessageParam['content'] = [{ type: 'text', text: message.content }]
const parts: MessageParam['content'] = [
{
type: 'text',
text: await this.getMessageContent(message)
}
]
for (const file of message.files || []) {
if (file.type === FileTypes.IMAGE) {
@ -83,11 +88,20 @@ export default class AnthropicProvider extends BaseProvider {
system: assistant.prompt
}
let time_first_token_millsec = 0
const start_time_millsec = new Date().getTime()
if (!streamOutput) {
const message = await this.sdk.messages.create({ ...body, stream: false })
const time_completion_millsec = new Date().getTime() - start_time_millsec
return onChunk({
text: message.content[0].type === 'text' ? message.content[0].text : '',
usage: message.usage
usage: message.usage,
metrics: {
completion_tokens: message.usage.output_tokens,
time_completion_millsec,
time_first_token_millsec: 0
}
})
}
@ -99,7 +113,18 @@ export default class AnthropicProvider extends BaseProvider {
stream.controller.abort()
return resolve()
}
onChunk({ text })
if (time_first_token_millsec == 0) {
time_first_token_millsec = new Date().getTime() - start_time_millsec
}
const time_completion_millsec = new Date().getTime() - start_time_millsec
onChunk({
text,
metrics: {
completion_tokens: undefined,
time_completion_millsec,
time_first_token_millsec
}
})
})
.on('finalMessage', (message) => {
onChunk({
@ -108,6 +133,11 @@ export default class AnthropicProvider extends BaseProvider {
prompt_tokens: message.usage.input_tokens,
completion_tokens: message.usage.output_tokens,
total_tokens: sum(Object.values(message.usage))
},
metrics: {
completion_tokens: message.usage.output_tokens,
time_completion_millsec: new Date().getTime() - start_time_millsec,
time_first_token_millsec
}
})
resolve()

View File

@ -19,6 +19,24 @@ export default abstract class BaseProvider {
this.apiKey = this.getApiKey()
}
abstract completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void>
abstract translate(message: Message, assistant: Assistant): Promise<string>
abstract summaries(messages: Message[], assistant: Assistant): Promise<string>
abstract suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]>
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
abstract check(): Promise<{ valid: boolean; error: Error | null }>
abstract models(): Promise<OpenAI.Models.Model[]>
abstract generateImage(_params: {
prompt: string
negativePrompt: string
imageSize: string
batchSize: number
seed?: string
numInferenceSteps: number
guidanceScale: number
signal?: AbortSignal
}): Promise<string[]>
public getBaseURL(): string {
const host = this.provider.apiHost
return host.endsWith('/') ? host : `${host}/v1/`
@ -63,7 +81,7 @@ export default abstract class BaseProvider {
}
}
public async getMessageContentWithKnowledgeBase(message: Message) {
public async getMessageContent(message: Message) {
if (!message.knowledgeBaseIds) {
return message.content
}
@ -81,8 +99,6 @@ export default abstract class BaseProvider {
config: getRagAppRequestParams(base)
})
console.debug('searchResults', searchResults)
const references = take(searchResults, 5)
.map((item, index) => {
let sourceUrl = ''
@ -93,16 +109,15 @@ export default abstract class BaseProvider {
if (baseItem) {
switch (baseItem.type) {
case 'file':
sourceUrl = `file://${(baseItem?.content as FileType).path}`
// sourceUrl = `file://${encodeURIComponent((baseItem?.content as FileType).path)}`
sourceName = (baseItem?.content as FileType).origin_name
break
case 'url':
sourceUrl = baseItem.content as string
sourceName = ''
sourceName = baseItem.content as string
break
case 'note':
sourceUrl = ''
sourceName = ''
sourceName = baseItem.content as string
break
}
}
@ -118,26 +133,9 @@ source_url: ${sourceUrl}
})
.join('\n\n')
const prompt = `回答问题请参考以下内容,并使用类似 [^1] content [source_name](source_url) 的脚注格式引用数据来源,脚注内容可以点击跳转。当 source_name 为空的时候可以使用 content 作为脚注内容。`
const prompt =
'回答问题请参考以下内容,并使用类似 [^1]: source 的脚注格式引用数据来源, source 根据 source_type 决定'
return [message.content, prompt, references].join('\n\n')
}
abstract completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void>
abstract translate(message: Message, assistant: Assistant): Promise<string>
abstract summaries(messages: Message[], assistant: Assistant): Promise<string>
abstract suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]>
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
abstract check(): Promise<{ valid: boolean; error: Error | null }>
abstract models(): Promise<OpenAI.Models.Model[]>
abstract generateImage(_params: {
prompt: string
negativePrompt: string
imageSize: string
batchSize: number
seed?: string
numInferenceSteps: number
guidanceScale: number
signal?: AbortSignal
}): Promise<string[]>
}

View File

@ -17,6 +17,7 @@ import axios from 'axios'
import { first, isEmpty, takeRight } from 'lodash'
import OpenAI from 'openai'
import { CompletionsParams } from '.'
import BaseProvider from './BaseProvider'
export default class GeminiProvider extends BaseProvider {
@ -34,7 +35,7 @@ export default class GeminiProvider extends BaseProvider {
private async getMessageContents(message: Message): Promise<Content> {
const role = message.role === 'user' ? 'user' : 'model'
const parts: Part[] = [{ text: message.content }]
const parts: Part[] = [{ text: await this.getMessageContent(message) }]
for (const file of message.files || []) {
if (file.type === FileTypes.IMAGE) {
@ -107,29 +108,47 @@ export default class GeminiProvider extends BaseProvider {
const chat = geminiModel.startChat({ history })
const messageContents = await this.getMessageContents(userLastMessage!)
const start_time_millsec = new Date().getTime()
if (!streamOutput) {
const { response } = await chat.sendMessage(messageContents.parts)
const time_completion_millsec = new Date().getTime() - start_time_millsec
onChunk({
text: response.candidates?.[0].content.parts[0].text,
usage: {
prompt_tokens: response.usageMetadata?.promptTokenCount || 0,
completion_tokens: response.usageMetadata?.candidatesTokenCount || 0,
total_tokens: response.usageMetadata?.totalTokenCount || 0
},
metrics: {
completion_tokens: response.usageMetadata?.candidatesTokenCount,
time_completion_millsec,
time_first_token_millsec: 0
}
})
return
}
const userMessagesStream = await chat.sendMessageStream(messageContents.parts)
let time_first_token_millsec = 0
for await (const chunk of userMessagesStream.stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
if (time_first_token_millsec == 0) {
time_first_token_millsec = new Date().getTime() - start_time_millsec
}
const time_completion_millsec = new Date().getTime() - start_time_millsec
onChunk({
text: chunk.text(),
usage: {
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
},
metrics: {
completion_tokens: chunk.usageMetadata?.candidatesTokenCount,
time_completion_millsec,
time_first_token_millsec
}
})
}

View File

@ -50,11 +50,12 @@ export default class OpenAIProvider extends BaseProvider {
model: Model
): Promise<OpenAI.Chat.Completions.ChatCompletionMessageParam> {
const isVision = isVisionModel(model)
const content = await this.getMessageContent(message)
if (!message.files) {
return {
role: message.role,
content: await this.getMessageContentWithKnowledgeBase(message)
content
}
}
@ -74,21 +75,21 @@ export default class OpenAIProvider extends BaseProvider {
return {
role: message.role,
content: message.content + divider + text
content: content + divider + text
}
}
}
return {
role: message.role,
content: message.content
content
}
}
const parts: ChatCompletionContentPart[] = [
{
type: 'text',
text: message.content
text: content
}
]

View File

@ -2,13 +2,13 @@ import { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
import db from '@renderer/databases'
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
import store from '@renderer/store'
import { removeProcessingItem, updateBaseItemUniqueId, updateProcessingStatus } from '@renderer/store/knowledge'
import { ProcessingItem } from '@renderer/types'
import { clearCompletedProcessing, updateBaseItemUniqueId, updateItemProcessingStatus } from '@renderer/store/knowledge'
import { KnowledgeItem } from '@renderer/types'
class KnowledgeQueue {
private processing: Map<string, boolean> = new Map()
private pollingInterval: NodeJS.Timeout | null = null
private readonly POLLING_INTERVAL = 5000
// private readonly POLLING_INTERVAL = 5000
private readonly MAX_RETRIES = 3
constructor() {
@ -21,10 +21,10 @@ class KnowledgeQueue {
const state = store.getState()
state.knowledge.bases.forEach((base) => {
base.processingQueue.forEach((item) => {
if (item.status === 'processing') {
base.items.forEach((item) => {
if (item.processingStatus === 'processing') {
store.dispatch(
updateProcessingStatus({
updateItemProcessingStatus({
baseId: base.id,
itemId: item.id,
status: 'pending',
@ -35,9 +35,9 @@ class KnowledgeQueue {
})
})
this.pollingInterval = setInterval(() => {
this.checkAllBases()
}, this.POLLING_INTERVAL)
// this.pollingInterval = setInterval(() => {
// this.checkAllBases()
// }, this.POLLING_INTERVAL)
}
private stopPolling(): void {
@ -47,27 +47,21 @@ class KnowledgeQueue {
}
}
private async checkAllBases(): Promise<void> {
public async checkAllBases(): Promise<void> {
const state = store.getState()
const bases = state.knowledge.bases
console.log('[KnowledgeQueue] Checking all bases for pending items...')
await Promise.all(
bases.map(async (base) => {
const processableItems = base.processingQueue.filter((item) => {
if (item.status === 'failed') {
const processableItems = base.items.filter((item) => {
if (item.processingStatus === 'failed') {
return !item.retryCount || item.retryCount < this.MAX_RETRIES
}
return item.status === 'pending'
return item.processingStatus === 'pending'
})
const hasProcessableItems = processableItems.length > 0
console.log(
`[KnowledgeQueue] Base ${base.id}: ${hasProcessableItems ? 'has processable items' : 'no processable items'}`
)
if (hasProcessableItems && !this.processing.get(base.id)) {
await this.processQueue(base.id)
}
@ -91,11 +85,11 @@ class KnowledgeQueue {
throw new Error('Knowledge base not found')
}
const processableItems = base.processingQueue.filter((item) => {
if (item.status === 'failed') {
const processableItems = base.items.filter((item) => {
if (item.processingStatus === 'failed') {
return !item.retryCount || item.retryCount < this.MAX_RETRIES
}
return item.status === 'pending'
return item.processingStatus === 'pending'
})
for (const item of processableItems) {
@ -124,7 +118,7 @@ class KnowledgeQueue {
}
}
private async processItem(baseId: string, item: ProcessingItem): Promise<void> {
private async processItem(baseId: string, item: KnowledgeItem): Promise<void> {
try {
if (item.retryCount && item.retryCount >= this.MAX_RETRIES) {
console.log(`[KnowledgeQueue] Item ${item.id} has reached max retries, skipping`)
@ -132,9 +126,9 @@ class KnowledgeQueue {
}
console.log(`[KnowledgeQueue] Starting to process item ${item.id} (${item.type})`)
// Update status to processing
store.dispatch(
updateProcessingStatus({
updateItemProcessingStatus({
baseId,
itemId: item.id,
status: 'processing',
@ -149,10 +143,10 @@ class KnowledgeQueue {
}
const requestParams = getRagAppRequestParams(base)
const sourceItem = base.items.find((i) => i.id === item.sourceId)
const sourceItem = base.items.find((i) => i.id === item.id)
if (!sourceItem) {
throw new Error(`[KnowledgeQueue] Source item ${item.sourceId} not found in base ${baseId}`)
throw new Error(`[KnowledgeQueue] Source item ${item.id} not found in base ${baseId}`)
}
let result: AddLoaderReturn | null = null
@ -169,10 +163,15 @@ class KnowledgeQueue {
result = await window.api.knowledgeBase.add({ data: sourceItem.content, config: requestParams })
console.log(`[KnowledgeQueue] Result: ${JSON.stringify(result)}`)
break
case 'sitemap':
console.log(`[KnowledgeQueue] Processing Sitemap: ${sourceItem.content}`)
result = await window.api.knowledgeBase.add({ data: sourceItem.content, config: requestParams })
console.log(`[KnowledgeQueue] Result: ${JSON.stringify(result)}`)
break
case 'note':
console.log(`[KnowledgeQueue] Processing note: ${sourceItem.content}`)
note = await db.knowledge_notes.get(item.sourceId)
if (!note) throw new Error(`Source note ${item.sourceId} not found`)
note = await db.knowledge_notes.get(item.id)
if (!note) throw new Error(`Source note ${item.id} not found`)
content = note.content as string
result = await window.api.knowledgeBase.add({ data: content, config: requestParams })
console.log(`[KnowledgeQueue] Result: ${JSON.stringify(result)}`)
@ -181,36 +180,31 @@ class KnowledgeQueue {
console.log(`[KnowledgeQueue] Successfully completed processing item ${item.id}`)
// Mark as completed
store.dispatch(
updateProcessingStatus({
updateItemProcessingStatus({
baseId,
itemId: item.id,
status: 'completed'
})
)
// Update uniqueId
if (result) {
store.dispatch(
updateBaseItemUniqueId({
baseId,
itemId: item.sourceId,
itemId: item.id,
uniqueId: result.uniqueId
})
)
}
console.debug(`[KnowledgeQueue] Updated uniqueId for item ${item.sourceId} in base ${baseId}`)
console.debug(`[KnowledgeQueue] Updated uniqueId for item ${item.id} in base ${baseId}`)
// Remove from queue after successful processing
setTimeout(() => {
store.dispatch(removeProcessingItem({ baseId, itemId: item.id }))
}, 1000)
setTimeout(() => store.dispatch(clearCompletedProcessing({ baseId })), 1000)
} catch (error) {
console.error(`[KnowledgeQueue] Error processing item ${item.id}:`, error)
store.dispatch(
updateProcessingStatus({
updateItemProcessingStatus({
baseId,
itemId: item.id,
status: 'failed',

View File

@ -1,7 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import FileManager from '@renderer/services/FileManager'
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
import { FileType, KnowledgeBase, KnowledgeItem, ProcessingItem, ProcessingStatus } from '@renderer/types'
import { FileType, KnowledgeBase, KnowledgeItem, ProcessingStatus } from '@renderer/types'
export interface KnowledgeState {
bases: KnowledgeBase[]
@ -15,12 +14,10 @@ const knowledgeSlice = createSlice({
name: 'knowledge',
initialState,
reducers: {
// 添加知识库
addBase(state, action: PayloadAction<KnowledgeBase>) {
state.bases.push(action.payload)
},
// 删除知识库
deleteBase(state, action: PayloadAction<{ baseId: string }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
@ -31,7 +28,6 @@ const knowledgeSlice = createSlice({
}
},
// 重命名知识库
renameBase(state, action: PayloadAction<{ baseId: string; name: string }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
@ -40,7 +36,6 @@ const knowledgeSlice = createSlice({
}
},
// 更新知识库
updateBase(state, action: PayloadAction<KnowledgeBase>) {
const index = state.bases.findIndex((b) => b.id === action.payload.id)
if (index !== -1) {
@ -48,7 +43,6 @@ const knowledgeSlice = createSlice({
}
},
// 添加条目
addItem(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
@ -68,26 +62,15 @@ const knowledgeSlice = createSlice({
}
},
// 删除条目
removeItem(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
const { baseId, item } = action.payload
const { baseId } = action.payload
const base = state.bases.find((b) => b.id === baseId)
if (base) {
base.items = base.items.filter((item) => item.id !== action.payload.item.id)
base.updated_at = Date.now()
if (item?.uniqueId) {
window.api.knowledgeBase.remove({
uniqueId: item.uniqueId,
config: getRagAppRequestParams(base)
})
}
if (item.type === 'file' && typeof item.content === 'object') {
FileManager.deleteFile(item.content.id)
}
}
},
// 更新文件
updateFiles(state, action: PayloadAction<{ baseId: string; items: KnowledgeItem[] }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
@ -98,7 +81,6 @@ const knowledgeSlice = createSlice({
}
},
// 更新笔记
updateNotes(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
@ -114,33 +96,7 @@ const knowledgeSlice = createSlice({
}
},
// 添加处理队列项
addProcessingItem(
state,
action: PayloadAction<{ baseId: string; type: 'file' | 'url' | 'note'; sourceId: string }>
) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
const newItem: ProcessingItem = {
id: `${action.payload.type}-${action.payload.sourceId}`,
type: action.payload.type,
sourceId: action.payload.sourceId,
status: 'pending',
createdAt: Date.now(),
updatedAt: Date.now(),
baseId: action.payload.baseId
}
// 避免重复添加
const exists = base.processingQueue.some((item) => item.sourceId === action.payload.sourceId)
if (!exists) {
base.processingQueue.push(newItem)
}
}
},
// 更新处理状态
updateProcessingStatus(
updateItemProcessingStatus(
state,
action: PayloadAction<{
baseId: string
@ -153,44 +109,42 @@ const knowledgeSlice = createSlice({
) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
const item = base.processingQueue.find((item) => item.id === action.payload.itemId)
const item = base.items.find((item) => item.id === action.payload.itemId)
if (item) {
item.status = action.payload.status
item.progress = action.payload.progress
item.error = action.payload.error
item.processingStatus = action.payload.status
item.processingProgress = action.payload.progress
item.processingError = action.payload.error
item.retryCount = action.payload.retryCount
item.updatedAt = Date.now()
}
}
},
// 移除处理队列项
removeProcessingItem(state, action: PayloadAction<{ baseId: string; itemId: string }>) {
clearCompletedProcessing(state, action: PayloadAction<{ baseId: string }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
base.processingQueue = base.processingQueue.filter((item) => item.id !== action.payload.itemId)
base.items.forEach((item) => {
if (item.processingStatus === 'completed' || item.processingStatus === 'failed') {
item.processingStatus = undefined
item.processingProgress = undefined
item.processingError = undefined
item.retryCount = undefined
}
})
}
},
// 清除已完成的项目
clearCompletedItems(state, action: PayloadAction<{ baseId: string }>) {
clearAllProcessing(state, action: PayloadAction<{ baseId: string }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
base.processingQueue = base.processingQueue.filter(
(item) => item.status !== 'completed' && item.status !== 'failed'
)
base.items.forEach((item) => {
item.processingStatus = undefined
item.processingProgress = undefined
item.processingError = undefined
item.retryCount = undefined
})
}
},
// 清除所有队列项目
clearAllItems(state, action: PayloadAction<{ baseId: string }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
base.processingQueue = []
}
},
// 更新知识库单个条目下面的 uniqueId
updateBaseItemUniqueId(state, action: PayloadAction<{ baseId: string; itemId: string; uniqueId: string }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
@ -203,25 +157,6 @@ const knowledgeSlice = createSlice({
}
})
// Selectors
export const selectProcessingItemBySource = (
state: KnowledgeState,
baseId: string,
sourceId: string
): ProcessingItem | undefined => {
const base = state.bases.find((b) => b.id === baseId)
return base?.processingQueue.find((item) => item.sourceId === sourceId)
}
export const selectProcessingItemsByType = (
state: KnowledgeState,
baseId: string,
type: 'file' | 'url' | 'note'
): ProcessingItem[] => {
const base = state.bases.find((b) => b.id === baseId)
return base?.processingQueue.filter((item) => item.type === type) || []
}
export const {
addBase,
deleteBase,
@ -231,11 +166,9 @@ export const {
updateFiles,
updateNotes,
removeItem,
addProcessingItem,
updateProcessingStatus,
removeProcessingItem,
clearCompletedItems,
clearAllItems,
updateItemProcessingStatus,
clearCompletedProcessing,
clearAllProcessing,
updateBaseItemUniqueId
} = knowledgeSlice.actions

View File

@ -183,27 +183,18 @@ export interface Shortcut {
export type ProcessingStatus = 'pending' | 'processing' | 'completed' | 'failed'
export type ProcessingItem = {
id: string
type: 'file' | 'url' | 'note'
status: ProcessingStatus
progress?: number
error?: string
createdAt: number
updatedAt: number
sourceId: string // file id, url, or note id
baseId: string
retryCount?: number
}
export type KnowledgeItem = {
id: string
baseId?: string
uniqueId?: string
type: 'file' | 'url' | 'note'
content: string | FileType // for files: FileType, for urls: string, for notes: string
type: 'file' | 'url' | 'note' | 'sitemap'
content: string | FileType
created_at: number
updated_at: number
processingStatus?: ProcessingStatus
processingProgress?: number
processingError?: string
retryCount?: number
}
export interface KnowledgeBase {
@ -214,7 +205,6 @@ export interface KnowledgeBase {
items: KnowledgeItem[]
created_at: number
updated_at: number
processingQueue: ProcessingItem[]
}
export type RagAppRequestParams = {