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:
2026-07-22 08:54:04 +08:00
72 changed files with 13883 additions and 61 deletions
@@ -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));
+83 -1
View File
@@ -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' });