@bbki.ng/site 5.7.0 → 5.8.1
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 +12 -0
- package/index.html +0 -11
- package/package.json +3 -2
- 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/{plugins → core/plugin-system}/manifest.ts +8 -6
- package/src/core/plugin-system/pluginManager.ts +118 -0
- package/src/core/{pluginManifestService.ts → plugin-system/pluginManifestService.ts} +4 -15
- package/src/core/plugin-system/pluginStore.ts +196 -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 +54 -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/notification/components/index.tsx +18 -0
- package/src/plugins/notification/index.ts +26 -0
- package/src/plugins/notification/services/INotificationService.ts +19 -0
- package/src/plugins/notification/services/NotificationService.ts +28 -0
- 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 +26 -7
- package/src/plugins/store/context/index.ts +4 -1
- package/src/plugins/store/index.ts +20 -8
- package/src/plugins/store/utils/index.ts +26 -0
- 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 +2 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/site",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.1",
|
|
4
4
|
"description": "code behind bbki.ng",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"react": "^18.0.0",
|
|
14
14
|
"react-dom": "^18.0.0",
|
|
15
15
|
"react-router-dom": "6",
|
|
16
|
+
"sonner": "^2.0.7",
|
|
16
17
|
"swr": "^2.2.5",
|
|
17
18
|
"@bbki.ng/ui": "0.2.19"
|
|
18
19
|
},
|
|
@@ -23,8 +24,8 @@
|
|
|
23
24
|
"@mdx-js/react": "^3.0.0",
|
|
24
25
|
"@mdx-js/rollup": "^3.0.0",
|
|
25
26
|
"@tailwindcss/postcss": "4.1.17",
|
|
26
|
-
"@tailwindcss/vite": "4.1.17",
|
|
27
27
|
"@tailwindcss/typography": "^0.5.0",
|
|
28
|
+
"@tailwindcss/vite": "4.1.17",
|
|
28
29
|
"@types/node": "^20.0.0",
|
|
29
30
|
"@types/react": "^18.0.15",
|
|
30
31
|
"@types/react-dom": "^18.0.6",
|
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) {
|
|
@@ -26,6 +26,7 @@ export const FALLBACK_MANIFEST: Array<IPluginManifestEntry> = [
|
|
|
26
26
|
version: '0.1.0',
|
|
27
27
|
description:
|
|
28
28
|
'提供额外的页面跳转链接。例如在标题列表末尾添加一个 "cd ~" 的选项,点击后跳转到主页',
|
|
29
|
+
dependencies: ['blog'],
|
|
29
30
|
},
|
|
30
31
|
{
|
|
31
32
|
name: '特效',
|
|
@@ -33,17 +34,12 @@ export const FALLBACK_MANIFEST: Array<IPluginManifestEntry> = [
|
|
|
33
34
|
version: '0.1.0',
|
|
34
35
|
description: '提供一些额外的视觉效果。例如版本、设备信息水印,加载状态螺旋线、背景纹理等',
|
|
35
36
|
},
|
|
36
|
-
// {
|
|
37
|
-
// name: 'extra-entry',
|
|
38
|
-
// id: 'extra-entry',
|
|
39
|
-
// version: '0.1.0',
|
|
40
|
-
// description: 'Provides extra entries for cover and extended routes.',
|
|
41
|
-
// },
|
|
42
37
|
{
|
|
43
38
|
name: '小乌鸦',
|
|
44
39
|
id: 'xwy',
|
|
45
40
|
version: '0.1.0',
|
|
46
41
|
description: '为小乌鸦合集实现定制功能或者样式的插件',
|
|
42
|
+
dependencies: ['blog'],
|
|
47
43
|
},
|
|
48
44
|
{
|
|
49
45
|
name: '博客',
|
|
@@ -51,4 +47,10 @@ export const FALLBACK_MANIFEST: Array<IPluginManifestEntry> = [
|
|
|
51
47
|
version: '0.1.0',
|
|
52
48
|
description: '一个简单的博客系统,支持 Markdown 格式的文章',
|
|
53
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: '通知',
|
|
52
|
+
id: 'notification',
|
|
53
|
+
version: '0.1.0',
|
|
54
|
+
description: '接收通知',
|
|
55
|
+
},
|
|
54
56
|
];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { IHostContext } from '#/types/hostApi';
|
|
2
|
+
import { IPlugin, PluginID, PluginPerm } from '#/types/plugin';
|
|
3
|
+
|
|
4
|
+
import { AdminPluginIDSet } from '../const';
|
|
5
|
+
import { ServiceRegistry } from '../shared-service/service-registry';
|
|
6
|
+
|
|
7
|
+
import { PluginStore } from './pluginStore';
|
|
8
|
+
import { CoreService } from './services/coreService';
|
|
9
|
+
import { SystemUIService } from './services/systemUIService';
|
|
10
|
+
|
|
11
|
+
const pluginModules = import.meta.glob('../../plugins/*/index.ts');
|
|
12
|
+
|
|
13
|
+
class PluginManager {
|
|
14
|
+
private activePlugins: Map<string, IPlugin> = new Map();
|
|
15
|
+
|
|
16
|
+
private constructor() {
|
|
17
|
+
ServiceRegistry.getInstance().register('core:baseService', CoreService.getInstance());
|
|
18
|
+
ServiceRegistry.getInstance().register('core:uiService', SystemUIService.getInstance());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private static instance: PluginManager;
|
|
22
|
+
|
|
23
|
+
private createHostContext(perm: PluginPerm = 'guest'): IHostContext {
|
|
24
|
+
const adminCtx =
|
|
25
|
+
perm === 'admin'
|
|
26
|
+
? {
|
|
27
|
+
store: PluginStore.getInstance(),
|
|
28
|
+
}
|
|
29
|
+
: {};
|
|
30
|
+
return {
|
|
31
|
+
...adminCtx,
|
|
32
|
+
service: ServiceRegistry.getInstance(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private loading = new Set<string>();
|
|
37
|
+
|
|
38
|
+
static getInstance() {
|
|
39
|
+
if (!PluginManager.instance) {
|
|
40
|
+
PluginManager.instance = new PluginManager();
|
|
41
|
+
}
|
|
42
|
+
return PluginManager.instance;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// enable abort ctrl
|
|
46
|
+
async loadPlugin(pluginId: PluginID) {
|
|
47
|
+
if (this.activePlugins.has(pluginId)) {
|
|
48
|
+
console.warn(`Plugin ${pluginId} is already loaded.`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this.loading.has(pluginId)) {
|
|
53
|
+
console.warn(`Plugin ${pluginId} is loading...`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// try load plugin
|
|
58
|
+
this.loading.add(pluginId);
|
|
59
|
+
CoreService.getInstance().setLoading(pluginId, true);
|
|
60
|
+
|
|
61
|
+
const modulePath = `../../plugins/${pluginId}/index.ts`;
|
|
62
|
+
const moduleLoader = pluginModules[modulePath];
|
|
63
|
+
if (!moduleLoader) {
|
|
64
|
+
console.error(`Plugin ${pluginId} not found in registry`);
|
|
65
|
+
|
|
66
|
+
this.loading.delete(pluginId);
|
|
67
|
+
CoreService.getInstance().setLoading(pluginId, false);
|
|
68
|
+
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const module = (await moduleLoader()) as {
|
|
74
|
+
default: IPlugin;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const p: IPlugin = module.default;
|
|
78
|
+
|
|
79
|
+
const perm = p.getMeta().perm || 'guest';
|
|
80
|
+
if (perm === 'admin' && !AdminPluginIDSet.has(pluginId)) {
|
|
81
|
+
console.error(`Plugin ${pluginId} requires admin permission, but it's not trusted.`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ctx = this.createHostContext(perm);
|
|
86
|
+
|
|
87
|
+
if (p.onInstall) {
|
|
88
|
+
await p.onInstall(ctx);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.activePlugins.set(pluginId, p);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`Failed to load plugin ${pluginId}:`, error);
|
|
94
|
+
} finally {
|
|
95
|
+
this.loading.delete(pluginId);
|
|
96
|
+
CoreService.getInstance().setLoading(pluginId, false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async disablePlugin(pluginId: PluginID) {
|
|
101
|
+
const plugin = this.activePlugins.get(pluginId);
|
|
102
|
+
if (!plugin) {
|
|
103
|
+
console.warn(`Plugin ${pluginId} is not loaded.`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (plugin.onDisable) {
|
|
108
|
+
await plugin.onDisable();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.activePlugins.delete(pluginId);
|
|
112
|
+
|
|
113
|
+
SystemUIService.getInstance().unregisterAllByPluginId(plugin.id);
|
|
114
|
+
CoreService.getInstance().notifyPluginUninstall(plugin.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const pluginManager = PluginManager.getInstance();
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import { cfApiFetcher } from '#/app/utils';
|
|
2
|
-
import { FALLBACK_MANIFEST } from '#/
|
|
2
|
+
import { FALLBACK_MANIFEST } from '#/core/plugin-system/manifest';
|
|
3
3
|
import { IPluginManifestEntry, PluginID } from '#/types/plugin';
|
|
4
4
|
|
|
5
|
-
const KnownPluginIDSet = new Set<string>(
|
|
6
|
-
'sticker',
|
|
7
|
-
'xwy',
|
|
8
|
-
'extra-cd',
|
|
9
|
-
'extra-entry',
|
|
10
|
-
'store',
|
|
11
|
-
'now',
|
|
12
|
-
'default',
|
|
13
|
-
'fx',
|
|
14
|
-
'blog',
|
|
15
|
-
]);
|
|
5
|
+
const KnownPluginIDSet = new Set<string>(FALLBACK_MANIFEST.map(entry => entry.id));
|
|
16
6
|
|
|
17
7
|
interface PluginsApiResponse {
|
|
18
8
|
status: string;
|
|
@@ -23,7 +13,7 @@ interface PluginsApiResponse {
|
|
|
23
13
|
description?: string;
|
|
24
14
|
perm?: string;
|
|
25
15
|
icon?: string;
|
|
26
|
-
dependencies?:
|
|
16
|
+
dependencies?: Array<PluginID>;
|
|
27
17
|
}>;
|
|
28
18
|
}
|
|
29
19
|
|
|
@@ -66,8 +56,7 @@ class PluginManifestService {
|
|
|
66
56
|
let dependencies: PluginID[] | undefined;
|
|
67
57
|
if (item.dependencies) {
|
|
68
58
|
try {
|
|
69
|
-
|
|
70
|
-
dependencies = parsed.filter((id): id is PluginID => KnownPluginIDSet.has(id));
|
|
59
|
+
dependencies = item.dependencies.filter(p => KnownPluginIDSet.has(p));
|
|
71
60
|
} catch {
|
|
72
61
|
console.warn(
|
|
73
62
|
`[PluginManifestService] Failed to parse dependencies for plugin: ${item.id}`
|
|
@@ -0,0 +1,196 @@
|
|
|
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<boolean> = 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 false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await pluginManager.loadPlugin(id);
|
|
175
|
+
this.installedSet.add(id);
|
|
176
|
+
this.stringify();
|
|
177
|
+
return true;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`Failed to install plugin ${id}:`, error);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
disablePlugin: (id: PluginID) => Promise<void> = async id => {
|
|
185
|
+
await pluginManager.disablePlugin(id);
|
|
186
|
+
this.installedSet.delete(id);
|
|
187
|
+
this.stringify();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
static getInstance() {
|
|
191
|
+
if (!PluginStore.instance) {
|
|
192
|
+
PluginStore.instance = new PluginStore();
|
|
193
|
+
}
|
|
194
|
+
return PluginStore.instance;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -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();
|