@bbki.ng/site 5.6.3 → 5.6.4

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @bbki.ng/site
2
2
 
3
+ ## 5.6.4
4
+
5
+ ### Patch Changes
6
+
7
+ - d1b95ed: add api to fetch manifest
8
+
3
9
  ## 5.6.3
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.6.3",
3
+ "version": "5.6.4",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -9,6 +9,8 @@ export interface FingerprintData {
9
9
  generatedAt: number;
10
10
  }
11
11
 
12
+ type GLenum = number;
13
+
12
14
  export interface FingerprintComponents {
13
15
  // 基础环境
14
16
  userAgent: string;
@@ -143,7 +145,7 @@ function getWebGLInfo(): WebGLInfo {
143
145
 
144
146
  params.forEach(p => {
145
147
  try {
146
- const val = gl.getParameter((gl as any)[p]);
148
+ const val = gl.getParameter((gl as unknown as Record<string, GLenum>)[p] as GLenum);
147
149
  result.params[p] = Array.isArray(val) ? val.join(',') : String(val);
148
150
  } catch {
149
151
  result.params[p] = 'unsupported';
@@ -220,7 +222,10 @@ function getFontList(): string[] {
220
222
  // 音频指纹(频率响应差异)
221
223
  async function getAudioFingerprint(): Promise<string | undefined> {
222
224
  try {
223
- const AudioContext = window.OfflineAudioContext || (window as any).webkitOfflineAudioContext;
225
+ const AudioContext =
226
+ window.OfflineAudioContext ||
227
+ (window as unknown as { webkitOfflineAudioContext?: typeof OfflineAudioContext })
228
+ .webkitOfflineAudioContext;
224
229
  if (!AudioContext) return undefined;
225
230
 
226
231
  const ctx = new AudioContext(1, 44100, 44100);
@@ -271,7 +276,7 @@ async function getFingerprint(): Promise<FingerprintData> {
271
276
  platform: navigator.platform,
272
277
  cookieEnabled: navigator.cookieEnabled,
273
278
  hardwareConcurrency: navigator.hardwareConcurrency || 0,
274
- deviceMemory: (navigator as any).deviceMemory,
279
+ deviceMemory: (navigator as unknown as { deviceMemory?: number }).deviceMemory,
275
280
  maxTouchPoints: navigator.maxTouchPoints || 0,
276
281
  screenResolution: `${screen.width}x${screen.height}`,
277
282
  screenColorDepth: screen.colorDepth,
@@ -1,13 +1,14 @@
1
- import { ManifestMap } from '#/plugins/manifest';
2
1
  import { IHostContext } from '#/types/hostApi';
3
2
  import { IPlugin, IPluginManifestEntry, PluginID } from '#/types/plugin';
4
3
 
4
+ import { PluginManifestService } from './pluginManifestService';
5
+
5
6
  export class BBPlugin implements IPlugin {
6
7
  id: PluginID = 'default';
7
8
  onInstall?: ((ctx: IHostContext) => void | Promise<void>) | undefined;
8
9
  onDisable?: (() => Promise<void> | void) | undefined;
9
10
  onDestroy?: (() => void) | undefined;
10
11
  getMeta(): IPluginManifestEntry {
11
- return ManifestMap.get(this.id) as IPluginManifestEntry;
12
+ return PluginManifestService.getInstance().getPlugin(this.id) as IPluginManifestEntry;
12
13
  }
13
14
  }
@@ -1,9 +1,10 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import { useEffect, useMemo, useState } from 'react';
2
2
 
3
3
  import { useGlobalLoading } from '@/hooks';
4
4
  import { pluginManager } from '#/core/pluginManager';
5
5
  import { PluginID } from '#/types/plugin';
6
6
 
7
+ import { PluginManifestService } from '../pluginManifestService';
7
8
  import { PluginStore } from '../pluginStore';
8
9
 
9
10
  const usePluginsLoading = () => {
@@ -46,26 +47,41 @@ export const usePlugins = () => {
46
47
 
47
48
  usePluginsLoading();
48
49
 
49
- const loadAllPlugins = useCallback(async () => {
50
- try {
51
- await Promise.all(pluginIds.map(id => pluginManager.loadPlugin(id)));
52
- setDone(true);
53
- } catch (error) {
54
- console.error('Error loading plugins:', error);
55
- }
56
- }, [pluginIds]);
57
-
58
50
  useEffect(() => {
59
- // 加载指定的插件
60
- loadAllPlugins();
51
+ let cancelled = false;
52
+
53
+ PluginManifestService.getInstance()
54
+ .fetch()
55
+ .then(() => {
56
+ if (cancelled) {
57
+ return;
58
+ }
59
+ Promise.all(pluginIds.map(id => pluginManager.loadPlugin(id)))
60
+ .then(() => {
61
+ if (!cancelled) {
62
+ setDone(true);
63
+ }
64
+ })
65
+ .catch(error => {
66
+ if (!cancelled) {
67
+ console.error('Error loading plugins:', error);
68
+ }
69
+ });
70
+ })
71
+ .catch(error => {
72
+ if (!cancelled) {
73
+ console.error('Error fetching plugin manifest:', error);
74
+ }
75
+ });
61
76
 
62
77
  return () => {
78
+ cancelled = true;
63
79
  // 卸载所有指定的插件
64
80
  pluginIds.forEach(id => {
65
81
  pluginManager.disablePlugin(id);
66
82
  });
67
83
  };
68
- }, [loadAllPlugins, pluginIds]);
84
+ }, [pluginIds]);
69
85
 
70
86
  return done;
71
87
  };
@@ -4,6 +4,7 @@ import { IHostContext } from '#/types/hostApi';
4
4
  import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
5
5
  import { IPlugin, PluginEvents, PluginID, PluginPerm } from '#/types/plugin';
6
6
  import { getStableDeviceId } from '@/utils/fingerprints';
7
+ import { cfApiFetcher } from '@/utils';
7
8
 
8
9
  import { registry } from './registry';
9
10
  import { createEventBus } from './utils/eventBus';
@@ -27,6 +28,7 @@ class PluginManager {
27
28
  return {
28
29
  ...adminCtx,
29
30
  api: {
31
+ fetch: cfApiFetcher,
30
32
  setLoading: (id: PluginID, loading: boolean) => {
31
33
  this.bus.emit('plugin:loading:changed', { id, loading });
32
34
  },
@@ -0,0 +1,123 @@
1
+ import { cfApiFetcher } from '@/utils';
2
+ import { FALLBACK_MANIFEST } from '#/plugins/manifest';
3
+ import { IPluginManifestEntry, PluginID } from '#/types/plugin';
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
+ ]);
15
+
16
+ interface PluginsApiResponse {
17
+ status: string;
18
+ data: Array<{
19
+ id: string;
20
+ name: string;
21
+ version: string;
22
+ description?: string;
23
+ perm?: string;
24
+ icon?: string;
25
+ dependencies?: string;
26
+ }>;
27
+ }
28
+
29
+ class PluginManifestService {
30
+ private static instance: PluginManifestService;
31
+
32
+ private manifestMap: Map<string, IPluginManifestEntry> = new Map();
33
+
34
+ private fetched = false;
35
+
36
+ private fetchPromise: Promise<void> | null = null;
37
+
38
+ private constructor() {
39
+ this.initWithFallback();
40
+ }
41
+
42
+ private initWithFallback = () => {
43
+ this.manifestMap = new Map(FALLBACK_MANIFEST.map(entry => [entry.id, entry]));
44
+ };
45
+
46
+ async fetch(): Promise<void> {
47
+ if (this.fetched) {
48
+ return;
49
+ }
50
+ if (this.fetchPromise) {
51
+ return this.fetchPromise;
52
+ }
53
+
54
+ this.fetchPromise = (async () => {
55
+ try {
56
+ const res = await cfApiFetcher<PluginsApiResponse>('plugins');
57
+ if (res.status === 'success' && Array.isArray(res.data)) {
58
+ const validated = res.data
59
+ .map((item): IPluginManifestEntry | null => {
60
+ if (!KnownPluginIDSet.has(item.id)) {
61
+ console.warn(`[PluginManifestService] Unknown plugin id from server: ${item.id}`);
62
+ return null;
63
+ }
64
+
65
+ let dependencies: PluginID[] | undefined;
66
+ if (item.dependencies) {
67
+ try {
68
+ const parsed = JSON.parse(item.dependencies) as string[];
69
+ dependencies = parsed.filter((id): id is PluginID => KnownPluginIDSet.has(id));
70
+ } catch {
71
+ console.warn(
72
+ `[PluginManifestService] Failed to parse dependencies for plugin: ${item.id}`
73
+ );
74
+ }
75
+ }
76
+
77
+ return {
78
+ id: item.id as PluginID,
79
+ name: item.name,
80
+ version: item.version,
81
+ description: item.description,
82
+ perm: (item.perm as 'guest' | 'admin' | undefined) || 'guest',
83
+ icon: item.icon,
84
+ dependencies,
85
+ };
86
+ })
87
+ .filter((item): item is IPluginManifestEntry => item !== null);
88
+
89
+ this.manifestMap = new Map(validated.map(entry => [entry.id, entry]));
90
+ }
91
+ this.fetched = true;
92
+ } catch (error) {
93
+ console.error('[PluginManifestService] Failed to fetch manifest:', error);
94
+ // Keep fallback data
95
+ }
96
+ })();
97
+
98
+ return this.fetchPromise;
99
+ }
100
+
101
+ getPlugin(id: string): IPluginManifestEntry | undefined {
102
+ return this.manifestMap.get(id);
103
+ }
104
+
105
+ getAllPlugins(): IPluginManifestEntry[] {
106
+ return Array.from(this.manifestMap.values());
107
+ }
108
+
109
+ reset(): void {
110
+ this.fetched = false;
111
+ this.fetchPromise = null;
112
+ this.initWithFallback();
113
+ }
114
+
115
+ static getInstance() {
116
+ if (!PluginManifestService.instance) {
117
+ PluginManifestService.instance = new PluginManifestService();
118
+ }
119
+ return PluginManifestService.instance;
120
+ }
121
+ }
122
+
123
+ export { PluginManifestService };
@@ -1,7 +1,7 @@
1
- import { PLUGIN_MANIFEST } from '#/plugins/manifest';
2
1
  import { IPluginStoreEntry, PluginID } from '#/types/plugin';
3
2
 
4
3
  import { pluginManager } from './pluginManager';
4
+ import { PluginManifestService } from './pluginManifestService';
5
5
 
6
6
  export class PluginStore {
7
7
  private static instance: PluginStore;
@@ -35,13 +35,15 @@ export class PluginStore {
35
35
  return this.parse();
36
36
  };
37
37
 
38
- getAllPlugins: () => Array<IPluginStoreEntry> = () => {
38
+ async getAllPlugins(): Promise<Array<IPluginStoreEntry>> {
39
39
  const installedPlugins = this.getInstalledPlugins();
40
- return PLUGIN_MANIFEST.map(plugin => ({
40
+ await PluginManifestService.getInstance().fetch();
41
+ const manifest = PluginManifestService.getInstance().getAllPlugins();
42
+ return manifest.map(plugin => ({
41
43
  ...plugin,
42
44
  enabled: installedPlugins.has(plugin.id),
43
45
  }));
44
- };
46
+ }
45
47
 
46
48
  isPluginInstalled: (id: PluginID) => boolean = id => {
47
49
  return this.getInstalledPlugins().has(id);
@@ -1,6 +1,6 @@
1
1
  import { IPluginManifestEntry } from '#/types/plugin';
2
2
 
3
- export const PLUGIN_MANIFEST: Array<IPluginManifestEntry> = [
3
+ export const FALLBACK_MANIFEST: Array<IPluginManifestEntry> = [
4
4
  {
5
5
  name: '贴纸',
6
6
  id: 'sticker',
@@ -46,8 +46,3 @@ export const PLUGIN_MANIFEST: Array<IPluginManifestEntry> = [
46
46
  description: '为小乌鸦合集实现定制功能或者样式的插件',
47
47
  },
48
48
  ];
49
-
50
- export const ManifestMap = PLUGIN_MANIFEST.reduce((map, entry) => {
51
- map.set(entry.id, entry);
52
- return map;
53
- }, new Map<string, IPluginManifestEntry>());
@@ -1,7 +1,10 @@
1
+ import { Fetcher } from 'swr';
2
+
1
3
  import { createPluginCtx } from '#/core/context';
2
4
 
3
5
  export interface INowCtx {
4
6
  setLoading: (loading: boolean) => void;
7
+ fetch: Fetcher;
5
8
  }
6
9
 
7
10
  export const NowCtx = createPluginCtx<INowCtx>();
@@ -1,15 +1,8 @@
1
- import useSWR from 'swr';
1
+ import useSWR, { type BareFetcher } from 'swr';
2
2
  import { useEffect, useState } from 'react';
3
3
 
4
- import { baseFetcher } from '@/utils';
5
-
6
4
  import { NowCtx } from '../context';
7
5
 
8
- // In dev, use /api prefix to leverage Vite proxy to localhost:8787
9
- // const isProd = typeof window !== 'undefined' && /^https:\/\/bbki.ng/.test(window.location.href);
10
- const isProd = true;
11
- const API_BASE = !isProd ? '/api' : 'https://cf.bbki.ng';
12
-
13
6
  export type StreamingItem = {
14
7
  id: string;
15
8
  author: string;
@@ -32,41 +25,13 @@ export interface StreamingQueryParams {
32
25
  offset?: number;
33
26
  }
34
27
 
35
- /**
36
- * Build streaming API URL with query parameters
37
- */
38
- function buildStreamingUrl(params: StreamingQueryParams = {}): string {
39
- const url = new URL(`${API_BASE}/streaming`, !isProd ? window.location.origin : undefined);
40
-
41
- if (params.before) {
42
- url.searchParams.set('before', params.before);
43
- }
44
- if (params.after) {
45
- url.searchParams.set('after', params.after);
46
- }
47
- if (params.offset) {
48
- url.searchParams.set('offset', params.offset.toString());
49
- }
50
-
51
- return !isProd ? url.pathname + url.search : url.toString();
52
- }
53
-
54
- /**
55
- * Fetch streaming data from API
56
- */
57
- async function fetchStreaming(params: StreamingQueryParams = {}): Promise<StreamingResponse> {
58
- const url = buildStreamingUrl(params);
59
- const response = await baseFetcher(url);
60
- return response as StreamingResponse;
61
- }
62
-
63
28
  // SWR key generator for streaming queries
64
29
  const getStreamingKey = (params: StreamingQueryParams) => {
65
30
  const parts = ['streaming'];
66
31
  if (params.before) parts.push(`before=${params.before}`);
67
32
  if (params.after) parts.push(`after=${params.after}`);
68
33
  if (params.offset) parts.push(`offset=${params.offset}`);
69
- return parts.join('?');
34
+ return parts;
70
35
  };
71
36
 
72
37
  interface UseStreamingOptions {
@@ -83,12 +48,18 @@ interface UseStreamingOptions {
83
48
  export function useStreaming(options: UseStreamingOptions = {}) {
84
49
  const { refreshInterval = 1000, offset = 8 } = options;
85
50
 
51
+ const { fetch } = NowCtx.useCtx();
52
+
86
53
  const key = getStreamingKey({ offset });
87
54
 
88
- const { data, error, mutate } = useSWR(key, () => fetchStreaming({ offset }), {
89
- revalidateOnFocus: true,
90
- refreshInterval,
91
- });
55
+ const { data, error, mutate } = useSWR<StreamingResponse>(
56
+ key,
57
+ fetch as BareFetcher<StreamingResponse>,
58
+ {
59
+ revalidateOnFocus: true,
60
+ refreshInterval,
61
+ }
62
+ );
92
63
 
93
64
  const isLoading = !data && !error;
94
65
 
@@ -22,6 +22,7 @@ export class NowPlugin extends BBPlugin {
22
22
  setLoading: loading => {
23
23
  ctx.api.setLoading('now', loading);
24
24
  },
25
+ fetch: ctx.api.fetch,
25
26
  },
26
27
  pageNow
27
28
  ),
@@ -6,7 +6,7 @@ import { buildEntryCreator } from '#/utils';
6
6
  import { StorePage } from './components/storePage';
7
7
  import { StoreCtx } from './context';
8
8
 
9
- export class PluginStore extends BBPlugin {
9
+ export class StorePlugin extends BBPlugin {
10
10
  id: PluginID = 'store';
11
11
  override onInstall = async (ctx: IHostContext): Promise<void> => {
12
12
  const entryCreator = buildEntryCreator(ctx);
@@ -19,9 +19,11 @@ export class PluginStore extends BBPlugin {
19
19
  install: ctx.store?.installPlugin || (async () => {}),
20
20
  uninstall: ctx.store?.disablePlugin || (async () => {}),
21
21
  isInstalled: ctx.store?.isPluginInstalled || ((_: PluginID) => false),
22
- list: () => {
23
- const installed = Array.from(ctx.store?.getAllPlugins() || []);
24
- return installed.filter(({ id }) => id !== this.id);
22
+ list: async () => {
23
+ ctx.api.setLoading('store', true);
24
+ const all = Array.from((await ctx.store?.getAllPlugins()) || []);
25
+ ctx.api.setLoading('store', false);
26
+ return all.filter(({ id }) => id !== this.id);
25
27
  },
26
28
  setLoading: (loading: boolean) => {
27
29
  ctx.api.setLoading('store', loading);
@@ -35,4 +37,4 @@ export class PluginStore extends BBPlugin {
35
37
  };
36
38
  }
37
39
 
38
- export default new PluginStore();
40
+ export default new StorePlugin();
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { Fetcher } from 'swr';
2
3
 
3
4
  import { type FingerprintData } from '@/utils/fingerprints';
4
5
  import { PluginStore } from '#/core/pluginStore';
@@ -25,6 +26,7 @@ export interface IHostApi {
25
26
  pluginId: string,
26
27
  weight?: number
27
28
  ) => void;
29
+ fetch: Fetcher;
28
30
  }
29
31
 
30
32
  export type PluginInitializer = (api: IHostApi) => void;
@@ -47,6 +47,8 @@ export interface IPluginManifestEntry {
47
47
  version: string;
48
48
  description?: string;
49
49
  perm?: PluginPerm;
50
+ icon?: string;
51
+ dependencies?: PluginID[];
50
52
  }
51
53
 
52
54
  export interface IPluginStoreEntry extends IPluginManifestEntry {