Merge branch 'codex/personal-assistant-phase2' into codex/multi-tenant-knowledge-platform
# Conflicts: # backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java # backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java # backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java # backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrModelSeedServiceTest.java # backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java # docs/API_INTEGRATION.md # docs/个人AI助理阶段二专项TechSpec.md # docs/个人AI助理阶段二开发推进计划.md # frontend/src/views/knowledge/processing.vue # mobile-uni/src/pages/user/sop/index.vue # mobile-uni/src/services/api.ts # mobile-uni/src/services/personal-assistant.ts # mobile-uni/tests/personal-assistant.test.mjs # scripts/demo-check.sh # scripts/personal-assistant-smoke.sh
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
export const normalizeBackendId = (value: string) => {
|
||||
const id = value.trim();
|
||||
if (!/^[1-9]\d*$/.test(id)) throw new Error('PERSONAL_ID_INVALID');
|
||||
return id;
|
||||
};
|
||||
|
||||
export const personalItemApiPath = (id: string, suffix = '') =>
|
||||
`/api/aihr/personal-assistant/items/${encodeURIComponent(normalizeBackendId(id))}${suffix}`;
|
||||
|
||||
export const personalSessionApiPath = (id: string) =>
|
||||
`/api/aihr/personal-assistant/sessions/${encodeURIComponent(normalizeBackendId(id))}`;
|
||||
|
||||
export const personalItemRoute = (id: string) =>
|
||||
`/pages/user/assistant/item?id=${encodeURIComponent(normalizeBackendId(id))}`;
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { PersonalOcrProgress } from '@/types/api';
|
||||
|
||||
export const ocrProgressPercent = (ocr?: PersonalOcrProgress) => {
|
||||
if (!ocr?.required || ocr.totalPages <= 0) return 0;
|
||||
return Math.min(100, Math.max(0, Math.round((ocr.processedPages / ocr.totalPages) * 100)));
|
||||
};
|
||||
|
||||
export const ocrProgressText = (ocr?: PersonalOcrProgress) => {
|
||||
if (!ocr?.required) return '';
|
||||
if (ocr.processedPages < ocr.totalPages && ['PENDING', 'RUNNING', 'RETRY'].includes(ocr.status)) {
|
||||
return `正在识别扫描 PDF:${ocr.processedPages}/${ocr.totalPages} 页`;
|
||||
}
|
||||
if (ocr.failedPages > 0 && ocr.successPages > 0) return `已收录,${ocr.failedPages} 页识别失败`;
|
||||
if (ocr.failedPages > 0) return `${ocr.failedPages} 页识别失败`;
|
||||
if (ocr.totalPages > 0) return `扫描 PDF 已识别:${ocr.successPages}/${ocr.totalPages} 页`;
|
||||
return '正在准备扫描 PDF 识别';
|
||||
};
|
||||
|
||||
export const isOcrActive = (ocr?: PersonalOcrProgress) =>
|
||||
Boolean(ocr?.required && ['PENDING', 'RUNNING', 'RETRY'].includes(ocr.status));
|
||||
@@ -3,10 +3,23 @@ import type {
|
||||
MemoryCandidate,
|
||||
MemoryConfirmRequest,
|
||||
MemoryConfirmResult,
|
||||
PersonalAskResponse,
|
||||
PersonalExportOutline,
|
||||
PersonalExportOutlineSection,
|
||||
PersonalItem,
|
||||
PersonalItemCreated,
|
||||
PersonalItemSourceType,
|
||||
PersonalKnowledgeScope,
|
||||
PersonalPage,
|
||||
PersonalPublishRequest,
|
||||
PersonalSession,
|
||||
PersonalSessionDetail,
|
||||
PersonalSpace,
|
||||
ServiceMemory
|
||||
} from '@/types/api';
|
||||
import type { CaptureStatusHistory } from '@/types/api';
|
||||
import { apiRequest, apiUrl } from './api';
|
||||
import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api';
|
||||
import { personalItemApiPath, personalSessionApiPath } from './personal-assistant-id';
|
||||
|
||||
const candidateBase = '/api/aihr/personal-assistant/memory-candidates';
|
||||
|
||||
@@ -86,3 +99,72 @@ export const getServiceMemories = async () => {
|
||||
}));
|
||||
return rows.map((item) => ({ ...item, version: item.version ?? item.currentVersion ?? 0 }));
|
||||
};
|
||||
|
||||
export type KnowledgeScope = PersonalKnowledgeScope;
|
||||
|
||||
export interface PersonalItemQuery {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
sourceType?: PersonalItemSourceType | '';
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
const personalQuery = (query: PersonalItemQuery) => {
|
||||
const entries = Object.entries(query).filter(([, value]) => value !== undefined && value !== '');
|
||||
return entries.length ? `?${entries.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join('&')}` : '';
|
||||
};
|
||||
|
||||
export const askPersonalAssistant = (data: {
|
||||
sessionId?: string; queryText: string; scope: KnowledgeScope[]; dateFrom?: string; dateTo?: string;
|
||||
itemIds?: string[]; outputFormat?: 'ANSWER' | 'ACTION_PLAN' | 'OUTLINE';
|
||||
}) => apiRequest<PersonalAskResponse>({ url: '/api/aihr/personal-assistant/ask', method: 'POST', data, timeout: 45000 });
|
||||
|
||||
export const mergePersonalRows = (current: PersonalItem[], incoming: PersonalItem[]) =>
|
||||
Array.from(new Map([...current, ...incoming].map((item) => [item.id, item])).values());
|
||||
|
||||
export const loadPersonalSpace = () => apiRequest<PersonalSpace>({ url: '/api/aihr/personal-assistant/space', method: 'GET' });
|
||||
export const loadPersonalItems = (query: PersonalItemQuery = {}) =>
|
||||
apiRequest<PersonalPage<PersonalItem>>({ url: `/api/aihr/personal-assistant/items${personalQuery(query)}`, method: 'GET' });
|
||||
export const loadPersonalItem = (id: string) => apiRequest<PersonalItem>({ url: personalItemApiPath(id), method: 'GET' });
|
||||
export const createTextItem = (data: { title?: string; content: string; capturedAt?: string; tags?: string[] }) =>
|
||||
apiRequest<PersonalItemCreated>({ url: '/api/aihr/personal-assistant/items/text', method: 'POST', data });
|
||||
export const createUrlItem = (data: { url: string; title?: string; capturedAt?: string }) =>
|
||||
apiRequest<PersonalItemCreated>({ url: '/api/aihr/personal-assistant/items/url', method: 'POST', data, timeout: 45000 });
|
||||
|
||||
export interface PersonalSelectedFile { path: string; name: string; size: number; }
|
||||
export const choosePersonalFile = () => new Promise<PersonalSelectedFile>((resolve, reject) => {
|
||||
uni.chooseFile({ count: 1, type: 'all', success: (result) => {
|
||||
const file = (Array.isArray(result.tempFiles) ? result.tempFiles[0] : undefined) as Partial<PersonalSelectedFile> | undefined;
|
||||
const path = file?.path || result.tempFilePaths?.[0] || '';
|
||||
if (!path) return reject(new Error('未获取到文件路径'));
|
||||
resolve({ path, name: file?.name || path.split('/').pop() || 'personal-file', size: Number(file?.size || 0) });
|
||||
}, fail: () => reject(new Error('未选择文件')) });
|
||||
});
|
||||
|
||||
export const uploadPersonalFile = (file: PersonalSelectedFile, title?: string, capturedAt?: string) =>
|
||||
new Promise<PersonalItemCreated>((resolve, reject) => uni.uploadFile({
|
||||
url: apiUrl('/api/aihr/personal-assistant/items/file'), filePath: file.path, name: 'file', header: authHeaders(false),
|
||||
formData: { ...(title?.trim() ? { title: title.trim() } : {}), ...(capturedAt ? { capturedAt } : {}) }, timeout: 45000,
|
||||
success: (response) => { try { resolve(readTextPayload<PersonalItemCreated>(response.statusCode, response.data)); } catch (error) { reject(error); } },
|
||||
fail: () => reject(new Error('文件收藏失败'))
|
||||
}));
|
||||
|
||||
export const retryPersonalItem = (id: string) => apiRequest<PersonalItem>({ url: personalItemApiPath(id, '/retry'), method: 'POST' });
|
||||
export const retryPersonalOcrPages = (id: string) => apiRequest<PersonalItem>({ url: personalItemApiPath(id, '/ocr/retry-failed'), method: 'POST' });
|
||||
export const deletePersonalItem = (id: string) => apiRequest<{ cleanupJobId: string }>({ url: personalItemApiPath(id), method: 'DELETE', timeout: 15000 });
|
||||
export const downloadPersonalItem = (id: string) => apiRequest<{ url: string; expiresAt?: string }>({ url: personalItemApiPath(id, '/download-url'), method: 'GET' });
|
||||
export const loadPersonalSessions = () => apiRequest<PersonalSession[]>({ url: '/api/aihr/personal-assistant/sessions', method: 'GET' });
|
||||
export const loadPersonalSession = (id: string) => apiRequest<PersonalSessionDetail>({ url: personalSessionApiPath(id), method: 'GET' });
|
||||
export const deletePersonalSession = (id: string) => apiRequest<void>({ url: personalSessionApiPath(id), method: 'DELETE' });
|
||||
export const createPersonalOutline = (data: { sessionId: string; title?: string }) => apiRequest<PersonalExportOutline>({ url: '/api/aihr/personal-assistant/exports/outline', method: 'POST', data });
|
||||
export const updatePersonalOutline = (id: string, data: { title: string; sections: PersonalExportOutlineSection[] }) =>
|
||||
apiRequest<PersonalExportOutline>({ url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/outline`, method: 'PUT', data });
|
||||
export const confirmPersonalOutline = (id: string) => apiRequest<void>({ url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/confirm`, method: 'POST' });
|
||||
export const loadPersonalExport = (id: string) => apiRequest<PersonalExportOutline>({ url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}`, method: 'GET' });
|
||||
export const enqueuePersonalPpt = (id: string) => apiRequest<void>({ url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/pptx`, method: 'POST', data: { templateCode: 'standard' } });
|
||||
export const downloadPersonalExport = (id: string) => apiRequest<{ url: string; expiresAt?: string }>({ url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/download-url`, method: 'GET' });
|
||||
export const requestPersonalPublish = (itemId: string, reason: string) => apiRequest<PersonalPublishRequest>({ url: `${personalItemApiPath(itemId)}/publish-requests`, method: 'POST', data: { targetScope: 'POSITION:生活顾问', reason } });
|
||||
export const loadPersonalPublishRequests = () => apiRequest<PersonalPublishRequest[]>({ url: '/api/aihr/personal-assistant/publish-requests', method: 'GET' });
|
||||
|
||||
Reference in New Issue
Block a user