@bbki.ng/site 5.7.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/index.html +0 -11
- package/package.json +1 -1
- package/src/app/app.tsx +1 -2
- package/src/core/components/SlotComp.tsx +2 -3
- package/src/core/hooks/useMiddlewareTransData.ts +12 -7
- package/src/core/hooks/useSlotComp.ts +8 -7
- package/src/core/hooks/use_plugins.ts +27 -14
- package/src/core/plugin-system/pluginManager.ts +129 -0
- package/src/core/{pluginManifestService.ts → plugin-system/pluginManifestService.ts} +2 -3
- package/src/core/plugin-system/pluginStore.ts +190 -0
- package/src/core/{registry.ts → plugin-system/registry.ts} +1 -3
- package/src/core/plugin-system/services/coreService.ts +56 -0
- package/src/core/plugin-system/services/systemUIService.ts +159 -0
- package/src/core/shared-service/contract/ICoreService.ts +23 -0
- package/src/core/shared-service/contract/IUIService.ts +49 -0
- package/src/core/shared-service/service-proxy.ts +26 -0
- package/src/core/shared-service/service-registry.ts +52 -0
- package/src/plugins/blog/components/BlogSlotCom.tsx +27 -0
- package/src/plugins/blog/components/article/index.tsx +2 -2
- package/src/plugins/blog/context/index.ts +0 -4
- package/src/plugins/blog/hooks/useMiddlewareTransData.ts +79 -0
- package/src/plugins/blog/hooks/use_blog_scroll_pos_restoration.ts +2 -2
- package/src/plugins/blog/hooks/use_blog_slot_com.ts +27 -0
- package/src/plugins/blog/hooks/use_posts.ts +3 -2
- package/src/plugins/blog/index.ts +32 -5
- package/src/plugins/blog/pages/extensions/txt/article.tsx +2 -2
- package/src/plugins/blog/services/BlogUIService.ts +120 -0
- package/src/plugins/blog/services/IBlogUIService.ts +31 -0
- package/src/plugins/extra-cd/index.ts +13 -2
- package/src/plugins/extra-entry/index.ts +8 -4
- package/src/plugins/fx/index.ts +18 -5
- package/src/plugins/manifest.ts +2 -6
- package/src/plugins/now/hooks/use_streaming.ts +3 -1
- package/src/plugins/now/index.ts +17 -6
- package/src/plugins/sticker/const.ts +2 -2
- package/src/plugins/sticker/index.ts +18 -3
- package/src/plugins/store/components/storePage.tsx +13 -2
- package/src/plugins/store/context/index.ts +1 -0
- package/src/plugins/store/index.ts +18 -7
- package/src/plugins/xwy/index.ts +24 -19
- package/src/plugins/xwy/types/index.ts +0 -18
- package/src/types/hostApi.ts +3 -34
- package/src/types/plugin.ts +1 -0
- package/src/utils/index.tsx +1 -48
- package/vite.config.js +1 -1
- package/src/core/pluginManager.ts +0 -191
- package/src/core/pluginStore.ts +0 -70
- /package/src/core/{bbplugin.ts → plugin-system/bbplugin.ts} +0 -0
package/CHANGELOG.md
CHANGED
package/index.html
CHANGED
|
@@ -13,17 +13,6 @@
|
|
|
13
13
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
14
14
|
<link rel="mask-icon" href="/favicon.svg" color="#FFFFFF" />
|
|
15
15
|
<meta name="theme-color" content="#ffffff" />
|
|
16
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
17
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
18
|
-
<!-- <link
|
|
19
|
-
href="/google-font.css"
|
|
20
|
-
rel="stylesheet"
|
|
21
|
-
media="print"
|
|
22
|
-
onload="
|
|
23
|
-
this.media = 'all';
|
|
24
|
-
this.onload = null;
|
|
25
|
-
"
|
|
26
|
-
/> -->
|
|
27
16
|
<style>
|
|
28
17
|
bb-img:not(:defined) {
|
|
29
18
|
display: block;
|
package/package.json
CHANGED
package/src/app/app.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Route, Routes } from 'react-router-dom';
|
|
3
|
-
import { NotFound } from '@bbki.ng/ui';
|
|
4
3
|
|
|
5
4
|
import { BotRedirect } from '#/app/components/bot';
|
|
6
5
|
import { BBContext } from '#/app/context/bbcontext';
|
|
@@ -28,7 +27,7 @@ const AppRoutes = () => {
|
|
|
28
27
|
element={<Slot name="route" data={route} />}
|
|
29
28
|
/>
|
|
30
29
|
))}
|
|
31
|
-
<Route path="*" element={
|
|
30
|
+
<Route path="*" element={null} />
|
|
32
31
|
</Route>
|
|
33
32
|
</Routes>
|
|
34
33
|
);
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { SlotName } from '#/types/slots';
|
|
4
|
-
|
|
5
3
|
import { useSlotComp } from '../hooks/useSlotComp';
|
|
4
|
+
import { SystemSlotName } from '../shared-service/contract/IUIService';
|
|
6
5
|
|
|
7
6
|
export interface ISlotProps {
|
|
8
|
-
name:
|
|
7
|
+
name: SystemSlotName;
|
|
9
8
|
data?: unknown;
|
|
10
9
|
|
|
11
10
|
placeholder?: React.ReactNode;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { SystemUIService } from '../plugin-system/services/systemUIService';
|
|
4
|
+
import { SystemDataHookPoint } from '../shared-service/contract/IUIService';
|
|
5
5
|
|
|
6
6
|
export interface UseMiddlewareTransDataOptions {
|
|
7
|
-
hookPoint:
|
|
7
|
+
hookPoint: SystemDataHookPoint;
|
|
8
8
|
onMiddlewareChange?: () => void; // 通知外部,由外部决定是否 run
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -26,7 +26,7 @@ export function useMiddlewareRunner<T>({
|
|
|
26
26
|
setLoading(true);
|
|
27
27
|
setError(null);
|
|
28
28
|
try {
|
|
29
|
-
const result = await
|
|
29
|
+
const result = await SystemUIService.getInstance().runMiddlewares(hookPoint, inputData);
|
|
30
30
|
return result;
|
|
31
31
|
} catch (err) {
|
|
32
32
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -41,8 +41,10 @@ export function useMiddlewareRunner<T>({
|
|
|
41
41
|
|
|
42
42
|
// 仅通知,不执行
|
|
43
43
|
useEffect(() => {
|
|
44
|
-
const unsubscribe =
|
|
45
|
-
|
|
44
|
+
const unsubscribe = SystemUIService.getInstance().subscribeMiddlewareChange(hook => {
|
|
45
|
+
if (hook === hookPoint) {
|
|
46
|
+
onMiddlewareChange?.();
|
|
47
|
+
}
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
return () => {
|
|
@@ -53,7 +55,10 @@ export function useMiddlewareRunner<T>({
|
|
|
53
55
|
return { loading, error, run };
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
export const useMiddlewareTransformedData = <T>(
|
|
58
|
+
export const useMiddlewareTransformedData = <T>(
|
|
59
|
+
hookPoint: SystemDataHookPoint,
|
|
60
|
+
defaultValue: T
|
|
61
|
+
) => {
|
|
57
62
|
const [result, setResult] = useState<T>(defaultValue);
|
|
58
63
|
|
|
59
64
|
const runRef = useRef<(input: T) => Promise<T>>(() => Promise.resolve(defaultValue));
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import { IComPropsRegisteredToSlot
|
|
3
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { SystemUIService } from '../plugin-system/services/systemUIService';
|
|
6
|
+
import { SystemSlotName } from '../shared-service/contract/IUIService';
|
|
6
7
|
|
|
7
|
-
export const useSlotComp = (slotName:
|
|
8
|
+
export const useSlotComp = (slotName: SystemSlotName) => {
|
|
8
9
|
const [components, setComponents] = useState<React.ComponentType<IComPropsRegisteredToSlot>[]>(
|
|
9
|
-
() =>
|
|
10
|
+
() => SystemUIService.getInstance().getComponents(slotName)
|
|
10
11
|
);
|
|
11
12
|
|
|
12
13
|
useEffect(() => {
|
|
13
|
-
setComponents(
|
|
14
|
+
setComponents(SystemUIService.getInstance().getComponents(slotName));
|
|
14
15
|
|
|
15
|
-
const unsubscribe =
|
|
16
|
-
const comps =
|
|
16
|
+
const unsubscribe = SystemUIService.getInstance().subscribeSlotChange(() => {
|
|
17
|
+
const comps = SystemUIService.getInstance().getComponents(slotName);
|
|
17
18
|
setComponents(comps);
|
|
18
19
|
});
|
|
19
20
|
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { useGlobalLoading } from '#/app/hooks/use_global_loading';
|
|
4
|
-
import { pluginManager } from '#/core/pluginManager';
|
|
4
|
+
import { pluginManager } from '#/core/plugin-system/pluginManager';
|
|
5
5
|
import { PluginID } from '#/types/plugin';
|
|
6
6
|
|
|
7
|
-
import { PluginManifestService } from '../pluginManifestService';
|
|
8
|
-
import { PluginStore } from '../pluginStore';
|
|
7
|
+
import { PluginManifestService } from '../plugin-system/pluginManifestService';
|
|
8
|
+
import { PluginStore } from '../plugin-system/pluginStore';
|
|
9
|
+
import { CoreService } from '../plugin-system/services/coreService';
|
|
9
10
|
|
|
10
11
|
const usePluginsLoading = () => {
|
|
11
12
|
const { setGlobalLoading, isLoading } = useGlobalLoading();
|
|
12
13
|
|
|
13
14
|
useEffect(() => {
|
|
14
|
-
|
|
15
|
+
CoreService.getInstance().setSiteLoading(isLoading);
|
|
15
16
|
}, [isLoading]);
|
|
16
17
|
|
|
17
18
|
useEffect(() => {
|
|
18
19
|
let unregister: (() => void) | undefined;
|
|
19
|
-
const
|
|
20
|
+
const unsubscribe = CoreService.getInstance().subscribePluginLoading(payload => {
|
|
20
21
|
unregister = setGlobalLoading(payload.id, payload.loading);
|
|
21
22
|
});
|
|
23
|
+
|
|
22
24
|
return () => {
|
|
23
|
-
|
|
25
|
+
unsubscribe();
|
|
24
26
|
unregister?.();
|
|
25
27
|
};
|
|
26
28
|
}, [setGlobalLoading]);
|
|
@@ -56,17 +58,28 @@ export const usePlugins = () => {
|
|
|
56
58
|
if (cancelled) {
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
+
const loadNext = (index: number) => {
|
|
62
|
+
if (cancelled || index >= pluginIds.length) {
|
|
61
63
|
if (!cancelled) {
|
|
62
64
|
setDone(true);
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const id = pluginIds[index];
|
|
70
|
+
pluginManager
|
|
71
|
+
.loadPlugin(id)
|
|
72
|
+
.then(() => loadNext(index + 1))
|
|
73
|
+
.catch(error => {
|
|
74
|
+
if (!cancelled) {
|
|
75
|
+
console.error(`Error loading plugin ${id}:`, error);
|
|
76
|
+
// 即使某个插件加载失败,也继续加载下一个
|
|
77
|
+
loadNext(index + 1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
loadNext(0);
|
|
70
83
|
})
|
|
71
84
|
.catch(error => {
|
|
72
85
|
if (!cancelled) {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { IHostContext } from '#/types/hostApi';
|
|
2
|
+
import { IPlugin, PluginEvents, PluginID, PluginPerm } from '#/types/plugin';
|
|
3
|
+
|
|
4
|
+
import { createEventBus } from '../utils/eventBus';
|
|
5
|
+
import { AdminPluginIDSet } from '../const';
|
|
6
|
+
import { ServiceRegistry } from '../shared-service/service-registry';
|
|
7
|
+
|
|
8
|
+
import { PluginStore } from './pluginStore';
|
|
9
|
+
import { CoreService } from './services/coreService';
|
|
10
|
+
import { SystemUIService } from './services/systemUIService';
|
|
11
|
+
|
|
12
|
+
const pluginModules = import.meta.glob('../../plugins/*/index.ts');
|
|
13
|
+
|
|
14
|
+
class PluginManager {
|
|
15
|
+
private activePlugins: Map<string, IPlugin> = new Map();
|
|
16
|
+
|
|
17
|
+
private bus = createEventBus<PluginEvents>();
|
|
18
|
+
|
|
19
|
+
private constructor() {
|
|
20
|
+
ServiceRegistry.getInstance().register('core:baseService', CoreService.getInstance());
|
|
21
|
+
ServiceRegistry.getInstance().register('core:uiService', SystemUIService.getInstance());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private static instance: PluginManager;
|
|
25
|
+
|
|
26
|
+
private createHostContext(perm: PluginPerm = 'guest'): IHostContext {
|
|
27
|
+
const adminCtx =
|
|
28
|
+
perm === 'admin'
|
|
29
|
+
? {
|
|
30
|
+
store: PluginStore.getInstance(),
|
|
31
|
+
}
|
|
32
|
+
: {};
|
|
33
|
+
return {
|
|
34
|
+
...adminCtx,
|
|
35
|
+
service: ServiceRegistry.getInstance(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private loading = new Set<string>();
|
|
40
|
+
|
|
41
|
+
static getInstance() {
|
|
42
|
+
if (!PluginManager.instance) {
|
|
43
|
+
PluginManager.instance = new PluginManager();
|
|
44
|
+
}
|
|
45
|
+
return PluginManager.instance;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
notifySiteLoadingChanged(loading: boolean) {
|
|
49
|
+
this.bus.emit('site:loading:changed', loading);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
subscribePluginLoading(listener: (payload: PluginEvents['plugin:loading:changed']) => void) {
|
|
53
|
+
return this.bus.on('plugin:loading:changed', listener);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// enable abort ctrl
|
|
57
|
+
async loadPlugin(pluginId: PluginID) {
|
|
58
|
+
if (this.activePlugins.has(pluginId)) {
|
|
59
|
+
console.warn(`Plugin ${pluginId} is already loaded.`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.loading.has(pluginId)) {
|
|
64
|
+
console.warn(`Plugin ${pluginId} is loading...`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// try load plugin
|
|
69
|
+
this.loading.add(pluginId);
|
|
70
|
+
this.bus.emit('plugin:loading:changed', { id: pluginId, loading: true });
|
|
71
|
+
|
|
72
|
+
const modulePath = `../../plugins/${pluginId}/index.ts`;
|
|
73
|
+
const moduleLoader = pluginModules[modulePath];
|
|
74
|
+
if (!moduleLoader) {
|
|
75
|
+
console.error(`Plugin ${pluginId} not found in registry`);
|
|
76
|
+
|
|
77
|
+
this.loading.delete(pluginId);
|
|
78
|
+
this.bus.emit('plugin:loading:changed', { id: pluginId, loading: false });
|
|
79
|
+
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const module = (await moduleLoader()) as {
|
|
85
|
+
default: IPlugin;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const p: IPlugin = module.default;
|
|
89
|
+
|
|
90
|
+
const perm = p.getMeta().perm || 'guest';
|
|
91
|
+
if (perm === 'admin' && !AdminPluginIDSet.has(pluginId)) {
|
|
92
|
+
console.error(`Plugin ${pluginId} requires admin permission, but it's not trusted.`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ctx = this.createHostContext(perm);
|
|
97
|
+
|
|
98
|
+
if (p.onInstall) {
|
|
99
|
+
await p.onInstall(ctx);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.activePlugins.set(pluginId, p);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`Failed to load plugin ${pluginId}:`, error);
|
|
105
|
+
} finally {
|
|
106
|
+
this.loading.delete(pluginId);
|
|
107
|
+
this.bus.emit('plugin:loading:changed', { id: pluginId, loading: false });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async disablePlugin(pluginId: PluginID) {
|
|
112
|
+
const plugin = this.activePlugins.get(pluginId);
|
|
113
|
+
if (!plugin) {
|
|
114
|
+
console.warn(`Plugin ${pluginId} is not loaded.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (plugin.onDisable) {
|
|
119
|
+
await plugin.onDisable();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.activePlugins.delete(pluginId);
|
|
123
|
+
|
|
124
|
+
SystemUIService.getInstance().unregisterAllByPluginId(plugin.id);
|
|
125
|
+
CoreService.getInstance().notifyPluginUninstall(plugin.id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const pluginManager = PluginManager.getInstance();
|
|
@@ -23,7 +23,7 @@ interface PluginsApiResponse {
|
|
|
23
23
|
description?: string;
|
|
24
24
|
perm?: string;
|
|
25
25
|
icon?: string;
|
|
26
|
-
dependencies?:
|
|
26
|
+
dependencies?: Array<PluginID>;
|
|
27
27
|
}>;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -66,8 +66,7 @@ class PluginManifestService {
|
|
|
66
66
|
let dependencies: PluginID[] | undefined;
|
|
67
67
|
if (item.dependencies) {
|
|
68
68
|
try {
|
|
69
|
-
|
|
70
|
-
dependencies = parsed.filter((id): id is PluginID => KnownPluginIDSet.has(id));
|
|
69
|
+
dependencies = item.dependencies.filter(p => KnownPluginIDSet.has(p));
|
|
71
70
|
} catch {
|
|
72
71
|
console.warn(
|
|
73
72
|
`[PluginManifestService] Failed to parse dependencies for plugin: ${item.id}`
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
2
|
+
|
|
3
|
+
import { pluginManager } from './pluginManager';
|
|
4
|
+
import { PluginManifestService } from './pluginManifestService';
|
|
5
|
+
|
|
6
|
+
export class PluginStore {
|
|
7
|
+
private static instance: PluginStore;
|
|
8
|
+
|
|
9
|
+
private installedSet: Set<PluginID> = new Set();
|
|
10
|
+
|
|
11
|
+
private constructor() {
|
|
12
|
+
this.parse();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private checkDep = (id: PluginID): boolean => {
|
|
16
|
+
const manifest = PluginManifestService.getInstance().getPlugin(id);
|
|
17
|
+
if (!manifest) {
|
|
18
|
+
console.warn(`[PluginStore] Manifest not found for plugin: ${id}`);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!manifest.dependencies || manifest.dependencies.length === 0) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const depId of manifest.dependencies) {
|
|
27
|
+
if (!this.isPluginInstalled(depId)) {
|
|
28
|
+
console.warn(`[PluginStore] Dependency ${depId} for plugin ${id} is not installed`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
private parse = () => {
|
|
37
|
+
this.installedSet.clear();
|
|
38
|
+
const installedPluginsStr = localStorage.getItem('installed_plugins');
|
|
39
|
+
if (installedPluginsStr) {
|
|
40
|
+
try {
|
|
41
|
+
const entries = JSON.parse(installedPluginsStr) as Array<PluginID>;
|
|
42
|
+
entries.forEach(id => this.installedSet.add(id));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error('Failed to parse installed plugins from localStorage', e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
this.ensureRightOrder(this.installedSet);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Failed to ensure correct plugin load order:', error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return this.installedSet;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
private buildDependencyGraph = (ids: Set<PluginID>): Map<PluginID, PluginID[]> => {
|
|
58
|
+
const graph = new Map<PluginID, PluginID[]>();
|
|
59
|
+
const manifest = PluginManifestService.getInstance().getAllPlugins();
|
|
60
|
+
|
|
61
|
+
ids.forEach(id => {
|
|
62
|
+
graph.set(id, []);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
manifest.forEach(plugin => {
|
|
66
|
+
if (ids.has(plugin.id)) {
|
|
67
|
+
const deps = plugin.dependencies || [];
|
|
68
|
+
deps.forEach(dep => {
|
|
69
|
+
if (ids.has(dep)) {
|
|
70
|
+
if (!graph.has(dep)) {
|
|
71
|
+
graph.set(dep, []);
|
|
72
|
+
}
|
|
73
|
+
graph.get(dep)!.push(plugin.id);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return graph;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
private topologicalSortKahn = (graph: Map<PluginID, PluginID[]>): PluginID[] => {
|
|
83
|
+
const inDegree = new Map<PluginID, number>();
|
|
84
|
+
graph.forEach((deps, plugin) => {
|
|
85
|
+
if (!inDegree.has(plugin)) {
|
|
86
|
+
inDegree.set(plugin, 0);
|
|
87
|
+
}
|
|
88
|
+
deps.forEach(dep => {
|
|
89
|
+
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const queue: PluginID[] = [];
|
|
94
|
+
inDegree.forEach((degree, plugin) => {
|
|
95
|
+
if (degree === 0) {
|
|
96
|
+
queue.push(plugin);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const loadOrder: PluginID[] = [];
|
|
101
|
+
while (queue.length > 0) {
|
|
102
|
+
const current = queue.shift()!;
|
|
103
|
+
loadOrder.push(current);
|
|
104
|
+
|
|
105
|
+
const deps = graph.get(current) || [];
|
|
106
|
+
deps.forEach(dep => {
|
|
107
|
+
inDegree.set(dep, inDegree.get(dep)! - 1);
|
|
108
|
+
if (inDegree.get(dep) === 0) {
|
|
109
|
+
queue.push(dep);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (loadOrder.length !== graph.size) {
|
|
115
|
+
throw new Error('Circular dependency detected among plugins');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return loadOrder;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
private ensureRightOrder = (ids: Set<PluginID>) => {
|
|
122
|
+
const graph = this.buildDependencyGraph(ids);
|
|
123
|
+
const loadOrder = this.topologicalSortKahn(graph);
|
|
124
|
+
|
|
125
|
+
// write back to installedSet in the correct order without create a new Set to preserve reference
|
|
126
|
+
if (loadOrder.length === ids.size) {
|
|
127
|
+
this.installedSet.clear();
|
|
128
|
+
loadOrder.forEach(id => this.installedSet.add(id));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
private stringify = () => {
|
|
133
|
+
localStorage.setItem('installed_plugins', JSON.stringify(Array.from(this.installedSet)));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
getInstalledPlugins: () => Set<PluginID> = () => {
|
|
137
|
+
return this.parse();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
async getAllPlugins(): Promise<Array<IPluginStoreEntry>> {
|
|
141
|
+
const installedPlugins = this.getInstalledPlugins();
|
|
142
|
+
await PluginManifestService.getInstance().fetch();
|
|
143
|
+
const manifest = PluginManifestService.getInstance().getAllPlugins();
|
|
144
|
+
return manifest.map(plugin => ({
|
|
145
|
+
...plugin,
|
|
146
|
+
enabled: installedPlugins.has(plugin.id),
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
isPluginInstalled: (id: PluginID) => boolean = id => {
|
|
151
|
+
return this.getInstalledPlugins().has(id);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
getPlugin: (id: PluginID) => IPluginStoreEntry | undefined = id => {
|
|
155
|
+
const manifest = PluginManifestService.getInstance().getPlugin(id);
|
|
156
|
+
if (!manifest) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
...manifest,
|
|
162
|
+
enabled: this.isPluginInstalled(id),
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
installPlugin: (id: PluginID) => Promise<void> = async id => {
|
|
167
|
+
if (!this.checkDep(id)) {
|
|
168
|
+
// throw new Error(`Cannot install plugin ${id} due to missing dependencies`);
|
|
169
|
+
console.warn(`Cannot install plugin ${id} due to missing dependencies`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await pluginManager.loadPlugin(id);
|
|
174
|
+
this.installedSet.add(id);
|
|
175
|
+
this.stringify();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
disablePlugin: (id: PluginID) => Promise<void> = async id => {
|
|
179
|
+
await pluginManager.disablePlugin(id);
|
|
180
|
+
this.installedSet.delete(id);
|
|
181
|
+
this.stringify();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
static getInstance() {
|
|
185
|
+
if (!PluginStore.instance) {
|
|
186
|
+
PluginStore.instance = new PluginStore();
|
|
187
|
+
}
|
|
188
|
+
return PluginStore.instance;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
|
|
4
4
|
|
|
5
|
-
import { createEventBus } from '
|
|
5
|
+
import { createEventBus } from '../utils/eventBus';
|
|
6
6
|
|
|
7
7
|
export interface ISlotEntry {
|
|
8
8
|
id: string;
|
|
@@ -120,5 +120,3 @@ export class Registry {
|
|
|
120
120
|
return result;
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
export const registry = new Registry();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Fetcher } from 'swr';
|
|
2
|
+
|
|
3
|
+
import { getStableDeviceId } from '#/app/utils/fingerprints';
|
|
4
|
+
import { cfApiFetcher } from '#/app/utils';
|
|
5
|
+
import { PluginEvents, PluginID } from '#/types/plugin';
|
|
6
|
+
|
|
7
|
+
import { ICoreService } from '../../shared-service/contract/ICoreService';
|
|
8
|
+
import { createEventBus } from '../../utils/eventBus';
|
|
9
|
+
|
|
10
|
+
export class CoreService implements ICoreService {
|
|
11
|
+
private static instance: CoreService;
|
|
12
|
+
|
|
13
|
+
private constructor() {}
|
|
14
|
+
|
|
15
|
+
private bus = createEventBus<PluginEvents>();
|
|
16
|
+
|
|
17
|
+
getDeviceId = getStableDeviceId;
|
|
18
|
+
|
|
19
|
+
getVersionHash() {
|
|
20
|
+
const hashStr: string = typeof GLOBAL_COMMIT_HASH === 'string' ? GLOBAL_COMMIT_HASH : '0000000';
|
|
21
|
+
return hashStr;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setLoading(id: PluginID, loading: boolean): void {
|
|
25
|
+
this.bus.emit('plugin:loading:changed', { id, loading });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
subscribePluginLoading(listener: (payload: PluginEvents['plugin:loading:changed']) => void) {
|
|
29
|
+
return this.bus.on('plugin:loading:changed', listener);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
subscribePluginUninstall(listener: (payload: PluginEvents['plugin:uninstall']) => void) {
|
|
33
|
+
return this.bus.on('plugin:uninstall', listener);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
notifyPluginUninstall(id: PluginID): void {
|
|
37
|
+
this.bus.emit('plugin:uninstall', id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
subscribeLoadingChange(listener: (payload: boolean) => void): () => void {
|
|
41
|
+
return this.bus.on('site:loading:changed', listener);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setSiteLoading(loading: boolean) {
|
|
45
|
+
this.bus.emit('site:loading:changed', loading);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fetch: Fetcher = cfApiFetcher;
|
|
49
|
+
|
|
50
|
+
static getInstance(): CoreService {
|
|
51
|
+
if (!CoreService.instance) {
|
|
52
|
+
CoreService.instance = new CoreService();
|
|
53
|
+
}
|
|
54
|
+
return CoreService.instance;
|
|
55
|
+
}
|
|
56
|
+
}
|