diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentDto.java index 466b1e48..430fad88 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentDto.java @@ -1,5 +1,6 @@ package org.dromara.aihr.agent; +import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.BroadcastContext; import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.Citation; import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.Resource; @@ -95,6 +96,7 @@ public final class AihrAgentDto { List sourceSummary, List citations, List resources, + BroadcastContext broadcastContext, Object data, ActionDraft actionDraft, Clarification clarification, diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java index 186f1e7e..4f5c80fe 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java @@ -147,7 +147,7 @@ public class AihrAgentOrchestrator { return new AgentResponse( runId(), response.conversationId(), response.contextVersion(), plan.intent(), status, answer, sources, - response.citations(), response.resources(), response.data(), actionDraft, null, List.of() + response.citations(), response.resources(), response.broadcastContext(), response.data(), actionDraft, null, List.of() ); } @@ -165,7 +165,7 @@ public class AihrAgentOrchestrator { return new AgentResponse( runId(), request.conversationId(), request.contextVersion(), plan.intent(), status, answer, List.of(new SourceSummary("WEB", response.provider(), OffsetDateTime.now().toString())), - List.of(), List.of(), response, null, + List.of(), List.of(), null, response, null, status == AgentStatus.NEEDS_INPUT ? new Clarification("是否同意查询全网?", List.of()) : null, @@ -199,7 +199,7 @@ public class AihrAgentOrchestrator { Clarification clarification) { return new AgentResponse( runId(), null, null, plan.intent(), status, answer, List.of(), List.of(), List.of(), - null, null, clarification, List.of() + null, null, null, clarification, List.of() ); } @@ -208,7 +208,7 @@ public class AihrAgentOrchestrator { return new AgentResponse( runId(), request == null ? null : request.conversationId(), request == null ? null : request.contextVersion(), plan.intent(), status, "", - List.of(), List.of(), List.of(), null, null, null, List.of() + List.of(), List.of(), List.of(), null, null, null, null, List.of() ); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentAuditServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentAuditServiceTest.java index c8a0f0f4..63ec7a4b 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentAuditServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentAuditServiceTest.java @@ -58,7 +58,7 @@ class AihrAgentAuditServiceTest { return new AgentResponse("agent_run_test", "conversation_1", 1L, Intent.LIVE_MY_WORK, AgentStatus.COMPLETED, "敏感答案", List.of(new SourceSummary("LIVE_DATA", "我的待办", "2026-07-24T20:00:00+08:00")), - List.of(), List.of(), null, null, null, List.of()); + List.of(), List.of(), null, null, null, null, List.of()); } private static final class CapturingJdbcTemplate extends JdbcTemplate { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java index 28761963..9d8f1ec7 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.dromara.aihr.agent.AihrAgentDto.AgentRequest; import org.dromara.aihr.agent.AihrAgentDto.AgentStatus; import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal; +import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.BroadcastContext; import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryRequest; import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryResponse; import org.dromara.aihr.knowledge.service.AihrKnowledgePrincipalResolver; @@ -64,6 +65,18 @@ class AihrAgentOrchestratorTest { assertTrue(result.sourceSummary().isEmpty()); } + @Test + void broadcastConsultationPreservesServerVerifiedContext() { + var query = new CapturingQueryService(response("按高温作业规定执行。", null, false, + new BroadcastContext(42L, "高温作业提醒", "2026-07-24T09:00:00"))); + + var result = orchestrator(query).handle(new AgentRequest( + "这条消息里的高温规定是什么?", "conversation_1", 0L, null, 42L, false)); + + assertEquals(42L, query.lastRequest.broadcastMessageId()); + assertEquals(42L, result.broadcastContext().messageId()); + } + @Test void socialAndClarificationDoNotInvokeKnowledge() { var query = new CapturingQueryService(response("unused", null, false)); @@ -147,8 +160,14 @@ class AihrAgentOrchestratorTest { } private static QueryResponse response(String answer, Object data, boolean noEvidence) { + return response(answer, data, noEvidence, null); + } + + private static QueryResponse response(String answer, Object data, boolean noEvidence, + BroadcastContext broadcastContext) { return new QueryResponse("request-1", "question", answer, List.of(), List.of(), noEvidence, - "test", null, data, "conversation_1", 1L, "QA", "question", List.of()); + "test", null, data, "conversation_1", 1L, "QA", "question", List.of(), null, + broadcastContext); } private static final class CapturingQueryService extends AihrKnowledgeQueryService { diff --git a/mobile-uni/src/pages/user/sop/index.vue b/mobile-uni/src/pages/user/sop/index.vue index 301973a8..90310bb9 100644 --- a/mobile-uni/src/pages/user/sop/index.vue +++ b/mobile-uni/src/pages/user/sop/index.vue @@ -2991,7 +2991,7 @@ onUnload(() => { border-radius: 999px; background: #eef3f8; color: #5b6b7f; - font-size: 11px; + font-size: 12px; } .agent-clarification { diff --git a/mobile-uni/src/services/agent.ts b/mobile-uni/src/services/agent.ts index cc4c6810..3e4c6aff 100644 --- a/mobile-uni/src/services/agent.ts +++ b/mobile-uni/src/services/agent.ts @@ -111,7 +111,7 @@ export const normalizeAgentResponse = ( rewrittenQuery: question, resources: response.resources || [], memoryCandidate, - broadcastContext: null, + broadcastContext: response.broadcastContext || null, agentStatus: response.status, agentIntent: response.intent, sourceSummary: response.sourceSummary || [], diff --git a/mobile-uni/src/types/api.ts b/mobile-uni/src/types/api.ts index 38e3a10a..7b0222b7 100644 --- a/mobile-uni/src/types/api.ts +++ b/mobile-uni/src/types/api.ts @@ -261,6 +261,7 @@ export interface AgentResponse { sourceSummary: AgentSourceSummary[]; citations: KnowledgeCitation[]; resources: KnowledgeResource[]; + broadcastContext?: BroadcastKnowledgeContext | null; data?: unknown; actionDraft?: AgentActionDraft | null; clarification?: AgentClarification | null; diff --git a/mobile-uni/tests/ask-memory.test.mjs b/mobile-uni/tests/ask-memory.test.mjs index 61eaac6e..0acc537f 100644 --- a/mobile-uni/tests/ask-memory.test.mjs +++ b/mobile-uni/tests/ask-memory.test.mjs @@ -81,17 +81,19 @@ test('问师傅在真正开始会话后收起首次使用引导,保留对话 test('消息咨询只以消息 ID 作为受控上下文,并让每轮文字或语音请求携带它', async () => { const [page, service, types] = await Promise.all([ read('../src/pages/user/sop/index.vue'), - read('../src/services/knowledge.ts'), + read('../src/services/agent.ts'), read('../src/types/api.ts') ]); assert.match(page, /getBroadcastMessage\(pending\.messageId\)/); assert.match(page, /messageId: message\.id,\s*title: message\.title,\s*publishedAt: message\.publishedAt/); assert.match(page, /broadcastMessageId \? undefined : currentProject\.value\?\.projectCode/); - assert.match(page, /options\.voice \? 'mobile_uni_voice' : 'mobile_uni_sop',\s*broadcastMessageId/); + assert.match(page, /const agentOptions = \{[\s\S]*?broadcastMessageId,[\s\S]*?\}/); + assert.match(page, /askAgent\(text, agentOptions\)/); assert.match(service, /broadcastMessageId\?: number/); - assert.match(service, /\? \{ broadcastMessageId \}/); - assert.match(service, /broadcastContext: response\.broadcastContext \|\| legacy\.broadcastContext \|\| null/); + assert.match(service, /broadcastMessageId: options\.broadcastMessageId/); + assert.match(service, /\/api\/aihr\/agent\/messages/); + assert.match(service, /broadcastContext: response\.broadcastContext \|\| null/); assert.match(types, /export interface BroadcastKnowledgeContext/); assert.match(types, /broadcastContext\?: BroadcastKnowledgeContext \| null/); }); diff --git a/mobile-uni/tests/mobile-user-pages.test.mjs b/mobile-uni/tests/mobile-user-pages.test.mjs index 15082d16..e58919c7 100644 --- a/mobile-uni/tests/mobile-user-pages.test.mjs +++ b/mobile-uni/tests/mobile-user-pages.test.mjs @@ -61,11 +61,11 @@ test('语音 profile 透传方言枚举,并保持普通话默认值', async () pageSource('../src/services/speech.ts') ]); - assert.match(apiTypes, /dialect\?: 'mandarin' \| 'cantonese' \| 'sichuanese'/); + assert.match(apiTypes, /dialect\?: 'mandarin' \| 'cantonese'[\s\S]*\| 'sichuanese'/); assert.match(speechService, /dialect: 'mandarin'/); assert.match(speechService, /customerVoiceProfile = \([\s\S]*?dialect:/); assert.match(speechService, /dialect === 'cantonese'.*'zh-HK'/); - assert.match(speechService, /dialect === 'sichuanese'[\s\S]*?当前设备语音不支持四川话/); + assert.match(speechService, /dialect && dialect !== 'mandarin'/); }); test('M3 将业主 TTS 绑定到当前训练会话与准确回合,供主管复盘回放', async () => { @@ -77,7 +77,7 @@ test('M3 将业主 TTS 绑定到当前训练会话与准确回合,供主管复 assert.match(apiTypes, /export interface PracticeTtsContext/); assert.match(speechService, /practiceContext\?: PracticeTtsContext/); - assert.match(speechService, /data: \{ text: text\.trim\(\)\.slice\(0, 300\), voiceProfile, practiceContext \}/); + assert.match(speechService, /data: \{ text: text\.trim\(\)\.slice\(0, 300\), voiceProfile: resolveTtsVoiceProfile\(voiceProfile\), practiceContext \}/); assert.match(practicePage, /\{ sessionId: sessionId\.value, turnIndex: Number\(customerTurnIndex\), role: 'customer' \}/); assert.match(practicePage, /speechPlayback\.preload\(spoken, profile, practiceContext\)/); assert.match(practicePage, /turnVoice\(index\)\.profile, turnVoice\(index\)\.practiceContext/); @@ -481,10 +481,10 @@ test('主管专项从正式团队名单按姓名多选并一次批量派发', as }); test('问师傅支持现场媒体、答案播报和总结卡图片下载', async () => { - const [source, composerSource, knowledgeService, speechService, imageService] = await Promise.all([ + const [source, composerSource, agentService, speechService, imageService] = await Promise.all([ pageSource('../src/pages/user/sop/index.vue'), pageSource('../src/components/chat/ChatComposer.vue'), - pageSource('../src/services/knowledge.ts'), + pageSource('../src/services/agent.ts'), pageSource('../src/services/speech.ts'), pageSource('../src/services/summary-card-image.ts') ]); @@ -492,11 +492,11 @@ test('问师傅支持现场媒体、答案播报和总结卡图片下载', async assert.match(source, /@pick-media="pickMedia"/); assert.match(composerSource, /图片\/视频/); assert.match(composerSource, /仅用于本次现场分析/); - assert.match(source, /searchKnowledgeWithMedia/); + assert.match(source, /askAgentWithMedia/); assert.match(source, /toggleAnswerSpeech/); assert.match(source, /toggleSummarySpeech/); assert.match(source, /downloadSummaryCardImage/); - assert.match(knowledgeService, /\/api\/knowledge\/query-media/); + assert.match(agentService, /\/api\/aihr\/agent\/messages\/media/); assert.match(speechService, /\/api\/ai\/tts/); assert.match(speechService, /SpeechSynthesisUtterance/); assert.match(speechService, /playWithBrowserSpeech/); @@ -580,7 +580,7 @@ test('播报复用已合成音频且总结图按内容高度生成并及时释 assert.match(speechService, /const sourceCache = new Map\(\)/); assert.match(speechService, /const maxCachedSources = 4/); - assert.match(speechService, /const sourceKey = cacheKey\(value, profile, practiceContext\)/); + assert.match(speechService, /const sourceKey = cacheKey\(value, effectiveProfile, practiceContext\)/); assert.match(speechService, /const cachedSource = sourceCache\.get\(sourceKey\)/); assert.match(speechService, /sourceCache\.clear\(\)/); assert.match(speechService, /registerAbort\?\.\(\(\) => task\.abort\(\)\)/); diff --git a/mobile-uni/tests/personal-assistant.test.mjs b/mobile-uni/tests/personal-assistant.test.mjs index 1c49f386..e15b455f 100644 --- a/mobile-uni/tests/personal-assistant.test.mjs +++ b/mobile-uni/tests/personal-assistant.test.mjs @@ -23,8 +23,10 @@ test('个人助理客户端使用确认式记忆接口且确认请求携带稳 const card = await read('../src/components/chat/MemoryCandidateCard.vue'); assert.match(service, /\/api\/aihr\/personal-assistant\/memory-candidates/); - assert.match(service, /\$\{candidateBase\}\/\$\{candidateId\}\/confirm/); - assert.match(service, /\$\{candidateBase\}\/\$\{candidateId\}\/dismiss/); + assert.match(service, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/confirm/); + assert.match(service, /\$\{candidateBase\}\/\$\{candidateRef\}\/confirm/); + assert.match(service, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/dismiss/); + assert.match(service, /\$\{candidateBase\}\/\$\{candidateRef\}\/dismiss/); assert.match(service, /\/api\/aihr\/service-memories/); assert.match(service, /\/api\/aihr\/personal-assistant\/assistant-captures/); assert.match(card, /const idempotencyKey = ref\(createMemoryIdempotencyKey\(props\.candidate\.id\)\)/);