mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-18 03:34:20 +00:00
feat(monitoring): 关联反馈记录与消息ID,新增反馈导出 (#2120)
* feat(monitoring): link feedback to LangBot message ID and add feedback export - Add pipeline→adapter notification hook so monitoring message ID is passed back to WecomBotAdapter after creation - Store stream_id→monitoring_message_id mapping with 10-min TTL cleanup - Replace feedback record stream_id with LangBot monitoring message ID so feedback can be linked to actual message records - Rename streamId label to "Related Query ID" in all 7 i18n locales - Remove non-functional message ID jump button from FeedbackList - Add feedback export option to ExportDropdown (backend already implemented) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(monitoring): add combined refresh handler for monitoring and feedback data * fix(wecombot): improve stream ID mapping and error logging in WecomBotAdapter * feat(lark): add monitoring message ID mapping for feedback correlation * feat(lark): rename monitoring message ID mappings for clarity and consistency feat(feedback): add button to view conversation for feedback items * feat(bot-session-monitor): add feedback handling for bot messages with visual indicators * feat(bot-session-monitor): enhance feedback display with hover content for like/dislike indicators * fix(dingtalk): use voice recognition text instead of raw audio binary When DingTalk sends a voice message to the bot, the callback JSON contains a 'recognition' field with the speech-to-text result (powered by Qwen). Previously, LangBot only extracted the 'downloadCode' to download the raw audio binary and passed it as 'file_base64' to LLM APIs, which caused 400 errors since most models don't support this content type. This patch: - Extracts the 'recognition' field from DingTalk audio message content - Uses it as plain text input to the LLM instead of raw audio - Falls back to audio binary only when no recognition text is available - Fixes duplicate text issue for audio messages with recognition Fixes voice messages returning 'Request failed' on all LLM models. * fix: add filereader for dingtalk,lark (#2122) * fix: add filereader for dingtalk * feat: add lark * feat: update uv.lock * chore: update version to 4.9.6 in pyproject.toml, __init__.py, and uv.lock * fix: update langbot-plugin version to 0.3.8 * fix: update langbot-plugin version to 0.3.8 * fix(wecombot): extend StreamSession TTL for feedback sessions to prevent context data loss StreamSessionManager.cleanup() removes sessions after 60s TTL, but feedback events (like → cancel → dislike) can arrive later. When the session expires before the dislike event, all context fields (session_id, user_id, message_id, stream_id) are lost because get_session_by_feedback_id() returns None. Fix: Sessions with registered feedback_ids now use a 10-minute TTL, aligned with the adapter's _stream_to_monitoring_msg TTL in wecombot.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: 6mvp6 <13727783693@163.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: fdc310 <2213070223@qq.com> Co-authored-by: haiyangbg <zhouhaiyangaa@gmail.com> Co-authored-by: Guanchao Wang <wangcham233@gmail.com> Co-authored-by: Rock Chin <1010553892@qq.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
AlertCircle,
|
||||
Users,
|
||||
Layers,
|
||||
ThumbsUp,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -25,7 +26,8 @@ export type ExportType =
|
||||
| 'llm-calls'
|
||||
| 'embedding-calls'
|
||||
| 'errors'
|
||||
| 'sessions';
|
||||
| 'sessions'
|
||||
| 'feedback';
|
||||
|
||||
interface ExportDropdownProps {
|
||||
filterState: FilterState;
|
||||
@@ -162,6 +164,11 @@ export function ExportDropdown({ filterState }: ExportDropdownProps) {
|
||||
label: t('monitoring.export.sessions'),
|
||||
icon: <Users className="w-4 h-4 mr-2" />,
|
||||
},
|
||||
{
|
||||
type: 'feedback',
|
||||
label: t('monitoring.export.feedback'),
|
||||
icon: <ThumbsUp className="w-4 h-4 mr-2" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -127,6 +127,20 @@ export function FeedbackList({
|
||||
{item.platform}
|
||||
</span>
|
||||
)}
|
||||
{item.streamId && onViewMessage && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 px-1.5 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewMessage(item.streamId!);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 mr-1" />
|
||||
{t('monitoring.messageList.viewConversation')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{item.feedbackContent && (
|
||||
@@ -221,21 +235,8 @@ export function FeedbackList({
|
||||
<div className="text-gray-500 dark:text-gray-400">
|
||||
{t('monitoring.feedback.messageId')}
|
||||
</div>
|
||||
<div className="font-medium text-gray-900 dark:text-white truncate flex items-center gap-1">
|
||||
<span className="truncate">{item.messageId}</span>
|
||||
{onViewMessage && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 px-1.5 text-xs shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewMessage(item.messageId!);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
<div className="font-medium text-gray-900 dark:text-white truncate">
|
||||
{item.messageId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Suspense, useState, useMemo } from 'react';
|
||||
import React, { Suspense, useState, useMemo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -69,6 +69,9 @@ function MonitoringPageContent() {
|
||||
useMonitoringFilters();
|
||||
const { data, loading, refetch } = useMonitoringData(filterState);
|
||||
|
||||
// Counter to force feedbackTimeRange recomputation on manual refresh
|
||||
const [feedbackRefreshKey, setFeedbackRefreshKey] = useState(0);
|
||||
|
||||
// Get time range for feedback data
|
||||
const feedbackTimeRange = useMemo(() => {
|
||||
const now = new Date();
|
||||
@@ -106,7 +109,8 @@ function MonitoringPageContent() {
|
||||
startTime: startTime?.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
};
|
||||
}, [filterState.timeRange, filterState.customDateRange]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filterState.timeRange, filterState.customDateRange, feedbackRefreshKey]);
|
||||
|
||||
// Feedback data hook
|
||||
const {
|
||||
@@ -127,6 +131,12 @@ function MonitoringPageContent() {
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
// Combined refresh handler for both monitoring and feedback data
|
||||
const handleRefresh = useCallback(() => {
|
||||
refetch();
|
||||
setFeedbackRefreshKey((k) => k + 1);
|
||||
}, [refetch]);
|
||||
|
||||
const [expandedMessageId, setExpandedMessageId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@@ -265,7 +275,7 @@ function MonitoringPageContent() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refetch}
|
||||
onClick={handleRefresh}
|
||||
className="shadow-sm flex-shrink-0"
|
||||
>
|
||||
<svg
|
||||
|
||||
Reference in New Issue
Block a user