feat: displaying plugin debug info (#1828)

This commit is contained in:
Junyan Qin (Chin)
2025-12-01 17:59:49 +08:00
committed by GitHub
parent 0ddc3d60e7
commit e49a161d0a
10 changed files with 188 additions and 1 deletions

View File

@@ -21,6 +21,22 @@ class PluginsRouterGroup(group.RouterGroup):
return self.success(data={'plugins': plugins})
@self.route('/debug-info', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
"""Get plugin debug information including debug URL and key"""
debug_info = await self.ap.plugin_connector.get_debug_info()
# Get debug URL from config
plugin_config = self.ap.instance_config.data.get('plugin', {})
debug_url = plugin_config.get('display_plugin_debug_url', 'http://localhost:5401')
return self.success(
data={
'debug_url': debug_url,
'plugin_debug_key': debug_info.get('plugin_debug_key', ''),
}
)
@self.route(
'/<author>/<plugin_name>/upgrade',
methods=['POST'],

View File

@@ -385,6 +385,12 @@ class PluginRuntimeConnector:
async def get_plugin_assets(self, plugin_author: str, plugin_name: str, filepath: str) -> dict[str, Any]:
return await self.handler.get_plugin_assets(plugin_author, plugin_name, filepath)
async def get_debug_info(self) -> dict[str, Any]:
"""Get debug information including debug key and WS URL"""
if not self.is_enable_plugin:
return {}
return await self.handler.get_debug_info()
async def emit_event(
self,
event: events.BaseEventModel,

View File

@@ -758,3 +758,12 @@ class RuntimeConnectionHandler(handler.Handler):
timeout=30,
)
return result
async def get_debug_info(self) -> dict[str, Any]:
"""Get debug information including debug key and WS URL"""
result = await self.call_action(
LangBotToRuntimeAction.GET_DEBUG_INFO,
{},
timeout=10,
)
return result

View File

@@ -48,3 +48,4 @@ plugin:
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
enable_marketplace: true
cloud_service_url: 'https://space.langbot.app'
display_plugin_debug_url: 'http://localhost:5401'

View File

@@ -24,6 +24,9 @@ import {
Power,
Github,
ChevronLeft,
Code,
Copy,
Bug,
} from 'lucide-react';
import {
DropdownMenu,
@@ -38,6 +41,11 @@ import {
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Input } from '@/components/ui/input';
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
@@ -105,6 +113,11 @@ export default function PluginConfigPage() {
);
const [isEditMode, setIsEditMode] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const [debugInfo, setDebugInfo] = useState<{
debug_url: string;
plugin_debug_key: string;
} | null>(null);
const [debugPopoverOpen, setDebugPopoverOpen] = useState(false);
useEffect(() => {
const fetchPluginSystemStatus = async () => {
@@ -374,6 +387,22 @@ export default function PluginConfigPage() {
[uploadPluginFile, isPluginSystemReady, t],
);
const handleShowDebugInfo = async () => {
try {
const info = await httpClient.getPluginDebugInfo();
setDebugInfo(info);
setDebugPopoverOpen(true);
} catch (error) {
console.error('Failed to fetch debug info:', error);
toast.error(t('plugins.failedToGetDebugInfo'));
}
};
const handleCopyDebugInfo = (text: string) => {
navigator.clipboard.writeText(text);
toast.success(t('plugins.copiedToClipboard'));
};
const renderPluginDisabledState = () => (
<div className="flex flex-col items-center justify-center h-[60vh] text-center pt-[10vh]">
<Power className="w-16 h-16 text-gray-400 mb-4" />
@@ -466,7 +495,92 @@ export default function PluginConfigPage() {
</TabsTrigger>
</TabsList>
<div className="flex flex-row justify-end items-center">
<div className="flex flex-row justify-end items-center gap-2">
{activeTab === 'installed' && (
<Popover
open={debugPopoverOpen}
onOpenChange={setDebugPopoverOpen}
>
<PopoverTrigger asChild>
<Button
variant="outline"
className="px-4 py-5 cursor-pointer"
onClick={handleShowDebugInfo}
>
<Code className="w-4 h-4 mr-2" />
{t('plugins.debugInfo')}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[380px]" align="end">
<div className="space-y-3">
{/* Header with icon and title */}
<div className="flex items-center gap-2 pb-2 border-b">
<Bug className="w-4 h-4" />
<h4 className="font-semibold text-sm">
{t('plugins.debugInfoTitle')}
</h4>
</div>
{/* Debug URL row */}
<div className="flex items-center gap-2">
<label className="text-sm font-medium whitespace-nowrap min-w-[50px]">
{t('plugins.debugUrl')}:
</label>
<Input
value={debugInfo?.debug_url || ''}
readOnly
className="w-[220px] font-mono text-xs h-8"
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() =>
handleCopyDebugInfo(debugInfo?.debug_url || '')
}
>
<Copy className="w-3.5 h-3.5" />
</Button>
</div>
{/* Debug Key row */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<label className="text-sm font-medium whitespace-nowrap min-w-[50px]">
{t('plugins.debugKey')}:
</label>
<Input
value={
debugInfo?.plugin_debug_key ||
t('plugins.noDebugKey')
}
readOnly
className="w-[220px] font-mono text-xs h-8"
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() =>
handleCopyDebugInfo(
debugInfo?.plugin_debug_key || '',
)
}
disabled={!debugInfo?.plugin_debug_key}
>
<Copy className="w-3.5 h-3.5" />
</Button>
</div>
{!debugInfo?.plugin_debug_key && (
<p className="text-xs text-muted-foreground ml-[58px]">
{t('plugins.debugKeyDisabled')}
</p>
)}
</div>
</div>
</PopoverContent>
</Popover>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" className="px-6 py-4 cursor-pointer">

View File

@@ -639,6 +639,13 @@ export class BackendClient extends BaseHttpClient {
return this.get('/api/v1/system/status/plugin-system');
}
public getPluginDebugInfo(): Promise<{
debug_url: string;
plugin_debug_key: string;
}> {
return this.get('/api/v1/plugins/debug-info');
}
// ============ User API ============
public checkIfInited(): Promise<{ initialized: boolean }> {
return this.get('/api/v1/user/init');

View File

@@ -231,6 +231,15 @@ const enUS = {
failedToGetStatus: 'Failed to get plugin system status',
pluginSystemNotReady:
'Plugin system is not ready, cannot perform this operation',
debugInfo: 'Debug Info',
debugInfoTitle: 'Plugin Debug Information',
debugUrl: 'Debug URL',
debugKey: 'Debug Key',
noDebugKey: '(Not Set)',
debugKeyDisabled:
'Debug key is not set, plugin debugging does not require authentication',
failedToGetDebugInfo: 'Failed to get debug information',
copiedToClipboard: 'Copied to clipboard',
deleting: 'Deleting...',
deletePlugin: 'Delete Plugin',
cancel: 'Cancel',

View File

@@ -233,6 +233,15 @@ const jaJP = {
failedToGetStatus: 'プラグインシステム状態の取得に失敗しました',
pluginSystemNotReady:
'プラグインシステムが準備されていません。この操作を実行できません',
debugInfo: 'デバッグ情報',
debugInfoTitle: 'プラグインデバッグ情報',
debugUrl: 'デバッグURL',
debugKey: 'デバッグキー',
noDebugKey: '(未設定)',
debugKeyDisabled:
'デバッグキーが設定されていません。プラグインデバッグには認証が不要です',
failedToGetDebugInfo: 'デバッグ情報の取得に失敗しました',
copiedToClipboard: 'クリップボードにコピーしました',
deleting: '削除中...',
deletePlugin: 'プラグインを削除',
cancel: 'キャンセル',

View File

@@ -222,6 +222,14 @@ const zhHans = {
loadingStatus: '正在检查插件系统状态...',
failedToGetStatus: '获取插件系统状态失败',
pluginSystemNotReady: '插件系统未就绪,无法执行此操作',
debugInfo: '调试信息',
debugInfoTitle: '插件调试信息',
debugUrl: '调试地址',
debugKey: '调试密钥',
noDebugKey: '(未设置)',
debugKeyDisabled: '未设置调试密钥,插件调试无需认证',
failedToGetDebugInfo: '获取调试信息失败',
copiedToClipboard: '已复制到剪贴板',
deleting: '删除中...',
deletePlugin: '删除插件',
cancel: '取消',

View File

@@ -222,6 +222,14 @@ const zhHant = {
loadingStatus: '正在檢查外掛系統狀態...',
failedToGetStatus: '取得外掛系統狀態失敗',
pluginSystemNotReady: '外掛系統未就緒,無法執行此操作',
debugInfo: '偵錯資訊',
debugInfoTitle: '外掛偵錯資訊',
debugUrl: '偵錯位址',
debugKey: '偵錯金鑰',
noDebugKey: '(未設定)',
debugKeyDisabled: '未設定偵錯金鑰,外掛偵錯無需認證',
failedToGetDebugInfo: '取得偵錯資訊失敗',
copiedToClipboard: '已複製到剪貼簿',
deleting: '刪除中...',
deletePlugin: '刪除外掛',
cancel: '取消',