diff --git a/src/agentkit/server/frontend/src/api/kb.ts b/src/agentkit/server/frontend/src/api/kb.ts index 1b70373..1c15fe0 100644 --- a/src/agentkit/server/frontend/src/api/kb.ts +++ b/src/agentkit/server/frontend/src/api/kb.ts @@ -12,12 +12,23 @@ export interface IKbSource { document_count: number last_synced: string | null config?: Record + /** Department binding for ACL (U4). Absent = global source. */ + department_id?: string | null } -export interface IAddSourceRequest { - name: string - type: 'local' | 'feishu' | 'confluence' | 'http' - config: Record +/** Document processing status flow: pending → parsing → segmenting → vectorizing → indexed | failed. */ +export type DocumentStatus = + | 'pending' + | 'parsing' + | 'segmenting' + | 'vectorizing' + | 'indexed' + | 'failed' + +export interface IPreviewChunk { + index: number + content: string + metadata?: Record } export interface IUploadedDocument { @@ -27,6 +38,56 @@ export interface IUploadedDocument { chunks: number status: string created_at: string + /** Present when status === 'failed'. */ + error_message?: string + /** Total chunk count (upload response includes this; list may omit). */ + total_chunks?: number + /** Chunk preview returned by the upload endpoint. */ + chunks_preview?: IPreviewChunk[] +} + +export interface IPreviewResult { + document_id: string + chunks: IPreviewChunk[] + total_chunks: number +} + +/** Retrieval query mode — mirrors backend ``QueryMode`` enum. */ +export type QueryMode = 'embedding' | 'keywords' | 'blend' + +/** Hit-processing strategy — mirrors backend constants. */ +export type HitProcessing = 'model_opt' | 'direct' + +/** Rerank provider options. */ +export type RerankProvider = 'cohere' | 'bge' | 'none' + +export interface IKBSettings { + kb_id: string + owner: string | null + default_query_mode: QueryMode + default_hit_processing: HitProcessing + caching_disabled: boolean + rerank_enabled: boolean + rerank_provider: RerankProvider + rerank_api_key: string | null + rerank_base_url: string | null + data_export_warning: boolean +} + +export interface IKBSettingsUpdate { + default_query_mode?: QueryMode + default_hit_processing?: HitProcessing + caching_disabled?: boolean + rerank_enabled?: boolean + rerank_provider?: RerankProvider + rerank_api_key?: string | null + rerank_base_url?: string | null +} + +export interface IAddSourceRequest { + name: string + type: 'local' | 'feishu' | 'confluence' | 'http' + config: Record } export interface ISearchResult { @@ -125,6 +186,41 @@ class KbApiClient extends BaseApiClient { body: JSON.stringify(data), }) } + + /** Preview document segmentation without persisting (U3). */ + async previewDocument( + file: File, + chunkSize: number = 512, + chunkOverlap: number = 50, + ): Promise { + const formData = new FormData() + formData.append('file', file) + const query = `?chunk_size=${chunkSize}&chunk_overlap=${chunkOverlap}` + return this.request(`/documents/preview${query}`, { + method: 'POST', + body: formData, + }) + } + + /** Trigger vectorization for a document (retry path for failed docs). */ + async vectorizeDocument(documentId: string): Promise<{ status: string }> { + return this.request<{ status: string }>(`/documents/${documentId}/vectorize`, { + method: 'POST', + }) + } + + /** Get KB-level settings (query mode / hit processing / caching / rerank). */ + async getKbSettings(kbId: string): Promise { + return this.request(`/kbs/${kbId}/settings`) + } + + /** Update KB-level settings (owner only). */ + async updateKbSettings(kbId: string, update: IKBSettingsUpdate): Promise { + return this.request(`/kbs/${kbId}/settings`, { + method: 'PUT', + body: JSON.stringify(update), + }) + } } export const kbApi = new KbApiClient() @@ -140,3 +236,7 @@ export const listDocuments = kbApi.listDocuments.bind(kbApi) export const deleteDocument = kbApi.deleteDocument.bind(kbApi) export const syncSource = kbApi.syncSource.bind(kbApi) export const updateSource = kbApi.updateSource.bind(kbApi) +export const previewDocument = kbApi.previewDocument.bind(kbApi) +export const vectorizeDocument = kbApi.vectorizeDocument.bind(kbApi) +export const getKbSettings = kbApi.getKbSettings.bind(kbApi) +export const updateKbSettings = kbApi.updateKbSettings.bind(kbApi) diff --git a/src/agentkit/server/frontend/src/components/kb/DocumentUpload.vue b/src/agentkit/server/frontend/src/components/kb/DocumentUpload.vue index 73407f3..f2d42c8 100644 --- a/src/agentkit/server/frontend/src/components/kb/DocumentUpload.vue +++ b/src/agentkit/server/frontend/src/components/kb/DocumentUpload.vue @@ -1,20 +1,28 @@ @@ -122,6 +251,21 @@ onMounted(() => { padding: 8px 0; } +.document-upload__toolbar { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.document-upload__dragger { + flex: 1; +} + +.document-upload__preview-btn { + flex-shrink: 0; + margin-top: 4px; +} + .upload-spin { display: block; text-align: center; diff --git a/src/agentkit/server/frontend/src/components/kb/KBSettings.vue b/src/agentkit/server/frontend/src/components/kb/KBSettings.vue new file mode 100644 index 0000000..33bbd7b --- /dev/null +++ b/src/agentkit/server/frontend/src/components/kb/KBSettings.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/kb/SearchTest.vue b/src/agentkit/server/frontend/src/components/kb/SearchTest.vue index ed46a08..925c10e 100644 --- a/src/agentkit/server/frontend/src/components/kb/SearchTest.vue +++ b/src/agentkit/server/frontend/src/components/kb/SearchTest.vue @@ -30,10 +30,11 @@ style="width: 80px" /> - - - 向量 - 混合 + + + 语义 + 关键词 + 混合 @@ -75,6 +76,7 @@ + + diff --git a/src/agentkit/server/frontend/src/components/kb/SourceConfig.vue b/src/agentkit/server/frontend/src/components/kb/SourceConfig.vue index 59f4d5b..ddd3d9a 100644 --- a/src/agentkit/server/frontend/src/components/kb/SourceConfig.vue +++ b/src/agentkit/server/frontend/src/components/kb/SourceConfig.vue @@ -27,6 +27,15 @@ :text="record.status === 'active' ? '正常' : record.status" /> + @@ -204,6 +213,7 @@ const columns = [ { title: '名称', dataIndex: 'name', key: 'name' }, { title: '类型', dataIndex: 'type', key: 'type', width: 120 }, { title: '状态', dataIndex: 'status', key: 'status', width: 100 }, + { title: '权限', key: 'acl', width: 110 }, { title: '文档数', dataIndex: 'document_count', key: 'document_count', width: 80 }, { title: '最近同步', dataIndex: 'last_synced', key: 'last_synced', width: 160 }, { title: '操作', key: 'actions', width: 260 }, diff --git a/src/agentkit/server/frontend/src/stores/knowledge.ts b/src/agentkit/server/frontend/src/stores/knowledge.ts index a64668d..2245761 100644 --- a/src/agentkit/server/frontend/src/stores/knowledge.ts +++ b/src/agentkit/server/frontend/src/stores/knowledge.ts @@ -1,7 +1,27 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import * as kbApi from '@/api/kb' -import type { IKbSource, ISearchResult, IUploadedDocument } from '@/api/kb' +import type { + IKbSource, + ISearchResult, + IUploadedDocument, + IPreviewResult, + IKBSettings, + IKBSettingsUpdate, +} from '@/api/kb' + +/** + * Default KB id used for KB-level settings. + * + * ponytail: the frontend has no KB selector UI; the settings endpoint + * keys on an arbitrary ``kb_id`` string. Using a single "default" KB is + * the smallest thing that works. Upgrade path: add a KB picker when + * multiple KBs are exposed. + */ +export const DEFAULT_KB_ID = 'default' + +/** Terminal document statuses (no further polling needed). */ +const TERMINAL_STATUSES = new Set(['indexed', 'failed']) export const useKnowledgeStore = defineStore('knowledge', () => { // --- State --- @@ -11,12 +31,20 @@ export const useKnowledgeStore = defineStore('knowledge', () => { const isLoading = ref(false) const isUploading = ref(false) const isSearching = ref(false) + const isPreviewing = ref(false) + const previewResult = ref(null) + const kbSettings = ref(null) + const isSavingSettings = ref(false) const error = ref(null) // --- Getters --- const sourceCount = computed(() => sources.value.length) const localSources = computed(() => sources.value.filter((s) => s.type === 'local')) const externalSources = computed(() => sources.value.filter((s) => s.type !== 'local')) + /** Documents still being processed (non-terminal) — drives status polling. */ + const processingDocuments = computed(() => + documents.value.filter((d) => !TERMINAL_STATUSES.has(d.status)), + ) // --- Actions --- async function fetchSources(): Promise { @@ -160,6 +188,84 @@ export const useKnowledgeStore = defineStore('knowledge', () => { } } + /** Preview segmentation for a file without persisting (read-only). */ + async function previewDocument( + file: File, + chunkSize: number = 512, + chunkOverlap: number = 50, + ): Promise { + isPreviewing.value = true + error.value = null + previewResult.value = null + try { + const data = await kbApi.previewDocument(file, chunkSize, chunkOverlap) + previewResult.value = data + return data + } catch (err) { + error.value = err instanceof Error ? err.message : '分段预览失败' + console.error('Failed to preview document:', err) + throw err + } finally { + isPreviewing.value = false + } + } + + /** Clear the preview result (when closing the preview modal). */ + function clearPreview(): void { + previewResult.value = null + } + + /** + * Retry vectorization for a failed document. + * + * ponytail: the backend vectorize endpoint currently returns 503 + * (requires PG-backed KBStore, U8). The frontend still attempts the + * call so that once the backend lands, retry works without changes. + * Upgrade path: backend implements async vectorization + status push. + */ + async function retryDocument(documentId: string): Promise { + try { + await kbApi.vectorizeDocument(documentId) + await fetchDocuments() + } catch (err) { + error.value = err instanceof Error ? err.message : '重试向量化失败' + console.error('Failed to retry vectorization:', err) + throw err + } + } + + /** Fetch KB-level settings for the given KB (defaults to the single KB). */ + async function fetchKbSettings(kbId: string = DEFAULT_KB_ID): Promise { + isLoading.value = true + error.value = null + try { + kbSettings.value = await kbApi.getKbSettings(kbId) + } catch (err) { + error.value = err instanceof Error ? err.message : '获取 KB 设置失败' + console.error('Failed to fetch KB settings:', err) + } finally { + isLoading.value = false + } + } + + /** Update KB-level settings (owner only). */ + async function saveKbSettings( + update: IKBSettingsUpdate, + kbId: string = DEFAULT_KB_ID, + ): Promise { + isSavingSettings.value = true + error.value = null + try { + kbSettings.value = await kbApi.updateKbSettings(kbId, update) + } catch (err) { + error.value = err instanceof Error ? err.message : '保存 KB 设置失败' + console.error('Failed to save KB settings:', err) + throw err + } finally { + isSavingSettings.value = false + } + } + return { // State sources, @@ -168,11 +274,16 @@ export const useKnowledgeStore = defineStore('knowledge', () => { isLoading, isUploading, isSearching, + isPreviewing, + previewResult, + kbSettings, + isSavingSettings, error, // Getters sourceCount, localSources, externalSources, + processingDocuments, // Actions fetchSources, addSource, @@ -184,5 +295,10 @@ export const useKnowledgeStore = defineStore('knowledge', () => { deleteDocument, syncSource, updateSource, + previewDocument, + clearPreview, + retryDocument, + fetchKbSettings, + saveKbSettings, } }) diff --git a/src/agentkit/server/frontend/src/views/KnowledgeBaseView.vue b/src/agentkit/server/frontend/src/views/KnowledgeBaseView.vue index 4c5ab84..2a7f247 100644 --- a/src/agentkit/server/frontend/src/views/KnowledgeBaseView.vue +++ b/src/agentkit/server/frontend/src/views/KnowledgeBaseView.vue @@ -10,6 +10,56 @@ + + + + +
+
+ + 刷新 + + + 文档处理任务列表({{ kbStore.documents.length }}) + +
+ + + + +
+
@@ -20,10 +70,62 @@ import { useKnowledgeStore } from '@/stores/knowledge' import DocumentUpload from '@/components/kb/DocumentUpload.vue' import SourceConfig from '@/components/kb/SourceConfig.vue' import SearchTest from '@/components/kb/SearchTest.vue' +import KBSettings from '@/components/kb/KBSettings.vue' const kbStore = useKnowledgeStore() const activeTab = ref('documents') +const taskColumns = [ + { title: '文件名', dataIndex: 'filename', key: 'filename', ellipsis: true }, + { title: '状态', dataIndex: 'status', key: 'status', width: 130 }, + { title: '分块数', dataIndex: 'chunks', key: 'chunks', width: 80 }, + { title: '错误信息', key: 'error', ellipsis: true }, + { title: '时间', dataIndex: 'created_at', key: 'created_at', width: 160 }, +] + +function taskStatusBadge(status: string): 'processing' | 'success' | 'error' | 'default' { + switch (status) { + case 'indexed': + return 'success' + case 'failed': + return 'error' + case 'pending': + case 'parsing': + case 'segmenting': + case 'vectorizing': + return 'processing' + default: + return 'default' + } +} + +function taskStatusLabel(status: string): string { + switch (status) { + case 'pending': + return '待处理' + case 'parsing': + return '解析中' + case 'segmenting': + return '分段中' + case 'vectorizing': + return '向量化中' + case 'indexed': + return '已索引' + case 'failed': + return '失败' + default: + return status + } +} + +function formatTime(isoStr: string): string { + try { + return new Date(isoStr).toLocaleString('zh-CN') + } catch { + return isoStr + } +} + onMounted(() => { kbStore.fetchSources() }) @@ -36,4 +138,30 @@ onMounted(() => { overflow-y: auto; background: var(--bg-primary); } + +.task-history { + padding: 8px 0; +} + +.task-history__toolbar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.task-history__hint { + color: var(--text-placeholder); + font-size: 13px; +} + +.task-history__error { + color: var(--color-error, #ff4d4f); + font-size: 13px; + cursor: help; +} + +.task-history__muted { + color: var(--text-placeholder); +}