import type { AnswerFeedbackVerdict, KnowledgeSearchResponse, KnowledgeMediaSelectedFile, KnowledgeConversationState, UnifiedKnowledgeResponse, KnowledgeSnippet, PositionSopSummary, SummaryCardResponse } from '@/types/api'; import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api'; import { getSelectedPosition } from './auth'; type RawKnowledgeSnippet = KnowledgeSnippet & { title?: string; text?: string; }; const mediaExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v']; const videoExtensions = new Set(['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v']); export const chooseKnowledgeMedia = () => new Promise((resolve, reject) => { uni.chooseFile({ count: 1, type: 'all', extension: mediaExtensions, success: (result) => { const tempFiles = Array.isArray(result.tempFiles) ? result.tempFiles : []; const file = (tempFiles[0] || {}) as { path?: string; name?: string; size?: number }; const path = file.path || result.tempFilePaths?.[0] || ''; const name = file.name || path.split('/').pop() || '现场附件'; const extension = `.${name.split('.').pop()?.toLowerCase() || ''}`; const kind = videoExtensions.has(extension) ? 'video' : 'image'; const size = Number(file.size || 0); const maxSize = kind === 'video' ? 100 * 1024 * 1024 : 10 * 1024 * 1024; if (!path) { reject(new Error('未获取到附件文件')); return; } if (size > maxSize) { reject(new Error(kind === 'video' ? '视频不能超过 100MB' : '图片不能超过 10MB')); return; } resolve({ path, name, size, kind }); }, fail: () => reject(new Error('未选择附件')) }); }); const normalizeSnippet = (snippet: RawKnowledgeSnippet): KnowledgeSnippet => ({ ...snippet, docTitle: snippet.docTitle || snippet.title || '', content: snippet.content || snippet.text || '' }); export const searchKnowledge = async ( queryText: string, position = getSelectedPosition(), conversation?: KnowledgeConversationState ) => { const response = await apiRequest({ url: '/api/knowledge/query', method: 'POST', data: { queryText, category: 'sop', position, limit: 5, conversationId: conversation?.conversationId, contextVersion: conversation?.contextVersion }, timeout: 30000 }); return normalizeUnifiedResponse(response); }; export const searchKnowledgeWithMedia = ( file: KnowledgeMediaSelectedFile, queryText: string, position = getSelectedPosition(), onUploadTask?: (task: ReturnType | null) => void, conversation?: KnowledgeConversationState ) => new Promise((resolve, reject) => { const task = uni.uploadFile({ url: apiUrl('/api/knowledge/query-media'), filePath: file.path, name: 'file', header: authHeaders(false), formData: { queryText, position, ...(conversation ? { conversationId: conversation.conversationId, contextVersion: String(conversation.contextVersion) } : {}) }, timeout: 360000, success: (response) => { try { const payload = readTextPayload(response.statusCode, response.data); resolve(normalizeUnifiedResponse(payload)); } catch (error) { reject(error); } }, fail: () => reject(new Error(file.kind === 'video' ? '视频上传或分析超时' : '图片上传失败')), complete: () => onUploadTask?.(null) }); onUploadTask?.(task); }); export const queryKnowledgeTool = async ( toolCode: 'MY_PRACTICE_SUMMARY' | 'TEAM_PRACTICE_SUMMARY', queryText: string, conversation?: KnowledgeConversationState ) => { const response = await apiRequest({ url: '/api/knowledge/query', method: 'POST', data: { queryText, source: 'mobile_uni_tool', limit: 5, toolCode, conversationId: conversation?.conversationId, contextVersion: conversation?.contextVersion }, timeout: 30000 }); return normalizeUnifiedResponse(response); }; const normalizeUnifiedResponse = (response: UnifiedKnowledgeResponse): KnowledgeSearchResponse => { const legacy = response.legacy || ({} as KnowledgeSearchResponse); const citationSnippets = (response.citations || []).map((item) => normalizeSnippet({ docTitle: item.title, content: item.snippet, fragmentId: item.fragmentId, spaceCode: item.spaceCode, sourceType: item.sourceType })); return { ...legacy, requestId: response.requestId, queryText: response.queryText, category: legacy.category || 'sop', answer: response.answer, reference: legacy.reference || (response.citations || []).map((item) => item.title).join('、'), docs: legacy.docs || [], snippets: (legacy.snippets?.length ? legacy.snippets : citationSnippets).map((item) => normalizeSnippet(item as RawKnowledgeSnippet)), training: legacy.training || [], records: legacy.records || [], citations: response.citations || [], usedSpaceCodes: response.usedSpaceCodes || [], noEvidence: response.noEvidence, promptVersion: response.promptVersion, data: response.data, conversationId: response.conversationId, contextVersion: response.contextVersion, intent: response.intent, rewrittenQuery: response.rewrittenQuery, resources: response.resources || [] }; }; export const getPositionSopSummary = (position = getSelectedPosition()) => apiRequest({ url: `/api/knowledge/position-sop?position=${encodeURIComponent(position)}&projectType=住宅`, method: 'GET', timeout: 15000 }); export const submitAnswerFeedback = (params: { queryText: string; fragmentIds: number[]; verdict: AnswerFeedbackVerdict; reviewId?: number | null; }) => apiRequest<{ saved: number }>({ url: '/api/knowledge/answer-feedback', method: 'POST', data: { queryText: params.queryText, fragmentIds: params.fragmentIds, verdict: params.verdict, category: 'sop', position: getSelectedPosition(), source: 'mobile_uni_sop', reviewId: params.reviewId ?? null }, timeout: 15000 }); export const generateSummaryCard = (queryText: string) => apiRequest({ url: '/api/knowledge/summary-card', method: 'POST', data: { queryText, category: 'sop' }, timeout: 45000 });