fix(mobile): preserve assistant IDs and scope state
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
<text class="domain-name">我的资料</text>
|
<text class="domain-name">我的资料</text>
|
||||||
<text class="domain-desc">个人收藏,仅自己可见</text>
|
<text class="domain-desc">个人收藏,仅自己可见</text>
|
||||||
</button>
|
</button>
|
||||||
<button class="domain-option" :class="{ active: enterpriseEnabled }" @click="toggleEnterprise">
|
<button class="domain-option" :class="{ active: enterpriseEnabled }" :disabled="asking" @click="changeScope">
|
||||||
<text class="domain-name">企业知识</text>
|
<text class="domain-name">企业知识</text>
|
||||||
<text class="domain-desc">企业知识按岗位权限</text>
|
<text class="domain-desc">企业知识按岗位权限</text>
|
||||||
</button>
|
</button>
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type { PersonalAskResponse, PersonalCitation } from '@/types/api';
|
import type { PersonalAskResponse, PersonalCitation } from '@/types/api';
|
||||||
import { askPersonalAssistant, type KnowledgeScope } from '@/services/personal-assistant';
|
import { askPersonalAssistant, type KnowledgeScope } from '@/services/personal-assistant';
|
||||||
|
import { personalItemRoute } from '@/services/personal-assistant-id';
|
||||||
|
|
||||||
const scopes = ref<KnowledgeScope[]>(['PERSONAL']);
|
const scopes = ref<KnowledgeScope[]>(['PERSONAL']);
|
||||||
const queryText = ref('');
|
const queryText = ref('');
|
||||||
@@ -77,7 +78,12 @@ const message = ref('');
|
|||||||
let requestGeneration = 0;
|
let requestGeneration = 0;
|
||||||
const enterpriseEnabled = computed(() => scopes.value.includes('ENTERPRISE'));
|
const enterpriseEnabled = computed(() => scopes.value.includes('ENTERPRISE'));
|
||||||
|
|
||||||
const toggleEnterprise = () => {
|
const scopeKey = (value: KnowledgeScope[]) => value.join(',');
|
||||||
|
const changeScope = () => {
|
||||||
|
if (asking.value) return;
|
||||||
|
requestGeneration += 1;
|
||||||
|
answer.value = undefined;
|
||||||
|
message.value = '';
|
||||||
scopes.value = enterpriseEnabled.value ? ['PERSONAL'] : ['PERSONAL', 'ENTERPRISE'];
|
scopes.value = enterpriseEnabled.value ? ['PERSONAL'] : ['PERSONAL', 'ENTERPRISE'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,22 +91,24 @@ const submitQuestion = async () => {
|
|||||||
const question = queryText.value.trim();
|
const question = queryText.value.trim();
|
||||||
if (!question || asking.value) return;
|
if (!question || asking.value) return;
|
||||||
const generation = ++requestGeneration;
|
const generation = ++requestGeneration;
|
||||||
|
const scopeSnapshot = [...scopes.value];
|
||||||
asking.value = true;
|
asking.value = true;
|
||||||
answer.value = undefined;
|
answer.value = undefined;
|
||||||
message.value = '';
|
message.value = '';
|
||||||
try {
|
try {
|
||||||
const response = await askPersonalAssistant({ queryText: question, scope: scopes.value, outputFormat: 'ACTION_PLAN' });
|
const response = await askPersonalAssistant({ queryText: question, scope: scopeSnapshot, outputFormat: 'ACTION_PLAN' });
|
||||||
if (generation !== requestGeneration) return;
|
if (generation !== requestGeneration || scopeKey(scopes.value) !== scopeKey(scopeSnapshot)) return;
|
||||||
answer.value = response;
|
answer.value = response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (generation !== requestGeneration) return;
|
if (generation !== requestGeneration) return;
|
||||||
const text = error instanceof Error ? error.message : '提问失败,请稍后重试';
|
const text = error instanceof Error ? error.message : '提问失败,请稍后重试';
|
||||||
if (enterpriseEnabled.value && /ENTERPRISE_SCOPE_FORBIDDEN|企业知识|FORBIDDEN/i.test(text)) {
|
if (enterpriseEnabled.value && /ENTERPRISE_SCOPE_FORBIDDEN|企业知识|FORBIDDEN/i.test(text)) {
|
||||||
scopes.value = ['PERSONAL'];
|
scopes.value = ['PERSONAL'];
|
||||||
|
const fallbackScope: KnowledgeScope[] = ['PERSONAL'];
|
||||||
message.value = '当前岗位没有企业知识权限,已仅查询我的资料。';
|
message.value = '当前岗位没有企业知识权限,已仅查询我的资料。';
|
||||||
try {
|
try {
|
||||||
const personalResponse = await askPersonalAssistant({ queryText: question, scope: ['PERSONAL'], outputFormat: 'ACTION_PLAN' });
|
const personalResponse = await askPersonalAssistant({ queryText: question, scope: fallbackScope, outputFormat: 'ACTION_PLAN' });
|
||||||
if (generation !== requestGeneration) return;
|
if (generation !== requestGeneration || scopeKey(scopes.value) !== scopeKey(fallbackScope)) return;
|
||||||
answer.value = personalResponse;
|
answer.value = personalResponse;
|
||||||
} catch (personalError) {
|
} catch (personalError) {
|
||||||
if (generation !== requestGeneration) return;
|
if (generation !== requestGeneration) return;
|
||||||
@@ -122,7 +130,7 @@ const citationLabel = (citation: PersonalCitation) => {
|
|||||||
|
|
||||||
const openCitation = (citation: PersonalCitation) => {
|
const openCitation = (citation: PersonalCitation) => {
|
||||||
if (citation.domain !== 'PERSONAL' || !citation.itemId) return;
|
if (citation.domain !== 'PERSONAL' || !citation.itemId) return;
|
||||||
uni.navigateTo({ url: `/pages/user/assistant/item?id=${citation.itemId}` });
|
uni.navigateTo({ url: personalItemRoute(citation.itemId) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const goBack = () => uni.navigateBack();
|
const goBack = () => uni.navigateBack();
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ import { ref } from 'vue';
|
|||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import type { PersonalItem, PersonalItemSourceType, PersonalItemStatus } from '@/types/api';
|
import type { PersonalItem, PersonalItemSourceType, PersonalItemStatus } from '@/types/api';
|
||||||
import { deletePersonalItem, downloadPersonalItem, loadPersonalItem, retryPersonalItem } from '@/services/personal-assistant';
|
import { deletePersonalItem, downloadPersonalItem, loadPersonalItem, retryPersonalItem } from '@/services/personal-assistant';
|
||||||
|
import { normalizeBackendId } from '@/services/personal-assistant-id';
|
||||||
|
|
||||||
const itemId = ref(0); const item = ref<PersonalItem>(); const loading = ref(false); const busy = ref(false); const message = ref('');
|
const itemId = ref(''); const item = ref<PersonalItem>(); const loading = ref(false); const busy = ref(false); const message = ref('');
|
||||||
const statusText = (status: PersonalItemStatus) => ({ QUEUED: '等待处理', PARSING: '解析中', READY: '可检索', FAILED: '解析失败', DELETING: '删除中', DELETED: '已删除' })[status];
|
const statusText = (status: PersonalItemStatus) => ({ QUEUED: '等待处理', PARSING: '解析中', READY: '可检索', FAILED: '解析失败', DELETING: '删除中', DELETED: '已删除' })[status];
|
||||||
const statusDescription = (status: PersonalItemStatus) => ({ QUEUED: '已进入解析队列。', PARSING: '正在提取正文和建立搜索索引。', READY: '已完成解析,可用于个人助理问答。', FAILED: '本次解析失败,可查看公开原因后重试。', DELETING: '正在清理附件、正文和索引。', DELETED: '资料已删除。' })[status];
|
const statusDescription = (status: PersonalItemStatus) => ({ QUEUED: '已进入解析队列。', PARSING: '正在提取正文和建立搜索索引。', READY: '已完成解析,可用于个人助理问答。', FAILED: '本次解析失败,可查看公开原因后重试。', DELETING: '正在清理附件、正文和索引。', DELETED: '资料已删除。' })[status];
|
||||||
const sourceText = (source: PersonalItemSourceType) => ({ TEXT: '文字', FILE: '文件', URL: '外部网页' })[source];
|
const sourceText = (source: PersonalItemSourceType) => ({ TEXT: '文字', FILE: '文件', URL: '外部网页' })[source];
|
||||||
@@ -45,7 +46,14 @@ const download = async () => { busy.value = true; try { const result = await dow
|
|||||||
const confirmDelete = () => uni.showModal({ title: '确认删除资料', content: '附件、解析正文和搜索索引会清除,删除后无法恢复。', confirmText: '确认删除', confirmColor: '#e70014', success: (result) => { if (result.confirm) void remove(); } });
|
const confirmDelete = () => uni.showModal({ title: '确认删除资料', content: '附件、解析正文和搜索索引会清除,删除后无法恢复。', confirmText: '确认删除', confirmColor: '#e70014', success: (result) => { if (result.confirm) void remove(); } });
|
||||||
const remove = async () => { busy.value = true; message.value = ''; try { await deletePersonalItem(itemId.value); uni.redirectTo({ url: '/pages/user/assistant/library' }); } catch (error) { message.value = error instanceof Error ? error.message : '删除失败,请重试'; } finally { busy.value = false; } };
|
const remove = async () => { busy.value = true; message.value = ''; try { await deletePersonalItem(itemId.value); uni.redirectTo({ url: '/pages/user/assistant/library' }); } catch (error) { message.value = error instanceof Error ? error.message : '删除失败,请重试'; } finally { busy.value = false; } };
|
||||||
const goBack = () => uni.navigateBack();
|
const goBack = () => uni.navigateBack();
|
||||||
onLoad((options) => { itemId.value = Number(options?.id || 0); if (!Number.isSafeInteger(itemId.value) || itemId.value <= 0) message.value = '资料不存在或无权访问'; else void load(); });
|
onLoad((options) => {
|
||||||
|
try {
|
||||||
|
itemId.value = normalizeBackendId(String(options?.id || ''));
|
||||||
|
void load();
|
||||||
|
} catch {
|
||||||
|
message.value = '资料不存在或无权访问';
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { computed, ref } from 'vue';
|
|||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
import type { PersonalItem, PersonalItemSourceType, PersonalItemStatus, PersonalSpace } from '@/types/api';
|
import type { PersonalItem, PersonalItemSourceType, PersonalItemStatus, PersonalSpace } from '@/types/api';
|
||||||
import { loadPersonalItems, loadPersonalSpace, mergePersonalRows } from '@/services/personal-assistant';
|
import { loadPersonalItems, loadPersonalSpace, mergePersonalRows } from '@/services/personal-assistant';
|
||||||
|
import { personalItemRoute } from '@/services/personal-assistant-id';
|
||||||
|
|
||||||
const pageNum = ref(1);
|
const pageNum = ref(1);
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
@@ -92,7 +93,7 @@ const changeStatus = (event: { detail: { value: string | number } }) => { status
|
|||||||
const sourceText = (value: PersonalItemSourceType) => ({ TEXT: '文字', FILE: '文件', URL: '外部网页' })[value];
|
const sourceText = (value: PersonalItemSourceType) => ({ TEXT: '文字', FILE: '文件', URL: '外部网页' })[value];
|
||||||
const statusText = (value: PersonalItemStatus) => ({ QUEUED: '等待处理', PARSING: '解析中', READY: '可检索', FAILED: '解析失败', DELETING: '删除中', DELETED: '已删除' })[value];
|
const statusText = (value: PersonalItemStatus) => ({ QUEUED: '等待处理', PARSING: '解析中', READY: '可检索', FAILED: '解析失败', DELETING: '删除中', DELETED: '已删除' })[value];
|
||||||
const formatBytes = (bytes: number) => bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)}MB` : `${Math.ceil(bytes / 1024)}KB`;
|
const formatBytes = (bytes: number) => bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)}MB` : `${Math.ceil(bytes / 1024)}KB`;
|
||||||
const openItem = (id: number) => uni.navigateTo({ url: `/pages/user/assistant/item?id=${id}` });
|
const openItem = (id: string) => uni.navigateTo({ url: personalItemRoute(id) });
|
||||||
const openCapture = () => uni.navigateTo({ url: '/pages/user/assistant/capture' });
|
const openCapture = () => uni.navigateTo({ url: '/pages/user/assistant/capture' });
|
||||||
const goBack = () => uni.navigateBack();
|
const goBack = () => uni.navigateBack();
|
||||||
onShow(refresh);
|
onShow(refresh);
|
||||||
|
|||||||
@@ -30,18 +30,19 @@ import { ref } from 'vue';
|
|||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
import type { PersonalCitation, PersonalSession, PersonalSessionDetail } from '@/types/api';
|
import type { PersonalCitation, PersonalSession, PersonalSessionDetail } from '@/types/api';
|
||||||
import { deletePersonalSession, loadPersonalSession, loadPersonalSessions } from '@/services/personal-assistant';
|
import { deletePersonalSession, loadPersonalSession, loadPersonalSessions } from '@/services/personal-assistant';
|
||||||
|
import { personalItemRoute } from '@/services/personal-assistant-id';
|
||||||
|
|
||||||
const sessions = ref<PersonalSession[]>([]); const detail = ref<PersonalSessionDetail>(); const loading = ref(false); const message = ref('');
|
const sessions = ref<PersonalSession[]>([]); const detail = ref<PersonalSessionDetail>(); const loading = ref(false); const message = ref('');
|
||||||
const loadList = async () => { loading.value = true; message.value = ''; try { sessions.value = await loadPersonalSessions(); } catch (error) { message.value = error instanceof Error ? error.message : '会话加载失败'; } finally { loading.value = false; } };
|
const loadList = async () => { loading.value = true; message.value = ''; try { sessions.value = await loadPersonalSessions(); } catch (error) { message.value = error instanceof Error ? error.message : '会话加载失败'; } finally { loading.value = false; } };
|
||||||
const openSession = async (id: number) => { loading.value = true; message.value = ''; try { detail.value = await loadPersonalSession(id); } catch (error) { message.value = error instanceof Error ? error.message : '会话不存在或无权访问'; } finally { loading.value = false; } };
|
const openSession = async (id: string) => { loading.value = true; message.value = ''; try { detail.value = await loadPersonalSession(id); } catch (error) { message.value = error instanceof Error ? error.message : '会话不存在或无权访问'; } finally { loading.value = false; } };
|
||||||
const confirmDelete = (id: number) => uni.showModal({ title: '删除会话', content: '只删除本次问答记录,不会删除资料库内容。', confirmColor: '#e70014', success: (result) => { if (result.confirm) void remove(id); } });
|
const confirmDelete = (id: string) => uni.showModal({ title: '删除会话', content: '只删除本次问答记录,不会删除资料库内容。', confirmColor: '#e70014', success: (result) => { if (result.confirm) void remove(id); } });
|
||||||
const remove = async (id: number) => { loading.value = true; try { await deletePersonalSession(id); detail.value = undefined; await loadList(); } catch (error) { message.value = error instanceof Error ? error.message : '删除会话失败'; } finally { loading.value = false; } };
|
const remove = async (id: string) => { loading.value = true; try { await deletePersonalSession(id); detail.value = undefined; await loadList(); } catch (error) { message.value = error instanceof Error ? error.message : '删除会话失败'; } finally { loading.value = false; } };
|
||||||
const scopeText = (scope: string) => scope.includes('ENTERPRISE') ? '我的资料 + 企业知识' : '我的资料';
|
const scopeText = (scope: string) => scope.includes('ENTERPRISE') ? '我的资料 + 企业知识' : '我的资料';
|
||||||
const citationLabel = (citation: PersonalCitation) => citation.domain === 'ENTERPRISE'
|
const citationLabel = (citation: PersonalCitation) => citation.domain === 'ENTERPRISE'
|
||||||
? '企业 SOP'
|
? '企业 SOP'
|
||||||
: citation.sourceType === 'URL' ? '外部网页' : '我的资料';
|
: citation.sourceType === 'URL' ? '外部网页' : '我的资料';
|
||||||
const openCitation = (citation: PersonalCitation) => {
|
const openCitation = (citation: PersonalCitation) => {
|
||||||
if (citation.itemId) uni.navigateTo({ url: `/pages/user/assistant/item?id=${citation.itemId}` });
|
if (citation.itemId) uni.navigateTo({ url: personalItemRoute(citation.itemId) });
|
||||||
};
|
};
|
||||||
const goBack = () => uni.navigateBack();
|
const goBack = () => uni.navigateBack();
|
||||||
onShow(loadList);
|
onShow(loadList);
|
||||||
|
|||||||
@@ -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))}`;
|
||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
PersonalSpace
|
PersonalSpace
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api';
|
import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api';
|
||||||
|
import { personalItemApiPath, personalSessionApiPath } from './personal-assistant-id';
|
||||||
|
|
||||||
export type KnowledgeScope = PersonalKnowledgeScope;
|
export type KnowledgeScope = PersonalKnowledgeScope;
|
||||||
|
|
||||||
@@ -31,12 +32,12 @@ const queryString = (query: PersonalItemQuery) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const askPersonalAssistant = (payload: {
|
export const askPersonalAssistant = (payload: {
|
||||||
sessionId?: number;
|
sessionId?: string;
|
||||||
queryText: string;
|
queryText: string;
|
||||||
scope: KnowledgeScope[];
|
scope: KnowledgeScope[];
|
||||||
dateFrom?: string;
|
dateFrom?: string;
|
||||||
dateTo?: string;
|
dateTo?: string;
|
||||||
itemIds?: number[];
|
itemIds?: string[];
|
||||||
outputFormat?: 'ANSWER' | 'ACTION_PLAN' | 'OUTLINE';
|
outputFormat?: 'ANSWER' | 'ACTION_PLAN' | 'OUTLINE';
|
||||||
}) => apiRequest<PersonalAskResponse>({
|
}) => apiRequest<PersonalAskResponse>({
|
||||||
url: '/api/aihr/personal-assistant/ask',
|
url: '/api/aihr/personal-assistant/ask',
|
||||||
@@ -46,7 +47,7 @@ export const askPersonalAssistant = (payload: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const mergePersonalRows = (current: PersonalItem[], incoming: PersonalItem[]) => {
|
export const mergePersonalRows = (current: PersonalItem[], incoming: PersonalItem[]) => {
|
||||||
const rows = new Map<number, PersonalItem>();
|
const rows = new Map<string, PersonalItem>();
|
||||||
[...current, ...incoming].forEach((item) => rows.set(item.id, item));
|
[...current, ...incoming].forEach((item) => rows.set(item.id, item));
|
||||||
return Array.from(rows.values());
|
return Array.from(rows.values());
|
||||||
};
|
};
|
||||||
@@ -61,8 +62,8 @@ export const loadPersonalItems = (query: PersonalItemQuery = {}) => apiRequest<P
|
|||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loadPersonalItem = (id: number) => apiRequest<PersonalItem>({
|
export const loadPersonalItem = (id: string) => apiRequest<PersonalItem>({
|
||||||
url: `/api/aihr/personal-assistant/items/${id}`,
|
url: personalItemApiPath(id),
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,19 +129,19 @@ export const uploadPersonalFile = (file: PersonalSelectedFile, title?: string, c
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const retryPersonalItem = (id: number) => apiRequest<PersonalItem>({
|
export const retryPersonalItem = (id: string) => apiRequest<PersonalItem>({
|
||||||
url: `/api/aihr/personal-assistant/items/${id}/retry`,
|
url: personalItemApiPath(id, '/retry'),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deletePersonalItem = (id: number) => apiRequest<{ cleanupJobId: number }>({
|
export const deletePersonalItem = (id: string) => apiRequest<{ cleanupJobId: string }>({
|
||||||
url: `/api/aihr/personal-assistant/items/${id}`,
|
url: personalItemApiPath(id),
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloadPersonalItem = (id: number) => apiRequest<{ url: string; expiresAt?: string }>({
|
export const downloadPersonalItem = (id: string) => apiRequest<{ url: string; expiresAt?: string }>({
|
||||||
url: `/api/aihr/personal-assistant/items/${id}/download-url`,
|
url: personalItemApiPath(id, '/download-url'),
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,12 +150,12 @@ export const loadPersonalSessions = () => apiRequest<PersonalSession[]>({
|
|||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loadPersonalSession = (id: number) => apiRequest<PersonalSessionDetail>({
|
export const loadPersonalSession = (id: string) => apiRequest<PersonalSessionDetail>({
|
||||||
url: `/api/aihr/personal-assistant/sessions/${id}`,
|
url: personalSessionApiPath(id),
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deletePersonalSession = (id: number) => apiRequest<void>({
|
export const deletePersonalSession = (id: string) => apiRequest<void>({
|
||||||
url: `/api/aihr/personal-assistant/sessions/${id}`,
|
url: personalSessionApiPath(id),
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|||||||
+10
-10
@@ -123,8 +123,8 @@ export type PersonalItemStatus = 'QUEUED' | 'PARSING' | 'READY' | 'FAILED' | 'DE
|
|||||||
export interface PersonalCitation {
|
export interface PersonalCitation {
|
||||||
domain: PersonalKnowledgeScope;
|
domain: PersonalKnowledgeScope;
|
||||||
sourceId: string;
|
sourceId: string;
|
||||||
itemId?: number;
|
itemId?: string;
|
||||||
fragmentId?: number;
|
fragmentId?: string;
|
||||||
sourceType?: PersonalItemSourceType | 'ENTERPRISE';
|
sourceType?: PersonalItemSourceType | 'ENTERPRISE';
|
||||||
title: string;
|
title: string;
|
||||||
excerpt: string;
|
excerpt: string;
|
||||||
@@ -132,7 +132,7 @@ export interface PersonalCitation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonalAskResponse {
|
export interface PersonalAskResponse {
|
||||||
sessionId: number;
|
sessionId: string;
|
||||||
answer: string;
|
answer: string;
|
||||||
citations: PersonalCitation[];
|
citations: PersonalCitation[];
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -140,7 +140,7 @@ export interface PersonalAskResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonalSpace {
|
export interface PersonalSpace {
|
||||||
spaceId: number;
|
spaceId: string;
|
||||||
status: string;
|
status: string;
|
||||||
quotaBytes: number;
|
quotaBytes: number;
|
||||||
usedBytes: number;
|
usedBytes: number;
|
||||||
@@ -148,7 +148,7 @@ export interface PersonalSpace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonalItem {
|
export interface PersonalItem {
|
||||||
id: number;
|
id: string;
|
||||||
sourceType: PersonalItemSourceType;
|
sourceType: PersonalItemSourceType;
|
||||||
title: string;
|
title: string;
|
||||||
originalUrl?: string;
|
originalUrl?: string;
|
||||||
@@ -171,20 +171,20 @@ export interface PersonalPage<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonalItemCreated {
|
export interface PersonalItemCreated {
|
||||||
itemId: number;
|
itemId: string;
|
||||||
status: PersonalItemStatus;
|
status: PersonalItemStatus;
|
||||||
duplicateOf?: number;
|
duplicateOf?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonalSession {
|
export interface PersonalSession {
|
||||||
id: number;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
defaultScope: string;
|
defaultScope: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonalChatMessage {
|
export interface PersonalChatMessage {
|
||||||
id: number;
|
id: string;
|
||||||
role: string;
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
citations: PersonalCitation[];
|
citations: PersonalCitation[];
|
||||||
@@ -192,7 +192,7 @@ export interface PersonalChatMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonalSessionDetail {
|
export interface PersonalSessionDetail {
|
||||||
id: number;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
messages: PersonalChatMessage[];
|
messages: PersonalChatMessage[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import { readFile } from 'node:fs/promises';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
const source = (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
const source = (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
||||||
|
const idHelpers = await import('../src/services/personal-assistant-id.ts');
|
||||||
|
|
||||||
|
test('Snowflake ID 全程保留字符串精度并安全生成详情路径', () => {
|
||||||
|
const hugeId = '1990000000000000000';
|
||||||
|
assert.equal(idHelpers.normalizeBackendId(hugeId), hugeId);
|
||||||
|
assert.equal(idHelpers.personalItemRoute(hugeId), `/pages/user/assistant/item?id=${hugeId}`);
|
||||||
|
assert.equal(idHelpers.personalItemApiPath(hugeId), `/api/aihr/personal-assistant/items/${hugeId}`);
|
||||||
|
assert.equal(idHelpers.personalSessionApiPath(hugeId), `/api/aihr/personal-assistant/sessions/${hugeId}`);
|
||||||
|
});
|
||||||
|
|
||||||
test('个人助理 API 固定使用登录态且不接收 owner 参数', async () => {
|
test('个人助理 API 固定使用登录态且不接收 owner 参数', async () => {
|
||||||
const [client, api] = await Promise.all([
|
const [client, api] = await Promise.all([
|
||||||
@@ -13,6 +22,10 @@ test('个人助理 API 固定使用登录态且不接收 owner 参数', async ()
|
|||||||
assert.match(client, /\/api\/aihr\/personal-assistant\/ask/);
|
assert.match(client, /\/api\/aihr\/personal-assistant\/ask/);
|
||||||
assert.match(client, /\/api\/aihr\/personal-assistant\/items\/file/);
|
assert.match(client, /\/api\/aihr\/personal-assistant\/items\/file/);
|
||||||
assert.match(client, /uni\.uploadFile/);
|
assert.match(client, /uni\.uploadFile/);
|
||||||
|
assert.match(client, /sessionId\?: string/);
|
||||||
|
assert.match(client, /itemIds\?: string\[\]/);
|
||||||
|
assert.match(client, /loadPersonalItem = \(id: string\)/);
|
||||||
|
assert.match(client, /loadPersonalSession = \(id: string\)/);
|
||||||
assert.doesNotMatch(client, /ownerUserId|tenantId/);
|
assert.doesNotMatch(client, /ownerUserId|tenantId/);
|
||||||
assert.doesNotMatch(client, /clearAuthOnUnauthorized:\s*false/);
|
assert.doesNotMatch(client, /clearAuthOnUnauthorized:\s*false/);
|
||||||
assert.doesNotMatch(api, /statusCode === 401 \|\| statusCode === 403/);
|
assert.doesNotMatch(api, /statusCode === 401 \|\| statusCode === 403/);
|
||||||
@@ -32,6 +45,10 @@ test('问答首页默认只查询个人资料并展示真实引用域', async ()
|
|||||||
assert.doesNotMatch(page, /loadPersonalItem/);
|
assert.doesNotMatch(page, /loadPersonalItem/);
|
||||||
assert.match(page, /answer\.value = undefined/);
|
assert.match(page, /answer\.value = undefined/);
|
||||||
assert.match(page, /requestGeneration/);
|
assert.match(page, /requestGeneration/);
|
||||||
|
assert.match(page, /:disabled="asking"/);
|
||||||
|
assert.match(page, /if \(asking\.value\) return/);
|
||||||
|
assert.match(page, /scopeSnapshot/);
|
||||||
|
assert.match(page, /scopeKey\(scopes\.value\)/);
|
||||||
assert.match(page, /收藏资料/);
|
assert.match(page, /收藏资料/);
|
||||||
assert.doesNotMatch(page, /ownerUserId|tenantId/);
|
assert.doesNotMatch(page, /ownerUserId|tenantId/);
|
||||||
});
|
});
|
||||||
@@ -69,6 +86,8 @@ test('个人资料详情包含全部状态、重试、下载和明确删除提
|
|||||||
assert.match(page, /deletePersonalItem/);
|
assert.match(page, /deletePersonalItem/);
|
||||||
assert.match(page, /downloadPersonalItem/);
|
assert.match(page, /downloadPersonalItem/);
|
||||||
assert.match(page, /附件、解析正文和搜索索引/);
|
assert.match(page, /附件、解析正文和搜索索引/);
|
||||||
|
assert.match(page, /normalizeBackendId\(String\(options\?\.id/);
|
||||||
|
assert.doesNotMatch(page, /Number\(options\?\.id|parseInt\(options\?\.id|Number\.isSafeInteger/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('会话页覆盖列表、详情和删除且不伪造会话', async () => {
|
test('会话页覆盖列表、详情和删除且不伪造会话', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user