From 3622117d85132636a98dbcaa8eafe047b661fe4f Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Mon, 13 Apr 2026 21:03:07 +0800 Subject: [PATCH] fix git bugs. --- backend/core/docker_manager.py | 49 +++++++++--- backend/services/bot_runtime_service.py | 6 +- frontend/src/App.tsx | 2 +- .../components/lucent/LucentPromptContext.ts | 24 ++++++ .../lucent/LucentPromptProvider.tsx | 35 +-------- .../src/components/lucent/useLucentPrompt.ts | 11 +++ frontend/src/hooks/useBotsSync.ts | 14 +++- frontend/src/i18n/dashboard.en.ts | 1 + frontend/src/i18n/dashboard.zh-cn.ts | 1 + .../src/modules/dashboard/chat/chatUtils.ts | 3 +- .../dashboard/components/BotListPanel.tsx | 7 +- .../DashboardChannelConfigModal.tsx | 7 +- .../components/DashboardSkillsMcpModals.tsx | 5 +- .../components/DashboardTopicConfigModal.tsx | 3 +- .../dashboard/config-managers/mcpManager.ts | 8 +- .../dashboard/config-managers/topicManager.ts | 16 ++-- .../dashboard/hooks/useBotDashboardModule.ts | 2 +- .../dashboard/hooks/useDashboardBotEditor.ts | 19 +++-- .../hooks/useDashboardBotManagement.ts | 11 ++- .../hooks/useDashboardChannelConfig.ts | 4 +- .../hooks/useDashboardChatHistory.ts | 14 ++-- .../hooks/useDashboardChatMessageActions.ts | 26 ++++--- .../hooks/useDashboardConfigPanels.ts | 15 ++-- .../hooks/useDashboardConversation.ts | 3 +- .../dashboard/hooks/useDashboardLifecycle.ts | 6 +- .../dashboard/hooks/useDashboardMcpConfig.ts | 5 +- .../hooks/useDashboardRuntimeControl.ts | 30 ++++---- .../hooks/useDashboardSkillsConfig.ts | 12 +-- .../hooks/useDashboardSupportData.ts | 50 +++++++------ .../hooks/useDashboardTemplateManager.ts | 8 +- .../hooks/useDashboardTopicConfig.ts | 5 +- .../dashboard/hooks/useDashboardVoiceInput.ts | 10 +-- frontend/src/modules/dashboard/localeTypes.ts | 17 +++++ .../dashboard/topic/TopicFeedPanel.tsx | 2 +- frontend/src/modules/dashboard/types.ts | 11 ++- .../src/modules/images/ImageFactoryModule.tsx | 13 ++-- .../modules/onboarding/BotWizardModule.tsx | 2 +- .../components/BotWizardBaseStep.tsx | 5 +- .../components/BotWizardChannelModal.tsx | 5 +- .../components/BotWizardImageStep.tsx | 3 +- .../components/BotWizardReviewStep.tsx | 3 +- .../components/BotWizardToolsModal.tsx | 3 +- .../modules/onboarding/hooks/useBotWizard.ts | 18 +++-- .../src/modules/onboarding/localeTypes.ts | 14 ++++ .../modules/platform/PlatformLoginLogPage.tsx | 13 ++-- frontend/src/modules/platform/api/skills.ts | 2 +- .../components/PlatformBotRuntimeSection.tsx | 2 +- .../components/PlatformSettingsPage.tsx | 27 +++---- .../components/SkillMarketManagerPage.tsx | 23 +++--- .../components/TemplateManagerPage.tsx | 74 +++++++++---------- .../platform/hooks/usePlatformDashboard.ts | 2 +- .../hooks/usePlatformManagementState.ts | 5 +- .../hooks/usePlatformOverviewState.ts | 13 ++-- frontend/src/shared/http/apiErrors.ts | 18 +++++ .../src/shared/workspace/useBotWorkspace.ts | 4 +- .../shared/workspace/useWorkspacePreview.ts | 28 +++++-- .../shared/workspace/workspaceMarkdown.tsx | 2 +- 57 files changed, 445 insertions(+), 276 deletions(-) create mode 100644 frontend/src/components/lucent/LucentPromptContext.ts create mode 100644 frontend/src/components/lucent/useLucentPrompt.ts create mode 100644 frontend/src/modules/dashboard/localeTypes.ts create mode 100644 frontend/src/modules/onboarding/localeTypes.ts create mode 100644 frontend/src/shared/http/apiErrors.ts diff --git a/backend/core/docker_manager.py b/backend/core/docker_manager.py index 2e15ac5..a57cbba 100644 --- a/backend/core/docker_manager.py +++ b/backend/core/docker_manager.py @@ -27,6 +27,13 @@ class BotDockerManager: self._storage_limit_supported: Optional[bool] = None self._storage_limit_warning_emitted = False + def _new_short_lived_client(self): + try: + return docker.from_env(timeout=6) + except Exception as e: + print(f"[DockerManager] failed to create short-lived client: {e}") + return None + @staticmethod def _normalize_resource_limits( cpu_cores: Optional[float], @@ -612,21 +619,41 @@ class BotDockerManager: self._last_delivery_error[bot_id] = reason return False - def _read_log_lines(self, bot_id: str, tail: Optional[int] = None) -> List[str]: + def _read_log_lines_with_client(self, client, bot_id: str, tail: Optional[int] = None) -> List[str]: + container = client.containers.get(f"worker_{bot_id}") + raw = container.logs(tail=max(1, int(tail))) if tail is not None else container.logs() + if isinstance(raw, (bytes, bytearray)): + text = raw.decode("utf-8", errors="ignore") + else: + text = str(raw or "") + return [line for line in text.splitlines() if line.strip()] + + def _read_log_lines(self, bot_id: str, tail: Optional[int] = None, *, fresh_client: bool = False) -> List[str]: + if fresh_client: + client = self._new_short_lived_client() + if not client: + return [] + try: + return self._read_log_lines_with_client(client, bot_id, tail=tail) + except Exception as e: + print(f"[DockerManager] Error reading logs for {bot_id} with short-lived client: {e}") + return [] + finally: + try: + client.close() + except Exception: + pass + if not self.client: return [] - container_name = f"worker_{bot_id}" try: - container = self.client.containers.get(container_name) - raw = container.logs(tail=max(1, int(tail))) if tail is not None else container.logs() - text = raw.decode("utf-8", errors="ignore") - return [line for line in text.splitlines() if line.strip()] + return self._read_log_lines_with_client(self.client, bot_id, tail=tail) except Exception as e: print(f"[DockerManager] Error reading logs for {bot_id}: {e}") return [] - def get_recent_logs(self, bot_id: str, tail: int = 300) -> List[str]: - return self._read_log_lines(bot_id, tail=max(1, int(tail))) + def get_recent_logs(self, bot_id: str, tail: int = 300, *, fresh_client: bool = False) -> List[str]: + return self._read_log_lines(bot_id, tail=max(1, int(tail)), fresh_client=fresh_client) def get_logs_page( self, @@ -634,6 +661,8 @@ class BotDockerManager: offset: int = 0, limit: int = 50, reverse: bool = True, + *, + fresh_client: bool = False, ) -> Dict[str, Any]: safe_offset = max(0, int(offset)) safe_limit = max(1, int(limit)) @@ -641,7 +670,7 @@ class BotDockerManager: # Docker logs API supports tail but not arbitrary offsets. For reverse pagination # we only read the minimal newest slice needed for the requested page. tail_count = safe_offset + safe_limit + 1 - lines = self._read_log_lines(bot_id, tail=tail_count) + lines = self._read_log_lines(bot_id, tail=tail_count, fresh_client=fresh_client) ordered = list(reversed(lines)) page = ordered[safe_offset:safe_offset + safe_limit] has_more = len(lines) > safe_offset + safe_limit @@ -654,7 +683,7 @@ class BotDockerManager: "reverse": reverse, } - lines = self._read_log_lines(bot_id, tail=None) + lines = self._read_log_lines(bot_id, tail=None, fresh_client=fresh_client) total = len(lines) page = lines[safe_offset:safe_offset + safe_limit] return { diff --git a/backend/services/bot_runtime_service.py b/backend/services/bot_runtime_service.py index b1717a3..af2899a 100644 --- a/backend/services/bot_runtime_service.py +++ b/backend/services/bot_runtime_service.py @@ -84,10 +84,14 @@ def get_bot_logs( offset=max(0, int(offset)), limit=max(1, int(limit)), reverse=bool(reverse), + fresh_client=True, ) return {"bot_id": bot_id, **page} effective_tail = max(1, int(tail or 300)) - return {"bot_id": bot_id, "logs": docker_manager.get_recent_logs(bot_id, tail=effective_tail)} + return { + "bot_id": bot_id, + "logs": docker_manager.get_recent_logs(bot_id, tail=effective_tail, fresh_client=True), + } async def relogin_weixin(session: Session, *, bot_id: str) -> Dict[str, Any]: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f43eee6..180e900 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -48,7 +48,7 @@ function AppShell() { const [botCompactPanelTab, setBotCompactPanelTab] = useState('chat'); const forcedBotId = route.kind === 'bot' ? route.botId : ''; - useBotsSync(forcedBotId || undefined); + useBotsSync(forcedBotId || undefined, route.kind === 'bot'); useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; diff --git a/frontend/src/components/lucent/LucentPromptContext.ts b/frontend/src/components/lucent/LucentPromptContext.ts new file mode 100644 index 0000000..e57a4e8 --- /dev/null +++ b/frontend/src/components/lucent/LucentPromptContext.ts @@ -0,0 +1,24 @@ +import { createContext } from 'react'; + +export type PromptTone = 'info' | 'success' | 'warning' | 'error'; + +export interface NotifyOptions { + title?: string; + tone?: PromptTone; + durationMs?: number; +} + +export interface ConfirmOptions { + title?: string; + message: string; + tone?: PromptTone; + confirmText?: string; + cancelText?: string; +} + +export interface LucentPromptApi { + notify: (message: string, options?: NotifyOptions) => void; + confirm: (options: ConfirmOptions) => Promise; +} + +export const LucentPromptContext = createContext(null); diff --git a/frontend/src/components/lucent/LucentPromptProvider.tsx b/frontend/src/components/lucent/LucentPromptProvider.tsx index cc278b6..7277399 100644 --- a/frontend/src/components/lucent/LucentPromptProvider.tsx +++ b/frontend/src/components/lucent/LucentPromptProvider.tsx @@ -1,25 +1,11 @@ -import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from 'react'; +import { useCallback, useMemo, useRef, useState, type ReactNode } from 'react'; import { AlertCircle, AlertTriangle, CheckCircle2, Info, X } from 'lucide-react'; import { useAppStore } from '../../store/appStore'; +import type { ConfirmOptions, LucentPromptApi, NotifyOptions, PromptTone } from './LucentPromptContext'; +import { LucentPromptContext } from './LucentPromptContext'; import { LucentIconButton } from './LucentIconButton'; import './lucent-prompt.css'; -type PromptTone = 'info' | 'success' | 'warning' | 'error'; - -interface NotifyOptions { - title?: string; - tone?: PromptTone; - durationMs?: number; -} - -interface ConfirmOptions { - title?: string; - message: string; - tone?: PromptTone; - confirmText?: string; - cancelText?: string; -} - interface ToastItem { id: number; title?: string; @@ -36,13 +22,6 @@ interface ConfirmState { resolve: (value: boolean) => void; } -interface LucentPromptApi { - notify: (message: string, options?: NotifyOptions) => void; - confirm: (options: ConfirmOptions) => Promise; -} - -const LucentPromptContext = createContext(null); - function ToneIcon({ tone }: { tone: PromptTone }) { if (tone === 'success') return ; if (tone === 'warning') return ; @@ -158,11 +137,3 @@ export function LucentPromptProvider({ children }: { children: ReactNode }) { ); } - -export function useLucentPrompt() { - const ctx = useContext(LucentPromptContext); - if (!ctx) { - throw new Error('useLucentPrompt must be used inside LucentPromptProvider'); - } - return ctx; -} diff --git a/frontend/src/components/lucent/useLucentPrompt.ts b/frontend/src/components/lucent/useLucentPrompt.ts new file mode 100644 index 0000000..9b7f2db --- /dev/null +++ b/frontend/src/components/lucent/useLucentPrompt.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react'; + +import { LucentPromptContext } from './LucentPromptContext'; + +export function useLucentPrompt() { + const ctx = useContext(LucentPromptContext); + if (!ctx) { + throw new Error('useLucentPrompt must be used inside LucentPromptProvider'); + } + return ctx; +} diff --git a/frontend/src/hooks/useBotsSync.ts b/frontend/src/hooks/useBotsSync.ts index c6dc537..0872b2e 100644 --- a/frontend/src/hooks/useBotsSync.ts +++ b/frontend/src/hooks/useBotsSync.ts @@ -121,7 +121,7 @@ function extractToolCallProgressHint(raw: string, isZh: boolean): string | null return `${isZh ? '工具调用' : 'Tool Call'}\n${callLabel}`; } -export function useBotsSync(forcedBotId?: string) { +export function useBotsSync(forcedBotId?: string, enableMonitorSockets: boolean = true) { const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore(); const socketsRef = useRef>({}); const heartbeatsRef = useRef>({}); @@ -254,6 +254,16 @@ export function useBotsSync(forcedBotId?: string) { }, [syncBotMessages]); useEffect(() => { + if (!enableMonitorSockets) { + Object.values(socketsRef.current).forEach((ws) => ws.close()); + Object.values(heartbeatsRef.current).forEach((timerId) => window.clearInterval(timerId)); + heartbeatsRef.current = {}; + socketsRef.current = {}; + return () => { + // no-op + }; + } + const runningIds = new Set( Object.values(activeBots) .filter((bot) => bot.docker_status === 'RUNNING') @@ -421,7 +431,7 @@ export function useBotsSync(forcedBotId?: string) { return () => { // no-op: clean in unmount effect below }; - }, [activeBots, addBotEvent, addBotLog, addBotMessage, forced, isZh, syncBotMessages, t.progress, t.replied, t.stateUpdated, updateBotState]); + }, [activeBots, addBotEvent, addBotLog, addBotMessage, enableMonitorSockets, forced, isZh, syncBotMessages, t.progress, t.replied, t.stateUpdated, updateBotState]); useEffect(() => { return () => { diff --git a/frontend/src/i18n/dashboard.en.ts b/frontend/src/i18n/dashboard.en.ts index 5b33197..f721077 100644 --- a/frontend/src/i18n/dashboard.en.ts +++ b/frontend/src/i18n/dashboard.en.ts @@ -9,6 +9,7 @@ export const dashboardEn = { channelSaved: 'Channel saved (effective after bot restart).', channelSaveFail: 'Failed to save channel.', channelAddFail: 'Failed to add channel.', + channelDeleted: 'Channel deleted.', channelDeleteConfirm: (channelType: string) => `Delete channel ${channelType}?`, channelDeleteFail: 'Failed to delete channel.', stopFail: 'Stop failed. Check backend logs.', diff --git a/frontend/src/i18n/dashboard.zh-cn.ts b/frontend/src/i18n/dashboard.zh-cn.ts index fa9ea4e..d807992 100644 --- a/frontend/src/i18n/dashboard.zh-cn.ts +++ b/frontend/src/i18n/dashboard.zh-cn.ts @@ -9,6 +9,7 @@ export const dashboardZhCn = { channelSaved: '渠道配置已保存(重启 Bot 后生效)。', channelSaveFail: '渠道保存失败。', channelAddFail: '新增渠道失败。', + channelDeleted: '渠道已删除。', channelDeleteConfirm: (channelType: string) => `确认删除渠道 ${channelType}?`, channelDeleteFail: '删除渠道失败。', stopFail: '停止失败,请查看后端日志。', diff --git a/frontend/src/modules/dashboard/chat/chatUtils.ts b/frontend/src/modules/dashboard/chat/chatUtils.ts index 2403610..8313dbf 100644 --- a/frontend/src/modules/dashboard/chat/chatUtils.ts +++ b/frontend/src/modules/dashboard/chat/chatUtils.ts @@ -2,6 +2,7 @@ import type { ChatMessage } from '../../../types/bot'; import { normalizeAssistantMessageText, normalizeUserMessageText } from '../../../shared/text/messageText'; import { normalizeAttachmentPaths } from '../../../shared/workspace/utils'; import { normalizeDashboardAttachmentPath } from '../../../shared/workspace/workspaceMarkdown'; +import type { BotMessageResponseRow } from '../types'; export function formatClock(ts: number) { const d = new Date(ts); @@ -37,7 +38,7 @@ export function formatDateInputValue(ts: number): string { return `${year}-${month}-${day}`; } -export function mapBotMessageResponseRow(row: any): ChatMessage { +export function mapBotMessageResponseRow(row: BotMessageResponseRow): ChatMessage { const roleRaw = String(row?.role || '').toLowerCase(); const role: ChatMessage['role'] = roleRaw === 'user' || roleRaw === 'assistant' || roleRaw === 'system' ? roleRaw : 'assistant'; diff --git a/frontend/src/modules/dashboard/components/BotListPanel.tsx b/frontend/src/modules/dashboard/components/BotListPanel.tsx index 89ecca0..6a5a92f 100644 --- a/frontend/src/modules/dashboard/components/BotListPanel.tsx +++ b/frontend/src/modules/dashboard/components/BotListPanel.tsx @@ -3,6 +3,7 @@ import { memo, type RefObject } from 'react'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; +import type { BotState } from '../../../types/bot'; import type { CompactPanelTab } from '../types'; import './DashboardMenus.css'; import './BotListPanel.css'; @@ -33,9 +34,9 @@ interface BotListLabels { } interface BotListPanelProps { - bots: any[]; - filteredBots: any[]; - pagedBots: any[]; + bots: BotState[]; + filteredBots: BotState[]; + pagedBots: BotState[]; selectedBotId: string; normalizedBotListQuery: string; botListQuery: string; diff --git a/frontend/src/modules/dashboard/components/DashboardChannelConfigModal.tsx b/frontend/src/modules/dashboard/components/DashboardChannelConfigModal.tsx index b830f1c..596da06 100644 --- a/frontend/src/modules/dashboard/components/DashboardChannelConfigModal.tsx +++ b/frontend/src/modules/dashboard/components/DashboardChannelConfigModal.tsx @@ -4,6 +4,7 @@ import type { RefObject } from 'react'; import { DrawerShell } from '../../../components/DrawerShell'; import { PasswordInput } from '../../../components/PasswordInput'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; +import type { ChannelLabels, DashboardLabels } from '../localeTypes'; import type { BotChannel, ChannelType, WeixinLoginStatus } from '../types'; import './DashboardManagementModals.css'; @@ -12,6 +13,8 @@ interface PasswordToggleLabels { hide: string; } +type ChannelConfigLabels = ChannelLabels & Pick; + function parseChannelListValue(raw: unknown): string { if (!Array.isArray(raw)) return ''; return raw @@ -57,7 +60,7 @@ function ChannelFieldsEditor({ onPatch, }: { channel: BotChannel; - labels: Record; + labels: ChannelConfigLabels; passwordToggleLabels: PasswordToggleLabels; onPatch: (patch: Partial) => void; }) { @@ -289,7 +292,7 @@ interface ChannelConfigModalProps { weixinLoginStatus: WeixinLoginStatus | null; hasSelectedBot: boolean; isZh: boolean; - labels: Record; + labels: ChannelConfigLabels; passwordToggleLabels: PasswordToggleLabels; onClose: () => void; onUpdateGlobalDeliveryFlag: (key: 'sendProgress' | 'sendToolHints', value: boolean) => void; diff --git a/frontend/src/modules/dashboard/components/DashboardSkillsMcpModals.tsx b/frontend/src/modules/dashboard/components/DashboardSkillsMcpModals.tsx index 7ec4765..920dd69 100644 --- a/frontend/src/modules/dashboard/components/DashboardSkillsMcpModals.tsx +++ b/frontend/src/modules/dashboard/components/DashboardSkillsMcpModals.tsx @@ -5,6 +5,7 @@ import { DrawerShell } from '../../../components/DrawerShell'; import { PasswordInput } from '../../../components/PasswordInput'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentSelect } from '../../../components/lucent/LucentSelect'; +import type { DashboardLabels } from '../localeTypes'; import type { MCPServerDraft, WorkspaceSkillOption } from '../types'; import './DashboardManagementModals.css'; @@ -19,7 +20,7 @@ interface SkillsModalProps { isSkillUploading: boolean; isZh: boolean; hasSelectedBot: boolean; - labels: Record; + labels: DashboardLabels; skillZipPickerRef: RefObject; skillAddMenuRef: RefObject; skillAddMenuOpen: boolean; @@ -144,7 +145,7 @@ interface McpConfigModalProps { newMcpPanelOpen: boolean; isSavingMcp: boolean; isZh: boolean; - labels: Record; + labels: DashboardLabels; passwordToggleLabels: PasswordToggleLabels; onClose: () => void; getMcpUiKey: (row: Pick, fallbackIndex: number) => string; diff --git a/frontend/src/modules/dashboard/components/DashboardTopicConfigModal.tsx b/frontend/src/modules/dashboard/components/DashboardTopicConfigModal.tsx index 37f8755..b1500ba 100644 --- a/frontend/src/modules/dashboard/components/DashboardTopicConfigModal.tsx +++ b/frontend/src/modules/dashboard/components/DashboardTopicConfigModal.tsx @@ -3,6 +3,7 @@ import type { RefObject } from 'react'; import { DrawerShell } from '../../../components/DrawerShell'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; +import type { DashboardLabels } from '../localeTypes'; import type { BotTopic, TopicPresetTemplate } from '../types'; import './DashboardManagementModals.css'; @@ -28,7 +29,7 @@ interface TopicConfigModalProps { isSavingTopic: boolean; hasSelectedBot: boolean; isZh: boolean; - labels: Record; + labels: DashboardLabels; onClose: () => void; getTopicUiKey: (topic: Pick, fallbackIndex: number) => string; countRoutingTextList: (raw: string) => number; diff --git a/frontend/src/modules/dashboard/config-managers/mcpManager.ts b/frontend/src/modules/dashboard/config-managers/mcpManager.ts index e1e95c3..d815802 100644 --- a/frontend/src/modules/dashboard/config-managers/mcpManager.ts +++ b/frontend/src/modules/dashboard/config-managers/mcpManager.ts @@ -1,6 +1,8 @@ import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; +import type { DashboardLabels } from '../localeTypes'; import type { MCPConfigResponse, MCPServerConfig, MCPServerDraft } from '../types'; import { mapMcpResponseToDrafts } from '../utils'; @@ -28,7 +30,7 @@ interface PromptApi { interface McpManagerDeps extends PromptApi { selectedBotId: string; isZh: boolean; - t: any; + t: DashboardLabels; currentMcpServers: MCPServerDraft[]; currentPersistedMcpServers: MCPServerDraft[]; currentNewMcpDraft: MCPServerDraft; @@ -172,8 +174,8 @@ export function createMcpManager({ resetNewMcpDraft(); } notify(t.mcpSaved, { tone: 'success' }); - } catch (error: any) { - notify(error?.response?.data?.detail || t.mcpSaveFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.mcpSaveFail), { tone: 'error' }); } finally { setIsSavingMcp(false); } diff --git a/frontend/src/modules/dashboard/config-managers/topicManager.ts b/frontend/src/modules/dashboard/config-managers/topicManager.ts index c4c8281..bc03c91 100644 --- a/frontend/src/modules/dashboard/config-managers/topicManager.ts +++ b/frontend/src/modules/dashboard/config-managers/topicManager.ts @@ -1,6 +1,8 @@ import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; +import type { DashboardLabels } from '../localeTypes'; import { isSystemFallbackTopic, normalizePresetTextList, resolvePresetText } from '../topic/topicPresetUtils'; import type { BotTopic, TopicPresetTemplate } from '../types'; @@ -28,7 +30,7 @@ interface PromptApi { interface TopicManagerDeps extends PromptApi { selectedBotId: string; isZh: boolean; - t: any; + t: DashboardLabels; effectiveTopicPresetTemplates: TopicPresetTemplate[]; setShowTopicModal: (value: boolean) => void; setTopics: (value: BotTopic[] | ((prev: BotTopic[]) => BotTopic[])) => void; @@ -263,8 +265,8 @@ export function createTopicManager({ }); await loadTopics(selectedBotId); notify(t.topicSaved, { tone: 'success' }); - } catch (error: any) { - notify(error?.response?.data?.detail || t.topicSaveFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.topicSaveFail), { tone: 'error' }); } finally { setIsSavingTopic(false); } @@ -298,8 +300,8 @@ export function createTopicManager({ resetNewTopicDraft(); setNewTopicPanelOpen(false); notify(t.topicSaved, { tone: 'success' }); - } catch (error: any) { - notify(error?.response?.data?.detail || t.topicSaveFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.topicSaveFail), { tone: 'error' }); } finally { setIsSavingTopic(false); } @@ -320,8 +322,8 @@ export function createTopicManager({ await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/topics/${encodeURIComponent(topicKey)}`); await loadTopics(selectedBotId); notify(t.topicDeleted, { tone: 'success' }); - } catch (error: any) { - notify(error?.response?.data?.detail || t.topicDeleteFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.topicDeleteFail), { tone: 'error' }); } finally { setIsSavingTopic(false); } diff --git a/frontend/src/modules/dashboard/hooks/useBotDashboardModule.ts b/frontend/src/modules/dashboard/hooks/useBotDashboardModule.ts index 5ef2d1d..2453fd5 100644 --- a/frontend/src/modules/dashboard/hooks/useBotDashboardModule.ts +++ b/frontend/src/modules/dashboard/hooks/useBotDashboardModule.ts @@ -1,6 +1,6 @@ import { useId, useState } from 'react'; -import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; +import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt'; import { pickLocale } from '../../../i18n'; import { useBotWorkspace } from '../../../shared/workspace/useBotWorkspace'; import { channelsEn } from '../../../i18n/channels.en'; diff --git a/frontend/src/modules/dashboard/hooks/useDashboardBotEditor.ts b/frontend/src/modules/dashboard/hooks/useDashboardBotEditor.ts index be7bf97..4fd7370 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardBotEditor.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardBotEditor.ts @@ -2,7 +2,10 @@ import { useCallback, useEffect, useState } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; +import type { BotState } from '../../../types/bot'; import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders'; +import type { DashboardLabels } from '../localeTypes'; import type { AgentTab, BotEditForm, BotParamDraft, NanobotImage } from '../types'; import { clampCpuCores, clampMaxTokens, clampMemoryMb, clampStorageGb, clampTemperature } from '../utils'; @@ -53,15 +56,15 @@ interface NotifyOptions { interface UseDashboardBotEditorOptions { defaultSystemTimezone: string; - ensureSelectedBotDetail: () => Promise; + ensureSelectedBotDetail: () => Promise; isZh: boolean; notify: (message: string, options?: NotifyOptions) => void; refresh: () => Promise; selectedBotId: string; - selectedBot?: any; + selectedBot?: BotState; setRuntimeMenuOpen: (open: boolean) => void; availableImages: NanobotImage[]; - t: any; + t: DashboardLabels; } export function useDashboardBotEditor({ @@ -86,7 +89,7 @@ export function useDashboardBotEditor({ const [showParamModal, setShowParamModal] = useState(false); const [showAgentModal, setShowAgentModal] = useState(false); - const applyEditFormFromBot = useCallback((bot?: any) => { + const applyEditFormFromBot = useCallback((bot?: BotState) => { if (!bot) return; const provider = String(bot.llm_provider || '').trim().toLowerCase(); setProviderTestResult(''); @@ -212,8 +215,8 @@ export function useDashboardBotEditor({ } else { setProviderTestResult(t.connFail(res.data?.detail || 'unknown error')); } - } catch (error: any) { - const msg = error?.response?.data?.detail || error?.message || 'request failed'; + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, 'request failed'); setProviderTestResult(t.connFail(msg)); } finally { setIsTestingProvider(false); @@ -286,8 +289,8 @@ export function useDashboardBotEditor({ await refresh(); closeModals(); notify(t.configUpdated, { tone: 'success' }); - } catch (error: any) { - const msg = error?.response?.data?.detail || t.saveFail; + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, t.saveFail); notify(msg, { tone: 'error' }); } finally { setIsSaving(false); diff --git a/frontend/src/modules/dashboard/hooks/useDashboardBotManagement.ts b/frontend/src/modules/dashboard/hooks/useDashboardBotManagement.ts index 4125bee..5db0bac 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardBotManagement.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardBotManagement.ts @@ -2,7 +2,10 @@ import { useCallback, useEffect } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import type { BotState } from '../../../types/bot'; +import type { ChatMessage } from '../../../types/bot'; +import type { DashboardLabels } from '../localeTypes'; type PromptTone = 'info' | 'success' | 'warning' | 'error'; @@ -27,9 +30,9 @@ interface UseDashboardBotManagementOptions { refresh: () => Promise; selectedBot?: BotState; selectedBotId: string; - setBotMessages: (botId: string, messages: any[]) => void; + setBotMessages: (botId: string, messages: ChatMessage[]) => void; setSelectedBotId: (botId: string) => void; - t: any; + t: DashboardLabels; } export function useDashboardBotManagement({ @@ -92,8 +95,8 @@ export function useDashboardBotManagement({ await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/messages`); setBotMessages(selectedBot.id, []); notify(t.clearHistoryDone, { tone: 'success' }); - } catch (error: any) { - const message = error?.response?.data?.detail || t.clearHistoryFail; + } catch (error: unknown) { + const message = resolveApiErrorMessage(error, t.clearHistoryFail); notify(message, { tone: 'error' }); } }, [confirm, notify, selectedBot, setBotMessages, t]); diff --git a/frontend/src/modules/dashboard/hooks/useDashboardChannelConfig.ts b/frontend/src/modules/dashboard/hooks/useDashboardChannelConfig.ts index e9f6c11..96deb56 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardChannelConfig.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardChannelConfig.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from 'react'; -import { channelsEn } from '../../../i18n/channels.en'; import type { BotState } from '../../../types/bot'; +import type { ChannelLabels } from '../localeTypes'; import { optionalChannelTypes } from '../constants'; import { createChannelManager, type ChannelManagerLabels, type GlobalDeliveryState } from '../config-managers/channelManager'; import type { BotChannel, WeixinLoginStatus } from '../types'; @@ -34,7 +34,7 @@ interface UseDashboardChannelConfigOptions { selectedBot?: Pick | null; selectedBotId: string; t: ChannelManagerLabels & { cancel: string; close: string }; - lc: typeof channelsEn; + lc: ChannelLabels; weixinLoginStatus: WeixinLoginStatus | null; } diff --git a/frontend/src/modules/dashboard/hooks/useDashboardChatHistory.ts b/frontend/src/modules/dashboard/hooks/useDashboardChatHistory.ts index 29d808b..0b46202 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardChatHistory.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardChatHistory.ts @@ -2,8 +2,10 @@ import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateA import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import type { ChatMessage } from '../../../types/bot'; -import type { BotMessagesByDateResponse } from '../types'; +import type { DashboardLabels } from '../localeTypes'; +import type { BotMessageResponseRow, BotMessagesByDateResponse } from '../types'; import { formatConversationDate, formatDateInputValue, @@ -23,7 +25,7 @@ interface UseDashboardChatHistoryOptions { setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void; notify: (message: string, options?: DashboardChatNotifyOptions) => void; confirm: (options: DashboardChatConfirmOptions) => Promise; - t: any; + t: DashboardLabels; isZh: boolean; } @@ -127,7 +129,7 @@ export function useDashboardChatHistory({ const fetchBotMessages = useCallback(async (botId: string): Promise => { const safeLimit = Math.max(100, Math.min(500, chatPullPageSize * 3)); - const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/messages`, { + const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/messages`, { params: { limit: safeLimit }, }); const rows = Array.isArray(res.data) ? res.data : []; @@ -171,7 +173,7 @@ export function useDashboardChatHistory({ : Math.max(10, Math.min(500, chatPullPageSize)); const beforeIdRaw = Number(options?.beforeId); const beforeId = Number.isFinite(beforeIdRaw) && beforeIdRaw > 0 ? Math.floor(beforeIdRaw) : undefined; - const res = await axios.get<{ items?: any[]; has_more?: boolean; next_before_id?: number | null }>( + const res = await axios.get<{ items?: BotMessageResponseRow[]; has_more?: boolean; next_before_id?: number | null }>( `${APP_ENDPOINTS.apiBase}/bots/${botId}/messages/page`, { params: { @@ -364,8 +366,8 @@ export function useDashboardChatHistory({ { tone: 'warning' }, ); } - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '按日期读取对话失败。' : 'Failed to load conversation by date.'), { + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '按日期读取对话失败。' : 'Failed to load conversation by date.'), { tone: 'error', }); } finally { diff --git a/frontend/src/modules/dashboard/hooks/useDashboardChatMessageActions.ts b/frontend/src/modules/dashboard/hooks/useDashboardChatMessageActions.ts index eee04ef..941d41e 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardChatMessageActions.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardChatMessageActions.ts @@ -2,8 +2,10 @@ import { useCallback, useEffect, useState, type MutableRefObject, type RefObject import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import type { ChatMessage } from '../../../types/bot'; import { normalizeAssistantMessageText, normalizeUserMessageText } from '../../../shared/text/messageText'; +import type { DashboardLabels } from '../localeTypes'; import type { DashboardChatConfirmOptions, DashboardChatNotifyOptions } from './dashboardChatShared'; interface UseDashboardChatMessageActionsOptions { @@ -16,7 +18,7 @@ interface UseDashboardChatMessageActionsOptions { setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void; notify: (message: string, options?: DashboardChatNotifyOptions) => void; confirm: (options: DashboardChatConfirmOptions) => Promise; - t: any; + t: DashboardLabels; } export function useDashboardChatMessageActions({ @@ -76,8 +78,8 @@ export function useDashboardChatMessageActions({ } else { notify(nextFeedback === 'up' ? t.feedbackUpSaved : t.feedbackDownSaved, { tone: 'success' }); } - } catch (error: any) { - const msg = error?.response?.data?.detail || t.feedbackSaveFail; + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, t.feedbackSaveFail); notify(msg, { tone: 'error' }); } finally { setFeedbackSavingByMessageId((prev) => { @@ -189,8 +191,8 @@ export function useDashboardChatMessageActions({ await deleteConversationMessageOnServer(targetMessageId); removeConversationMessageLocally(message, targetMessageId); notify(t.deleteMessageDone, { tone: 'success' }); - } catch (error: any) { - if (error?.response?.status === 404) { + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response?.status === 404) { try { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/messages/${targetMessageId}/delete`); removeConversationMessageLocally(message, targetMessageId); @@ -200,7 +202,7 @@ export function useDashboardChatMessageActions({ // continue to secondary re-match fallback below } } - if (error?.response?.status === 404) { + if (axios.isAxiosError(error) && error.response?.status === 404) { const refreshedMessageId = Number(await resolveMessageIdFromLatest(message)); if (Number.isFinite(refreshedMessageId) && refreshedMessageId > 0 && refreshedMessageId !== targetMessageId) { try { @@ -208,24 +210,24 @@ export function useDashboardChatMessageActions({ removeConversationMessageLocally(message, refreshedMessageId); notify(t.deleteMessageDone, { tone: 'success' }); return; - } catch (retryError: any) { - if (retryError?.response?.status === 404) { + } catch (retryError: unknown) { + if (axios.isAxiosError(retryError) && retryError.response?.status === 404) { try { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/messages/${refreshedMessageId}/delete`); removeConversationMessageLocally(message, refreshedMessageId); notify(t.deleteMessageDone, { tone: 'success' }); return; - } catch (postRetryError: any) { - notify(postRetryError?.response?.data?.detail || t.deleteMessageFail, { tone: 'error' }); + } catch (postRetryError: unknown) { + notify(resolveApiErrorMessage(postRetryError, t.deleteMessageFail), { tone: 'error' }); return; } } - notify(retryError?.response?.data?.detail || t.deleteMessageFail, { tone: 'error' }); + notify(resolveApiErrorMessage(retryError, t.deleteMessageFail), { tone: 'error' }); return; } } } - notify(error?.response?.data?.detail || t.deleteMessageFail, { tone: 'error' }); + notify(resolveApiErrorMessage(error, t.deleteMessageFail), { tone: 'error' }); } finally { setDeletingMessageIdMap((prev) => { const next = { ...prev }; diff --git a/frontend/src/modules/dashboard/hooks/useDashboardConfigPanels.ts b/frontend/src/modules/dashboard/hooks/useDashboardConfigPanels.ts index 3bc84ec..351e964 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardConfigPanels.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardConfigPanels.ts @@ -1,4 +1,7 @@ import { useCallback, useState, type ChangeEvent } from 'react'; +import type { BotState } from '../../../types/bot'; +import type { BotSkillMarketItem } from '../../platform/types'; +import type { ChannelLabels, DashboardLabels } from '../localeTypes'; import { useDashboardSkillsConfig } from './useDashboardSkillsConfig'; import { useDashboardChannelConfig } from './useDashboardChannelConfig'; import { useDashboardMcpConfig } from './useDashboardMcpConfig'; @@ -25,14 +28,14 @@ interface UseDashboardConfigPanelsOptions { confirm: (options: ConfirmOptions) => Promise; cronActionJobId: string | null; cronActionType?: 'starting' | 'stopping' | 'deleting' | ''; - cronJobs: any[]; + cronJobs: import('../types').CronJob[]; cronLoading: boolean; createEnvParam: (key: string, value: string) => Promise; deleteCronJob: (jobId: string) => Promise; deleteEnvParam: (key: string) => Promise; effectiveTopicPresetTemplates: TopicPresetTemplate[]; envEntries: [string, string][]; - installMarketSkill: (skill: any) => Promise; + installMarketSkill: (skill: BotSkillMarketItem) => Promise; isMarketSkillsLoading: boolean; isSkillUploading: boolean; isZh: boolean; @@ -43,7 +46,7 @@ interface UseDashboardConfigPanelsOptions { loadTopicFeedStats: (botId: string) => Promise; loadWeixinLoginStatus: (botId: string, silent?: boolean) => Promise; marketSkillInstallingId: number | null; - marketSkills: any[]; + marketSkills: BotSkillMarketItem[]; notify: (message: string, options?: NotifyOptions) => void; onPickSkillZip: (event: ChangeEvent) => Promise; passwordToggleLabels: { show: string; hide: string }; @@ -52,12 +55,12 @@ interface UseDashboardConfigPanelsOptions { removeBotSkill: (skill: WorkspaceSkillOption) => Promise; resetSupportState: () => void; saveSingleEnvParam: (originalKey: string, nextKey: string, nextValue: string) => Promise; - selectedBot?: any; + selectedBot?: BotState | null; selectedBotId: string; startCronJob: (jobId: string) => Promise; stopCronJob: (jobId: string) => Promise; - t: any; - lc: any; + t: DashboardLabels; + lc: ChannelLabels; weixinLoginStatus: WeixinLoginStatus | null; } diff --git a/frontend/src/modules/dashboard/hooks/useDashboardConversation.ts b/frontend/src/modules/dashboard/hooks/useDashboardConversation.ts index 2d57a04..2d33566 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardConversation.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardConversation.ts @@ -1,6 +1,7 @@ import type { Dispatch, SetStateAction } from 'react'; import type { ChatMessage } from '../../../types/bot'; +import type { DashboardLabels } from '../localeTypes'; import type { DashboardChatConfirmOptions, DashboardChatNotifyOptions } from './dashboardChatShared'; import { useDashboardChatComposer } from './useDashboardChatComposer'; import { useDashboardChatHistory } from './useDashboardChatHistory'; @@ -24,7 +25,7 @@ interface UseDashboardConversationOptions { setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void; notify: (message: string, options?: DashboardChatNotifyOptions) => void; confirm: (options: DashboardChatConfirmOptions) => Promise; - t: any; + t: DashboardLabels; isZh: boolean; } diff --git a/frontend/src/modules/dashboard/hooks/useDashboardLifecycle.ts b/frontend/src/modules/dashboard/hooks/useDashboardLifecycle.ts index 28f6f0d..99f2263 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardLifecycle.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardLifecycle.ts @@ -1,5 +1,7 @@ import { useEffect, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; + type PromptTone = 'info' | 'success' | 'warning' | 'error'; interface NotifyOptions { @@ -131,8 +133,8 @@ export function useDashboardLifecycle({ loadInitialConfigData(selectedBotId), ]); requestAnimationFrame(() => scrollConversationToBottom('auto')); - } catch (error: any) { - const detail = String(error?.response?.data?.detail || '').trim(); + } catch (error: unknown) { + const detail = resolveApiErrorMessage(error, '').trim(); if (!cancelled && detail) { notify(detail, { tone: 'error' }); } diff --git a/frontend/src/modules/dashboard/hooks/useDashboardMcpConfig.ts b/frontend/src/modules/dashboard/hooks/useDashboardMcpConfig.ts index e6c6eb5..20046c1 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardMcpConfig.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardMcpConfig.ts @@ -1,6 +1,7 @@ import { useCallback, useState } from 'react'; import { createMcpManager } from '../config-managers/mcpManager'; +import type { DashboardLabels, DashboardSelectedBot } from '../localeTypes'; import type { MCPServerDraft } from '../types'; type PromptTone = 'info' | 'success' | 'warning' | 'error'; @@ -25,9 +26,9 @@ interface UseDashboardMcpConfigOptions { isZh: boolean; notify: (message: string, options?: NotifyOptions) => void; passwordToggleLabels: { show: string; hide: string }; - selectedBot?: any; + selectedBot?: DashboardSelectedBot; selectedBotId: string; - t: any; + t: DashboardLabels; } export function useDashboardMcpConfig({ diff --git a/frontend/src/modules/dashboard/hooks/useDashboardRuntimeControl.ts b/frontend/src/modules/dashboard/hooks/useDashboardRuntimeControl.ts index e976a25..2118dbc 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardRuntimeControl.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardRuntimeControl.ts @@ -2,7 +2,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import type { BotState } from '../../../types/bot'; +import type { ChannelLabels, DashboardLabels } from '../localeTypes'; import type { BotResourceSnapshot, NanobotImage, WeixinLoginStatus } from '../types'; type PromptTone = 'info' | 'success' | 'warning' | 'error'; @@ -28,8 +30,8 @@ interface UseDashboardRuntimeControlOptions { selectedBotId: string; selectedBot?: BotState; isZh: boolean; - t: any; - lc: any; + t: DashboardLabels; + lc: ChannelLabels; mergeBot: (bot: BotState) => void; setBots: (bots: BotState[]) => void; updateBotStatus: (botId: string, dockerStatus: string) => void; @@ -112,8 +114,9 @@ export function useDashboardRuntimeControl({ try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/resources`); setResourceSnapshot(res.data); - } catch (error: any) { - const msg = error?.response?.data?.detail || (isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.'); + } catch (error: unknown) { + const fallback = isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.'; + const msg = resolveApiErrorMessage(error, fallback); setResourceError(String(msg)); } finally { setResourceLoading(false); @@ -126,7 +129,8 @@ export function useDashboardRuntimeControl({ void loadResourceSnapshot(botId); }, [loadResourceSnapshot]); - const loadWeixinLoginStatus = useCallback(async (botId: string, _silent: boolean = false) => { + const loadWeixinLoginStatus = useCallback(async (botId: string, silent?: boolean) => { + void silent; if (!botId) { setWeixinLoginStatus(null); return; @@ -169,8 +173,8 @@ export function useDashboardRuntimeControl({ notify(lc.weixinReloginDone, { tone: 'success' }); await loadWeixinLoginStatus(selectedBot.id); await refresh(); - } catch (error: any) { - notify(error?.response?.data?.detail || lc.weixinReloginFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, lc.weixinReloginFail), { tone: 'error' }); } }, [lc.weixinReloginDone, lc.weixinReloginFail, loadWeixinLoginStatus, notify, refresh, selectedBot]); @@ -289,9 +293,9 @@ export function useDashboardRuntimeControl({ updateBotStatus(id, 'RUNNING'); await refresh(); await ensureControlVisible(startedAt); - } catch (error: any) { + } catch (error: unknown) { await refresh(); - notify(error?.response?.data?.detail || t.startFail, { tone: 'error' }); + notify(resolveApiErrorMessage(error, t.startFail), { tone: 'error' }); await ensureControlVisible(startedAt); } finally { setOperatingBotId(null); @@ -320,9 +324,9 @@ export function useDashboardRuntimeControl({ updateBotStatus(id, 'RUNNING'); await refresh(); await ensureControlVisible(startedAt); - } catch (error: any) { + } catch (error: unknown) { await refresh(); - notify(error?.response?.data?.detail || t.restartFail, { tone: 'error' }); + notify(resolveApiErrorMessage(error, t.restartFail), { tone: 'error' }); await ensureControlVisible(startedAt); } finally { setOperatingBotId(null); @@ -347,8 +351,8 @@ export function useDashboardRuntimeControl({ } await refresh(); notify(enabled ? t.enableDone : t.disableDone, { tone: 'success' }); - } catch (error: any) { - notify(error?.response?.data?.detail || (enabled ? t.enableFail : t.disableFail), { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, enabled ? t.enableFail : t.disableFail), { tone: 'error' }); } finally { setOperatingBotId(null); clearControlState(id); diff --git a/frontend/src/modules/dashboard/hooks/useDashboardSkillsConfig.ts b/frontend/src/modules/dashboard/hooks/useDashboardSkillsConfig.ts index 68d6ee7..010e34e 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardSkillsConfig.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardSkillsConfig.ts @@ -1,23 +1,25 @@ import { useCallback, useEffect, useRef, useState, type ChangeEvent, type Dispatch, type SetStateAction } from 'react'; +import type { BotSkillMarketItem } from '../../platform/types'; +import type { DashboardLabels, DashboardSelectedBot } from '../localeTypes'; import type { WorkspaceSkillOption } from '../types'; import { formatBytes } from '../utils'; interface UseDashboardSkillsConfigOptions { botSkills: WorkspaceSkillOption[]; closeRuntimeMenu: () => void; - installMarketSkill: (skill: any) => Promise; + installMarketSkill: (skill: BotSkillMarketItem) => Promise; isMarketSkillsLoading: boolean; isSkillUploading: boolean; isZh: boolean; - labels: Record; + labels: DashboardLabels; loadBotSkills: (botId: string) => Promise; loadMarketSkills: (botId: string) => Promise; marketSkillInstallingId: number | null; - marketSkills: any[]; + marketSkills: BotSkillMarketItem[]; onPickSkillZip: (event: ChangeEvent) => Promise; removeBotSkill: (skill: WorkspaceSkillOption) => Promise; - selectedBot?: any; + selectedBot?: DashboardSelectedBot; } export function useDashboardSkillsConfig({ @@ -115,7 +117,7 @@ export function useDashboardSkillsConfig({ if (!selectedBot) return; await loadMarketSkills(selectedBot.id); }, - onInstall: async (skill: any) => { + onInstall: async (skill: BotSkillMarketItem) => { await installMarketSkill(skill); if (selectedBot) { await loadBotSkills(selectedBot.id); diff --git a/frontend/src/modules/dashboard/hooks/useDashboardSupportData.ts b/frontend/src/modules/dashboard/hooks/useDashboardSupportData.ts index e92a28f..2ddf0df 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardSupportData.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardSupportData.ts @@ -2,7 +2,9 @@ import { useCallback, useMemo, useState, type ChangeEvent } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import type { BotSkillMarketItem } from '../../platform/types'; +import type { DashboardLabels } from '../localeTypes'; import type { BotEnvParams, CronJob, @@ -34,7 +36,7 @@ interface ConfirmOptions { interface UseDashboardSupportDataOptions { selectedBotId: string; isZh: boolean; - t: any; + t: DashboardLabels; notify: (message: string, options?: NotifyOptions) => void; confirm: (options: ConfirmOptions) => Promise; onCloseEnvParamsModal?: () => void; @@ -138,12 +140,12 @@ export function useDashboardSupportData({ return merged; }); if (!append) setTopicFeedError(''); - } catch (error: any) { + } catch (error: unknown) { if (!append) { setTopicFeedItems([]); setTopicFeedNextCursor(null); } - setTopicFeedError(error?.response?.data?.detail || (isZh ? '读取主题消息失败。' : 'Failed to load topic feed.')); + setTopicFeedError(resolveApiErrorMessage(error, isZh ? '读取主题消息失败。' : 'Failed to load topic feed.')); } finally { if (append) { setTopicFeedLoadingMore(false); @@ -202,12 +204,12 @@ export function useDashboardSupportData({ try { await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/topic-items/${targetId}`); setTopicFeedItems((prev) => prev.filter((row) => Number(row.id) !== targetId)); - if (!Boolean(item?.is_read)) { + if (!item?.is_read) { setTopicFeedUnreadCount((prev) => Math.max(0, prev - 1)); } notify(isZh ? '主题消息已删除。' : 'Topic item deleted.', { tone: 'success' }); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '删除主题消息失败。' : 'Failed to delete topic item.'), { + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '删除主题消息失败。' : 'Failed to delete topic item.'), { tone: 'error', }); } finally { @@ -231,9 +233,9 @@ export function useDashboardSupportData({ try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/skill-market`); setMarketSkills(Array.isArray(res.data) ? res.data : []); - } catch (error: any) { + } catch (error: unknown) { setMarketSkills([]); - notify(error?.response?.data?.detail || t.toolsLoadFail, { tone: 'error' }); + notify(resolveApiErrorMessage(error, t.toolsLoadFail), { tone: 'error' }); } finally { setIsMarketSkillsLoading(false); } @@ -268,8 +270,8 @@ export function useDashboardSupportData({ } notify(t.envParamsSaved, { tone: 'success' }); return true; - } catch (error: any) { - notify(error?.response?.data?.detail || t.envParamsSaveFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.envParamsSaveFail), { tone: 'error' }); return false; } }, [notify, onCloseEnvParamsModal, selectedBotId, t.envParamsSaveFail, t.envParamsSaved]); @@ -320,10 +322,10 @@ export function useDashboardSupportData({ await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/skills/${encodeURIComponent(skill.id)}`); await loadBotSkills(selectedBotId); await loadMarketSkills(selectedBotId); - } catch (error: any) { - notify(error?.response?.data?.detail || t.toolsRemoveFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.toolsRemoveFail), { tone: 'error' }); } - }, [confirm, loadBotSkills, loadMarketSkills, notify, selectedBotId, t.removeSkill, t.toolsRemoveConfirm, t.toolsRemoveFail]); + }, [confirm, loadBotSkills, loadMarketSkills, notify, selectedBotId, t]); const installMarketSkill = useCallback(async (marketSkill: BotSkillMarketItem) => { if (!selectedBotId) return; @@ -342,8 +344,8 @@ export function useDashboardSupportData({ : `Installed: ${(res.data?.installed || []).join(', ') || marketSkill.display_name || marketSkill.skill_key}`, { tone: 'success' }, ); - } catch (error: any) { - notify(error?.response?.data?.detail || t.toolsAddFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.toolsAddFail), { tone: 'error' }); await loadMarketSkills(selectedBotId); } finally { setMarketSkillInstallingId(null); @@ -370,8 +372,8 @@ export function useDashboardSupportData({ const nextSkills = Array.isArray(res.data?.skills) ? res.data.skills : []; setBotSkills(nextSkills); await loadMarketSkills(selectedBotId); - } catch (error: any) { - notify(error?.response?.data?.detail || t.toolsAddFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.toolsAddFail), { tone: 'error' }); } finally { setIsSkillUploading(false); event.target.value = ''; @@ -400,8 +402,8 @@ export function useDashboardSupportData({ try { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}/start`); await loadCronJobs(selectedBotId); - } catch (error: any) { - notify(error?.response?.data?.detail || t.cronStartFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.cronStartFail), { tone: 'error' }); } finally { setCronActionJobId(''); setCronActionType(''); @@ -415,8 +417,8 @@ export function useDashboardSupportData({ try { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}/stop`); await loadCronJobs(selectedBotId); - } catch (error: any) { - notify(error?.response?.data?.detail || t.cronStopFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.cronStopFail), { tone: 'error' }); } finally { setCronActionJobId(''); setCronActionType(''); @@ -436,13 +438,13 @@ export function useDashboardSupportData({ try { await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}`); await loadCronJobs(selectedBotId); - } catch (error: any) { - notify(error?.response?.data?.detail || t.cronDeleteFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.cronDeleteFail), { tone: 'error' }); } finally { setCronActionJobId(''); setCronActionType(''); } - }, [confirm, loadCronJobs, notify, selectedBotId, t.cronDelete, t.cronDeleteConfirm, t.cronDeleteFail]); + }, [confirm, loadCronJobs, notify, selectedBotId, t]); return { botSkills, diff --git a/frontend/src/modules/dashboard/hooks/useDashboardTemplateManager.ts b/frontend/src/modules/dashboard/hooks/useDashboardTemplateManager.ts index e5298ad..1dda394 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardTemplateManager.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardTemplateManager.ts @@ -1,5 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; +import type { DashboardLabels } from '../localeTypes'; import { fetchDashboardSystemTemplates, updateDashboardSystemTemplates } from '../api/system'; import { parseTopicPresets } from '../topic/topicPresetUtils'; import type { TopicPresetTemplate } from '../types'; @@ -16,7 +17,7 @@ interface UseDashboardTemplateManagerOptions { currentTopicPresetCount: number; notify: (message: string, options?: NotifyOptions) => void; setTopicPresetTemplates: (value: TopicPresetTemplate[]) => void; - t: any; + t: DashboardLabels; } function countAgentTemplateFields(raw: string) { @@ -109,8 +110,9 @@ export function useDashboardTemplateManager({ topic_presets: parsedTopic, }; } - } catch (error: any) { - notify(error?.message || t.templateParseFail, { tone: 'error' }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : t.templateParseFail; + notify(message || t.templateParseFail, { tone: 'error' }); return; } diff --git a/frontend/src/modules/dashboard/hooks/useDashboardTopicConfig.ts b/frontend/src/modules/dashboard/hooks/useDashboardTopicConfig.ts index dc55f61..b9ec1b7 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardTopicConfig.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardTopicConfig.ts @@ -1,6 +1,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { createTopicManager } from '../config-managers/topicManager'; +import type { DashboardLabels, DashboardSelectedBot } from '../localeTypes'; import { resolvePresetText } from '../topic/topicPresetUtils'; import type { BotTopic, TopicPresetTemplate } from '../types'; @@ -26,9 +27,9 @@ interface UseDashboardTopicConfigOptions { effectiveTopicPresetTemplates: TopicPresetTemplate[]; isZh: boolean; notify: (message: string, options?: NotifyOptions) => void; - selectedBot?: any; + selectedBot?: DashboardSelectedBot; selectedBotId: string; - t: any; + t: DashboardLabels; } export function useDashboardTopicConfig({ diff --git a/frontend/src/modules/dashboard/hooks/useDashboardVoiceInput.ts b/frontend/src/modules/dashboard/hooks/useDashboardVoiceInput.ts index 58117e1..def00c9 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardVoiceInput.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardVoiceInput.ts @@ -2,7 +2,9 @@ import { useEffect, useRef, useState, type Dispatch, type RefObject, type SetSta import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import { normalizeUserMessageText } from '../../../shared/text/messageText'; +import type { DashboardLabels } from '../localeTypes'; type PromptTone = 'info' | 'success' | 'warning' | 'error'; @@ -20,7 +22,7 @@ interface UseDashboardVoiceInputOptions { setCommand: Dispatch>; composerTextareaRef: RefObject; notify: (message: string, options?: NotifyOptions) => void; - t: any; + t: DashboardLabels; } export function useDashboardVoiceInput({ @@ -89,13 +91,11 @@ export function useDashboardVoiceInput({ }); window.requestAnimationFrame(() => composerTextareaRef.current?.focus()); notify(t.voiceTranscribeDone, { tone: 'success' }); - } catch (error: any) { - const msg = String(error?.response?.data?.detail || '').trim(); + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, '').trim(); console.error('Speech transcription failed', { botId, message: msg || t.voiceTranscribeFail, - status: error?.response?.status, - response: error?.response?.data, error, }); notify(msg || t.voiceTranscribeFail, { tone: 'error' }); diff --git a/frontend/src/modules/dashboard/localeTypes.ts b/frontend/src/modules/dashboard/localeTypes.ts new file mode 100644 index 0000000..65ec36d --- /dev/null +++ b/frontend/src/modules/dashboard/localeTypes.ts @@ -0,0 +1,17 @@ +import { channelsEn } from '../../i18n/channels.en'; +import { dashboardEn } from '../../i18n/dashboard.en'; +import type { BotState } from '../../types/bot'; + +type WidenLocaleValue = + T extends string ? string + : T extends number ? number + : T extends boolean ? boolean + : T extends (...args: infer Args) => infer Result ? (...args: Args) => Result + : T extends readonly (infer Item)[] ? WidenLocaleValue[] + : T extends object ? { [K in keyof T]: WidenLocaleValue } + : T; + +export type DashboardLabels = WidenLocaleValue; +export type ChannelLabels = WidenLocaleValue; +export type DashboardChannelLabels = DashboardLabels & ChannelLabels; +export type DashboardSelectedBot = BotState | null | undefined; diff --git a/frontend/src/modules/dashboard/topic/TopicFeedPanel.tsx b/frontend/src/modules/dashboard/topic/TopicFeedPanel.tsx index 50f1ec5..3af09ab 100644 --- a/frontend/src/modules/dashboard/topic/TopicFeedPanel.tsx +++ b/frontend/src/modules/dashboard/topic/TopicFeedPanel.tsx @@ -274,7 +274,7 @@ export function TopicFeedPanel({ const itemId = Number(item.id || 0); const level = String(item.level || 'info').trim().toLowerCase(); const levelText = level === 'warn' ? 'WARN' : level === 'error' ? 'ERROR' : level === 'success' ? 'SUCCESS' : 'INFO'; - const unread = !Boolean(item.is_read); + const unread = !item.is_read; const card = deriveTopicSummaryCard(item); const rawContent = String(item.content || '').trim(); return ( diff --git a/frontend/src/modules/dashboard/types.ts b/frontend/src/modules/dashboard/types.ts index 2e14904..cbce824 100644 --- a/frontend/src/modules/dashboard/types.ts +++ b/frontend/src/modules/dashboard/types.ts @@ -22,8 +22,17 @@ export type StagedSubmissionDraft = { }; export type BotEnvParams = Record; +export interface BotMessageResponseRow { + id?: number | string | null; + role?: string | null; + feedback?: string | null; + text?: string | null; + media?: unknown; + ts?: number | string | null; +} + export interface BotMessagesByDateResponse { - items?: any[]; + items?: BotMessageResponseRow[]; anchor_id?: number | null; resolved_ts?: number | null; matched_exact_date?: boolean; diff --git a/frontend/src/modules/images/ImageFactoryModule.tsx b/frontend/src/modules/images/ImageFactoryModule.tsx index cf52af1..4ca79e3 100644 --- a/frontend/src/modules/images/ImageFactoryModule.tsx +++ b/frontend/src/modules/images/ImageFactoryModule.tsx @@ -2,11 +2,12 @@ import { useEffect, useMemo, useState } from 'react'; import axios from 'axios'; import { RefreshCw, Trash2 } from 'lucide-react'; import { APP_ENDPOINTS } from '../../config/env'; +import { resolveApiErrorMessage } from '../../shared/http/apiErrors'; import { useAppStore } from '../../store/appStore'; import { pickLocale } from '../../i18n'; -import { imageFactoryZhCn } from '../../i18n/image-factory.zh-cn'; import { imageFactoryEn } from '../../i18n/image-factory.en'; -import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; +import { imageFactoryZhCn } from '../../i18n/image-factory.zh-cn'; +import { useLucentPrompt } from '../../components/lucent/useLucentPrompt'; import { LucentIconButton } from '../../components/lucent/LucentIconButton'; interface NanobotImage { @@ -100,8 +101,8 @@ export function ImageFactoryModule() { source_dir: 'manual', }); await fetchData(); - } catch (error: any) { - const msg = error?.response?.data?.detail || t.registerFail; + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, t.registerFail); notify(msg, { tone: 'error' }); } finally { setIsRegisteringTag(''); @@ -120,8 +121,8 @@ export function ImageFactoryModule() { try { await axios.delete(`${APP_ENDPOINTS.apiBase}/images/${encodeURIComponent(tag)}`); await fetchData(); - } catch (error: any) { - const msg = error?.response?.data?.detail || t.deleteFail; + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, t.deleteFail); notify(msg, { tone: 'error' }); } finally { setIsDeletingTag(''); diff --git a/frontend/src/modules/onboarding/BotWizardModule.tsx b/frontend/src/modules/onboarding/BotWizardModule.tsx index aa4aae5..cdfef01 100644 --- a/frontend/src/modules/onboarding/BotWizardModule.tsx +++ b/frontend/src/modules/onboarding/BotWizardModule.tsx @@ -5,7 +5,7 @@ import { channelsZhCn } from '../../i18n/channels.zh-cn'; import { wizardEn } from '../../i18n/wizard.en'; import { wizardZhCn } from '../../i18n/wizard.zh-cn'; import { LucentIconButton } from '../../components/lucent/LucentIconButton'; -import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; +import { useLucentPrompt } from '../../components/lucent/useLucentPrompt'; import { useAppStore } from '../../store/appStore'; import { BotWizardAgentStep } from './components/BotWizardAgentStep'; import { BotWizardBaseStep } from './components/BotWizardBaseStep'; diff --git a/frontend/src/modules/onboarding/components/BotWizardBaseStep.tsx b/frontend/src/modules/onboarding/components/BotWizardBaseStep.tsx index 85a1a5d..0f60efb 100644 --- a/frontend/src/modules/onboarding/components/BotWizardBaseStep.tsx +++ b/frontend/src/modules/onboarding/components/BotWizardBaseStep.tsx @@ -5,12 +5,13 @@ import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentSelect } from '../../../components/lucent/LucentSelect'; import { PasswordInput } from '../../../components/PasswordInput'; import { buildLlmProviderOptions } from '../../../utils/llmProviders'; +import type { OnboardingChannelLabels, WizardLabels } from '../localeTypes'; import type { BotWizardForm } from '../types'; interface BotWizardBaseStepProps { isZh: boolean; - ui: any; - lc: any; + ui: WizardLabels; + lc: OnboardingChannelLabels; passwordToggleLabels: { show: string; hide: string }; form: BotWizardForm; setForm: Dispatch>; diff --git a/frontend/src/modules/onboarding/components/BotWizardChannelModal.tsx b/frontend/src/modules/onboarding/components/BotWizardChannelModal.tsx index f627ee3..cfa24ee 100644 --- a/frontend/src/modules/onboarding/components/BotWizardChannelModal.tsx +++ b/frontend/src/modules/onboarding/components/BotWizardChannelModal.tsx @@ -3,12 +3,13 @@ import { Plus, Trash2 } from 'lucide-react'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentSelect } from '../../../components/lucent/LucentSelect'; import { PasswordInput } from '../../../components/PasswordInput'; +import type { OnboardingChannelLabels } from '../localeTypes'; import type { ChannelType, WizardChannelConfig } from '../types'; import { EMPTY_CHANNEL_PICKER } from '../types'; interface BotWizardChannelModalProps { open: boolean; - lc: any; + lc: OnboardingChannelLabels; passwordToggleLabels: { show: string; hide: string }; channels: WizardChannelConfig[]; sendProgress: boolean; @@ -32,7 +33,7 @@ function renderChannelFields({ }: { channel: WizardChannelConfig; idx: number; - lc: any; + lc: OnboardingChannelLabels; passwordToggleLabels: { show: string; hide: string }; onUpdateChannel: (index: number, patch: Partial) => void; }) { diff --git a/frontend/src/modules/onboarding/components/BotWizardImageStep.tsx b/frontend/src/modules/onboarding/components/BotWizardImageStep.tsx index 43d56d1..1fafd36 100644 --- a/frontend/src/modules/onboarding/components/BotWizardImageStep.tsx +++ b/frontend/src/modules/onboarding/components/BotWizardImageStep.tsx @@ -1,9 +1,10 @@ import type { Dispatch, SetStateAction } from 'react'; +import type { WizardLabels } from '../localeTypes'; import type { BotWizardForm, NanobotImage } from '../types'; interface BotWizardImageStepProps { - ui: any; + ui: WizardLabels; readyImages: NanobotImage[]; isLoadingImages: boolean; form: BotWizardForm; diff --git a/frontend/src/modules/onboarding/components/BotWizardReviewStep.tsx b/frontend/src/modules/onboarding/components/BotWizardReviewStep.tsx index a01a8b0..a0ba3b4 100644 --- a/frontend/src/modules/onboarding/components/BotWizardReviewStep.tsx +++ b/frontend/src/modules/onboarding/components/BotWizardReviewStep.tsx @@ -1,8 +1,9 @@ +import type { WizardLabels } from '../localeTypes'; import type { BotWizardForm } from '../types'; interface BotWizardReviewStepProps { isZh: boolean; - ui: any; + ui: WizardLabels; autoStart: boolean; setAutoStart: (value: boolean) => void; form: BotWizardForm; diff --git a/frontend/src/modules/onboarding/components/BotWizardToolsModal.tsx b/frontend/src/modules/onboarding/components/BotWizardToolsModal.tsx index 2cfa20d..ad24cdd 100644 --- a/frontend/src/modules/onboarding/components/BotWizardToolsModal.tsx +++ b/frontend/src/modules/onboarding/components/BotWizardToolsModal.tsx @@ -2,10 +2,11 @@ import { Plus, Trash2 } from 'lucide-react'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { PasswordInput } from '../../../components/PasswordInput'; +import type { WizardLabels } from '../localeTypes'; interface BotWizardToolsModalProps { open: boolean; - ui: any; + ui: WizardLabels; closeLabel: string; envEntries: Array<[string, string]>; envDraftKey: string; diff --git a/frontend/src/modules/onboarding/hooks/useBotWizard.ts b/frontend/src/modules/onboarding/hooks/useBotWizard.ts index d49572b..64303ce 100644 --- a/frontend/src/modules/onboarding/hooks/useBotWizard.ts +++ b/frontend/src/modules/onboarding/hooks/useBotWizard.ts @@ -2,8 +2,10 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders'; import { getSystemTimezoneOptions } from '../../../utils/systemTimezones'; +import type { WizardLabels } from '../localeTypes'; import type { AgentTab, BotWizardForm, @@ -26,7 +28,7 @@ interface NotifyOptions { } interface UseBotWizardOptions { - ui: any; + ui: WizardLabels; notify: (message: string, options?: NotifyOptions) => void; onCreated?: () => void; onGoDashboard?: () => void; @@ -172,8 +174,8 @@ export function useBotWizard({ await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(raw)}`); setBotIdStatus('exists'); setBotIdStatusText(ui.botIdExists); - } catch (error: any) { - if (error?.response?.status === 404) { + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response?.status === 404) { setBotIdStatus('available'); setBotIdStatusText(ui.botIdAvailable); return; @@ -292,8 +294,8 @@ export function useBotWizard({ } else { setTestResult(ui.connFailed(res.data?.detail || 'unknown error')); } - } catch (error: any) { - const msg = error?.response?.data?.detail || error?.message || 'request failed'; + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, 'request failed'); setTestResult(ui.connFailed(msg)); } finally { setIsTestingProvider(false); @@ -355,12 +357,12 @@ export function useBotWizard({ setBotIdStatus('idle'); setBotIdStatusText(''); notify(ui.created, { tone: 'success' }); - } catch (error: any) { - if (axios.isCancel(error) || error?.code === 'ERR_CANCELED') { + } catch (error: unknown) { + if (axios.isAxiosError(error) && (axios.isCancel(error) || error.code === 'ERR_CANCELED')) { notify(ui.createCanceled, { tone: 'info' }); return; } - const msg = error?.response?.data?.detail || ui.createFailed; + const msg = resolveApiErrorMessage(error, ui.createFailed); notify(msg, { tone: 'error' }); } finally { if (createAbortControllerRef.current === controller) { diff --git a/frontend/src/modules/onboarding/localeTypes.ts b/frontend/src/modules/onboarding/localeTypes.ts new file mode 100644 index 0000000..1cfe683 --- /dev/null +++ b/frontend/src/modules/onboarding/localeTypes.ts @@ -0,0 +1,14 @@ +import { channelsEn } from '../../i18n/channels.en'; +import { wizardEn } from '../../i18n/wizard.en'; + +type WidenLocaleValue = + T extends string ? string + : T extends number ? number + : T extends boolean ? boolean + : T extends (...args: infer Args) => infer Result ? (...args: Args) => Result + : T extends readonly (infer Item)[] ? WidenLocaleValue[] + : T extends object ? { [K in keyof T]: WidenLocaleValue } + : T; + +export type WizardLabels = WidenLocaleValue; +export type OnboardingChannelLabels = WidenLocaleValue; diff --git a/frontend/src/modules/platform/PlatformLoginLogPage.tsx b/frontend/src/modules/platform/PlatformLoginLogPage.tsx index 01fb216..abf7e44 100644 --- a/frontend/src/modules/platform/PlatformLoginLogPage.tsx +++ b/frontend/src/modules/platform/PlatformLoginLogPage.tsx @@ -1,11 +1,12 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { ChevronLeft, ChevronRight, RefreshCw, ShieldCheck } from 'lucide-react'; import '../../components/skill-market/SkillMarketShared.css'; import { ProtectedSearchInput } from '../../components/ProtectedSearchInput'; import { LucentIconButton } from '../../components/lucent/LucentIconButton'; import { LucentSelect } from '../../components/lucent/LucentSelect'; -import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; +import { useLucentPrompt } from '../../components/lucent/useLucentPrompt'; +import { resolveApiErrorMessage } from '../../shared/http/apiErrors'; import { fetchPlatformLoginLogs, fetchPreferredPlatformPageSize } from './api/settings'; import type { PlatformLoginLogItem } from './types'; import './PlatformDashboardPage.css'; @@ -65,7 +66,7 @@ export function PlatformLoginLogPage({ isZh }: PlatformLoginLogPageProps) { const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); - const loadRows = async () => { + const loadRows = useCallback(async () => { setLoading(true); try { const data = await fetchPlatformLoginLogs({ @@ -77,14 +78,14 @@ export function PlatformLoginLogPage({ isZh }: PlatformLoginLogPageProps) { }); setItems(Array.isArray(data?.items) ? data.items : []); setTotal(Number(data?.total || 0)); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '读取登录日志失败。' : 'Failed to load login logs.'), { + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '读取登录日志失败。' : 'Failed to load login logs.'), { tone: 'error', }); } finally { setLoading(false); } - }; + }, [authType, isZh, notify, page, pageSize, search, status]); useEffect(() => { void loadRows(); diff --git a/frontend/src/modules/platform/api/skills.ts b/frontend/src/modules/platform/api/skills.ts index e8d01e4..9dcae2c 100644 --- a/frontend/src/modules/platform/api/skills.ts +++ b/frontend/src/modules/platform/api/skills.ts @@ -3,7 +3,7 @@ import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; import type { SkillMarketItem } from '../types'; -export interface SkillMarketListResponse extends Array {} +export type SkillMarketListResponse = SkillMarketItem[]; export function fetchPlatformSkillMarket() { return axios.get(`${APP_ENDPOINTS.apiBase}/platform/skills`).then((res) => { diff --git a/frontend/src/modules/platform/components/PlatformBotRuntimeSection.tsx b/frontend/src/modules/platform/components/PlatformBotRuntimeSection.tsx index a2ff1d5..a638d09 100644 --- a/frontend/src/modules/platform/components/PlatformBotRuntimeSection.tsx +++ b/frontend/src/modules/platform/components/PlatformBotRuntimeSection.tsx @@ -3,7 +3,7 @@ import { ChevronLeft, ChevronRight, RefreshCw, Terminal } from 'lucide-react'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; -import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; +import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt'; import { dashboardEn } from '../../../i18n/dashboard.en'; import { dashboardZhCn } from '../../../i18n/dashboard.zh-cn'; import { pickLocale } from '../../../i18n'; diff --git a/frontend/src/modules/platform/components/PlatformSettingsPage.tsx b/frontend/src/modules/platform/components/PlatformSettingsPage.tsx index 45b039f..adbf8af 100644 --- a/frontend/src/modules/platform/components/PlatformSettingsPage.tsx +++ b/frontend/src/modules/platform/components/PlatformSettingsPage.tsx @@ -1,11 +1,12 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { ChevronLeft, ChevronRight, Pencil, Plus, RefreshCw, Settings2, Trash2 } from 'lucide-react'; import '../../../components/skill-market/SkillMarketShared.css'; import { DrawerShell } from '../../../components/DrawerShell'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; -import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; +import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import type { PlatformSettings, SystemSettingItem } from '../types'; import { createPlatformSystemSetting, @@ -88,19 +89,19 @@ function PlatformSettingsView({ const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); - const loadRows = async () => { + const loadRows = useCallback(async () => { setLoading(true); try { const data = await fetchPlatformSystemSettings(); setItems(Array.isArray(data?.items) ? data.items : []); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '读取系统参数失败。' : 'Failed to load system settings.'), { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '读取系统参数失败。' : 'Failed to load system settings.'), { tone: 'error' }); } finally { setLoading(false); } - }; + }, [isZh, notify]); - const refreshSnapshot = async () => { + const refreshSnapshot = useCallback(async () => { try { const data = await fetchPlatformSettings(); onSaved(data); @@ -108,14 +109,14 @@ function PlatformSettingsView({ // Ignore snapshot refresh failures here; the table is still the source of truth in the view. } setPageSize(await fetchPreferredPlatformPageSize(10)); - }; + }, [onSaved]); useEffect(() => { setPage(1); void (async () => { await Promise.allSettled([loadRows(), refreshSnapshot()]); })(); - }, []); + }, [loadRows, refreshSnapshot]); useEffect(() => { setPage(1); @@ -159,10 +160,10 @@ function PlatformSettingsView({ await refreshSnapshot(); notify(isZh ? '系统参数已保存。' : 'System setting saved.', { tone: 'success' }); setShowEditor(false); - } catch (error: any) { + } catch (error: unknown) { const detail = error instanceof SyntaxError ? (isZh ? 'JSON 参数格式错误。' : 'Invalid JSON value.') - : (error?.response?.data?.detail || (isZh ? '保存参数失败。' : 'Failed to save setting.')); + : resolveApiErrorMessage(error, isZh ? '保存参数失败。' : 'Failed to save setting.'); notify(detail, { tone: 'error' }); } finally { setSaving(false); @@ -268,8 +269,8 @@ function PlatformSettingsView({ await deletePlatformSystemSetting(item.key); await loadRows(); await refreshSnapshot(); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '删除失败。' : 'Delete failed.'), { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '删除失败。' : 'Delete failed.'), { tone: 'error' }); } })(); }} diff --git a/frontend/src/modules/platform/components/SkillMarketManagerPage.tsx b/frontend/src/modules/platform/components/SkillMarketManagerPage.tsx index 0546be6..2dd8f18 100644 --- a/frontend/src/modules/platform/components/SkillMarketManagerPage.tsx +++ b/frontend/src/modules/platform/components/SkillMarketManagerPage.tsx @@ -1,11 +1,12 @@ -import { useEffect, useMemo, useState, type ChangeEvent } from 'react'; +import { useCallback, useEffect, useMemo, useState, type ChangeEvent } from 'react'; import { ChevronLeft, ChevronRight, FileArchive, Hammer, Pencil, Plus, RefreshCw, Trash2, Upload } from 'lucide-react'; import '../../../components/skill-market/SkillMarketShared.css'; import { DrawerShell } from '../../../components/DrawerShell'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import type { SkillMarketItem } from '../types'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; -import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; +import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import { fetchPreferredPlatformPageSize } from '../api/settings'; import { createPlatformSkillMarketItem, @@ -74,21 +75,21 @@ function SkillMarketManagerView({ ? '技能市场仅接收人工上传的 ZIP 技能包。平台将统一保存技能元数据与归档文件,并为 Bot 安装提供标准化来源,不再自动扫描 /data/skills 目录。' : 'The marketplace accepts manually uploaded ZIP skill packages only. The platform stores the package metadata and archives as the standard source for bot installation, without auto-scanning /data/skills.'; - const loadRows = async () => { + const loadRows = useCallback(async () => { setLoading(true); try { const rows = await fetchPlatformSkillMarket(); setItems(rows); return rows; - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '读取技能市场失败。' : 'Failed to load the skill marketplace.'), { + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '读取技能市场失败。' : 'Failed to load the skill marketplace.'), { tone: 'error', }); return [] as SkillMarketItem[]; } finally { setLoading(false); } - }; + }, [isZh, notify]); useEffect(() => { void loadRows(); @@ -96,7 +97,7 @@ function SkillMarketManagerView({ void (async () => { setPageSize(await fetchPreferredPlatformPageSize(10)); })(); - }, []); + }, [loadRows]); useEffect(() => { setPage(1); @@ -167,8 +168,8 @@ function SkillMarketManagerView({ setPage(Math.floor(nextIndex / pageSize) + 1); } } - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '保存技能失败。' : 'Failed to save skill.'), { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '保存技能失败。' : 'Failed to save skill.'), { tone: 'error' }); } finally { setSaving(false); } @@ -190,8 +191,8 @@ function SkillMarketManagerView({ } await loadRows(); notify(isZh ? '技能已删除。' : 'Skill deleted.', { tone: 'success' }); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '删除技能失败。' : 'Failed to delete skill.'), { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '删除技能失败。' : 'Failed to delete skill.'), { tone: 'error' }); } }; diff --git a/frontend/src/modules/platform/components/TemplateManagerPage.tsx b/frontend/src/modules/platform/components/TemplateManagerPage.tsx index 5804c52..e949205 100644 --- a/frontend/src/modules/platform/components/TemplateManagerPage.tsx +++ b/frontend/src/modules/platform/components/TemplateManagerPage.tsx @@ -1,9 +1,10 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { FileText, RefreshCw } from 'lucide-react'; import '../../../components/skill-market/SkillMarketShared.css'; -import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; +import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt'; import { MarkdownLiteEditor } from '../../../components/markdown/MarkdownLiteEditor'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import { fetchPlatformSystemTemplates, updatePlatformSystemTemplates } from '../api/templates'; interface TemplateManagerPageProps { @@ -71,33 +72,45 @@ function TemplateManagerView({ const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); + const applyTemplates = useCallback((res: Awaited>) => { + setTemplates({ + soul_md: String(res?.agent_md_templates?.soul_md || ''), + agents_md: String(res?.agent_md_templates?.agents_md || ''), + user_md: String(res?.agent_md_templates?.user_md || ''), + tools_md: String(res?.agent_md_templates?.tools_md || ''), + identity_md: String(res?.agent_md_templates?.identity_md || ''), + }); + setTopicPresetsText(JSON.stringify(res?.topic_presets || { presets: [] }, null, 2)); + }, []); + + const reloadTemplates = useCallback(async () => { + setLoading(true); + try { + const res = await fetchPlatformSystemTemplates(); + applyTemplates(res); + return true; + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '读取模板失败。' : 'Failed to load templates.'), { tone: 'error' }); + return false; + } finally { + setLoading(false); + } + }, [applyTemplates, isZh, notify]); + useEffect(() => { let alive = true; const load = async () => { - setLoading(true); - try { - const res = await fetchPlatformSystemTemplates(); - if (!alive) return; - setTemplates({ - soul_md: String(res?.agent_md_templates?.soul_md || ''), - agents_md: String(res?.agent_md_templates?.agents_md || ''), - user_md: String(res?.agent_md_templates?.user_md || ''), - tools_md: String(res?.agent_md_templates?.tools_md || ''), - identity_md: String(res?.agent_md_templates?.identity_md || ''), - }); - setTopicPresetsText(JSON.stringify(res?.topic_presets || { presets: [] }, null, 2)); + const ok = await reloadTemplates(); + if (!alive || !ok) return; + if (alive) { setActiveTab('agents_md'); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '读取模板失败。' : 'Failed to load templates.'), { tone: 'error' }); - } finally { - if (alive) setLoading(false); } }; void load(); return () => { alive = false; }; - }, [isZh, notify]); + }, [reloadTemplates]); const activeMeta = useMemo(() => templateMeta[activeTab], [activeTab]); @@ -144,24 +157,7 @@ function TemplateManagerView({ className="btn btn-secondary btn-sm skill-market-button-with-icon" type="button" disabled={loading} - onClick={() => void (async () => { - setLoading(true); - try { - const res = await fetchPlatformSystemTemplates(); - setTemplates({ - soul_md: String(res?.agent_md_templates?.soul_md || ''), - agents_md: String(res?.agent_md_templates?.agents_md || ''), - user_md: String(res?.agent_md_templates?.user_md || ''), - tools_md: String(res?.agent_md_templates?.tools_md || ''), - identity_md: String(res?.agent_md_templates?.identity_md || ''), - }); - setTopicPresetsText(JSON.stringify(res?.topic_presets || { presets: [] }, null, 2)); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '刷新模板失败。' : 'Failed to refresh templates.'), { tone: 'error' }); - } finally { - setLoading(false); - } - })()} + onClick={() => void reloadTemplates()} > {loading ? : null} {isZh ? '重载' : 'Reload'} @@ -203,10 +199,10 @@ function TemplateManagerView({ topic_presets: topicPresets, }); notify(isZh ? '模版已保存。' : 'Templates saved.', { tone: 'success' }); - } catch (error: any) { + } catch (error: unknown) { const detail = error instanceof SyntaxError ? (isZh ? 'Topic 预设 JSON 解析失败。' : 'Invalid topic presets JSON.') - : (error?.response?.data?.detail || (isZh ? '保存模板失败。' : 'Failed to save templates.')); + : resolveApiErrorMessage(error, isZh ? '保存模板失败。' : 'Failed to save templates.'); notify(detail, { tone: 'error' }); } finally { setSaving(false); diff --git a/frontend/src/modules/platform/hooks/usePlatformDashboard.ts b/frontend/src/modules/platform/hooks/usePlatformDashboard.ts index b784950..67563d2 100644 --- a/frontend/src/modules/platform/hooks/usePlatformDashboard.ts +++ b/frontend/src/modules/platform/hooks/usePlatformDashboard.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo, useState } from 'react'; import axios from 'axios'; -import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; +import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt'; import { APP_ENDPOINTS } from '../../../config/env'; import { sortBotsByCreatedAtDesc } from '../../../shared/bot/sortBots'; import { useAppStore } from '../../../store/appStore'; diff --git a/frontend/src/modules/platform/hooks/usePlatformManagementState.ts b/frontend/src/modules/platform/hooks/usePlatformManagementState.ts index 5065e61..c7ad351 100644 --- a/frontend/src/modules/platform/hooks/usePlatformManagementState.ts +++ b/frontend/src/modules/platform/hooks/usePlatformManagementState.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import type { BotChannel, MCPConfigResponse, WorkspaceSkillOption } from '../../dashboard/types'; import type { BotState } from '../../../types/bot'; import type { PlatformBotResourceSnapshot, PlatformUsageResponse } from '../types'; @@ -147,8 +148,8 @@ export function usePlatformManagementState({ try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/resources`); setResourceSnapshot(res.data); - } catch (error: any) { - const msg = error?.response?.data?.detail || (isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.'); + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.'); setResourceError(String(msg)); } finally { setResourceLoading(false); diff --git a/frontend/src/modules/platform/hooks/usePlatformOverviewState.ts b/frontend/src/modules/platform/hooks/usePlatformOverviewState.ts index f097fb3..0c40b94 100644 --- a/frontend/src/modules/platform/hooks/usePlatformOverviewState.ts +++ b/frontend/src/modules/platform/hooks/usePlatformOverviewState.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../../config/env'; +import { resolveApiErrorMessage } from '../../../shared/http/apiErrors'; import { normalizePlatformPageSize, readCachedPlatformPageSize, @@ -49,8 +50,8 @@ export function usePlatformOverviewState({ ); writeCachedPlatformPageSize(normalizedPageSize); setPlatformPageSize(normalizedPageSize); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' }); } finally { setLoading(false); } @@ -66,8 +67,8 @@ export function usePlatformOverviewState({ }, }); setUsageData(res.data); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '读取用量统计失败。' : 'Failed to load usage analytics.'), { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '读取用量统计失败。' : 'Failed to load usage analytics.'), { tone: 'error' }); } finally { setUsageLoading(false); } @@ -78,8 +79,8 @@ export function usePlatformOverviewState({ try { const res = await axios.get<{ items: BotActivityStatsItem[] }>(`${APP_ENDPOINTS.apiBase}/platform/activity-stats`); setActivityStatsData(Array.isArray(res.data?.items) ? res.data.items : []); - } catch (error: any) { - notify(error?.response?.data?.detail || (isZh ? '读取 Bot 活跃度统计失败。' : 'Failed to load bot activity analytics.'), { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, isZh ? '读取 Bot 活跃度统计失败。' : 'Failed to load bot activity analytics.'), { tone: 'error' }); } finally { setActivityLoading(false); } diff --git a/frontend/src/shared/http/apiErrors.ts b/frontend/src/shared/http/apiErrors.ts new file mode 100644 index 0000000..2913746 --- /dev/null +++ b/frontend/src/shared/http/apiErrors.ts @@ -0,0 +1,18 @@ +import axios from 'axios'; + +export interface ApiErrorDetail { + detail?: string; +} + +export function resolveApiErrorMessage(error: unknown, fallback: string): string { + if (axios.isAxiosError(error)) { + const detail = String(error.response?.data?.detail || '').trim(); + if (detail) return detail; + const message = String(error.message || '').trim(); + if (message) return message; + } else if (error instanceof Error) { + const message = String(error.message || '').trim(); + if (message) return message; + } + return fallback; +} diff --git a/frontend/src/shared/workspace/useBotWorkspace.ts b/frontend/src/shared/workspace/useBotWorkspace.ts index a7b06b0..eb9a4a0 100644 --- a/frontend/src/shared/workspace/useBotWorkspace.ts +++ b/frontend/src/shared/workspace/useBotWorkspace.ts @@ -15,7 +15,7 @@ import { } from './utils'; import type { WorkspaceAttachmentPolicySnapshot, WorkspaceNotifyOptions } from './workspaceShared'; import { useWorkspaceAttachments, type WorkspaceAttachmentLabels } from './useWorkspaceAttachments'; -import { useWorkspacePreview } from './useWorkspacePreview'; +import { useWorkspacePreview, type WorkspacePreviewLabels } from './useWorkspacePreview'; interface WorkspaceTreeLabels { workspaceLoadFail: string; @@ -45,7 +45,7 @@ interface UseBotWorkspaceOptions { refreshAttachmentPolicy: () => Promise; restorePendingAttachments?: (botId: string) => string[]; notify: (message: string, options?: WorkspaceNotifyOptions) => void; - t: WorkspaceTreeLabels & WorkspaceAttachmentLabels & Record; + t: WorkspaceTreeLabels & WorkspaceAttachmentLabels & WorkspacePreviewLabels & Record; isZh: boolean; fileNotPreviewableLabel: string; } diff --git a/frontend/src/shared/workspace/useWorkspacePreview.ts b/frontend/src/shared/workspace/useWorkspacePreview.ts index bd38530..b017262 100644 --- a/frontend/src/shared/workspace/useWorkspacePreview.ts +++ b/frontend/src/shared/workspace/useWorkspacePreview.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../config/env'; +import { resolveApiErrorMessage } from '../http/apiErrors'; import type { WorkspaceFileResponse, WorkspacePreviewMode, WorkspacePreviewState } from './types'; import { buildWorkspaceDownloadHref, @@ -18,13 +19,22 @@ import { } from './utils'; import type { WorkspaceNotifyOptions } from './workspaceShared'; +export interface WorkspacePreviewLabels { + fileReadFail: string; + fileEditDisabled: string; + fileSaveFail: string; + fileSaved: string; + urlCopied: string; + urlCopyFail: string; +} + interface UseWorkspacePreviewOptions { selectedBotId: string; workspaceCurrentPath: string; workspaceDownloadExtensionSet: ReadonlySet; loadWorkspaceTree: (botId: string, path?: string) => Promise; notify: (message: string, options?: WorkspaceNotifyOptions) => void; - t: any; + t: WorkspacePreviewLabels; isZh: boolean; } @@ -61,6 +71,8 @@ export function useWorkspacePreview({ const workspacePreviewCanEdit = Boolean(workspacePreview?.isMarkdown && !workspacePreview?.truncated); const workspacePreviewEditorEnabled = workspacePreviewCanEdit && workspacePreviewMode === 'edit'; + const workspacePreviewPath = workspacePreview?.path || ''; + const workspacePreviewContent = workspacePreview?.content || ''; const getWorkspaceDownloadHref = useCallback((filePath: string, forceDownload: boolean = true) => buildWorkspaceDownloadHref(selectedBotId, filePath, forceDownload), @@ -153,8 +165,8 @@ export function useWorkspacePreview({ isVideo: false, isAudio: false, }); - } catch (error: any) { - const msg = error?.response?.data?.detail || t.fileReadFail; + } catch (error: unknown) { + const msg = resolveApiErrorMessage(error, t.fileReadFail); notify(msg, { tone: 'error' }); } finally { setWorkspaceFileLoading(false); @@ -187,8 +199,8 @@ export function useWorkspacePreview({ }); notify(t.fileSaved, { tone: 'success' }); void loadWorkspaceTree(selectedBotId, workspaceCurrentPath); - } catch (error: any) { - notify(error?.response?.data?.detail || t.fileSaveFail, { tone: 'error' }); + } catch (error: unknown) { + notify(resolveApiErrorMessage(error, t.fileSaveFail), { tone: 'error' }); } finally { setWorkspacePreviewSaving(false); } @@ -266,15 +278,15 @@ export function useWorkspacePreview({ }, []); useEffect(() => { - if (!workspacePreview) { + if (!workspacePreviewPath) { setWorkspacePreviewMode('preview'); setWorkspacePreviewSaving(false); setWorkspacePreviewDraft(''); return; } setWorkspacePreviewSaving(false); - setWorkspacePreviewDraft(workspacePreview.content || ''); - }, [workspacePreview?.content, workspacePreview?.path]); + setWorkspacePreviewDraft(workspacePreviewContent); + }, [workspacePreviewContent, workspacePreviewPath]); return { closeWorkspacePreview, diff --git a/frontend/src/shared/workspace/workspaceMarkdown.tsx b/frontend/src/shared/workspace/workspaceMarkdown.tsx index c79c997..b2292b1 100644 --- a/frontend/src/shared/workspace/workspaceMarkdown.tsx +++ b/frontend/src/shared/workspace/workspaceMarkdown.tsx @@ -4,7 +4,7 @@ export const WORKSPACE_LINK_PREFIX = 'https://workspace.local/open/'; const WORKSPACE_ABS_PATH_PATTERN = /\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|markdown|json|txt|log|csv|tsv|yaml|yml|toml|html|htm|pdf|png|jpg|jpeg|gif|webp|svg|mp3|wav|m4a|flac|ogg|opus|aac|amr|wma|mp4|mov|avi|mkv|webm|m4v|3gp|mpeg|mpg|ts|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b/gi; const WORKSPACE_RELATIVE_PATH_PATTERN = - /(^|[\s(\[])(\/[^\n\r<>"'`)\]]+?\.(?:md|markdown|json|txt|log|csv|tsv|yaml|yml|toml|html|htm|pdf|png|jpg|jpeg|gif|webp|svg|mp3|wav|m4a|flac|ogg|opus|aac|amr|wma|mp4|mov|avi|mkv|webm|m4v|3gp|mpeg|mpg|ts|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))(?![A-Za-z0-9_./-])/gim; + /(^|[\s([])(\/[^\n\r<>"'`)\]]+?\.(?:md|markdown|json|txt|log|csv|tsv|yaml|yml|toml|html|htm|pdf|png|jpg|jpeg|gif|webp|svg|mp3|wav|m4a|flac|ogg|opus|aac|amr|wma|mp4|mov|avi|mkv|webm|m4v|3gp|mpeg|mpg|ts|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))(?![A-Za-z0-9_./-])/gim; const WORKSPACE_RENDER_PATTERN = /\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\((https:\/\/workspace\.local\/open\/[^)\r\n]*)\)|\/root\/\.nanobot\/workspace\/[^\s<>"'`)\],,。!?;:]+|https:\/\/workspace\.local\/open\/[^)\r\n]+/gi;