feat(agent): move mobile ask to unified agent

This commit is contained in:
2026-07-24 22:36:14 +08:00
parent 6ba4e06a2f
commit 5b7c5613f5
12 changed files with 368 additions and 62 deletions
+84 -52
View File
@@ -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;