@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/index.html +0 -11
  3. package/package.json +3 -2
  4. package/src/app/app.tsx +1 -2
  5. package/src/core/components/SlotComp.tsx +2 -3
  6. package/src/core/hooks/useMiddlewareTransData.ts +12 -7
  7. package/src/core/hooks/useSlotComp.ts +8 -7
  8. package/src/core/hooks/use_plugins.ts +27 -14
  9. package/src/{plugins → core/plugin-system}/manifest.ts +8 -6
  10. package/src/core/plugin-system/pluginManager.ts +118 -0
  11. package/src/core/{pluginManifestService.ts → plugin-system/pluginManifestService.ts} +4 -15
  12. package/src/core/plugin-system/pluginStore.ts +196 -0
  13. package/src/core/{registry.ts → plugin-system/registry.ts} +1 -3
  14. package/src/core/plugin-system/services/coreService.ts +56 -0
  15. package/src/core/plugin-system/services/systemUIService.ts +159 -0
  16. package/src/core/shared-service/contract/ICoreService.ts +23 -0
  17. package/src/core/shared-service/contract/IUIService.ts +49 -0
  18. package/src/core/shared-service/service-proxy.ts +26 -0
  19. package/src/core/shared-service/service-registry.ts +54 -0
  20. package/src/plugins/blog/components/BlogSlotCom.tsx +27 -0
  21. package/src/plugins/blog/components/article/index.tsx +2 -2
  22. package/src/plugins/blog/context/index.ts +0 -4
  23. package/src/plugins/blog/hooks/useMiddlewareTransData.ts +79 -0
  24. package/src/plugins/blog/hooks/use_blog_scroll_pos_restoration.ts +2 -2
  25. package/src/plugins/blog/hooks/use_blog_slot_com.ts +27 -0
  26. package/src/plugins/blog/hooks/use_posts.ts +3 -2
  27. package/src/plugins/blog/index.ts +32 -5
  28. package/src/plugins/blog/pages/extensions/txt/article.tsx +2 -2
  29. package/src/plugins/blog/services/BlogUIService.ts +120 -0
  30. package/src/plugins/blog/services/IBlogUIService.ts +31 -0
  31. package/src/plugins/extra-cd/index.ts +13 -2
  32. package/src/plugins/extra-entry/index.ts +8 -4
  33. package/src/plugins/fx/index.ts +18 -5
  34. package/src/plugins/notification/components/index.tsx +18 -0
  35. package/src/plugins/notification/index.ts +26 -0
  36. package/src/plugins/notification/services/INotificationService.ts +19 -0
  37. package/src/plugins/notification/services/NotificationService.ts +28 -0
  38. package/src/plugins/now/hooks/use_streaming.ts +3 -1
  39. package/src/plugins/now/index.ts +17 -6
  40. package/src/plugins/sticker/const.ts +2 -2
  41. package/src/plugins/sticker/index.ts +18 -3
  42. package/src/plugins/store/components/storePage.tsx +26 -7
  43. package/src/plugins/store/context/index.ts +4 -1
  44. package/src/plugins/store/index.ts +20 -8
  45. package/src/plugins/store/utils/index.ts +26 -0
  46. package/src/plugins/xwy/index.ts +24 -19
  47. package/src/plugins/xwy/types/index.ts +0 -18
  48. package/src/types/hostApi.ts +3 -34
  49. package/src/types/plugin.ts +2 -0
  50. package/src/utils/index.tsx +1 -48
  51. package/vite.config.js +1 -1
  52. package/src/core/pluginManager.ts +0 -191
  53. package/src/core/pluginStore.ts +0 -70
  54. /package/src/core/{bbplugin.ts → plugin-system/bbplugin.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @bbki.ng/site
2
2
 
3
+ ## 5.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 0790fd3: add notification plugin
8
+
9
+ ## 5.8.0
10
+
11
+ ### Minor Changes
12
+
13
+ - c920d33: add service registry
14
+
3
15
  ## 5.7.0
4
16
 
5
17
  ### Minor Changes
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.7.0",
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={pluginEntries?.length ? <NotFound /> : null} />
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: SlotName;
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 { registry } from '#/core/registry';
4
- import type { HookPoint } from '#/types/slots';
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: 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 registry.runMiddleware(hookPoint, inputData);
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 = registry.subscribeMiddleware(hookPoint, () => {
45
- onMiddlewareChange?.();
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>(hookPoint: HookPoint, defaultValue: 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, SlotName } from '#/types/slots';
3
+ import { IComPropsRegisteredToSlot } from '#/types/slots';
4
4
 
5
- import { registry } from '../registry';
5
+ import { SystemUIService } from '../plugin-system/services/systemUIService';
6
+ import { SystemSlotName } from '../shared-service/contract/IUIService';
6
7
 
7
- export const useSlotComp = (slotName: SlotName) => {
8
+ export const useSlotComp = (slotName: SystemSlotName) => {
8
9
  const [components, setComponents] = useState<React.ComponentType<IComPropsRegisteredToSlot>[]>(
9
- () => registry.getComponents(slotName)
10
+ () => SystemUIService.getInstance().getComponents(slotName)
10
11
  );
11
12
 
12
13
  useEffect(() => {
13
- setComponents(registry.getComponents(slotName));
14
+ setComponents(SystemUIService.getInstance().getComponents(slotName));
14
15
 
15
- const unsubscribe = registry.subscribe(() => {
16
- const comps = registry.getComponents(slotName);
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
- pluginManager.notifySiteLoadingChanged(isLoading);
15
+ CoreService.getInstance().setSiteLoading(isLoading);
15
16
  }, [isLoading]);
16
17
 
17
18
  useEffect(() => {
18
19
  let unregister: (() => void) | undefined;
19
- const unscribe = pluginManager.subscribePluginLoading(payload => {
20
+ const unsubscribe = CoreService.getInstance().subscribePluginLoading(payload => {
20
21
  unregister = setGlobalLoading(payload.id, payload.loading);
21
22
  });
23
+
22
24
  return () => {
23
- unscribe();
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
- Promise.all(pluginIds.map(id => pluginManager.loadPlugin(id)))
60
- .then(() => {
61
+ const loadNext = (index: number) => {
62
+ if (cancelled || index >= pluginIds.length) {
61
63
  if (!cancelled) {
62
64
  setDone(true);
63
65
  }
64
- })
65
- .catch(error => {
66
- if (!cancelled) {
67
- console.error('Error loading plugins:', error);
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 '#/plugins/manifest';
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?: string;
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
- const parsed = JSON.parse(item.dependencies) as string[];
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 './utils/eventBus';
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();