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:
手瓜一十雪
2025-11-13 15:39:42 +08:00
committed by GitHub
parent e2486606f9
commit 4360775eff
778 changed files with 2356 additions and 26391 deletions

View 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;

View 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;

View 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;

View 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;

View File

@@ -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 };
}

View 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>();

View 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 };
};

View File

@@ -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,
};
}

View 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 };
};

View File

@@ -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;