@bbki.ng/site 5.8.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 +6 -0
- package/package.json +3 -2
- package/src/{plugins → core/plugin-system}/manifest.ts +6 -0
- package/src/core/plugin-system/pluginManager.ts +4 -15
- package/src/core/plugin-system/pluginManifestService.ts +2 -12
- package/src/core/plugin-system/pluginStore.ts +11 -5
- package/src/core/shared-service/service-proxy.ts +1 -1
- package/src/core/shared-service/service-registry.ts +3 -1
- 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/store/components/storePage.tsx +23 -15
- package/src/plugins/store/context/index.ts +3 -1
- package/src/plugins/store/index.ts +2 -1
- package/src/plugins/store/utils/index.ts +26 -0
- package/src/types/plugin.ts +1 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/site",
|
|
3
|
-
"version": "5.8.
|
|
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",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { IHostContext } from '#/types/hostApi';
|
|
2
|
-
import { IPlugin,
|
|
2
|
+
import { IPlugin, PluginID, PluginPerm } from '#/types/plugin';
|
|
3
3
|
|
|
4
|
-
import { createEventBus } from '../utils/eventBus';
|
|
5
4
|
import { AdminPluginIDSet } from '../const';
|
|
6
5
|
import { ServiceRegistry } from '../shared-service/service-registry';
|
|
7
6
|
|
|
@@ -14,8 +13,6 @@ const pluginModules = import.meta.glob('../../plugins/*/index.ts');
|
|
|
14
13
|
class PluginManager {
|
|
15
14
|
private activePlugins: Map<string, IPlugin> = new Map();
|
|
16
15
|
|
|
17
|
-
private bus = createEventBus<PluginEvents>();
|
|
18
|
-
|
|
19
16
|
private constructor() {
|
|
20
17
|
ServiceRegistry.getInstance().register('core:baseService', CoreService.getInstance());
|
|
21
18
|
ServiceRegistry.getInstance().register('core:uiService', SystemUIService.getInstance());
|
|
@@ -45,14 +42,6 @@ class PluginManager {
|
|
|
45
42
|
return PluginManager.instance;
|
|
46
43
|
}
|
|
47
44
|
|
|
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
45
|
// enable abort ctrl
|
|
57
46
|
async loadPlugin(pluginId: PluginID) {
|
|
58
47
|
if (this.activePlugins.has(pluginId)) {
|
|
@@ -67,7 +56,7 @@ class PluginManager {
|
|
|
67
56
|
|
|
68
57
|
// try load plugin
|
|
69
58
|
this.loading.add(pluginId);
|
|
70
|
-
|
|
59
|
+
CoreService.getInstance().setLoading(pluginId, true);
|
|
71
60
|
|
|
72
61
|
const modulePath = `../../plugins/${pluginId}/index.ts`;
|
|
73
62
|
const moduleLoader = pluginModules[modulePath];
|
|
@@ -75,7 +64,7 @@ class PluginManager {
|
|
|
75
64
|
console.error(`Plugin ${pluginId} not found in registry`);
|
|
76
65
|
|
|
77
66
|
this.loading.delete(pluginId);
|
|
78
|
-
|
|
67
|
+
CoreService.getInstance().setLoading(pluginId, false);
|
|
79
68
|
|
|
80
69
|
return;
|
|
81
70
|
}
|
|
@@ -104,7 +93,7 @@ class PluginManager {
|
|
|
104
93
|
console.error(`Failed to load plugin ${pluginId}:`, error);
|
|
105
94
|
} finally {
|
|
106
95
|
this.loading.delete(pluginId);
|
|
107
|
-
|
|
96
|
+
CoreService.getInstance().setLoading(pluginId, false);
|
|
108
97
|
}
|
|
109
98
|
}
|
|
110
99
|
|
|
@@ -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;
|
|
@@ -163,16 +163,22 @@ export class PluginStore {
|
|
|
163
163
|
};
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
-
installPlugin: (id: PluginID) => Promise<
|
|
166
|
+
installPlugin: (id: PluginID) => Promise<boolean> = async id => {
|
|
167
167
|
if (!this.checkDep(id)) {
|
|
168
168
|
// throw new Error(`Cannot install plugin ${id} due to missing dependencies`);
|
|
169
169
|
console.warn(`Cannot install plugin ${id} due to missing dependencies`);
|
|
170
|
-
return;
|
|
170
|
+
return false;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
176
182
|
};
|
|
177
183
|
|
|
178
184
|
disablePlugin: (id: PluginID) => Promise<void> = async id => {
|
|
@@ -14,7 +14,7 @@ export function createServiceProxy<T extends object, K extends keyof ServiceIden
|
|
|
14
14
|
console.warn(
|
|
15
15
|
`[System] Service ${serviceId} is currently unavailable. Call to ${String(prop)} ignored.`
|
|
16
16
|
);
|
|
17
|
-
return
|
|
17
|
+
return {}; // 或者返回一个待定 Promise
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -35,7 +35,9 @@ export class ServiceRegistry {
|
|
|
35
35
|
return target as ServiceIdentifierMap[T];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
get<T extends keyof ServiceIdentifierMap>(
|
|
38
|
+
get<T extends keyof ServiceIdentifierMap>(
|
|
39
|
+
name: T
|
|
40
|
+
): ServiceIdentifierMap[T] | Record<string, never> {
|
|
39
41
|
// const target = this.services.get(name);
|
|
40
42
|
// if (!target) {
|
|
41
43
|
// console.warn(`Service with name ${name} is not registered.`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React, { FC } from 'react';
|
|
2
|
+
import { Toaster } from 'sonner';
|
|
3
|
+
|
|
4
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
|
+
|
|
6
|
+
export const NotifyComp: FC<IComPropsRegisteredToSlot> = _ => {
|
|
7
|
+
return (
|
|
8
|
+
<Toaster
|
|
9
|
+
position="bottom-center"
|
|
10
|
+
toastOptions={{
|
|
11
|
+
style: {
|
|
12
|
+
borderRadius: 0,
|
|
13
|
+
border: 'none',
|
|
14
|
+
},
|
|
15
|
+
}}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BBPlugin } from '#/core/plugin-system/bbplugin';
|
|
2
|
+
import { IHostContext } from '#/types/hostApi';
|
|
3
|
+
import { PluginID } from '#/types/plugin';
|
|
4
|
+
|
|
5
|
+
import { NotifyComp } from './components';
|
|
6
|
+
import { NotificationService } from './services/NotificationService';
|
|
7
|
+
|
|
8
|
+
export class NotificationPlugin extends BBPlugin {
|
|
9
|
+
id: PluginID = 'notification' as const;
|
|
10
|
+
|
|
11
|
+
private _serviceRegistry?: IHostContext['service'];
|
|
12
|
+
|
|
13
|
+
override onInstall = async (ctx: IHostContext) => {
|
|
14
|
+
this._serviceRegistry = ctx.service;
|
|
15
|
+
this._serviceRegistry.register('notification', NotificationService.getInstance());
|
|
16
|
+
|
|
17
|
+
const coreUIService = ctx.service.get('core:uiService');
|
|
18
|
+
coreUIService.registerSlot('pageFooter', NotifyComp, this.id);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
override onDestroy = () => {
|
|
22
|
+
this._serviceRegistry?.unregister('notification');
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default new NotificationPlugin();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare module '#/core/shared-service/service-registry' {
|
|
2
|
+
interface ServiceIdentifierMap {
|
|
3
|
+
notification: INotificationService;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type NotificationType = 'toast' | 'banner' | 'modal';
|
|
8
|
+
|
|
9
|
+
export interface INotificationOptions {
|
|
10
|
+
type?: NotificationType;
|
|
11
|
+
level?: 'info' | 'success' | 'warning' | 'error';
|
|
12
|
+
message: string;
|
|
13
|
+
duration?: number; // 持续时间,单位为毫秒,默认为4000ms
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface INotificationService {
|
|
17
|
+
notify: (options: INotificationOptions) => string;
|
|
18
|
+
dismiss: (id: string) => void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { toast } from 'sonner';
|
|
2
|
+
|
|
3
|
+
import { INotificationOptions, INotificationService } from './INotificationService';
|
|
4
|
+
|
|
5
|
+
export class NotificationService implements INotificationService {
|
|
6
|
+
private static instance: NotificationService;
|
|
7
|
+
|
|
8
|
+
private constructor() {}
|
|
9
|
+
|
|
10
|
+
static getInstance(): NotificationService {
|
|
11
|
+
if (!NotificationService.instance) {
|
|
12
|
+
NotificationService.instance = new NotificationService();
|
|
13
|
+
}
|
|
14
|
+
return NotificationService.instance;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
notify(options: INotificationOptions): string {
|
|
18
|
+
const id = toast(options.message, {
|
|
19
|
+
duration: options.duration || 3000,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return id as string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
dismiss(id: string): void {
|
|
26
|
+
toast.dismiss(id);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -6,6 +6,7 @@ import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
|
6
6
|
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
7
7
|
|
|
8
8
|
import { StoreCtx } from '../context';
|
|
9
|
+
import { pluginDepDescForHuman } from '../utils';
|
|
9
10
|
|
|
10
11
|
// 空状态展示
|
|
11
12
|
const EmptyState = () => (
|
|
@@ -25,7 +26,8 @@ const ErrorState = ({ error, onRetry }: { error: Error; onRetry: () => void }) =
|
|
|
25
26
|
);
|
|
26
27
|
|
|
27
28
|
export const StorePage = (_: IComPropsRegisteredToSlot) => {
|
|
28
|
-
const { list, setLoading, isInstalled, install, uninstall, get } =
|
|
29
|
+
const { list, setLoading, isInstalled, install, uninstall, get, notificationService } =
|
|
30
|
+
StoreCtx.useCtx();
|
|
29
31
|
const [plugins, setPlugins] = useState<Array<IPluginStoreEntry>>([]);
|
|
30
32
|
const [error, setError] = useState<Error | null>(null);
|
|
31
33
|
|
|
@@ -75,15 +77,25 @@ export const StorePage = (_: IComPropsRegisteredToSlot) => {
|
|
|
75
77
|
setError(null);
|
|
76
78
|
|
|
77
79
|
try {
|
|
78
|
-
await install(id);
|
|
79
|
-
|
|
80
|
+
const success = await install(id);
|
|
81
|
+
if (success) {
|
|
82
|
+
await refreshList();
|
|
83
|
+
} else {
|
|
84
|
+
notificationService.notify({
|
|
85
|
+
level: 'error',
|
|
86
|
+
message: `安装插件失败,依赖缺失或其他错误`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
80
89
|
} catch (err) {
|
|
81
|
-
|
|
90
|
+
notificationService.notify({
|
|
91
|
+
level: 'error',
|
|
92
|
+
message: `安装插件失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
93
|
+
});
|
|
82
94
|
} finally {
|
|
83
95
|
setLoading(false);
|
|
84
96
|
}
|
|
85
97
|
},
|
|
86
|
-
[install, refreshList, setLoading]
|
|
98
|
+
[install, refreshList, setLoading, notificationService]
|
|
87
99
|
);
|
|
88
100
|
|
|
89
101
|
const handleUninstall = useCallback(
|
|
@@ -108,7 +120,6 @@ export const StorePage = (_: IComPropsRegisteredToSlot) => {
|
|
|
108
120
|
<>
|
|
109
121
|
<Table.HCell>功能</Table.HCell>
|
|
110
122
|
<Table.HCell>描述</Table.HCell>
|
|
111
|
-
<Table.HCell>依赖</Table.HCell>
|
|
112
123
|
<Table.HCell style={{ textAlign: 'right' }}>安装/卸载</Table.HCell>
|
|
113
124
|
</>
|
|
114
125
|
);
|
|
@@ -126,16 +137,13 @@ export const StorePage = (_: IComPropsRegisteredToSlot) => {
|
|
|
126
137
|
return (
|
|
127
138
|
<>
|
|
128
139
|
<Table.Cell>{plugin.name}</Table.Cell>
|
|
129
|
-
<Table.Cell>{plugin.description}</Table.Cell>
|
|
130
140
|
<Table.Cell>
|
|
131
|
-
{plugin.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.join(', ')
|
|
138
|
-
: ''}
|
|
141
|
+
{plugin.description}
|
|
142
|
+
{plugin.dependencies && plugin.dependencies.length > 0 && (
|
|
143
|
+
<div className="mt-4 text-xs text-content-secondary">
|
|
144
|
+
{pluginDepDescForHuman(plugin, get)}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
139
147
|
</Table.Cell>
|
|
140
148
|
<Table.Cell style={{ textAlign: 'right' }}>
|
|
141
149
|
{pluginInstalled ? (
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createPluginCtx } from '#/core/context';
|
|
2
|
+
import { NotificationService } from '#/plugins/notification/services/NotificationService';
|
|
2
3
|
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
3
4
|
|
|
4
5
|
export interface IStoreCtx {
|
|
5
|
-
install: (pid: PluginID) => Promise<
|
|
6
|
+
install: (pid: PluginID) => Promise<boolean>;
|
|
6
7
|
uninstall: (pid: PluginID) => Promise<void>;
|
|
7
8
|
isInstalled: (pid: PluginID) => boolean;
|
|
8
9
|
list: () => Promise<Array<IPluginStoreEntry>> | Array<IPluginStoreEntry>;
|
|
9
10
|
get: (pid: PluginID) => IPluginStoreEntry | undefined;
|
|
10
11
|
setLoading: (loading: boolean) => void;
|
|
12
|
+
notificationService: NotificationService | Record<string, never>;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export const StoreCtx = createPluginCtx<IStoreCtx>();
|
|
@@ -26,7 +26,7 @@ export class StorePlugin extends BBPlugin {
|
|
|
26
26
|
label: 'cd ./store',
|
|
27
27
|
pageComponent: StoreCtx.withCtx(
|
|
28
28
|
{
|
|
29
|
-
install: ctx.store?.installPlugin || (async () =>
|
|
29
|
+
install: ctx.store?.installPlugin || (async () => false),
|
|
30
30
|
uninstall: ctx.store?.disablePlugin || (async () => {}),
|
|
31
31
|
isInstalled: ctx.store?.isPluginInstalled || ((_: PluginID) => false),
|
|
32
32
|
get: ctx.store?.getPlugin || ((_: PluginID) => undefined),
|
|
@@ -36,6 +36,7 @@ export class StorePlugin extends BBPlugin {
|
|
|
36
36
|
coreService.setLoading('store', false);
|
|
37
37
|
return all.filter(({ id }) => id !== this.id);
|
|
38
38
|
},
|
|
39
|
+
notificationService: ctx.service.get('notification'),
|
|
39
40
|
setLoading: (loading: boolean) => {
|
|
40
41
|
coreService.setLoading('store', loading);
|
|
41
42
|
},
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
2
|
+
|
|
3
|
+
export const pluginDepDescForHuman = (
|
|
4
|
+
plugin: IPluginStoreEntry,
|
|
5
|
+
infoGetter: (id: PluginID) => IPluginStoreEntry | undefined
|
|
6
|
+
): string => {
|
|
7
|
+
if (!plugin.dependencies || plugin.dependencies.length === 0) {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const desc =
|
|
12
|
+
plugin.dependencies && plugin.dependencies.length > 0
|
|
13
|
+
? plugin.dependencies
|
|
14
|
+
.map(dep => {
|
|
15
|
+
const depPlugin = infoGetter(dep);
|
|
16
|
+
return depPlugin ? depPlugin.name : dep;
|
|
17
|
+
})
|
|
18
|
+
.join(', ')
|
|
19
|
+
: '';
|
|
20
|
+
|
|
21
|
+
if (!desc) {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return `需要先安装${desc}。`;
|
|
26
|
+
};
|