diff --git a/README.md b/README.md index 9080ffbe..03ec08ac 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ English / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本 [![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers) Website | -Features | -Docs | -API | +Features | +Docs | +APIPlugin MarketRoadmap @@ -44,7 +44,7 @@ LangBot is an **open-source, production-grade platform** for building AI-powered - **Web Management Panel** — Configure, manage, and monitor your bots through an intuitive browser interface. No YAML editing required. - **Multi-Pipeline Architecture** — Different bots for different scenarios, with comprehensive monitoring and exception handling. -[→ Learn more about all features](https://docs.langbot.app/en/insight/features.html) +[→ Learn more about all features](https://docs.langbot.app/en/insight/features) --- @@ -71,7 +71,7 @@ docker compose up -d [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) -**More options:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Manual](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md) +**More options:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker) · [Manual](https://docs.langbot.app/en/deploy/langbot/manual) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt) · [Kubernetes](./docker/README_K8S.md) --- @@ -119,7 +119,7 @@ docker compose up -d | [接口 AI](https://jiekou.ai/) | Gateway | ✅ | | [302.AI](https://share.302.ai/SuTG99) | Gateway | ✅ | -[→ View all integrations](https://docs.langbot.app/en/insight/features.html) +[→ View all integrations](https://docs.langbot.app/en/insight/features) --- diff --git a/src/langbot/pkg/api/http/controller/groups/monitoring.py b/src/langbot/pkg/api/http/controller/groups/monitoring.py index 3b9f1e08..3cf08b7c 100644 --- a/src/langbot/pkg/api/http/controller/groups/monitoring.py +++ b/src/langbot/pkg/api/http/controller/groups/monitoring.py @@ -52,6 +52,7 @@ class MonitoringRouterGroup(group.RouterGroup): # Parse query parameters bot_ids = quart.request.args.getlist('botId') pipeline_ids = quart.request.args.getlist('pipelineId') + session_ids = quart.request.args.getlist('sessionId') start_time_str = quart.request.args.get('startTime') end_time_str = quart.request.args.get('endTime') limit = int(quart.request.args.get('limit', 100)) @@ -64,6 +65,7 @@ class MonitoringRouterGroup(group.RouterGroup): messages, total = await self.ap.monitoring_service.get_messages( bot_ids=bot_ids if bot_ids else None, pipeline_ids=pipeline_ids if pipeline_ids else None, + session_ids=session_ids if session_ids else None, start_time=start_time, end_time=end_time, limit=limit, diff --git a/src/langbot/pkg/api/http/service/monitoring.py b/src/langbot/pkg/api/http/service/monitoring.py index b9983519..886b4ccc 100644 --- a/src/langbot/pkg/api/http/service/monitoring.py +++ b/src/langbot/pkg/api/http/service/monitoring.py @@ -32,6 +32,7 @@ class MonitoringService: user_id: str | None = None, runner_name: str | None = None, variables: str | None = None, + role: str = 'user', ) -> str: """Record a message""" message_id = str(uuid.uuid4()) @@ -50,6 +51,7 @@ class MonitoringService: 'user_id': user_id, 'runner_name': runner_name, 'variables': variables, + 'role': role, } await self.ap.persistence_mgr.execute_async( @@ -355,6 +357,7 @@ class MonitoringService: self, bot_ids: list[str] | None = None, pipeline_ids: list[str] | None = None, + session_ids: list[str] | None = None, start_time: datetime.datetime | None = None, end_time: datetime.datetime | None = None, limit: int = 100, @@ -367,6 +370,8 @@ class MonitoringService: conditions.append(persistence_monitoring.MonitoringMessage.bot_id.in_(bot_ids)) if pipeline_ids: conditions.append(persistence_monitoring.MonitoringMessage.pipeline_id.in_(pipeline_ids)) + if session_ids: + conditions.append(persistence_monitoring.MonitoringMessage.session_id.in_(session_ids)) if start_time: conditions.append(persistence_monitoring.MonitoringMessage.timestamp >= start_time) if end_time: diff --git a/src/langbot/pkg/entity/persistence/monitoring.py b/src/langbot/pkg/entity/persistence/monitoring.py index 62121de6..82d8ece5 100644 --- a/src/langbot/pkg/entity/persistence/monitoring.py +++ b/src/langbot/pkg/entity/persistence/monitoring.py @@ -22,6 +22,7 @@ class MonitoringMessage(Base): user_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) runner_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) # Runner name for this query variables = sqlalchemy.Column(sqlalchemy.Text, nullable=True) # Query variables as JSON string + role = sqlalchemy.Column(sqlalchemy.String(50), nullable=True, default='user') # user, assistant class MonitoringLLMCall(Base): diff --git a/src/langbot/pkg/persistence/migrations/dbm019_monitoring_message_role.py b/src/langbot/pkg/persistence/migrations/dbm019_monitoring_message_role.py new file mode 100644 index 00000000..b1372aa7 --- /dev/null +++ b/src/langbot/pkg/persistence/migrations/dbm019_monitoring_message_role.py @@ -0,0 +1,24 @@ +import sqlalchemy +from .. import migration + + +@migration.migration_class(19) +class DBMigrateMonitoringMessageRole(migration.DBMigration): + """Add role column to monitoring_messages table""" + + async def upgrade(self): + """Upgrade""" + try: + sql_text = sqlalchemy.text("ALTER TABLE monitoring_messages ADD COLUMN role VARCHAR(50) DEFAULT 'user'") + await self.ap.persistence_mgr.execute_async(sql_text) + except Exception: + # Column may already exist + pass + + async def downgrade(self): + """Downgrade""" + try: + sql_text = sqlalchemy.text('ALTER TABLE monitoring_messages DROP COLUMN role') + await self.ap.persistence_mgr.execute_async(sql_text) + except Exception: + pass diff --git a/src/langbot/pkg/pipeline/monitoring_helper.py b/src/langbot/pkg/pipeline/monitoring_helper.py index e5289934..6ad9a30d 100644 --- a/src/langbot/pkg/pipeline/monitoring_helper.py +++ b/src/langbot/pkg/pipeline/monitoring_helper.py @@ -114,6 +114,60 @@ class MonitoringHelper: except Exception as e: ap.logger.error(f'Failed to record query success: {e}') + @staticmethod + async def record_query_response( + ap: app.Application, + query: pipeline_query.Query, + bot_id: str, + bot_name: str, + pipeline_id: str, + pipeline_name: str, + runner_name: str | None = None, + ): + """Record bot response message to monitoring""" + try: + session_id = f'{query.launcher_type}_{query.launcher_id}' + + # Extract response content from resp_message_chain + if hasattr(query, 'resp_message_chain') and query.resp_message_chain: + # Serialize the last response message chain + last_resp = query.resp_message_chain[-1] + if hasattr(last_resp, 'model_dump'): + message_content = json.dumps(last_resp.model_dump(), ensure_ascii=False) + else: + message_content = str(last_resp) + elif hasattr(query, 'resp_messages') and query.resp_messages: + last_resp = query.resp_messages[-1] + if hasattr(last_resp, 'get_content_platform_message_chain'): + chain = last_resp.get_content_platform_message_chain() + if hasattr(chain, 'model_dump'): + message_content = json.dumps(chain.model_dump(), ensure_ascii=False) + else: + message_content = str(chain) + else: + message_content = str(last_resp) + else: + return # No response to record + + await ap.monitoring_service.record_message( + bot_id=bot_id, + bot_name=bot_name, + pipeline_id=pipeline_id, + pipeline_name=pipeline_name, + message_content=message_content, + session_id=session_id, + status='success', + level='info', + platform=query.launcher_type.value + if hasattr(query.launcher_type, 'value') + else str(query.launcher_type), + user_id=query.sender_id, + runner_name=runner_name, + role='assistant', + ) + except Exception as e: + ap.logger.error(f'Failed to record query response: {e}') + @staticmethod async def record_query_error( ap: app.Application, diff --git a/src/langbot/pkg/pipeline/pipelinemgr.py b/src/langbot/pkg/pipeline/pipelinemgr.py index 08c449a7..d56f626c 100644 --- a/src/langbot/pkg/pipeline/pipelinemgr.py +++ b/src/langbot/pkg/pipeline/pipelinemgr.py @@ -339,6 +339,20 @@ class RuntimePipeline: except Exception as e: self.ap.logger.error(f'Failed to record query success: {e}') + # Record bot response message + try: + await monitoring_helper.MonitoringHelper.record_query_response( + ap=self.ap, + query=query, + bot_id=query.bot_uuid or 'unknown', + bot_name=bot_name, + pipeline_id=self.pipeline_entity.uuid, + pipeline_name=pipeline_name, + runner_name=runner_name, + ) + except Exception as e: + self.ap.logger.error(f'Failed to record query response: {e}') + except Exception as e: inst_name = query.current_stage_name if query.current_stage_name else 'unknown' self.ap.logger.error(f'Error processing query {query.query_id} stage={inst_name} : {e}') @@ -369,8 +383,6 @@ class RuntimePipeline: class PipelineManager: """流水线管理器""" - # ====== 4.0 ====== - ap: app.Application pipelines: list[RuntimePipeline] diff --git a/src/langbot/pkg/utils/constants.py b/src/langbot/pkg/utils/constants.py index 6e66b989..393012c1 100644 --- a/src/langbot/pkg/utils/constants.py +++ b/src/langbot/pkg/utils/constants.py @@ -2,7 +2,7 @@ import langbot semantic_version = f'v{langbot.__version__}' -required_database_version = 18 +required_database_version = 19 """Tag the version of the database schema, used to check if the database needs to be migrated""" debug_mode = False diff --git a/uv.lock b/uv.lock index ac0e47f5..fef488bd 100644 --- a/uv.lock +++ b/uv.lock @@ -1799,7 +1799,7 @@ wheels = [ [[package]] name = "langbot" -version = "4.8.3" +version = "4.8.4" source = { editable = "." } dependencies = [ { name = "aiocqhttp" }, @@ -1902,7 +1902,7 @@ requires-dist = [ { name = "ebooklib", specifier = ">=0.18" }, { name = "gewechat-client", specifier = ">=0.1.5" }, { name = "html2text", specifier = ">=2024.2.26" }, - { name = "langbot-plugin", specifier = "==0.2.5" }, + { name = "langbot-plugin", specifier = "==0.2.7" }, { name = "langchain", specifier = ">=0.2.0" }, { name = "langchain-text-splitters", specifier = ">=0.0.1" }, { name = "lark-oapi", specifier = ">=1.4.15" }, @@ -1958,7 +1958,7 @@ dev = [ [[package]] name = "langbot-plugin" -version = "0.2.5" +version = "0.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -1976,9 +1976,9 @@ dependencies = [ { name = "watchdog" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/0e/117dfc00f36323cce2369be5176d5cd5247ff52edb34791413af9623f290/langbot_plugin-0.2.5.tar.gz", hash = "sha256:a1bf04c1c07b30c72fb9b28e1330372bb4a43ae2db309394435fc088c513cfd5", size = 103910, upload-time = "2026-01-29T13:55:34.328Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/a0/babd76596e5de38149da67b8da20e0519cc5f10080de9dc2b16919486f29/langbot_plugin-0.2.7.tar.gz", hash = "sha256:5c8ad1820283901a33356f79a56c84b4744712a463e1c7aecc6e9defe4db4446", size = 162458, upload-time = "2026-02-25T06:00:52.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/0e/19b9a427206fa46aafbff03437296e38f425365c9ea6a97cbcfa791da2f8/langbot_plugin-0.2.5-py3-none-any.whl", hash = "sha256:b784248fc1f4754cd143bd9a16a7abd89a5c9735a4aa2b03c1c1e771b7d361e9", size = 133362, upload-time = "2026-01-29T13:55:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/6575cf5d5babb7a9400a8aca243e4b8341d83b673e5e9c0394c0393f1c3e/langbot_plugin-0.2.7-py3-none-any.whl", hash = "sha256:17344e61537a5bb97fc77cd83812b5db926f29005e92fefbcbaca5bb47bf55f0", size = 133476, upload-time = "2026-02-25T06:00:50.988Z" }, ] [[package]] diff --git a/web/src/app/home/bots/BotDetailDialog.tsx b/web/src/app/home/bots/BotDetailDialog.tsx index fbb8c359..55c66fa7 100644 --- a/web/src/app/home/bots/BotDetailDialog.tsx +++ b/web/src/app/home/bots/BotDetailDialog.tsx @@ -5,6 +5,7 @@ import { Dialog, DialogContent, DialogHeader, + DialogDescription, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; @@ -21,6 +22,7 @@ import { import { Button } from '@/components/ui/button'; import BotForm from '@/app/home/bots/components/bot-form/BotForm'; import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent'; +import BotSessionMonitor from '@/app/home/bots/components/bot-session/BotSessionMonitor'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { httpClient } from '@/app/infra/http/HttpClient'; @@ -82,6 +84,19 @@ export default function BotDetailDialog({ ), }, + { + key: 'sessions', + label: t('bots.sessionMonitor.title'), + icon: ( + + + + ), + }, ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -122,6 +137,9 @@ export default function BotDetailDialog({
{t('bots.createBot')} + + {t('bots.createBot')} +
- + {activeMenu === 'config' ? t('bots.editBot') - : t('bots.botLogTitle')} + : activeMenu === 'logs' + ? t('bots.botLogTitle') + : t('bots.sessionMonitor.title')} + + {activeMenu === 'config' + ? t('bots.editBot') + : activeMenu === 'logs' + ? t('bots.botLogTitle') + : t('bots.sessionMonitor.title')} + -
+
{activeMenu === 'config' && ( )} + {activeMenu === 'sessions' && botId && ( + + )}
{activeMenu === 'config' && ( @@ -238,6 +274,9 @@ export default function BotDetailDialog({ {t('common.confirmDelete')} + + {t('bots.deleteConfirmation')} +
{t('bots.deleteConfirmation')}
diff --git a/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx b/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx new file mode 100644 index 00000000..f3c8e19e --- /dev/null +++ b/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx @@ -0,0 +1,502 @@ +'use client'; + +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { + MessageChainComponent, + Plain, + At, + Image, + Quote, + Voice, +} from '@/app/infra/entities/message'; + +interface SessionInfo { + session_id: string; + bot_id: string; + bot_name: string; + pipeline_id: string; + pipeline_name: string; + message_count: number; + start_time: string; + last_activity: string; + is_active: boolean; + platform?: string | null; + user_id?: string | null; +} + +interface SessionMessage { + id: string; + timestamp: string; + bot_id: string; + bot_name: string; + pipeline_id: string; + pipeline_name: string; + message_content: string; + session_id: string; + status: string; + level: string; + platform?: string | null; + user_id?: string | null; + runner_name?: string | null; + variables?: string | null; + role?: string | null; +} + +interface BotSessionMonitorProps { + botId: string; +} + +export default function BotSessionMonitor({ botId }: BotSessionMonitorProps) { + const { t } = useTranslation(); + const [sessions, setSessions] = useState([]); + const [selectedSessionId, setSelectedSessionId] = useState( + null, + ); + const [messages, setMessages] = useState([]); + const [loadingSessions, setLoadingSessions] = useState(false); + const [loadingMessages, setLoadingMessages] = useState(false); + const messagesContainerRef = useRef(null); + + const loadSessions = useCallback(async () => { + setLoadingSessions(true); + try { + const response = await httpClient.getBotSessions(botId); + setSessions(response.sessions ?? []); + } catch (error) { + console.error('Failed to load sessions:', error); + } finally { + setLoadingSessions(false); + } + }, [botId]); + + const loadMessages = useCallback(async (sessionId: string) => { + setLoadingMessages(true); + try { + const response = await httpClient.getSessionMessages(sessionId); + const sorted = (response.messages ?? []).sort( + (a, b) => + new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(), + ); + setMessages(sorted); + } catch (error) { + console.error('Failed to load session messages:', error); + } finally { + setLoadingMessages(false); + } + }, []); + + useEffect(() => { + loadSessions(); + }, [loadSessions]); + + useEffect(() => { + if (selectedSessionId) { + loadMessages(selectedSessionId); + } else { + setMessages([]); + } + }, [selectedSessionId, loadMessages]); + + useEffect(() => { + const container = messagesContainerRef.current; + if (container) { + const viewport = container.querySelector( + '[data-radix-scroll-area-viewport]', + ); + const scrollTarget = viewport || container; + scrollTarget.scrollTop = scrollTarget.scrollHeight; + } + }, [messages]); + + const parseMessageChain = (content: string): MessageChainComponent[] => { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + return parsed as MessageChainComponent[]; + } + } catch { + // Not JSON, return as plain text + } + return [{ type: 'Plain', text: content } as Plain]; + }; + + const isUserMessage = (msg: SessionMessage): boolean => { + if (msg.role === 'assistant') return false; + if (msg.role === 'user') return true; + return !msg.runner_name; + }; + + const renderMessageComponent = ( + component: MessageChainComponent, + index: number, + ) => { + switch (component.type) { + case 'Plain': + return {(component as Plain).text}; + + case 'At': { + const atComponent = component as At; + const displayName = + atComponent.display || atComponent.target?.toString() || ''; + return ( + + @{displayName} + + ); + } + + case 'AtAll': + return ( + + @All + + ); + + case 'Image': { + const img = component as Image; + const imageUrl = img.url || (img.base64 ? img.base64 : ''); + if (!imageUrl) { + return ( + + [Image] + + ); + } + return ( +
+ Image +
+ ); + } + + case 'Voice': { + const voice = component as Voice; + const voiceUrl = voice.url || (voice.base64 ? voice.base64 : ''); + if (!voiceUrl) { + return ( + + 🎙 [Voice] + + ); + } + return ( +
+
+ ); + } + + case 'Quote': { + const quote = component as Quote; + return ( +
+
+ {quote.origin?.map((comp, idx) => + renderMessageComponent(comp as MessageChainComponent, idx), + )} +
+
+ ); + } + + case 'Source': + return null; + + case 'File': { + const file = component as MessageChainComponent & { name?: string }; + return ( + + 📎 {file.name || 'File'} + + ); + } + + default: + return ( + + [{component.type}] + + ); + } + }; + + const renderMessageContent = (msg: SessionMessage) => { + const chain = parseMessageChain(msg.message_content); + return ( +
+ {chain.map((component, index) => + renderMessageComponent(component, index), + )} +
+ ); + }; + + const formatTime = (timestamp: string): string => { + if (!timestamp) return ''; + const date = new Date(timestamp); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}`; + }; + + const formatRelativeTime = (timestamp: string): string => { + if (!timestamp) return ''; + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return '<1m'; + if (diffMins < 60) return `${diffMins}m`; + if (diffHours < 24) return `${diffHours}h`; + return `${diffDays}d`; + }; + + const selectedSession = sessions.find( + (s) => s.session_id === selectedSessionId, + ); + + return ( +
+ {/* Left Panel: Session List */} +
+ {/* Refresh Button */} +
+ +
+ + {/* Session List */} + + {loadingSessions && sessions.length === 0 ? ( +
+ {t('bots.sessionMonitor.loading')} +
+ ) : sessions.length === 0 ? ( +
+ {t('bots.sessionMonitor.noSessions')} +
+ ) : ( +
+ {sessions.map((session) => { + const isSelected = selectedSessionId === session.session_id; + return ( + + ); + })} +
+ )} +
+
+ + {/* Right Panel: Messages */} +
+ {!selectedSessionId ? ( +
+ {t('bots.sessionMonitor.selectSession')} +
+ ) : ( + <> + {/* Chat Header */} +
+
+
+ {selectedSession?.user_id || selectedSessionId.slice(0, 20)} +
+
+ {selectedSession?.platform && ( + {selectedSession.platform} + )} + {selectedSession?.pipeline_name && ( + <> + {selectedSession?.platform && ·} + {selectedSession.pipeline_name} + + )} + {selectedSession?.is_active && ( + <> + · + + + Active + + + )} +
+
+ +
+ + {/* Messages Area — matches DebugDialog style */} + +
+ {loadingMessages ? ( +
+ {t('bots.sessionMonitor.loading')} +
+ ) : messages.length === 0 ? ( +
+ {t('bots.sessionMonitor.noMessages')} +
+ ) : ( + messages.map((msg) => { + const isUser = isUserMessage(msg); + return ( +
+
+ {renderMessageContent(msg)} + {/* Role label + timestamp inside bubble, matching DebugDialog */} +
+ + {isUser + ? t('bots.sessionMonitor.userMessage', { + defaultValue: 'User', + }) + : t('bots.sessionMonitor.botMessage', { + defaultValue: 'Assistant', + })} + + + {formatTime(msg.timestamp)} + + {msg.status === 'error' && ( + error + )} + {msg.runner_name && ( + + {msg.runner_name} + + )} +
+
+
+ ); + }) + )} +
+
+ + )} +
+
+ ); +} diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index 7570a084..4d9532eb 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -141,6 +141,11 @@ export default function DynamicFormComponent({ } }, [initialValues, form, itemConfigList]); + // Stable ref for onSubmit to avoid re-triggering the effect when the + // parent passes a new closure on every render. + const onSubmitRef = useRef(onSubmit); + onSubmitRef.current = onSubmit; + // 监听表单值变化 useEffect(() => { // Emit initial form values immediately so the parent always has a valid snapshot, @@ -154,7 +159,7 @@ export default function DynamicFormComponent({ }, {} as Record, ); - onSubmit?.(initialFinalValues); + onSubmitRef.current?.(initialFinalValues); const subscription = form.watch(() => { const formValues = form.getValues(); @@ -165,10 +170,10 @@ export default function DynamicFormComponent({ }, {} as Record, ); - onSubmit?.(finalValues); + onSubmitRef.current?.(finalValues); }); return () => subscription.unsubscribe(); - }, [form, onSubmit, itemConfigList]); + }, [form, itemConfigList]); return (
diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index bb913bd0..22838676 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -339,6 +339,64 @@ export class BackendClient extends BaseHttpClient { return this.post(`/api/v1/platform/bots/${botId}/logs`, request); } + public getBotSessions( + botId: string, + limit: number = 100, + offset: number = 0, + ): Promise<{ + sessions: Array<{ + session_id: string; + bot_id: string; + bot_name: string; + pipeline_id: string; + pipeline_name: string; + message_count: number; + start_time: string; + last_activity: string; + is_active: boolean; + platform: string | null; + user_id: string | null; + }>; + total: number; + }> { + const queryParams = new URLSearchParams(); + queryParams.append('botId', botId); + queryParams.append('limit', limit.toString()); + queryParams.append('offset', offset.toString()); + return this.get(`/api/v1/monitoring/sessions?${queryParams.toString()}`); + } + + public getSessionMessages( + sessionId: string, + limit: number = 200, + offset: number = 0, + ): Promise<{ + messages: Array<{ + id: string; + timestamp: string; + bot_id: string; + bot_name: string; + pipeline_id: string; + pipeline_name: string; + message_content: string; + session_id: string; + status: string; + level: string; + platform: string | null; + user_id: string | null; + runner_name: string | null; + variables: string | null; + role: string | null; + }>; + total: number; + }> { + const queryParams = new URLSearchParams(); + queryParams.append('sessionId', sessionId); + queryParams.append('limit', limit.toString()); + queryParams.append('offset', offset.toString()); + return this.get(`/api/v1/monitoring/messages?${queryParams.toString()}`); + } + // ============ File management API ============ public uploadDocumentFile(file: File): Promise<{ file_id: string }> { const formData = new FormData(); diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index a8faed14..56a5855b 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -280,6 +280,25 @@ const enUS = { viewDetails: 'Details', collapse: 'Collapse', imagesAttached: 'image(s) attached', + sessionMonitor: { + title: 'Sessions', + sessions: 'Sessions', + noSessions: 'No sessions found', + selectSession: 'Select a session to view messages', + noMessages: 'No messages in this session', + messages: 'messages', + messageCount: '{{count}} messages', + loading: 'Loading...', + loadingSessions: 'Loading sessions...', + loadingMessages: 'Loading messages...', + user: 'User', + variables: 'Variables', + platform: 'Platform', + lastActive: 'Last active', + refresh: 'Refresh', + active: 'Active', + inactive: 'Inactive', + }, }, plugins: { title: 'Extensions', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 7166aa07..9f076e1a 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -281,6 +281,25 @@ const jaJP = { allLevels: 'すべてのレベル', selectLevel: 'レベルを選択', levelsSelected: 'レベル選択済み', + sessionMonitor: { + title: 'セッション監視', + sessions: 'セッション一覧', + noSessions: 'セッションが見つかりません', + selectSession: 'セッションを選択してメッセージを表示', + noMessages: 'このセッションにはメッセージがありません', + messages: '件のメッセージ', + messageCount: '{{count}} 件のメッセージ', + loading: '読み込み中...', + loadingSessions: 'セッションを読み込み中...', + loadingMessages: 'メッセージを読み込み中...', + user: 'ユーザー', + variables: '変数', + platform: 'プラットフォーム', + lastActive: '最終アクティブ', + refresh: '更新', + active: 'アクティブ', + inactive: '非アクティブ', + }, }, plugins: { title: '拡張機能', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 9e3c577e..74f0cd08 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -269,6 +269,25 @@ const zhHans = { viewDetails: '详情', collapse: '收起', imagesAttached: '张图片', + sessionMonitor: { + title: '会话监控', + sessions: '会话列表', + noSessions: '暂无会话', + selectSession: '选择一个会话查看消息', + noMessages: '该会话暂无消息', + messages: '条消息', + messageCount: '{{count}} 条消息', + loading: '加载中...', + loadingSessions: '加载会话中...', + loadingMessages: '加载消息中...', + user: '用户', + variables: '变量', + platform: '平台', + lastActive: '最近活跃', + refresh: '刷新', + active: '活跃', + inactive: '不活跃', + }, }, plugins: { title: '插件扩展', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 94a0a380..1f937264 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -264,6 +264,25 @@ const zhHant = { allLevels: '全部級別', selectLevel: '選擇級別', levelsSelected: '個級別已選', + sessionMonitor: { + title: '會話監控', + sessions: '會話列表', + noSessions: '暫無會話', + selectSession: '選擇一個會話查看訊息', + noMessages: '該會話暫無訊息', + messages: '條訊息', + messageCount: '{{count}} 條訊息', + loading: '載入中...', + loadingSessions: '載入會話中...', + loadingMessages: '載入訊息中...', + user: '用戶', + variables: '變數', + platform: '平台', + lastActive: '最近活躍', + refresh: '重新整理', + active: '活躍', + inactive: '不活躍', + }, }, plugins: { title: '外掛擴展',