diff --git a/back/api/script.ts b/back/api/script.ts index 58a40a53..3b087d2d 100644 --- a/back/api/script.ts +++ b/back/api/script.ts @@ -34,7 +34,7 @@ export default (app: Router) => { const logger: Logger = Container.get('logger'); try { let result = []; - const blacklist = ['node_modules', '.git']; + const blacklist = ['node_modules', '.git', '.pnpm']; if (req.query.path) { const targetPath = path.join( config.scriptPath, diff --git a/back/services/sock.ts b/back/services/sock.ts index 60e78929..b09a43bb 100644 --- a/back/services/sock.ts +++ b/back/services/sock.ts @@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock'; export default class SockService { private clients: Connection[] = []; - constructor(@Inject('logger') private logger: winston.Logger) {} + constructor(@Inject('logger') private logger: winston.Logger) { } public getClients() { return this.clients; diff --git a/shell/update.sh b/shell/update.sh index a33cd87a..94224ea3 100755 --- a/shell/update.sh +++ b/shell/update.sh @@ -273,13 +273,13 @@ update_qinglong() { exit_status=$? if [[ $exit_status -eq 0 ]]; then - echo -e "\n更新青龙源文件成功...\n" + echo -e "更新青龙源文件成功...\n" unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp} update_qinglong_static else - echo -e "\n更新青龙源文件失败,请检查网络...\n" + echo -e "更新青龙源文件失败,请检查网络...\n" fi } @@ -288,28 +288,29 @@ update_qinglong_static() { exit_status=$? if [[ $exit_status -eq 0 ]]; then - echo -e "\n更新青龙静态资源成功...\n" + echo -e "更新青龙静态资源成功...\n" unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp} check_update_dep else - echo -e "\n更新青龙静态资源失败,请检查网络...\n" + echo -e "更新青龙静态资源失败,请检查网络...\n" fi } check_update_dep() { echo -e "\n开始检测依赖...\n" - if [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then + if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then cp -f $dir_sample/package.json $dir_scripts/package.json npm_install_2 $dir_scripts fi + if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then npm_install_2 "${dir_tmp}/qinglong-${primary_branch}" fi if [[ $exit_status -eq 0 ]]; then echo -e "\n依赖检测安装成功...\n" - echo -e "\n更新包下载成功...\n" + echo -e "更新包下载成功..." if [[ "$needRestart" == 'true' ]]; then cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/ diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index a307128f..491a2cc7 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -1,5 +1,5 @@ import intl from 'react-intl-universal'; -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState } from 'react'; import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import * as DarkReader from '@umijs/ssr-darkreader'; import defaultProps from './defaultProps'; @@ -29,9 +29,9 @@ import { MenuProps, } from 'antd'; // @ts-ignore -import SockJS from 'sockjs-client'; import * as Sentry from '@sentry/react'; import { init } from '../utils/init'; +import WebSocketManager from '../utils/websocket'; export interface SharedContext { headerStyle: React.CSSProperties; @@ -40,7 +40,6 @@ export interface SharedContext { user: any; reloadUser: (needLoading?: boolean) => void; reloadTheme: () => void; - socketMessage: any; systemInfo: TSystemInfo; } @@ -60,8 +59,6 @@ export default function () { const [user, setUser] = useState({}); const [loading, setLoading] = useState(true); const [systemInfo, setSystemInfo] = useState(); - const ws = useRef(null); - const [socketMessage, setSocketMessage] = useState(); const [collapsed, setCollapsed] = useState(false); const [initLoading, setInitLoading] = useState(true); const { @@ -180,32 +177,14 @@ export default function () { useEffect(() => { if (!user || !user.username) return; - ws.current = new SockJS( + const ws = WebSocketManager.getInstance( `${window.location.origin}/api/ws?token=${localStorage.getItem( config.authKey, )}`, ); - ws.current.onmessage = (e: any) => { - try { - const data = JSON.parse(e.data); - if (data.type === 'ping') { - if (data && data.message === 'hanhh') { - console.log('WS connection succeeded !!!'); - } else { - console.log('WS connection Failed !!!', e); - } - } - setSocketMessage(data); - } catch (error) { - console.log('websocket连接失败', e); - } - }; - - const wsCurrent = ws.current; - return () => { - wsCurrent.close(); + ws.close(); }; }, [user]); @@ -387,7 +366,6 @@ export default function () { user, reloadUser, reloadTheme, - socketMessage, systemInfo, }} /> diff --git a/src/pages/dependence/index.tsx b/src/pages/dependence/index.tsx index 520ea441..126eb696 100644 --- a/src/pages/dependence/index.tsx +++ b/src/pages/dependence/index.tsx @@ -35,6 +35,7 @@ import { useOutletContext } from '@umijs/max'; import { SharedContext } from '@/layouts'; import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import dayjs from 'dayjs'; +import WebSocketManager from '@/utils/websocket'; const { Text } = Typography; const { Search } = Input; @@ -87,8 +88,7 @@ const StatusMap: Record = { }; const Dependence = () => { - const { headerStyle, isPhone, socketMessage } = - useOutletContext(); + const { headerStyle, isPhone } = useOutletContext(); const columns: any = [ { title: intl.get('序号'), @@ -109,11 +109,12 @@ const Dependence = () => { width: 120, dataIndex: 'status', render: (text: string, record: any, index: number) => { + console.log(record.status); return ( {intl.get(Status[record.status])} @@ -395,63 +396,62 @@ const Dependence = () => { } }, [logDependence]); - useEffect(() => { - if (!socketMessage) return; - const { type, message, references } = socketMessage; - if ( - type === 'installDependence' && - message.includes('开始时间') && - references.length > 0 - ) { - const result = [...value]; - for (let i = 0; i < references.length; i++) { - const index = value.findIndex((x) => x.id === references[i]); - if (index !== -1) { - result.splice(index, 1, { - ...value[index], - status: message.includes('安装') ? Status.安装中 : Status.删除中, - }); - } - } - setValue(result); + const handleMessage = useCallback((payload: any) => { + const { message, references } = payload; + let status: number | undefined = undefined; + if (message.includes('开始时间') && references.length > 0) { + status = message.includes('安装') ? Status.安装中 : Status.删除中; } - if ( - type === 'installDependence' && - message.includes('结束时间') && - references.length > 0 - ) { - let status; + if (message.includes('结束时间') && references.length > 0) { if (message.includes('安装')) { status = message.includes('成功') ? Status.已安装 : Status.安装失败; } else { status = message.includes('成功') ? Status.已删除 : Status.删除失败; } - const result = [...value]; - for (let i = 0; i < references.length; i++) { - const index = value.findIndex((x) => x.id === references[i]); - if (index !== -1) { - result.splice(index, 1, { - ...value[index], - status, - }); - } - } - setValue(result); if (status === Status.已删除) { setTimeout(() => { - const _result = [...value]; - for (let i = 0; i < references.length; i++) { - const index = value.findIndex((x) => x.id === references[i]); - if (index !== -1) { - _result.splice(index, 1); + setValue((p) => { + const _result = [...p]; + for (let i = 0; i < references.length; i++) { + const index = p.findIndex((x) => x.id === references[i]); + if (index !== -1) { + _result.splice(index, 1); + } } - } - setValue(_result); + return _result; + }); }, 5000); + return; } } - }, [socketMessage]); + if (typeof status === 'number') { + setValue((p) => { + const result = [...p]; + for (let i = 0; i < references.length; i++) { + const index = p.findIndex((x) => x.id === references[i]); + if (index !== -1) { + result.splice(index, 1, { + ...p[index], + status, + }); + } + } + return result; + }); + } + }, []); + + useEffect(() => { + const ws = WebSocketManager.getInstance(); + ws.subscribe('installDependence', handleMessage); + ws.subscribe('uninstallDependence', handleMessage); + + return () => { + ws.unsubscribe('installDependence', handleMessage); + ws.unsubscribe('uninstallDependence', handleMessage); + }; + }, []); const onTabChange = (activeKey: string) => { setSelectedRowIds([]); @@ -548,24 +548,25 @@ const Dependence = () => { dependence={editedDependence} defaultType={type} /> - { - setIsLogModalVisible(false); - if (needRemove) { - const index = value.findIndex((x) => x.id === logDependence.id); - const result = [...value]; - if (index !== -1) { - result.splice(index, 1); - setValue(result); + {logDependence && ( + { + setIsLogModalVisible(false); + if (needRemove) { + const index = value.findIndex((x) => x.id === logDependence.id); + const result = [...value]; + if (index !== -1) { + result.splice(index, 1); + setValue(result); + } + } else if ([...value].map((x) => x.id).includes(logDependence.id)) { + getDependenceDetail(logDependence); } - } else if ([...value].map((x) => x.id).includes(logDependence.id)) { - getDependenceDetail(logDependence); - } - }} - socketMessage={socketMessage} - dependence={logDependence} - /> + }} + dependence={logDependence} + /> + )} ); }; diff --git a/src/pages/dependence/logModal.tsx b/src/pages/dependence/logModal.tsx index 7d6a2986..afb16bcb 100644 --- a/src/pages/dependence/logModal.tsx +++ b/src/pages/dependence/logModal.tsx @@ -9,17 +9,16 @@ import { } from '@ant-design/icons'; import { PageLoading } from '@ant-design/pro-layout'; import Ansi from 'ansi-to-react'; +import WebSocketManager from '@/utils/websocket'; const DependenceLogModal = ({ dependence, handleCancel, visible, - socketMessage, }: { dependence?: any; visible: boolean; handleCancel: (needRemove?: boolean) => void; - socketMessage: any; }) => { const [value, setValue] = useState(''); const [executing, setExecuting] = useState(true); @@ -54,7 +53,7 @@ const DependenceLogModal = ({ code === 200 && localStorage.getItem('logDependence') === String(dependence.id) ) { - const log = (data.log || []).join('') as string; + const log = (data?.log || []).join('') as string; setValue(log); setExecuting(!log.includes('结束时间')); setIsRemoveFailed(log.includes('删除失败')); @@ -95,21 +94,25 @@ const DependenceLogModal = ({ } }, [dependence]); - useEffect(() => { - if (!socketMessage || !dependence) return; - const { type, message, references } = socketMessage; - if ( - type === 'installDependence' && - references.length > 0 && - references.includes(dependence.id) - ) { + const handleMessage = (payload: any) => { + const { message, references } = payload; + if (references.length > 0 && references.includes(dependence.id)) { if (message.includes('结束时间')) { setExecuting(false); setIsRemoveFailed(message.includes('删除失败')); } - setValue(`${value}${message}`); + setValue((p) => `${p}${message}`); } - }, [socketMessage]); + }; + + useEffect(() => { + const ws = WebSocketManager.getInstance(); + ws.subscribe('installDependence', handleMessage); + + return () => { + ws.unsubscribe('installDependence', handleMessage); + }; + }, []); useEffect(() => { setIsPhone(document.body.clientWidth < 768); diff --git a/src/pages/script/editModal.tsx b/src/pages/script/editModal.tsx index 2fe5b8e0..87af6bde 100644 --- a/src/pages/script/editModal.tsx +++ b/src/pages/script/editModal.tsx @@ -1,5 +1,11 @@ import intl from 'react-intl-universal'; -import React, { useEffect, useState, useRef } from 'react'; +import React, { + useEffect, + useState, + useRef, + useCallback, + useReducer, +} from 'react'; import { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd'; import { request } from '@/utils/http'; import config from '@/utils/config'; @@ -9,6 +15,7 @@ import SaveModal from './saveModal'; import SettingModal from './setting'; import { useTheme } from '@/utils/hooks'; import { getEditorMode, logEnded } from '@/utils'; +import WebSocketManager from '@/utils/websocket'; const { Option } = Select; @@ -18,12 +25,10 @@ const EditModal = ({ content, handleCancel, visible, - socketMessage, }: { treeData?: any; content?: string; visible: boolean; - socketMessage: any; currentNode: any; handleCancel: () => void; }) => { @@ -34,12 +39,11 @@ const EditModal = ({ const [saveModalVisible, setSaveModalVisible] = useState(false); const [settingModalVisible, setSettingModalVisible] = useState(false); - const [log, setLog] = useState(''); + const [log, setLog] = useState(''); const { theme } = useTheme(); const editorRef = useRef(null); const [isRunning, setIsRunning] = useState(false); const [currentPid, setCurrentPid] = useState(null); - const cancel = () => { handleCancel(); }; @@ -104,28 +108,25 @@ const EditModal = ({ }); }; - useEffect(() => { - if (!socketMessage) { - return; - } - - let { type, message: _message, references } = socketMessage; - - if (type !== 'manuallyRunScript') { - return; - } - + const handleMessage = useCallback((payload: any) => { + let { message: _message } = payload; if (logEnded(_message)) { setTimeout(() => { setIsRunning(false); }, 300); } - if (log) { - _message = `\n${_message}`; - } - setLog(`${log}${_message}`); - }, [socketMessage]); + setLog(p=>`${p}${_message}`); + }, []); + + useEffect(() => { + const ws = WebSocketManager.getInstance(); + ws.subscribe('manuallyRunScript', handleMessage); + + return () => { + ws.unsubscribe('manuallyRunScript', handleMessage); + }; + }, []); useEffect(() => { setLog(''); diff --git a/src/pages/script/index.tsx b/src/pages/script/index.tsx index 10d6d532..d9aeacc4 100644 --- a/src/pages/script/index.tsx +++ b/src/pages/script/index.tsx @@ -48,7 +48,7 @@ import { langs } from '@uiw/codemirror-extensions-langs'; const { Text } = Typography; const Script = () => { - const { headerStyle, isPhone, theme, socketMessage } = + const { headerStyle, isPhone, theme } = useOutletContext(); const [value, setValue] = useState(intl.get('请选择脚本文件')); const [select, setSelect] = useState(''); @@ -591,16 +591,15 @@ const Script = () => { }} /> )} - { setIsLogModalVisible(false); }} - /> + />} { +const CheckUpdate = ({ systemInfo }: any) => { const [updateLoading, setUpdateLoading] = useState(false); const [value, setValue] = useState(''); const modalRef = useRef(); @@ -149,17 +151,8 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => { }; useEffect(() => { - if (!modalRef.current || !socketMessage) { - return; - } - const { type, message: _message, references } = socketMessage; - - if (type !== 'updateSystemVersion') { - return; - } - - const newMessage = `${value}${_message}`; - const updateFailed = newMessage.includes('失败'); + if (!value) return; + const updateFailed = value.includes('失败,请检查'); modalRef.current.update({ maskClosable: updateFailed, @@ -167,29 +160,46 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => { okButtonProps: { disabled: !updateFailed }, content: ( <> -
{newMessage}
+
+            {value}
+          
), }); + }, [value]); - if (updateFailed && !value.includes('失败,请检查')) { + const handleMessage = useCallback((payload: any) => { + let { message: _message } = payload; + const updateFailed = _message.includes('失败,请检查'); + + if (updateFailed) { message.error(intl.get('更新失败,请检查网络及日志或稍后再试')); } - setValue(newMessage); - - document.getElementById('log-identifier') && + setTimeout(() => { document - .getElementById('log-identifier')! - .scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + .querySelector('#log-identifier')! + .scrollIntoView({ behavior: 'smooth' }); + }, 600); if (_message.includes('更新包下载成功')) { setTimeout(() => { showReloadModal(); }, 1000); } - }, [socketMessage]); + + setValue((p) => `${p}${_message}`); + }, []); + + useEffect(() => { + const ws = WebSocketManager.getInstance(); + ws.subscribe('updateSystemVersion', handleMessage); + + return () => { + ws.unsubscribe('updateSystemVersion', handleMessage); + }; + }, []); return ( <> diff --git a/src/pages/setting/index.tsx b/src/pages/setting/index.tsx index 2caf0a98..a037dcdf 100644 --- a/src/pages/setting/index.tsx +++ b/src/pages/setting/index.tsx @@ -46,7 +46,6 @@ const Setting = () => { theme, reloadUser, reloadTheme, - socketMessage, systemInfo, } = useOutletContext(); const columns = [ @@ -376,7 +375,6 @@ const Setting = () => { children: ( ), diff --git a/src/pages/setting/other.tsx b/src/pages/setting/other.tsx index 668031af..c0804a19 100644 --- a/src/pages/setting/other.tsx +++ b/src/pages/setting/other.tsx @@ -24,9 +24,8 @@ import useProgress from './progress'; const Other = ({ systemInfo, - socketMessage, reloadTheme, -}: Pick) => { +}: Pick) => { const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const [systemConfig, setSystemConfig] = useState<{ logRemoveFrequency?: number | null; @@ -274,7 +273,7 @@ const Other = ({ - + ); diff --git a/src/pages/subscription/index.tsx b/src/pages/subscription/index.tsx index 948c2363..5ebaa3f8 100644 --- a/src/pages/subscription/index.tsx +++ b/src/pages/subscription/index.tsx @@ -1,5 +1,5 @@ import intl from 'react-intl-universal'; -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Button, message, @@ -36,6 +36,7 @@ import './index.less'; import SubscriptionLogModal from './logModal'; import { SharedContext } from '@/layouts'; import useTableScrollHeight from '@/hooks/useTableScrollHeight'; +import WebSocketManager from '@/utils/websocket'; const { Text, Paragraph } = Typography; const { Search } = Input; @@ -61,8 +62,7 @@ export enum SubscriptionType { } const Subscription = () => { - const { headerStyle, isPhone, socketMessage } = - useOutletContext(); + const { headerStyle, isPhone } = useOutletContext(); const columns: any = [ { @@ -508,23 +508,31 @@ const Subscription = () => { : 'subscription'; }; - useEffect(() => { - if (!socketMessage) return; - const { type, message, references } = socketMessage; - if (type === 'runSubscriptionEnd' && references.length > 0) { - const result = [...value]; + const handleMessage = useCallback((payload: any) => { + const { message, references } = payload; + setValue((p) => { + const result = [...p]; for (let i = 0; i < references.length; i++) { - const index = value.findIndex((x) => x.id === references[i]); + const index = p.findIndex((x) => x.id === references[i]); if (index !== -1) { result.splice(index, 1, { - ...value[index], + ...p[index], status: SubscriptionStatus.idle, }); } } - setValue(result); - } - }, [socketMessage]); + return result; + }); + }, []); + + useEffect(() => { + const ws = WebSocketManager.getInstance(); + ws.subscribe('runSubscriptionEnd', handleMessage); + + return () => { + ws.unsubscribe('runSubscriptionEnd', handleMessage); + }; + }, []); useEffect(() => { if (logSubscription) { diff --git a/src/utils/type.ts b/src/utils/type.ts new file mode 100644 index 00000000..9ce55269 --- /dev/null +++ b/src/utils/type.ts @@ -0,0 +1,8 @@ +export type SockMessageType = + | 'ping' + | 'installDependence' + | 'uninstallDependence' + | 'updateSystemVersion' + | 'manuallyRunScript' + | 'runSubscriptionEnd' + | 'reloadSystem'; \ No newline at end of file diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts new file mode 100644 index 00000000..d894ad30 --- /dev/null +++ b/src/utils/websocket.ts @@ -0,0 +1,154 @@ +import SockJS from 'sockjs-client'; +import { SockMessageType } from './type'; + +class WebSocketManager { + private static instance: WebSocketManager | null = null; + private url: string; + private socket: WebSocket | null = null; + private subscriptions: Map void>> = new Map(); + private options: { + maxReconnectAttempts: number; + reconnectInterval: number; + heartbeatInterval: number; + }; + private reconnectAttempts: number = 0; + private heartbeatTimeout: NodeJS.Timeout | null = null; + private state: 'closed' | 'connecting' | 'open' = 'closed'; + + constructor(url: string, options: Partial = {}) { + this.url = url; + this.options = { + maxReconnectAttempts: options.maxReconnectAttempts || 5, + reconnectInterval: options.reconnectInterval || 3000, + heartbeatInterval: options.heartbeatInterval || 30000, + }; + + this.init(); + } + + public static getInstance(url: string = '', options?: Partial): WebSocketManager { + if (!WebSocketManager.instance) { + WebSocketManager.instance = new WebSocketManager(url, options); + } + return WebSocketManager.instance; + } + + private async init() { + try { + this.state = 'connecting'; + this.emit('connecting'); + + while (this.reconnectAttempts < this.options.maxReconnectAttempts) { + this.socket = new SockJS(this.url); + this.setupEventListeners(); + this.startHeartbeat(); + await this.waitForClose(); + this.stopHeartbeat(); + this.socket = null; + this.reconnectAttempts++; + + await new Promise((resolve) => setTimeout(resolve, this.options.reconnectInterval)); + } + } catch (error) { + this.handleError(error); + } + } + + private setupEventListeners() { + if (!this.socket) return; + + this.socket.onopen = () => { + this.state = 'open'; + this.emit('open'); + }; + + this.socket.onmessage = (event) => { + const message = JSON.parse(event.data); + this.dispatchMessage(message); + }; + + this.socket.onclose = () => { + this.state = 'closed'; + this.emit('close'); + }; + } + + private async waitForClose() { + while (this.socket?.readyState !== SockJS.CLOSED) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + + public subscribe(topic: SockMessageType, callback: (v: any) => void) { + const topicSubscriptions = this.subscriptions.get(topic) || new Set(); + + if (!topicSubscriptions.has(callback)) { + topicSubscriptions.add(callback); + this.subscriptions.set(topic, topicSubscriptions); + + const subscriptionMessage = { action: 'subscribe', topic }; + this.send(subscriptionMessage); + } + } + + public unsubscribe(topic: SockMessageType, callback: (v: any) => void) { + const topicSubscriptions = this.subscriptions.get(topic) || new Set(); + if (topicSubscriptions.has(callback)) { + topicSubscriptions.delete(callback); + + const unsubscribeMessage = { action: 'unsubscribe', topic }; + this.send(unsubscribeMessage); + } + } + + public send(message: any) { + if (this.socket?.readyState === SockJS.OPEN) { + this.socket.send(JSON.stringify(message)); + } + } + + private dispatchMessage(message: any) { + const { type, ...others } = message; + const topicSubscriptions = this.subscriptions.get(type) || new Set(); + + [...topicSubscriptions].forEach((callback) => callback(others)); + } + + private startHeartbeat() { + this.heartbeatTimeout = setInterval(() => { + if (this.socket?.readyState === SockJS.OPEN) { + this.socket.send(JSON.stringify({ type: 'heartbeat' })); + } + }, this.options.heartbeatInterval); + } + + private stopHeartbeat() { + if (this.heartbeatTimeout) { + clearInterval(this.heartbeatTimeout); + } + } + + public close() { + if (this.socket) { + this.state = 'closed'; + this.stopHeartbeat(); + this.socket.close(); + this.emit('close'); + } + } + + private handleError(error: any) { + console.error('WebSocket错误:', error); + this.emit('error', error); + } + + public on(event: string, listener: Function) { + // this.addListener(event, listener); + } + + public emit(event: string, data?: any) { + // this.listeners(event).forEach((listener) => listener(data)); + } +} + +export default WebSocketManager;