feat(agent): move mobile ask to unified agent
This commit is contained in:
+1
-1
@@ -38,7 +38,7 @@ public class AihrAgentActionService {
|
||||
index.set(draftId, DRAFT_TTL);
|
||||
}
|
||||
redissonClient.<Long>getBucket(PREFIX + "draft:" + draftId).set(candidate.id(), DRAFT_TTL);
|
||||
return new ActionDraft(draftId, candidate.targetDomain(), candidate.version(), candidate.draft());
|
||||
return new ActionDraft(draftId, candidate.targetDomain(), candidate.version(), candidate);
|
||||
}
|
||||
|
||||
public ConfirmMemoryResponse confirm(String draftId, ConfirmMemoryRequest request) {
|
||||
|
||||
+9
-3
@@ -134,13 +134,19 @@ public class AihrAgentOrchestrator {
|
||||
: AgentStatus.NEEDS_INPUT;
|
||||
sourceType = "MEMORY_DRAFT";
|
||||
}
|
||||
String sourceLabel = response.citations().isEmpty() ? sourceType
|
||||
: response.citations().get(0).title();
|
||||
List<SourceSummary> sources = response.citations().isEmpty()
|
||||
&& "ENTERPRISE_KNOWLEDGE".equals(sourceType)
|
||||
? List.of()
|
||||
: List.of(new SourceSummary(
|
||||
sourceType,
|
||||
response.citations().isEmpty() ? sourceType : response.citations().get(0).title(),
|
||||
OffsetDateTime.now().toString()
|
||||
));
|
||||
String answer = actionDraft == null ? response.answer()
|
||||
: "已整理为待确认记录,请核对后保存。";
|
||||
return new AgentResponse(
|
||||
runId(), response.conversationId(), response.contextVersion(), plan.intent(), status, answer,
|
||||
List.of(new SourceSummary(sourceType, sourceLabel, OffsetDateTime.now().toString())),
|
||||
sources,
|
||||
response.citations(), response.resources(), response.data(), actionDraft, null, List.of()
|
||||
);
|
||||
}
|
||||
|
||||
+8
@@ -52,6 +52,14 @@ public class AihrAgentPlanner {
|
||||
return plan(Intent.RESOURCE_DELIVERY, request, Tool.KNOWLEDGE_RESOURCE, false, false,
|
||||
ResponseStyle.GROUNDED);
|
||||
}
|
||||
if (containsAny(request, "团队训练概况", "团队训练情况", "团队完训情况")) {
|
||||
return plan(Intent.LIVE_TEAM_WORK, request, Tool.TEAM_PRACTICE_SUMMARY, false, false,
|
||||
ResponseStyle.FACT);
|
||||
}
|
||||
if (containsAny(request, "我的训练概况", "我的训练情况", "我的练习概况", "我练得怎么样")) {
|
||||
return plan(Intent.PRACTICE_COACHING, request, Tool.MY_PRACTICE_SUMMARY, false, false,
|
||||
ResponseStyle.FACT);
|
||||
}
|
||||
if (containsAny(request, "我今天有什么待办", "我有什么待办", "我的待办", "我的任务",
|
||||
"我今天该做什么", "我现在该做什么", "今天要做什么", "当前待办")) {
|
||||
return plan(Intent.LIVE_MY_WORK, request, Tool.MY_CURRENT_TASKS, false, false, ResponseStyle.FACT);
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ class AihrAgentActionServiceTest {
|
||||
|
||||
var result = service.confirm(draft.draftId(), request);
|
||||
|
||||
assertEquals(candidate, draft.payload());
|
||||
assertEquals("ASSISTANT_CAPTURE", result.targetDomain());
|
||||
assertEquals(88L, result.targetId());
|
||||
verify(memory).confirm(7L, request);
|
||||
|
||||
+11
@@ -21,6 +21,7 @@ import static org.dromara.aihr.agent.AihrAgentDto.Intent.KNOWLEDGE_QA;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_MY_WORK;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Tag("dev")
|
||||
class AihrAgentOrchestratorTest {
|
||||
@@ -52,6 +53,16 @@ class AihrAgentOrchestratorTest {
|
||||
assertEquals(AgentStatus.COMPLETED, result.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void knowledgeNoEvidenceDoesNotInventASource() {
|
||||
var query = new CapturingQueryService(response("当前授权资料中没有明确依据。", null, true));
|
||||
|
||||
var result = orchestrator(query).handle(request("装修人员怎么进场?"));
|
||||
|
||||
assertEquals(AgentStatus.NO_EVIDENCE, result.status());
|
||||
assertTrue(result.sourceSummary().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void socialAndClarificationDoNotInvokeKnowledge() {
|
||||
var query = new CapturingQueryService(response("unused", null, false));
|
||||
|
||||
+20
@@ -11,7 +11,9 @@ import static org.dromara.aihr.agent.AihrAgentDto.Intent.CAPTURE_FACT;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.CLARIFY;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.KNOWLEDGE_QA;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_MY_WORK;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_TEAM_WORK;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.MEDIA_UNDERSTANDING;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.PRACTICE_COACHING;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.RESOURCE_DELIVERY;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.SOCIAL;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.CAPTURE_MEMORY_DRAFT;
|
||||
@@ -19,7 +21,9 @@ import static org.dromara.aihr.agent.AihrAgentDto.Tool.KNOWLEDGE_RESOURCE;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.KNOWLEDGE_SEARCH;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MEDIA_ANALYZE;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MY_CURRENT_TASKS;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MY_PRACTICE_SUMMARY;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.NONE;
|
||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.TEAM_PRACTICE_SUMMARY;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -38,6 +42,22 @@ class AihrAgentPlannerTest {
|
||||
assertFalse(plan.requiresConfirmation());
|
||||
}
|
||||
|
||||
@Test
|
||||
void routesOwnTrainingSummaryToLivePracticeData() {
|
||||
var plan = planner.plan("查询我的训练概况", false);
|
||||
|
||||
assertEquals(PRACTICE_COACHING, plan.intent());
|
||||
assertEquals(MY_PRACTICE_SUMMARY, plan.tool());
|
||||
}
|
||||
|
||||
@Test
|
||||
void routesTeamTrainingSummaryToSupervisorData() {
|
||||
var plan = planner.plan("查询我的团队训练概况", false);
|
||||
|
||||
assertEquals(LIVE_TEAM_WORK, plan.intent());
|
||||
assertEquals(TEAM_PRACTICE_SUMMARY, plan.tool());
|
||||
}
|
||||
|
||||
@Test
|
||||
void routesExplicitCaptureToConfirmationDraft() {
|
||||
var plan = planner.plan("记一下,3栋1201要回访", false);
|
||||
|
||||
@@ -278,7 +278,7 @@ const confirm = async (saveScope: '' | 'PRIVATE' | 'COMPANY' = '') => {
|
||||
errorText.value = '';
|
||||
const requestAccountKey = currentAccountKey();
|
||||
try {
|
||||
const result = await confirmMemoryCandidate(props.candidate.id, {
|
||||
const result = await confirmMemoryCandidate(props.candidate.agentDraftId || props.candidate.id, {
|
||||
expectedVersion: props.candidate.version,
|
||||
idempotencyKey: idempotencyKey.value,
|
||||
draft: {
|
||||
@@ -306,7 +306,7 @@ const dismiss = async () => {
|
||||
errorText.value = '';
|
||||
const requestAccountKey = currentAccountKey();
|
||||
try {
|
||||
await dismissMemoryCandidate(props.candidate.id);
|
||||
await dismissMemoryCandidate(props.candidate.agentDraftId || props.candidate.id);
|
||||
if (requestAccountKey !== currentAccountKey()) return;
|
||||
emit('dismissed', props.candidate.id);
|
||||
} catch (error) {
|
||||
|
||||
@@ -121,7 +121,26 @@
|
||||
v-if="msg.result.rewrittenQuery && msg.result.rewrittenQuery !== msg.result.queryText"
|
||||
class="context-used"
|
||||
>已结合本次对话理解</text>
|
||||
<view v-if="msg.result.sourceSummary?.length" class="agent-source-row">
|
||||
<text
|
||||
v-for="source in msg.result.sourceSummary"
|
||||
:key="`${source.type}-${source.label}`"
|
||||
class="agent-source-chip"
|
||||
>{{ source.label }}</text>
|
||||
</view>
|
||||
<text class="bubble-text">{{ msg.result.answer || '未生成直接答案,请查看引用依据。' }}</text>
|
||||
<view
|
||||
v-if="msg.result.agentStatus === 'NEEDS_INPUT' && msg.result.clarification"
|
||||
class="agent-clarification"
|
||||
>
|
||||
<text class="agent-clarification-text">{{ msg.result.clarification.prompt }}</text>
|
||||
<button
|
||||
v-if="msg.result.agentIntent === 'WEB_RESEARCH'"
|
||||
class="bubble-action primary"
|
||||
:disabled="loading"
|
||||
@click="consentWebResearch(msg)"
|
||||
>同意查询全网</button>
|
||||
</view>
|
||||
<view v-if="msg.result.keyPoints?.length" class="bubble-keypoints">
|
||||
<text v-for="(point, index) in msg.result.keyPoints" :key="point" class="bubble-keypoint">
|
||||
{{ index + 1 }}. {{ cleanOrderedText(point) }}
|
||||
@@ -133,7 +152,7 @@
|
||||
<text>{{ item }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="msg.result.noEvidence" class="bubble-noevidence">当前授权空间内没有足够依据。</text>
|
||||
<text v-if="msg.result.noEvidence" class="bubble-noevidence">{{ noEvidenceText(msg.result) }}</text>
|
||||
|
||||
<view class="bubble-actions">
|
||||
<button v-if="msg.voiceStatus === 'unavailable'" class="bubble-action" @click="toggleAnswerSpeech(msg)">
|
||||
@@ -142,12 +161,12 @@
|
||||
<button v-if="msg.result.snippets?.length" class="bubble-action" @click="msg.citationsOpen = !msg.citationsOpen">
|
||||
引用 {{ msg.result.snippets.length }} 条 {{ msg.citationsOpen ? '▲' : '▼' }}
|
||||
</button>
|
||||
<button v-if="msg.toolCode === '' && !broadcastContext" class="bubble-action" :disabled="summaryLoading" @click="buildSummaryCard(msg)">
|
||||
<button v-if="msg.result.agentIntent === 'KNOWLEDGE_QA' && !broadcastContext" class="bubble-action" :disabled="summaryLoading" @click="buildSummaryCard(msg)">
|
||||
{{ summaryLoading ? '生成中' : '总结卡' }}
|
||||
</button>
|
||||
<button v-if="msg.result.noEvidence && !broadcastContext" class="bubble-action primary" @click="openWebAi">去查全网</button>
|
||||
<button v-if="msg.result.noEvidence && msg.result.agentIntent === 'KNOWLEDGE_QA' && !broadcastContext" class="bubble-action primary" @click="openWebAi">去查全网</button>
|
||||
</view>
|
||||
<view v-if="msg.toolCode === ''" class="bubble-actions feedback-row">
|
||||
<view v-if="msg.result.snippets?.length" class="bubble-actions feedback-row">
|
||||
<button
|
||||
class="bubble-action"
|
||||
:class="{ active: msg.feedbackVerdict === 'up' }"
|
||||
@@ -422,12 +441,10 @@ import MemoryCandidateCard from '@/components/chat/MemoryCandidateCard.vue';
|
||||
import {
|
||||
chooseKnowledgeMedia,
|
||||
generateSummaryCard,
|
||||
queryKnowledgeTool,
|
||||
requestKnowledgeResourceDownloadLink,
|
||||
searchKnowledge,
|
||||
searchKnowledgeWithMedia,
|
||||
submitAnswerFeedback
|
||||
} from '@/services/knowledge';
|
||||
import { askAgent, askAgentWithMedia } from '@/services/agent';
|
||||
import {
|
||||
getSelectedPosition,
|
||||
getSelectedPositionProfile,
|
||||
@@ -909,7 +926,10 @@ const requestErrorText = (error: unknown, fallback: string) => {
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
};
|
||||
|
||||
const sendQuestion = async (rawText: string, options: { voice?: boolean } = {}) => {
|
||||
const sendQuestion = async (
|
||||
rawText: string,
|
||||
options: { voice?: boolean; externalConsent?: boolean } = {}
|
||||
) => {
|
||||
if (loading.value) {
|
||||
uni.showToast({ title: '上一条还在检索中,请稍候', icon: 'none' });
|
||||
return;
|
||||
@@ -921,7 +941,7 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
|
||||
uni.showToast({ title: '消息咨询暂不支持图片或视频,请改用文字或语音', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const text = rawText.trim() || (media ? '请根据附件现场情况,告诉我应该按什么 SOP 处理' : '');
|
||||
const text = rawText.trim() || (media ? '请分析附件内容并回答我的问题' : '');
|
||||
if (!text) {
|
||||
uni.showToast({ title: '请先输入问题', icon: 'none' });
|
||||
return;
|
||||
@@ -955,18 +975,17 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
|
||||
loading.value = true;
|
||||
const generation = ++knowledgeSearchGeneration;
|
||||
try {
|
||||
const agentOptions = {
|
||||
conversation: conversationState(),
|
||||
projectCode: broadcastMessageId ? undefined : currentProject.value?.projectCode,
|
||||
broadcastMessageId,
|
||||
externalConsent: Boolean(options.externalConsent)
|
||||
};
|
||||
const nextResult = media
|
||||
? await searchKnowledgeWithMedia(media, text, getSelectedPosition(), (task) => {
|
||||
? await askAgentWithMedia(media, text, agentOptions, (task) => {
|
||||
if (generation === knowledgeSearchGeneration) activeMediaUploadTask = task;
|
||||
}, conversationState(), currentProject.value?.projectCode)
|
||||
: await searchKnowledge(
|
||||
text,
|
||||
getSelectedPosition(),
|
||||
conversationState(),
|
||||
broadcastMessageId ? undefined : currentProject.value?.projectCode,
|
||||
options.voice ? 'mobile_uni_voice' : 'mobile_uni_sop',
|
||||
broadcastMessageId
|
||||
);
|
||||
})
|
||||
: await askAgent(text, agentOptions);
|
||||
if (generation !== knowledgeSearchGeneration) return;
|
||||
applyConversationState(nextResult);
|
||||
if (broadcastMessageId && nextResult.broadcastContext?.messageId === broadcastMessageId) {
|
||||
@@ -1038,38 +1057,7 @@ const runDataTool = async (toolCode: 'MY_PRACTICE_SUMMARY' | 'TEAM_PRACTICE_SUMM
|
||||
: toolCode;
|
||||
const question = effectiveToolCode === 'TEAM_PRACTICE_SUMMARY' ? '查询我的团队训练概况' : '查询我的训练概况';
|
||||
selectedMedia.value = null;
|
||||
pushMsg<UserMsg>({ role: 'user', text: question });
|
||||
const pending = pushMsg<PendingMsg>({ role: 'pending' });
|
||||
loading.value = true;
|
||||
const generation = ++knowledgeSearchGeneration;
|
||||
try {
|
||||
const nextResult = await queryKnowledgeTool(
|
||||
effectiveToolCode, question, conversationState(), currentProject.value?.projectCode);
|
||||
if (generation !== knowledgeSearchGeneration) return;
|
||||
applyConversationState(nextResult);
|
||||
replaceMsg(pending.id, {
|
||||
id: pending.id,
|
||||
role: 'master',
|
||||
result: nextResult,
|
||||
toolCode: effectiveToolCode,
|
||||
feedbackVerdict: '',
|
||||
feedbackSubmitting: false,
|
||||
feedbackPanelOpen: false,
|
||||
feedbackReasonCodes: [],
|
||||
feedbackComment: '',
|
||||
citationsOpen: false,
|
||||
voiceStatus: 'unavailable',
|
||||
voiceDuration: null,
|
||||
unread: false,
|
||||
transcriptOpen: false
|
||||
});
|
||||
} catch (error) {
|
||||
if (generation !== knowledgeSearchGeneration) return;
|
||||
replaceMsg(pending.id, null);
|
||||
pushSystem(requestErrorText(error, '训练概况查询失败'));
|
||||
} finally {
|
||||
if (generation === knowledgeSearchGeneration) loading.value = false;
|
||||
}
|
||||
void sendQuestion(question);
|
||||
};
|
||||
|
||||
const pickMedia = async () => {
|
||||
@@ -1271,6 +1259,12 @@ const serviceMemoryStatusLabel = (status?: string | null) => ({
|
||||
|
||||
const displayCitationDate = (value?: string | null) => value ? value.replace('T', ' ').slice(0, 16) : '未记录';
|
||||
|
||||
const noEvidenceText = (result: KnowledgeSearchResponse) => {
|
||||
if (result.agentIntent === 'WEB_RESEARCH') return '公开网络中没有找到可核验来源。';
|
||||
if (result.agentIntent === 'MEDIA_UNDERSTANDING') return '已分析附件,但当前资料中没有补充依据。';
|
||||
return '当前授权空间内没有足够依据。';
|
||||
};
|
||||
|
||||
const feedbackReasonOptions = [
|
||||
{ code: 'NO_ANSWER', label: '缺少制度或SOP依据' },
|
||||
{ code: 'NOT_SPECIFIC', label: '处理步骤不够具体' },
|
||||
@@ -1419,6 +1413,11 @@ const openWebAi = () => {
|
||||
uni.navigateTo({ url: '/pages/user/web-ai/index' });
|
||||
};
|
||||
|
||||
const consentWebResearch = (msg: MasterMsg) => {
|
||||
if (loading.value || msg.result.agentIntent !== 'WEB_RESEARCH') return;
|
||||
void sendQuestion(msg.result.queryText, { externalConsent: true });
|
||||
};
|
||||
|
||||
const openTodayResult = () => {
|
||||
uni.navigateTo({ url: '/pages/user/work-results/index' });
|
||||
};
|
||||
@@ -1437,7 +1436,10 @@ const refreshMemoryCandidate = async (msg: MasterMsg, candidateId: number) => {
|
||||
try {
|
||||
const candidates = await getMemoryCandidates('DRAFT');
|
||||
if (generation !== candidateRefreshGeneration || accountKey !== currentAccountKey()) return;
|
||||
msg.result.memoryCandidate = candidates.find((item) => item.id === candidateId) || null;
|
||||
const refreshed = candidates.find((item) => item.id === candidateId);
|
||||
msg.result.memoryCandidate = refreshed
|
||||
? { ...refreshed, agentDraftId: msg.result.memoryCandidate?.agentDraftId }
|
||||
: null;
|
||||
} catch (error) {
|
||||
if (generation !== candidateRefreshGeneration || accountKey !== currentAccountKey()) return;
|
||||
uni.showToast({ title: requestErrorText(error, '确认卡刷新失败'), icon: 'none' });
|
||||
@@ -2977,6 +2979,36 @@ onUnload(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-source-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.agent-source-chip {
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #eef3f8;
|
||||
color: #5b6b7f;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.agent-clarification {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed #eceff2;
|
||||
}
|
||||
|
||||
.agent-clarification-text {
|
||||
color: #38424e;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bubble-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
AgentResponse,
|
||||
KnowledgeConversationState,
|
||||
KnowledgeMediaSelectedFile,
|
||||
KnowledgeSearchResponse,
|
||||
MemoryCandidate
|
||||
} from '@/types/api';
|
||||
import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api';
|
||||
|
||||
interface AgentMessageOptions {
|
||||
conversation?: KnowledgeConversationState;
|
||||
projectCode?: string;
|
||||
broadcastMessageId?: number;
|
||||
externalConsent?: boolean;
|
||||
}
|
||||
|
||||
export const askAgent = async (question: string, options: AgentMessageOptions = {}) =>
|
||||
normalizeAgentResponse(await apiRequest<AgentResponse>({
|
||||
url: '/api/aihr/agent/messages',
|
||||
method: 'POST',
|
||||
data: {
|
||||
question,
|
||||
conversationId: options.conversation?.conversationId,
|
||||
contextVersion: options.conversation?.contextVersion,
|
||||
projectCode: options.projectCode,
|
||||
broadcastMessageId: options.broadcastMessageId,
|
||||
externalConsent: Boolean(options.externalConsent)
|
||||
},
|
||||
timeout: 45000
|
||||
}), question);
|
||||
|
||||
export const askAgentWithMedia = (
|
||||
file: KnowledgeMediaSelectedFile,
|
||||
question: string,
|
||||
options: AgentMessageOptions = {},
|
||||
onUploadTask?: (task: ReturnType<typeof uni.uploadFile> | null) => void
|
||||
) => new Promise<KnowledgeSearchResponse>((resolve, reject) => {
|
||||
const task = uni.uploadFile({
|
||||
url: apiUrl('/api/aihr/agent/messages/media'),
|
||||
filePath: file.path,
|
||||
name: 'file',
|
||||
header: authHeaders(false),
|
||||
formData: {
|
||||
question,
|
||||
...(options.conversation ? {
|
||||
conversationId: options.conversation.conversationId,
|
||||
contextVersion: String(options.conversation.contextVersion)
|
||||
} : {}),
|
||||
...(options.projectCode ? { projectCode: options.projectCode } : {}),
|
||||
...(typeof options.broadcastMessageId === 'number'
|
||||
? { broadcastMessageId: String(options.broadcastMessageId) }
|
||||
: {}),
|
||||
externalConsent: String(Boolean(options.externalConsent))
|
||||
},
|
||||
timeout: 360000,
|
||||
success: (result) => {
|
||||
try {
|
||||
resolve(normalizeAgentResponse(
|
||||
readTextPayload<AgentResponse>(result.statusCode, result.data),
|
||||
question
|
||||
));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error(file.kind === 'video' ? '视频上传或分析超时' : '图片上传失败')),
|
||||
complete: () => onUploadTask?.(null)
|
||||
});
|
||||
onUploadTask?.(task);
|
||||
});
|
||||
|
||||
export const normalizeAgentResponse = (
|
||||
response: AgentResponse,
|
||||
question: string
|
||||
): KnowledgeSearchResponse => {
|
||||
const memoryCandidate = response.actionDraft?.payload
|
||||
? {
|
||||
...(response.actionDraft.payload as MemoryCandidate),
|
||||
agentDraftId: response.actionDraft.draftId
|
||||
}
|
||||
: null;
|
||||
const snippets = (response.citations || []).map((citation) => ({
|
||||
docTitle: citation.title,
|
||||
content: citation.snippet,
|
||||
docId: citation.docId,
|
||||
fragmentId: citation.fragmentId,
|
||||
spaceCode: citation.spaceCode,
|
||||
sourceType: citation.sourceType,
|
||||
domain: citation.domain,
|
||||
status: citation.status,
|
||||
occurredAt: citation.occurredAt,
|
||||
updatedAt: citation.updatedAt
|
||||
}));
|
||||
return {
|
||||
requestId: response.runId,
|
||||
queryText: question,
|
||||
category: 'sop',
|
||||
answer: response.answer || response.clarification?.prompt || '',
|
||||
reference: (response.sourceSummary || []).map((source) => source.label).filter(Boolean).join('、'),
|
||||
docs: [],
|
||||
snippets,
|
||||
training: [],
|
||||
records: [],
|
||||
citations: response.citations || [],
|
||||
usedSpaceCodes: [],
|
||||
noEvidence: response.status === 'NO_EVIDENCE',
|
||||
data: response.data,
|
||||
conversationId: response.conversationId,
|
||||
contextVersion: response.contextVersion,
|
||||
intent: response.intent,
|
||||
rewrittenQuery: question,
|
||||
resources: response.resources || [],
|
||||
memoryCandidate,
|
||||
broadcastContext: null,
|
||||
agentStatus: response.status,
|
||||
agentIntent: response.intent,
|
||||
sourceSummary: response.sourceSummary || [],
|
||||
clarification: response.clarification || null
|
||||
};
|
||||
};
|
||||
@@ -44,17 +44,21 @@ export const getMemoryCandidates = async (status = 'DRAFT') =>
|
||||
timeout: 15000
|
||||
}));
|
||||
|
||||
export const confirmMemoryCandidate = (candidateId: number, request: MemoryConfirmRequest) =>
|
||||
export const confirmMemoryCandidate = (candidateRef: number | string, request: MemoryConfirmRequest) =>
|
||||
apiRequest<MemoryConfirmResult>({
|
||||
url: `${candidateBase}/${candidateId}/confirm`,
|
||||
url: typeof candidateRef === 'string'
|
||||
? `/api/aihr/agent/actions/${encodeURIComponent(String(candidateRef))}/confirm`
|
||||
: `${candidateBase}/${candidateRef}/confirm`,
|
||||
method: 'POST',
|
||||
data: request,
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
export const dismissMemoryCandidate = (candidateId: number) =>
|
||||
export const dismissMemoryCandidate = (candidateRef: number | string) =>
|
||||
apiRequest<{ id: number; status: string }>({
|
||||
url: `${candidateBase}/${candidateId}/dismiss`,
|
||||
url: typeof candidateRef === 'string'
|
||||
? `/api/aihr/agent/actions/${encodeURIComponent(String(candidateRef))}/dismiss`
|
||||
: `${candidateBase}/${candidateRef}/dismiss`,
|
||||
method: 'POST',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
@@ -152,6 +152,10 @@ export interface KnowledgeSearchResponse {
|
||||
resources?: KnowledgeResource[];
|
||||
memoryCandidate?: MemoryCandidate | null;
|
||||
broadcastContext?: BroadcastKnowledgeContext | null;
|
||||
agentStatus?: AgentStatus;
|
||||
agentIntent?: AgentIntent;
|
||||
sourceSummary?: AgentSourceSummary[];
|
||||
clarification?: AgentClarification | null;
|
||||
}
|
||||
|
||||
export interface KnowledgeMediaSelectedFile {
|
||||
@@ -214,6 +218,55 @@ export interface UnifiedKnowledgeResponse {
|
||||
broadcastContext?: BroadcastKnowledgeContext | null;
|
||||
}
|
||||
|
||||
export type AgentStatus = 'COMPLETED' | 'NEEDS_INPUT' | 'NEEDS_CONFIRMATION' | 'NO_EVIDENCE' | 'FORBIDDEN' | 'UNAVAILABLE' | 'FAILED';
|
||||
|
||||
export type AgentIntent =
|
||||
| 'KNOWLEDGE_QA'
|
||||
| 'RESOURCE_DELIVERY'
|
||||
| 'LIVE_MY_WORK'
|
||||
| 'LIVE_TEAM_WORK'
|
||||
| 'PRACTICE_COACHING'
|
||||
| 'CAPTURE_FACT'
|
||||
| 'DRAFT_ACTION'
|
||||
| 'MEDIA_UNDERSTANDING'
|
||||
| 'WEB_RESEARCH'
|
||||
| 'CLARIFY'
|
||||
| 'SOCIAL';
|
||||
|
||||
export interface AgentSourceSummary {
|
||||
type: 'ENTERPRISE_KNOWLEDGE' | 'LIVE_DATA' | 'MEMORY_DRAFT' | 'MEDIA' | 'WEB' | string;
|
||||
label: string;
|
||||
verifiedAt: string;
|
||||
}
|
||||
|
||||
export interface AgentActionDraft {
|
||||
draftId: string;
|
||||
type: string;
|
||||
version: number;
|
||||
payload: MemoryCandidate;
|
||||
}
|
||||
|
||||
export interface AgentClarification {
|
||||
prompt: string;
|
||||
fields: string[];
|
||||
}
|
||||
|
||||
export interface AgentResponse {
|
||||
runId: string;
|
||||
conversationId?: string | null;
|
||||
contextVersion?: number | null;
|
||||
intent: AgentIntent;
|
||||
status: AgentStatus;
|
||||
answer: string;
|
||||
sourceSummary: AgentSourceSummary[];
|
||||
citations: KnowledgeCitation[];
|
||||
resources: KnowledgeResource[];
|
||||
data?: unknown;
|
||||
actionDraft?: AgentActionDraft | null;
|
||||
clarification?: AgentClarification | null;
|
||||
nextActions: string[];
|
||||
}
|
||||
|
||||
export type MemoryCandidateStatus = 'DRAFT' | 'NEEDS_INPUT' | 'CONFIRMED' | 'DISMISSED' | 'EXPIRED' | string;
|
||||
|
||||
export type MemoryTargetDomain = 'ASSISTANT_CAPTURE' | 'PERSONAL' | 'COMPANY' | 'PROJECT_SERVICE' | string;
|
||||
@@ -254,6 +307,7 @@ export interface MemoryCandidate {
|
||||
missingFields: string[];
|
||||
expiresAt: string | null;
|
||||
source?: WorkSourceSnapshot | null;
|
||||
agentDraftId?: string;
|
||||
}
|
||||
|
||||
export interface MemoryConfirmRequest {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const source = async (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
||||
|
||||
test('问数字师傅统一使用 Agent 文本与媒体入口', async () => {
|
||||
const [service, page] = await Promise.all([
|
||||
source('../src/services/agent.ts'),
|
||||
source('../src/pages/user/sop/index.vue')
|
||||
]);
|
||||
|
||||
assert.match(service, /\/api\/aihr\/agent\/messages'/);
|
||||
assert.match(service, /\/api\/aihr\/agent\/messages\/media/);
|
||||
assert.doesNotMatch(service, /toolCode/);
|
||||
assert.doesNotMatch(service, /\/api\/knowledge\/query/);
|
||||
assert.match(page, /askAgent/);
|
||||
assert.match(page, /askAgentWithMedia/);
|
||||
assert.doesNotMatch(page, /await searchKnowledge\(/);
|
||||
assert.doesNotMatch(page, /await queryKnowledgeTool\(/);
|
||||
});
|
||||
|
||||
test('Agent 响应按结构化状态渲染来源、澄清和确认动作', async () => {
|
||||
const [types, service, page] = await Promise.all([
|
||||
source('../src/types/api.ts'),
|
||||
source('../src/services/agent.ts'),
|
||||
source('../src/pages/user/sop/index.vue')
|
||||
]);
|
||||
|
||||
assert.match(types, /export type AgentStatus = 'COMPLETED' \| 'NEEDS_INPUT' \| 'NEEDS_CONFIRMATION'/);
|
||||
assert.match(types, /sourceSummary\?: AgentSourceSummary\[\]/);
|
||||
assert.match(types, /agentDraftId\?: string/);
|
||||
assert.match(service, /status === 'NO_EVIDENCE'/);
|
||||
assert.match(service, /agentDraftId: response\.actionDraft\.draftId/);
|
||||
assert.match(page, /agent-source-chip/);
|
||||
assert.match(page, /msg\.result\.agentStatus === 'NEEDS_INPUT'/);
|
||||
assert.match(page, /consentWebResearch/);
|
||||
assert.doesNotMatch(page, /result\.answer\.(?:includes|match|startsWith)/);
|
||||
});
|
||||
|
||||
test('Agent 确认卡通过不透明 draftId 调用动作端点', async () => {
|
||||
const [assistantService, card] = await Promise.all([
|
||||
source('../src/services/personal-assistant.ts'),
|
||||
source('../src/components/chat/MemoryCandidateCard.vue')
|
||||
]);
|
||||
|
||||
assert.match(assistantService, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/confirm/);
|
||||
assert.match(assistantService, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/dismiss/);
|
||||
assert.match(card, /props\.candidate\.agentDraftId \|\| props\.candidate\.id/);
|
||||
});
|
||||
Reference in New Issue
Block a user