diff --git a/apps/extension/src/components/content-selector/hooks/use-content-selector.tsx b/apps/extension/src/components/content-selector/hooks/use-content-selector.tsx index 209624f60..bded8a59d 100644 --- a/apps/extension/src/components/content-selector/hooks/use-content-selector.tsx +++ b/apps/extension/src/components/content-selector/hooks/use-content-selector.tsx @@ -22,6 +22,7 @@ import { getElementType } from '../utils'; import { genContentSelectorID } from '@refly/utils/id'; import { getMarkdown } from '@refly/utils/html2md'; import { BLOCK_SELECTED_MARK_ID, INLINE_SELECTED_MARK_ID } from '../utils/index'; +import { logger } from '../../../utils/logger'; // utils import { getSelectionNodesMarkdown } from '@refly/utils/html2md'; @@ -126,7 +127,7 @@ export const useContentSelector = ( containerElem?.removeChild(menuContainer); document.head.removeChild(styleContainer); } catch (err) { - console.log('remove err', err); + logger.log('remove err', err); } }, 300); }; @@ -392,7 +393,7 @@ export const useContentSelector = ( } as Mark, }, }; - console.log('contentSelectorClickHandler', safeStringifyJSON(msg)); + logger.debug('contentSelectorClickHandler', safeStringifyJSON(msg)); sendMessage(msg); }; @@ -437,13 +438,13 @@ export const useContentSelector = ( const onMouseMove = (ev: MouseEvent) => { ev.stopImmediatePropagation(); - console.log('isMouseOutsideContainer', isMouseOutsideContainer(ev), selector); + logger.debug('isMouseOutsideContainer', isMouseOutsideContainer(ev), selector); if (isMouseOutsideContainer(ev)) { return; } - console.log('contentActionHandler', ev, statusRef, markRef, showContentSelectorRef); + logger.debug('contentActionHandler', ev, statusRef, markRef, showContentSelectorRef); if ( statusRef.current && markRef.current && @@ -523,7 +524,7 @@ export const useContentSelector = ( const selection = window.getSelection(); const text = selection?.toString(); - console.log('onMouseDownUpEvent'); + logger.debug('onMouseDownUpEvent'); if (statusRef.current && markRef.current && showContentSelectorRef.current) { if (text && text?.trim()?.length > 0) { diff --git a/apps/extension/src/entrypoints/background/events/activated.ts b/apps/extension/src/entrypoints/background/events/activated.ts index ae4db7e4d..dc350d450 100644 --- a/apps/extension/src/entrypoints/background/events/activated.ts +++ b/apps/extension/src/entrypoints/background/events/activated.ts @@ -1,9 +1,10 @@ import { sendHeartBeatMessage } from './utils'; import { Tabs } from 'wxt/browser'; +import { logger } from '../../../utils/logger'; export const onActivated = (activeInfo: Tabs.OnActivatedActiveInfoType) => { // 在此处处理标签切换 - console.log(`Tab with ID ${activeInfo.tabId} was activated in window ${activeInfo.windowId}`); + logger.debug(`Tab with ID ${activeInfo.tabId} was activated in window ${activeInfo.windowId}`); sendHeartBeatMessage(activeInfo); }; diff --git a/apps/extension/src/entrypoints/background/events/detached.ts b/apps/extension/src/entrypoints/background/events/detached.ts index a748c2409..18ebd0b77 100644 --- a/apps/extension/src/entrypoints/background/events/detached.ts +++ b/apps/extension/src/entrypoints/background/events/detached.ts @@ -1,6 +1,7 @@ import { Tabs } from 'wxt/browser'; +import { logger } from '../../../utils/logger'; export const onDetached = (tabId: number, detachInfo: Tabs.OnDetachedDetachInfoType) => { // 在此处处理标签切换 - console.log(`Tab with ID ${tabId} was detached in window ${detachInfo.oldWindowId}`); + logger.debug(`Tab with ID ${tabId} was detached in window ${detachInfo.oldWindowId}`); }; diff --git a/apps/extension/src/entrypoints/background/events/ports.ts b/apps/extension/src/entrypoints/background/events/ports.ts index 761d5caf5..d689b831c 100644 --- a/apps/extension/src/entrypoints/background/events/ports.ts +++ b/apps/extension/src/entrypoints/background/events/ports.ts @@ -3,6 +3,7 @@ import { TASK_STATUS, SkillEvent } from '@refly/common-types'; import { getCookie } from '@/utils/cookie'; import { ssePost } from '@refly-packages/ai-workspace-common/utils/sse-post'; import { getAbortController, getLastUniqueId, setAbortController, setLastUniqueId } from '../index'; +import { logger } from '../../../utils/logger'; export const handleStreamingChat = async ( req: { body: any; uniqueId: string }, @@ -10,7 +11,7 @@ export const handleStreamingChat = async ( ) => { const { type, payload } = req?.body || {}; const { uniqueId } = req; - console.log('receive request', req.body); + logger.debug('receive request', req.body); setLastUniqueId(uniqueId); try { @@ -76,7 +77,7 @@ export const handleStreamingChat = async ( }, }); - console.log('after abortController', abortController); + logger.debug('after abortController', abortController); } else if (type === TASK_STATUS.SHUTDOWN) { const abortController = getAbortController(uniqueId); @@ -85,21 +86,21 @@ export const handleStreamingChat = async ( } } } catch (err) { - console.log('err', err); + logger.error('streaming chat error', err); try { const abortController = getAbortController(uniqueId); if (!abortController?.signal?.aborted) { abortController?.abort?.(); } } catch (err) { - console.log('err', err); + logger.error('abort controller cleanup error', err); } } }; export const onPort = async (port: Runtime.Port) => { port.onMessage.addListener(async (message: any, comingPort: Runtime.Port) => { - console.log('onPort', message, comingPort); + logger.debug('onPort', message, comingPort); if (comingPort.name === 'streaming-chat') { return handleStreamingChat(message, { send: async (msg: any) => { @@ -115,7 +116,7 @@ export const onPort = async (port: Runtime.Port) => { try { abortController?.abort?.(); } catch (err) { - console.log('err', err); + logger.error('port disconnect cleanup error', err); } }); }; diff --git a/apps/extension/src/entrypoints/contentSelector-csui.content/index.tsx b/apps/extension/src/entrypoints/contentSelector-csui.content/index.tsx index dd0ff0ab7..f3f5bfdc6 100644 --- a/apps/extension/src/entrypoints/contentSelector-csui.content/index.tsx +++ b/apps/extension/src/entrypoints/contentSelector-csui.content/index.tsx @@ -3,6 +3,7 @@ import { defineContentScript } from 'wxt/sandbox'; import { createShadowRootUi } from 'wxt/client'; import { App } from './App'; import { setRuntime } from '@refly/utils/env'; +import { logger } from '@/utils/logger'; export default defineContentScript({ matches: [''], @@ -16,7 +17,7 @@ export default defineContentScript({ async main(ctx) { setRuntime('extension-csui'); - console.log('ctx', ctx); + logger.debug('contentSelector ctx', ctx); const injectSelectorCSS = () => { const style = document.createElement('style'); diff --git a/apps/extension/src/entrypoints/floatingSphere-csui.content/App.tsx b/apps/extension/src/entrypoints/floatingSphere-csui.content/App.tsx index 3df3b9c0f..2fb56c8cd 100644 --- a/apps/extension/src/entrypoints/floatingSphere-csui.content/App.tsx +++ b/apps/extension/src/entrypoints/floatingSphere-csui.content/App.tsx @@ -22,6 +22,7 @@ import { useSaveSelectedContent } from '@/hooks/use-save-selected-content'; import { BackgroundMessage, SyncMarkEvent, type MessageName } from '@refly/common-types'; import { useGetUserSettings } from '@/hooks/use-get-user-settings'; import { useUserStore } from '@refly-packages/ai-workspace-common/stores/user'; +import { logger } from '@/utils/logger'; const getPopupContainer = () => { const elem = document @@ -54,7 +55,7 @@ export const App = () => { getLoginStatus(); }, []); - console.log('i18n', i18n?.languages); + logger.debug('i18n', i18n?.languages); // 加载快捷键 const [_shortcut] = useState(reflyEnv.getOsType() === 'OSX' ? '⌘ J' : 'Ctrl J'); @@ -229,7 +230,7 @@ export const App = () => { icon={} size="small" onClick={() => { - console.log('saveResource', saveResource); + logger.debug('saveResource', saveResource); handleSaveResourceAndNotify(saveResource); }} className="refly-floating-sphere-dropdown-item assist-action-item" @@ -274,7 +275,7 @@ export const App = () => { }; const handleMouseUp = () => { - console.log('handleMouseUp'); + logger.debug('handleMouseUp'); setIsDragging(false); isDraggingRef.current = false; }; @@ -294,6 +295,18 @@ export const App = () => { const [isVisible, setIsVisible] = useState(true); + const handleDebugClick = useCallback(() => { + const showMessage = (content: string, type: 'info' | 'success' = 'info') => { + if (type === 'success') { + Message.success({ content, duration: 4000 }); + } else { + Message.info({ content, duration: 2000 }); + } + }; + + logger.handleDebugClick(showMessage); + }, []); + const handleClose = (e: React.MouseEvent) => { Message.info({ content: t('extension.floatingSphere.toggleCopilotClose'), @@ -357,7 +370,8 @@ export const App = () => { {t('extension.floatingSphere.toggleCopilot')} Refly diff --git a/apps/extension/src/entrypoints/floatingSphere-csui.content/index.tsx b/apps/extension/src/entrypoints/floatingSphere-csui.content/index.tsx index 86ecedc79..a868a188d 100644 --- a/apps/extension/src/entrypoints/floatingSphere-csui.content/index.tsx +++ b/apps/extension/src/entrypoints/floatingSphere-csui.content/index.tsx @@ -7,6 +7,7 @@ import { setRuntime } from '@refly/utils/env'; import { ConfigProvider } from 'antd'; import { MemoryRouter, Route } from '@refly-packages/ai-workspace-common/utils/router'; import { AppRouter } from '@/routes'; +import { logger } from '@/utils/logger'; export default defineContentScript({ matches: [''], @@ -22,7 +23,7 @@ export default defineContentScript({ async main(ctx) { setRuntime('extension-csui'); - console.log('ctx', ctx); + logger.debug('floatingSphere ctx', ctx); let _removeInjectCSS: () => void; // 3. Define your UI` const ui = await createShadowRootUi(ctx, { diff --git a/apps/extension/src/entrypoints/utils-csui.content/index.tsx b/apps/extension/src/entrypoints/utils-csui.content/index.tsx index 387862548..1718894f2 100644 --- a/apps/extension/src/entrypoints/utils-csui.content/index.tsx +++ b/apps/extension/src/entrypoints/utils-csui.content/index.tsx @@ -4,6 +4,7 @@ import ReactDOM from 'react-dom/client'; import { setRuntime } from '@refly/utils/env'; import { createShadowRootUi } from 'wxt/client'; import App from './App'; +import { logger } from '@/utils/logger'; export default defineContentScript({ matches: [''], @@ -19,7 +20,7 @@ export default defineContentScript({ async main(ctx) { setRuntime('extension-csui'); - console.log('ctx', ctx); + logger.debug('utils-csui ctx', ctx); // 3. Define your UI` const ui = await createShadowRootUi(ctx, { name: 'refly-utils-app', diff --git a/apps/extension/src/hooks/use-get-user-settings.ts b/apps/extension/src/hooks/use-get-user-settings.ts index 4938fa70d..ee4890eb4 100644 --- a/apps/extension/src/hooks/use-get-user-settings.ts +++ b/apps/extension/src/hooks/use-get-user-settings.ts @@ -15,6 +15,7 @@ import { getRuntime } from '@refly/utils/env'; import { UserSettings } from '@refly/openapi-schema'; import { browser } from 'wxt/browser'; import debounce from 'lodash.debounce'; +import { logger } from '@/utils/logger'; export const useGetUserSettings = () => { const userStore = useUserStore((state) => ({ @@ -40,7 +41,7 @@ export const useGetUserSettings = () => { userStore.setIsCheckingLoginStatus(true); const res = await getClient().getSettings(); - console.log('loginStatus', res); + logger.debug('loginStatus', res); if (!res?.error || !res) { userStore.resetState(); @@ -92,7 +93,7 @@ export const useGetUserSettings = () => { } } } catch (err) { - console.log('getLoginStatus err', err); + logger.debug('getLoginStatus err', err); userStore.setIsCheckingLoginStatus(false); userStore.resetState(); diff --git a/apps/extension/src/hooks/use-storage.ts b/apps/extension/src/hooks/use-storage.ts index 59a676359..01b789033 100644 --- a/apps/extension/src/hooks/use-storage.ts +++ b/apps/extension/src/hooks/use-storage.ts @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { storage } from '@refly-packages/ai-workspace-common/utils/storage'; import { safeParseJSON } from '@refly-packages/ai-workspace-common/utils/parse'; +import { logger } from '@/utils/logger'; export type StorageLocation = 'local' | 'sync' | 'session' | 'managed'; @@ -29,7 +30,7 @@ export const useStorage = ( useEffect(() => { storageItem.watch((newValue) => { - console.log('new Syn storage value', newValue); + logger.debug('new Syn storage value', newValue); setStorageValue(newValue as T); }); }, []); diff --git a/apps/extension/src/modules/toggle-copilot/hooks/use-toggle-copilot.ts b/apps/extension/src/modules/toggle-copilot/hooks/use-toggle-copilot.ts index 953545642..84b341aff 100644 --- a/apps/extension/src/modules/toggle-copilot/hooks/use-toggle-copilot.ts +++ b/apps/extension/src/modules/toggle-copilot/hooks/use-toggle-copilot.ts @@ -5,6 +5,7 @@ import pTimeout from 'p-timeout'; import { checkPageUnsupported } from '@refly-packages/ai-workspace-common/utils/extension/check'; import { checkBrowserArc } from '@/utils/browser'; import { useCopilotTypeStore } from '@/modules/toggle-copilot/stores/use-copilot-type'; +import { logger } from '@/utils/logger'; export const useToggleCopilot = () => { const { copilotType } = useCopilotTypeStore((state) => ({ @@ -22,9 +23,9 @@ export const useToggleCopilot = () => { await handleCheckArcBrowser(); - console.log('isArcBrowserRef.current', isArcBrowserRef.current, isCopilotOpen); + logger.debug('isArcBrowserRef.current', isArcBrowserRef.current, isCopilotOpen); if (isArcBrowserRef.current) { - console.log('sendMessage', 'toggleCopilotSidePanel'); + logger.debug('sendMessage', 'toggleCopilotCSUI'); sendMessage({ type: 'others', name: 'toggleCopilotCSUI', @@ -35,7 +36,7 @@ export const useToggleCopilot = () => { source: getRuntime(), }); } else { - console.log('sendMessage', 'toggleCopilotSidePanel'); + logger.debug('sendMessage', 'toggleCopilotSidePanel'); sendMessage({ type: 'toggleCopilot', name: 'toggleCopilotSidePanel', @@ -93,7 +94,7 @@ export const useToggleCopilot = () => { }); useEffect(() => { - console.log('useToggleCopilot copilotType', copilotType); + logger.debug('useToggleCopilot copilotType', copilotType); // // Initial check if the page is already visible diff --git a/apps/extension/src/utils/logger.ts b/apps/extension/src/utils/logger.ts new file mode 100644 index 000000000..105f554aa --- /dev/null +++ b/apps/extension/src/utils/logger.ts @@ -0,0 +1,266 @@ +/** + * Development logger that only outputs logs in development mode + * This prevents console spam in production builds + * + * Features: + * - Auto-detects development mode via multiple methods + * - 10-click debug mode toggle (click sphere logo 10 times) + * - Safe fallbacks for extension environment limitations + * + * Usage: + * - logger.debug('message') - Only shows in debug mode + * - logger.handleDebugClick() - Call from UI to enable/disable debug mode + */ + +class DevLogger { + // =================================== + // PRIVATE PROPERTIES + // =================================== + private isDevelopment: boolean; + + // Debug click activation state + private debugClickCount = 0; + private lastClickTime = 0; + private clickTimeout: NodeJS.Timeout | null = null; + + // =================================== + // CONSTRUCTOR & INITIALIZATION + // =================================== + constructor() { + this.isDevelopment = this.checkDevelopmentMode(); + this.initializeLogger(); + this.setupStorageListener(); + } + + private initializeLogger() { + // Only show initialization log when debug mode is enabled + if (this.isDevelopment) { + console.log('🔧 [Refly Logger] Debug mode: ON ✅'); + } + } + + private setupStorageListener() { + // Listen for storage changes to update debug mode dynamically + if (typeof window !== 'undefined') { + window.addEventListener('storage', (e) => { + if (e.key === 'reflydebug') { + const wasEnabled = this.isDevelopment; + this.isDevelopment = this.checkDevelopmentMode(); + + // Only log when debug mode is enabled (either before or after change) + if (wasEnabled || this.isDevelopment) { + console.log(`🔧 [Refly Logger] Debug mode: ${this.isDevelopment ? 'ON ✅' : 'OFF ❌'}`); + } + } + }); + } + } + + // =================================== + // PUBLIC LOGGING METHODS + // =================================== + log(...args: any[]) { + if (this.isDevelopment) { + console.log(...args); + } + } + + debug(...args: any[]) { + if (this.isDevelopment) { + console.log(...args); + } + } + + info(...args: any[]) { + if (this.isDevelopment) { + console.info(...args); + } + } + + warn(...args: any[]) { + if (this.isDevelopment) { + console.warn(...args); + } + } + + error(...args: any[]) { + if (this.isDevelopment) { + console.error(...args); + } + } + + // =================================== + // PUBLIC DEBUG STATE MANAGEMENT + // =================================== + refreshDebugState() { + this.isDevelopment = this.checkDevelopmentMode(); + return this.isDevelopment; + } + + test() { + // Always show test output since it's explicitly called + console.log(`🔧 [Refly Logger] Debug mode: ${this.isDevelopment ? 'ON ✅' : 'OFF ❌'}`); + return this.isDevelopment; + } + + // =================================== + // DEBUG CLICK ACTIVATION (10 CLICKS) + // =================================== + handleDebugClick(showMessage?: (content: string, type?: 'info' | 'success') => void) { + this.updateClickCount(); + this.resetClickCounterAfterDelay(); + + if (this.shouldShowHints()) { + this.showActivationHints(showMessage); + } else if (this.shouldToggleDebugMode()) { + this.toggleDebugMode(showMessage); + } + + return this.debugClickCount; + } + + private updateClickCount() { + const now = Date.now(); + const timeSinceLastClick = now - this.lastClickTime; + + // Reset counter if more than 3 seconds between clicks + if (timeSinceLastClick > 3000) { + this.debugClickCount = 1; + } else { + this.debugClickCount += 1; + } + + this.lastClickTime = now; + } + + private resetClickCounterAfterDelay() { + // Clear existing timeout + if (this.clickTimeout) { + clearTimeout(this.clickTimeout); + } + + // Reset counter after 5 seconds of inactivity + this.clickTimeout = setTimeout(() => { + this.debugClickCount = 0; + }, 5000); + } + + private shouldShowHints(): boolean { + return this.debugClickCount === 8 || this.debugClickCount === 9; + } + + private shouldToggleDebugMode(): boolean { + return this.debugClickCount >= 10; + } + + private showActivationHints(showMessage?: (content: string, type?: 'info' | 'success') => void) { + const remainingClicks = 10 - this.debugClickCount; + showMessage?.( + `🔧 Debug mode activation: ${remainingClicks} more click${remainingClicks > 1 ? 's' : ''} needed`, + 'info', + ); + } + + private toggleDebugMode(showMessage?: (content: string, type?: 'info' | 'success') => void) { + const isCurrentlyEnabled = this.refreshDebugState(); + + if (isCurrentlyEnabled) { + this.deactivateDebugMode(showMessage); + } else { + this.activateDebugMode(showMessage); + } + + this.debugClickCount = 0; + } + + private activateDebugMode(showMessage?: (content: string, type?: 'info' | 'success') => void) { + try { + localStorage.setItem('reflydebug', 'true'); + this.refreshDebugState(); + showMessage?.('🎉 Debug mode activated! Check console for logs.', 'success'); + } catch (_e) { + // Fallback method for turning on + document.documentElement.setAttribute('data-refly-debug', 'true'); + this.refreshDebugState(); + showMessage?.( + '🎉 Debug mode activated (fallback method)! Check console for logs.', + 'success', + ); + } + } + + private deactivateDebugMode(showMessage?: (content: string, type?: 'info' | 'success') => void) { + try { + localStorage.removeItem('reflydebug'); + document.documentElement.removeAttribute('data-refly-debug'); + this.refreshDebugState(); + showMessage?.('🔴 Debug mode deactivated! Console logs disabled.', 'success'); + } catch (_e) { + // Fallback method for turning off + document.documentElement.removeAttribute('data-refly-debug'); + this.refreshDebugState(); + showMessage?.( + '🔴 Debug mode deactivated (fallback method)! Console logs disabled.', + 'success', + ); + } + } + + // =================================== + // PRIVATE DEBUG MODE DETECTION + // =================================== + private checkDevelopmentMode(): boolean { + try { + return ( + this.checkLocalStorageFlag() || + this.checkDocumentAttribute() || + this.checkBuildEnvironment() || + this.checkProcessEnvironment() || + this.checkUrlParameter() + ); + } catch (_e) { + // If any check fails, default to production mode + return false; + } + } + + private checkLocalStorageFlag(): boolean { + try { + if (typeof window !== 'undefined' && window.localStorage) { + return window.localStorage.getItem('reflydebug') === 'true'; + } + } catch (_storageError) { + // Silent fallback to alternatives + } + return false; + } + + private checkDocumentAttribute(): boolean { + if (typeof document !== 'undefined' && document.documentElement) { + return document.documentElement.getAttribute('data-refly-debug') === 'true'; + } + return false; + } + + private checkBuildEnvironment(): boolean { + // @ts-ignore - Vite replaces this at build time + return typeof import.meta !== 'undefined' && import.meta.env?.DEV === true; + } + + private checkProcessEnvironment(): boolean { + return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development'; + } + + private checkUrlParameter(): boolean { + if (typeof window !== 'undefined' && window.location) { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get('refly_debug') === 'true'; + } + return false; + } +} + +// =================================== +// EXPORT SINGLETON INSTANCE +// =================================== +export const logger = new DevLogger();