mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-01 16:20:25 +00:00
refactor: 整体重构 (#1381)
* feat: pnpm new * Refactor build and release workflows, update dependencies Switch build scripts and workflows from npm to pnpm, update build and artifact paths, and simplify release workflow by removing version detection and changelog steps. Add new dependencies (silk-wasm, express, ws, node-pty-prebuilt-multiarch), update exports in package.json files, and add vite config for napcat-framework. Also, rename manifest.json for framework package and fix static asset copying in shell build config.
This commit is contained in:
15
packages/napcat-webui-frontend/src/hooks/auth.ts
Normal file
15
packages/napcat-webui-frontend/src/hooks/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||
|
||||
import key from '@/const/key';
|
||||
|
||||
const useAuth = () => {
|
||||
const [token, setToken] = useLocalStorage<string>(key.token, '');
|
||||
|
||||
return {
|
||||
token,
|
||||
isAuth: !!token,
|
||||
revokeAuth: () => setToken(''),
|
||||
};
|
||||
};
|
||||
|
||||
export default useAuth;
|
||||
189
packages/napcat-webui-frontend/src/hooks/use-config.ts
Normal file
189
packages/napcat-webui-frontend/src/hooks/use-config.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { updateConfig as storeUpdateConfig } from '@/store/modules/config';
|
||||
|
||||
import { deepClone } from '@/utils/object';
|
||||
|
||||
import QQManager from '@/controllers/qq_manager';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from './use-store';
|
||||
|
||||
const useConfig = () => {
|
||||
const config = useAppSelector((state) => state.config.value);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const createNetworkConfig = async <T extends keyof OneBotConfig['network']>(
|
||||
key: T,
|
||||
value: OneBotConfig['network'][T][0]
|
||||
) => {
|
||||
const allNetworkNames = Object.keys(config.network).reduce((acc, key) => {
|
||||
const _key = key as keyof OneBotConfig['network'];
|
||||
return acc.concat(config.network[_key].map((item) => item.name));
|
||||
}, [] as string[]);
|
||||
|
||||
if (value.name && allNetworkNames.includes(value.name)) {
|
||||
throw new Error('已经存在相同的配置项名');
|
||||
}
|
||||
|
||||
const newConfig = deepClone(config)
|
||||
|
||||
;(newConfig.network[key] as (typeof value)[]).push(value);
|
||||
|
||||
await QQManager.setOB11Config(newConfig);
|
||||
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const updateNetworkConfig = async <T extends keyof OneBotConfig['network']>(
|
||||
key: T,
|
||||
value: OneBotConfig['network'][T][0]
|
||||
) => {
|
||||
const newConfig = deepClone(config);
|
||||
const name = value.name;
|
||||
const index = newConfig.network[key].findIndex((item) => item.name === name);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('找不到对应的配置项');
|
||||
}
|
||||
|
||||
newConfig.network[key][index] = value;
|
||||
|
||||
await QQManager.setOB11Config(newConfig);
|
||||
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const deleteNetworkConfig = async <T extends keyof OneBotConfig['network']>(
|
||||
key: T,
|
||||
name: string
|
||||
) => {
|
||||
const newConfig = deepClone(config);
|
||||
const index = newConfig.network[key].findIndex((item) => item.name === name);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('找不到对应的配置项');
|
||||
}
|
||||
|
||||
newConfig.network[key].splice(index, 1);
|
||||
|
||||
await QQManager.setOB11Config(newConfig);
|
||||
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const enableNetworkConfig = async <T extends keyof OneBotConfig['network']>(
|
||||
key: T,
|
||||
name: string
|
||||
) => {
|
||||
const newConfig = deepClone(config);
|
||||
const index = newConfig.network[key].findIndex((item) => item.name === name);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('找不到对应的配置项');
|
||||
}
|
||||
|
||||
newConfig.network[key][index].enable = !newConfig.network[key][index].enable;
|
||||
|
||||
await QQManager.setOB11Config(newConfig);
|
||||
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const enableDebugNetworkConfig = async <
|
||||
T extends keyof OneBotConfig['network']
|
||||
>(
|
||||
key: T,
|
||||
name: string
|
||||
) => {
|
||||
const newConfig = deepClone(config);
|
||||
const index = newConfig.network[key].findIndex((item) => item.name === name);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('找不到对应的配置项');
|
||||
}
|
||||
|
||||
newConfig.network[key][index].debug = !newConfig.network[key][index].debug;
|
||||
|
||||
await QQManager.setOB11Config(newConfig);
|
||||
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const updateSingleConfig = async <T extends keyof OneBotConfig>(
|
||||
key: T,
|
||||
value: OneBotConfig[T]
|
||||
) => {
|
||||
const newConfig = deepClone(config);
|
||||
|
||||
newConfig[key] = value;
|
||||
|
||||
await QQManager.setOB11Config(newConfig);
|
||||
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const updateConfig = async (newConfig: OneBotConfig) => {
|
||||
await QQManager.setOB11Config(newConfig);
|
||||
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const refreshConfig = async () => {
|
||||
const newConfig = await QQManager.getOB11Config();
|
||||
|
||||
if (JSON.stringify(newConfig) === JSON.stringify(config)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const mergeConfig = async (newConfig: OneBotConfig) => {
|
||||
const mergedConfig = deepClone(config);
|
||||
|
||||
Object.assign(mergedConfig, newConfig);
|
||||
|
||||
await QQManager.setOB11Config(mergedConfig);
|
||||
|
||||
dispatch(storeUpdateConfig(mergedConfig));
|
||||
|
||||
return mergedConfig;
|
||||
};
|
||||
|
||||
const saveConfigWithoutNetwork = async (newConfig: OneBotConfig) => {
|
||||
newConfig.network = config.network;
|
||||
await QQManager.setOB11Config(newConfig);
|
||||
dispatch(storeUpdateConfig(newConfig));
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
return {
|
||||
config,
|
||||
createNetworkConfig,
|
||||
refreshConfig,
|
||||
updateConfig,
|
||||
updateSingleConfig,
|
||||
updateNetworkConfig,
|
||||
deleteNetworkConfig,
|
||||
enableNetworkConfig,
|
||||
enableDebugNetworkConfig,
|
||||
mergeConfig,
|
||||
saveConfigWithoutNetwork,
|
||||
};
|
||||
};
|
||||
|
||||
export default useConfig;
|
||||
11
packages/napcat-webui-frontend/src/hooks/use-dialog.ts
Normal file
11
packages/napcat-webui-frontend/src/hooks/use-dialog.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { DialogContext } from '@/contexts/dialog';
|
||||
|
||||
const useDialog = () => {
|
||||
const dialog = React.useContext(DialogContext);
|
||||
|
||||
return dialog;
|
||||
};
|
||||
|
||||
export default useDialog;
|
||||
11
packages/napcat-webui-frontend/src/hooks/use-music.ts
Normal file
11
packages/napcat-webui-frontend/src/hooks/use-music.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AudioContext } from '@/contexts/songs';
|
||||
|
||||
const useMusic = () => {
|
||||
const music = React.useContext(AudioContext);
|
||||
|
||||
return music;
|
||||
};
|
||||
|
||||
export default useMusic;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// 全局图片缓存
|
||||
const imageCache = new Map<string, HTMLImageElement>();
|
||||
|
||||
export function usePreloadImages (urls: string[]) {
|
||||
const [loadedUrls, setLoadedUrls] = useState<Record<string, boolean>>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
// 检查是否所有图片都已缓存
|
||||
const allCached = urls.every((url) => imageCache.has(url));
|
||||
if (allCached) {
|
||||
setLoadedUrls(urls.reduce((acc, url) => ({ ...acc, [url]: true }), {}));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const loadedImages: Record<string, boolean> = {};
|
||||
let pendingCount = urls.length;
|
||||
|
||||
urls.forEach((url) => {
|
||||
// 如果已经缓存,直接标记为已加载
|
||||
if (imageCache.has(url)) {
|
||||
loadedImages[url] = true;
|
||||
pendingCount--;
|
||||
if (pendingCount === 0) {
|
||||
setLoadedUrls(loadedImages);
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (!isMounted.current) return;
|
||||
loadedImages[url] = true;
|
||||
imageCache.set(url, img);
|
||||
pendingCount--;
|
||||
|
||||
if (pendingCount === 0) {
|
||||
setLoadedUrls(loadedImages);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
if (!isMounted.current) return;
|
||||
loadedImages[url] = false;
|
||||
pendingCount--;
|
||||
|
||||
if (pendingCount === 0) {
|
||||
setLoadedUrls(loadedImages);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [urls]);
|
||||
|
||||
return { loadedUrls, isLoading };
|
||||
}
|
||||
6
packages/napcat-webui-frontend/src/hooks/use-store.ts
Normal file
6
packages/napcat-webui-frontend/src/hooks/use-store.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import type { AppDispatch, RootState } from '@/store';
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||
42
packages/napcat-webui-frontend/src/hooks/use-theme.ts
Normal file
42
packages/napcat-webui-frontend/src/hooks/use-theme.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// originally written by @imoaazahmed
|
||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
const ThemeProps = {
|
||||
key: 'theme',
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
} as const;
|
||||
|
||||
type Theme = typeof ThemeProps.light | typeof ThemeProps.dark;
|
||||
|
||||
export const useTheme = (defaultTheme?: Theme) => {
|
||||
const [theme, setTheme] = useLocalStorage<Theme>(ThemeProps.key, defaultTheme);
|
||||
|
||||
const isDark = useMemo(() => {
|
||||
return theme === ThemeProps.dark;
|
||||
}, [theme]);
|
||||
|
||||
const isLight = useMemo(() => {
|
||||
return theme === ThemeProps.light;
|
||||
}, [theme]);
|
||||
|
||||
const _setTheme = (theme: Theme) => {
|
||||
setTheme(theme);
|
||||
document.documentElement.classList.remove(ThemeProps.light, ThemeProps.dark);
|
||||
document.documentElement.classList.add(theme);
|
||||
};
|
||||
|
||||
const setLightTheme = () => _setTheme(ThemeProps.light);
|
||||
|
||||
const setDarkTheme = () => _setTheme(ThemeProps.dark);
|
||||
|
||||
const toggleTheme = () =>
|
||||
theme === ThemeProps.dark ? setLightTheme() : setDarkTheme();
|
||||
|
||||
useEffect(() => {
|
||||
_setTheme(theme);
|
||||
});
|
||||
|
||||
return { theme, isDark, isLight, setLightTheme, setDarkTheme, toggleTheme };
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Selection } from '@react-types/shared';
|
||||
import { useReactive } from 'ahooks';
|
||||
import { useCallback, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useWebSocket, { ReadyState } from 'react-use-websocket';
|
||||
|
||||
import { renderFilterMessageType } from '@/components/onebot/filter_message_type';
|
||||
|
||||
import { isOB11Event, isOB11RequestResponse } from '@/utils/onebot';
|
||||
|
||||
import type { AllOB11WsResponse } from '@/types/onebot';
|
||||
|
||||
export { ReadyState } from 'react-use-websocket';
|
||||
export function useWebSocketDebug (url: string, token: string) {
|
||||
const messageHistory = useReactive<AllOB11WsResponse[]>([]);
|
||||
const [filterTypes, setFilterTypes] = useState<Selection>('all');
|
||||
|
||||
const filteredMessages = messageHistory.filter((msg) => {
|
||||
if (filterTypes === 'all' || filterTypes.size === 0) return true;
|
||||
if (isOB11Event(msg)) return filterTypes.has(msg.post_type);
|
||||
if (isOB11RequestResponse(msg)) return filterTypes.has('request');
|
||||
return false;
|
||||
});
|
||||
|
||||
const { sendMessage, readyState } = useWebSocket(url, {
|
||||
onMessage: useCallback((event: WebSocketEventMap['message']) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
messageHistory.unshift(data);
|
||||
} catch (_error) {
|
||||
toast.error('WebSocket 消息解析失败');
|
||||
}
|
||||
}, []),
|
||||
queryParams: {
|
||||
access_token: token,
|
||||
},
|
||||
onError: (event) => {
|
||||
toast.error('WebSocket 连接失败');
|
||||
console.error('WebSocket error:', event);
|
||||
},
|
||||
onOpen: () => {
|
||||
messageHistory.splice(0, messageHistory.length);
|
||||
},
|
||||
});
|
||||
|
||||
const _sendMessage = (msg: string) => {
|
||||
if (readyState !== ReadyState.OPEN) {
|
||||
throw new Error('WebSocket 连接未建立');
|
||||
}
|
||||
sendMessage(msg);
|
||||
};
|
||||
|
||||
const FilterMessagesType = renderFilterMessageType(
|
||||
filterTypes,
|
||||
setFilterTypes
|
||||
);
|
||||
|
||||
return {
|
||||
sendMessage: _sendMessage,
|
||||
readyState,
|
||||
messageHistory,
|
||||
filteredMessages,
|
||||
filterTypes,
|
||||
setFilterTypes,
|
||||
FilterMessagesType,
|
||||
};
|
||||
}
|
||||
31
packages/napcat-webui-frontend/src/hooks/use_custom_quill.ts
Normal file
31
packages/napcat-webui-frontend/src/hooks/use_custom_quill.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import Quill from 'quill';
|
||||
import 'quill/dist/quill.core.css';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseCustomQuillProps {
|
||||
modules: Record<string, unknown>
|
||||
formats: string[]
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
export const useCustomQuill = ({
|
||||
modules,
|
||||
formats,
|
||||
placeholder,
|
||||
}: UseCustomQuillProps) => {
|
||||
const quillRef = useRef<HTMLDivElement | null>(null);
|
||||
const [quill, setQuill] = useState<Quill | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (quillRef.current) {
|
||||
const quillInstance = new Quill(quillRef.current, {
|
||||
modules,
|
||||
formats,
|
||||
placeholder,
|
||||
});
|
||||
setQuill(quillInstance);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { quillRef, quill, Quill };
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import ShowStructedMessage from '@/components/chat_input/components/show_structed_message';
|
||||
|
||||
import { OB11Segment } from '@/types/onebot';
|
||||
|
||||
import useDialog from './use-dialog';
|
||||
|
||||
const useShowStructuredMessage = () => {
|
||||
const dialog = useDialog();
|
||||
|
||||
const showStructuredMessage = (messages: OB11Segment[]) => {
|
||||
dialog.alert({
|
||||
title: '消息内容',
|
||||
size: '3xl',
|
||||
content: createElement(ShowStructedMessage, {
|
||||
messages,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return showStructuredMessage;
|
||||
};
|
||||
|
||||
export default useShowStructuredMessage;
|
||||
Reference in New Issue
Block a user