Files
prop-ai-hr/mobile-uni/src/services/knowledge.ts
T

205 lines
6.5 KiB
TypeScript

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<KnowledgeMediaSelectedFile>((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<UnifiedKnowledgeResponse>({
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<typeof uni.uploadFile> | null) => void,
conversation?: KnowledgeConversationState
) => new Promise<KnowledgeSearchResponse>((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<UnifiedKnowledgeResponse>(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<UnifiedKnowledgeResponse>({
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<PositionSopSummary>({
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<SummaryCardResponse>({
url: '/api/knowledge/summary-card',
method: 'POST',
data: {
queryText,
category: 'sop'
},
timeout: 45000
});