Files
prop-ai-hr/mobile-uni/src/services/personal-assistant.ts
T
admin 2488febf19 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
2026-07-22 08:54:04 +08:00

171 lines
9.2 KiB
TypeScript

import type {
AssistantCapture,
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, authHeaders, readTextPayload } from './api';
import { personalItemApiPath, personalSessionApiPath } from './personal-assistant-id';
const candidateBase = '/api/aihr/personal-assistant/memory-candidates';
type RawServiceMemory = Omit<ServiceMemory, 'version'> & { version?: number; currentVersion?: number };
const rowsOf = <T>(value: T[] | { rows?: T[]; records?: T[] } | null | undefined): T[] => {
if (Array.isArray(value)) return value;
return value?.rows || value?.records || [];
};
export const createMemoryIdempotencyKey = (candidateId: number) => {
const suffix = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
return `memory-confirm-${candidateId}-${suffix}`;
};
export const getMemoryCandidates = async (status = 'DRAFT') =>
rowsOf(await apiRequest<MemoryCandidate[] | { rows?: MemoryCandidate[]; records?: MemoryCandidate[] }>({
url: `${candidateBase}?status=${encodeURIComponent(status)}`,
method: 'GET',
timeout: 15000
}));
export const confirmMemoryCandidate = (candidateId: number, request: MemoryConfirmRequest) =>
apiRequest<MemoryConfirmResult>({
url: `${candidateBase}/${candidateId}/confirm`,
method: 'POST',
data: request,
timeout: 15000
});
export const dismissMemoryCandidate = (candidateId: number) =>
apiRequest<{ id: number; status: string }>({
url: `${candidateBase}/${candidateId}/dismiss`,
method: 'POST',
timeout: 15000
});
export const getAssistantCaptures = async () =>
rowsOf(await apiRequest<AssistantCapture[] | { rows?: AssistantCapture[]; records?: AssistantCapture[] }>({
url: '/api/aihr/personal-assistant/assistant-captures?limit=50',
method: 'GET',
timeout: 15000
}));
export const updateAssistantCaptureStatus = (
captureId: number,
businessStatus: 'RECORDED' | 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'VOID',
note = ''
) => apiRequest<AssistantCapture>({
url: `/api/aihr/personal-assistant/assistant-captures/${captureId}/status`,
method: 'POST',
data: {
businessStatus,
note,
idempotencyKey: `capture-status-${captureId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
},
timeout: 15000
});
export const getAssistantCaptureStatusHistory = (captureId: number) =>
apiRequest<CaptureStatusHistory[]>({
url: `/api/aihr/personal-assistant/assistant-captures/${captureId}/status-history`,
method: 'GET',
timeout: 15000
});
export const assistantCaptureSourceUrl = (captureId: number) =>
apiUrl(`/api/aihr/personal-assistant/assistant-captures/${captureId}/source`);
export const getServiceMemories = async () => {
const rows = rowsOf(await apiRequest<RawServiceMemory[] | { rows?: RawServiceMemory[]; records?: RawServiceMemory[] }>({
url: '/api/aihr/service-memories',
method: 'GET',
timeout: 15000
}));
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' });