feat: 实现依赖注入系统、插件架构和窗口管理

- 引入ExamAware的DI系统并适配SecScore项目
- 添加服务注册表和窗口管理器
- 实现基于Koishi的Disposable设计的插件系统
- 集成Winston日志服务
- 分离设置窗口为独立页面
- 将非核心功能改为内置插件
- 更新README文件
- 添加相关hooks和工具函数
This commit is contained in:
Yukino_fox
2026-04-26 19:39:56 +08:00
parent a39355d62a
commit 18eada8ff3
47 changed files with 5347 additions and 180 deletions
+164
View File
@@ -0,0 +1,164 @@
import { useEffect, useState, useCallback } from "react"
import {
BuiltinPluginMeta,
getAvailablePlugins,
getInstalledPlugins,
installPlugin,
uninstallPlugin,
enablePlugin,
disablePlugin,
isPluginEnabled,
} from "../plugins/builtin"
/**
* React Hook: 使用内置插件系统
*/
export function useBuiltinPlugins() {
const [plugins, setPlugins] = useState<
Array<BuiltinPluginMeta & { installed: boolean; enabled: boolean }>
>([])
const [installedPlugins, setInstalledPlugins] = useState<
Array<BuiltinPluginMeta & { enabled: boolean; installedAt?: number }>
>([])
const [loading, setLoading] = useState(true)
const refreshPlugins = useCallback(async () => {
setLoading(true)
try {
const [available, installed] = await Promise.all([
getAvailablePlugins(),
getInstalledPlugins(),
])
setPlugins(available)
setInstalledPlugins(installed)
} catch (error) {
console.error("Failed to refresh plugins:", error)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
refreshPlugins()
// 监听插件状态变化
const handleStateChanged = () => {
refreshPlugins()
}
window.addEventListener("ss:plugins-state-changed", handleStateChanged)
window.addEventListener("ss:plugin-installed", handleStateChanged)
window.addEventListener("ss:plugin-uninstalled", handleStateChanged)
window.addEventListener("ss:plugin-enabled", handleStateChanged)
window.addEventListener("ss:plugin-disabled", handleStateChanged)
return () => {
window.removeEventListener("ss:plugins-state-changed", handleStateChanged)
window.removeEventListener("ss:plugin-installed", handleStateChanged)
window.removeEventListener("ss:plugin-uninstalled", handleStateChanged)
window.removeEventListener("ss:plugin-enabled", handleStateChanged)
window.removeEventListener("ss:plugin-disabled", handleStateChanged)
}
}, [refreshPlugins])
const install = useCallback(
async (pluginId: string) => {
const result = await installPlugin(pluginId)
if (result) {
await refreshPlugins()
}
return result
},
[refreshPlugins]
)
const uninstall = useCallback(
async (pluginId: string) => {
const result = await uninstallPlugin(pluginId)
if (result) {
await refreshPlugins()
}
return result
},
[refreshPlugins]
)
const enable = useCallback(
async (pluginId: string) => {
const result = await enablePlugin(pluginId)
if (result) {
await refreshPlugins()
}
return result
},
[refreshPlugins]
)
const disable = useCallback(
async (pluginId: string) => {
const result = await disablePlugin(pluginId)
if (result) {
await refreshPlugins()
}
return result
},
[refreshPlugins]
)
const isEnabled = useCallback(async (pluginId: string) => {
return await isPluginEnabled(pluginId)
}, [])
return {
plugins,
installedPlugins,
loading,
refreshPlugins,
install,
uninstall,
enable,
disable,
isEnabled,
}
}
/**
* React Hook: 检查特定插件是否启用
*/
export function usePluginEnabled(pluginId: string): boolean {
const [enabled, setEnabled] = useState(false)
useEffect(() => {
let mounted = true
const checkEnabled = async () => {
try {
const result = await isPluginEnabled(pluginId)
if (mounted) {
setEnabled(result)
}
} catch (error) {
console.error(`Failed to check plugin ${pluginId} state:`, error)
}
}
checkEnabled()
const handleStateChanged = () => {
checkEnabled()
}
window.addEventListener("ss:plugins-state-changed", handleStateChanged)
window.addEventListener("ss:plugin-enabled", handleStateChanged)
window.addEventListener("ss:plugin-disabled", handleStateChanged)
return () => {
mounted = false
window.removeEventListener("ss:plugins-state-changed", handleStateChanged)
window.removeEventListener("ss:plugin-enabled", handleStateChanged)
window.removeEventListener("ss:plugin-disabled", handleStateChanged)
}
}, [pluginId])
return enabled
}
+99
View File
@@ -0,0 +1,99 @@
import { useEffect, useState, useCallback } from "react"
import { ConfigService, ConfigSpec } from "../services/ConfigService"
export function useConfig<K extends keyof ConfigSpec>(
key: K,
defaultValue?: ConfigSpec[K]
): [ConfigSpec[K], (value: ConfigSpec[K]) => Promise<void>, boolean] {
const [value, setValue] = useState<ConfigSpec[K]>(
ConfigService.has(key) ? ConfigService.getAll()[key] : (defaultValue as ConfigSpec[K])
)
const [loading, setLoading] = useState(!ConfigService.has(key))
useEffect(() => {
let mounted = true
const init = async () => {
try {
await ConfigService.initialize()
const currentValue = ConfigService.getAll()[key]
if (mounted) {
setValue(currentValue ?? defaultValue)
setLoading(false)
}
} catch (error) {
console.error("Failed to load config:", error)
if (mounted) {
setValue(defaultValue as ConfigSpec[K])
setLoading(false)
}
}
}
init()
const unsubscribe = ConfigService.onChange((event) => {
if (event.key === key && mounted) {
setValue(event.value)
}
})
return () => {
mounted = false
unsubscribe()
}
}, [key, defaultValue])
const updateValue = useCallback(
async (newValue: ConfigSpec[K]) => {
try {
await ConfigService.set(key, newValue)
setValue(newValue)
} catch (error) {
console.error("Failed to update config:", error)
throw error
}
},
[key]
)
return [value, updateValue, loading]
}
export function useConfigAll(): [ConfigSpec, (key: keyof ConfigSpec, value: any) => Promise<void>] {
const [config, setConfig] = useState<ConfigSpec>(ConfigService.getAll())
useEffect(() => {
let mounted = true
const init = async () => {
try {
await ConfigService.initialize()
if (mounted) {
setConfig(ConfigService.getAll())
}
} catch (error) {
console.error("Failed to load config:", error)
}
}
init()
const unsubscribe = ConfigService.onChange(() => {
if (mounted) {
setConfig(ConfigService.getAll())
}
})
return () => {
mounted = false
unsubscribe()
}
}, [])
const updateConfig = useCallback(async (key: keyof ConfigSpec, value: any) => {
await ConfigService.set(key, value)
}, [])
return [config, updateConfig]
}
+55
View File
@@ -0,0 +1,55 @@
import { useCallback } from "react"
import { logger, LogLevel } from "../services/LoggerService"
/**
* React Hook: 使用日志服务
*/
export function useLogger(source?: string) {
const log = useCallback(
(_level: LogLevel, message: string, meta?: any) => {
if (source) {
const childLogger = logger.createChild(source)
childLogger.debug(message, meta)
} else {
logger.debug(message, meta)
}
},
[source]
)
const debug = useCallback(
(message: string, meta?: any) => {
log("debug", message, meta)
},
[log]
)
const info = useCallback(
(message: string, meta?: any) => {
log("info", message, meta)
},
[log]
)
const warn = useCallback(
(message: string, meta?: any) => {
log("warn", message, meta)
},
[log]
)
const error = useCallback(
(message: string, meta?: any) => {
log("error", message, meta)
},
[log]
)
return {
debug,
info,
warn,
error,
log,
}
}