@bbki.ng/site 5.5.35 → 5.5.36

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.5.36
4
+
5
+ ### Patch Changes
6
+
7
+ - e6d75b2: move now page to plugin
8
+
3
9
  ## 5.5.35
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.5.35",
3
+ "version": "5.5.36",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/blog/app.tsx CHANGED
@@ -11,11 +11,11 @@ import { usePlugins } from '#/core/hooks/use_plugins';
11
11
  import { SWR } from '@/swr';
12
12
  import type { PluginID } from '#/types/plugin';
13
13
 
14
- import { Cover, Streaming } from './pages';
14
+ import { Cover } from './pages';
15
15
  import { usePluginEntries } from './hooks/use_plugin_entries';
16
16
  import { BaseLayout } from './components/BaseLayout';
17
17
 
18
- const APP_PLUGIN_IDS: Array<PluginID> = [/*'sticker',*/ 'xwy', 'extra-cd' /*'extra-entry'*/];
18
+ const APP_PLUGIN_IDS: Array<PluginID> = [/*'sticker',*/ 'xwy', 'extra-cd', 'now'];
19
19
 
20
20
  const AppRoutes = () => {
21
21
  usePlugins(APP_PLUGIN_IDS);
@@ -33,7 +33,6 @@ const AppRoutes = () => {
33
33
  </Route>
34
34
 
35
35
  <Route path="bot" element={<BotRedirect />} />
36
- <Route path="now" element={<Streaming />} />
37
36
  {pluginEntries?.map(route => (
38
37
  <Route key={route.path} path={route.path} element={<Slot name="route" data={route} />} />
39
38
  ))}
@@ -1,4 +1,2 @@
1
1
  export { usePaths } from './use_paths';
2
- export { useStreaming } from './use_streaming';
3
- export type { StreamingItem } from './use_streaming';
4
2
  export { useGlobalLoading } from './use_loading';
@@ -3,14 +3,7 @@ import { type PathRouteProps } from 'react-router-dom';
3
3
 
4
4
  import { useMiddlewareRunner } from '#/core/hooks';
5
5
 
6
- const protectedRoutesSet = new Set<string>([
7
- '/bot',
8
- '/now',
9
- '/',
10
- '/blog',
11
- '/blog/:title',
12
- 'default',
13
- ]);
6
+ const protectedRoutesSet = new Set<string>(['/bot', '/', '/blog', '/blog/:title', 'default']);
14
7
 
15
8
  type PluginRoute = Omit<PathRouteProps, 'element'>;
16
9
 
@@ -11,10 +11,6 @@ export const Cover = (_: { className?: string }) => {
11
11
  to: '/blog',
12
12
  children: 'cd ./blog',
13
13
  },
14
- {
15
- to: '/now',
16
- children: 'cd ./now',
17
- },
18
14
  ],
19
15
  []
20
16
  );
@@ -1,2 +1,2 @@
1
1
  export { Cover } from './cover';
2
- export { default as Streaming } from './streaming';
2
+ export { default as Streaming } from '../../plugins/now/components/streaming';
@@ -2,7 +2,7 @@ import { API_ENDPOINT } from '@/constants/routes';
2
2
 
3
3
  import { getStableDeviceId } from './fingerprints';
4
4
 
5
- type Fetcher = (resource: string, init?: any) => Promise<any>;
5
+ type Fetcher = (resource: string, init?: RequestInit) => Promise<unknown>;
6
6
 
7
7
  export const floatNumberToPercentageString = (num: number): string => {
8
8
  return `${num * 100}%`;
@@ -0,0 +1,3 @@
1
+ import { PluginID } from '#/types/plugin';
2
+
3
+ export const AdminPluginIDSet: Set<PluginID> = new Set(['store']);
@@ -2,11 +2,12 @@ import React from 'react';
2
2
 
3
3
  import { IHostContext } from '#/types/hostApi';
4
4
  import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
5
- import { IPlugin, PluginEvents, PluginID } from '#/types/plugin';
5
+ import { IPlugin, PluginEvents, PluginID, PluginPerm } from '#/types/plugin';
6
6
  import { getStableDeviceId } from '@/utils/fingerprints';
7
7
 
8
8
  import { registry } from './registry';
9
9
  import { createEventBus } from './utils/eventBus';
10
+ import { AdminPluginIDSet } from './const';
10
11
 
11
12
  const pluginModules = import.meta.glob('../plugins/*/index.ts');
12
13
 
@@ -15,9 +16,15 @@ class PluginManager {
15
16
 
16
17
  private bus = createEventBus<PluginEvents>();
17
18
 
18
- private createHostContext(): IHostContext {
19
+ private createHostContext(perm: PluginPerm = 'guest'): IHostContext {
20
+ const adminOnlyApi = {
21
+ installPlugin: (id: PluginID) => this.loadPlugin(id),
22
+ disablePlugin: (id: PluginID) => this.disablePlugin(id),
23
+ };
24
+
19
25
  return {
20
26
  api: {
27
+ ...(perm === 'admin' ? adminOnlyApi : {}),
21
28
  setLoading: (id: PluginID, loading: boolean) => {
22
29
  this.bus.emit('plugin:loading:changed', { id, loading });
23
30
  },
@@ -82,7 +89,13 @@ class PluginManager {
82
89
 
83
90
  const p: IPlugin = module.default;
84
91
 
85
- const ctx = this.createHostContext();
92
+ const perm = (p as IPlugin & { perm?: PluginPerm }).perm || 'guest';
93
+ if (perm === 'admin' && !AdminPluginIDSet.has(pluginId)) {
94
+ console.error(`Plugin ${pluginId} requires admin permission, but it's not trusted.`);
95
+ return;
96
+ }
97
+
98
+ const ctx = this.createHostContext(perm);
86
99
 
87
100
  if (p.onInstall) {
88
101
  await p.onInstall(ctx);
@@ -5,6 +5,19 @@ export const PLUGIN_MANIFEST = [
5
5
  version: '0.1.0',
6
6
  description: 'A sticker plugin',
7
7
  },
8
+ {
9
+ name: 'store',
10
+ id: 'store',
11
+ version: '0.1.0',
12
+ description: 'A plugin for state management and data persistence.',
13
+ perm: 'admin',
14
+ },
15
+ {
16
+ name: 'now',
17
+ id: 'now',
18
+ version: '1.0.0',
19
+ description: 'show real-time site status and logs',
20
+ },
8
21
  {
9
22
  name: 'extra-cd',
10
23
  id: 'extra-cd',
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { PathRouteProps } from 'react-router-dom';
3
+
4
+ import { IComPropsRegisteredToSlot } from '#/types/slots';
5
+
6
+ import Streaming from './streaming';
7
+
8
+ export const pageNow = ({ data }: IComPropsRegisteredToSlot) => {
9
+ const route = data as Omit<PathRouteProps, 'element'>;
10
+
11
+ if (route.path !== '/now') {
12
+ return <></>;
13
+ }
14
+
15
+ return <Streaming />;
16
+ };
@@ -5,7 +5,7 @@ export const ArrowDownIcon = ({ show }: { show: boolean }) => (
5
5
  <svg
6
6
  data-testid="geist-icon"
7
7
  height="16"
8
- stroke-linejoin="round"
8
+ strokeLinejoin="round"
9
9
  viewBox="0 0 16 16"
10
10
  width="16"
11
11
  style={{
@@ -17,8 +17,8 @@ export const ArrowDownIcon = ({ show }: { show: boolean }) => (
17
17
  })}
18
18
  >
19
19
  <path
20
- fill-rule="evenodd"
21
- clip-rule="evenodd"
20
+ fillRule="evenodd"
21
+ clipRule="evenodd"
22
22
  d="M8.70711 1.39644C8.31659 1.00592 7.68342 1.00592 7.2929 1.39644L2.21968 6.46966L1.68935 6.99999L2.75001 8.06065L3.28034 7.53032L7.25001 3.56065V14.25V15H8.75001V14.25V3.56065L12.7197 7.53032L13.25 8.06065L14.3107 6.99999L13.7803 6.46966L8.70711 1.39644Z"
23
23
  fill="currentColor"
24
24
  ></path>
@@ -1,13 +1,13 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
- import { useStreaming, StreamingItem } from '@/hooks/use_streaming';
3
- import { formatStreamingData } from '@/utils/streaming';
4
- import { Button, Panel } from '@bbki.ng/ui';
5
- import { useScrollBtnVisibility } from './useScrollBtnVisibility';
6
- import { Link } from '@bbki.ng/ui';
7
- import { ArrowDownIcon } from './arrow-down';
2
+ import { Panel, Link } from '@bbki.ng/ui';
8
3
  import classNames from 'classnames';
9
4
 
10
- // Extend JSX IntrinsicElements for the web component
5
+ import { formatStreamingData } from '#/plugins/now/utils/streaming';
6
+
7
+ import { useStreaming } from '../../hooks/use_streaming';
8
+
9
+ import { useScrollBtnVisibility } from './useScrollBtnVisibility';
10
+
11
11
  declare global {
12
12
  namespace JSX {
13
13
  interface IntrinsicElements {
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
+
2
3
  import { BbMsgHistoryElement } from '.';
3
4
 
4
5
  export const useScrollBtnVisibility = (ele: BbMsgHistoryElement) => {
@@ -0,0 +1,7 @@
1
+ import { createPluginCtx } from '#/core/context';
2
+
3
+ export interface INowCtx {
4
+ setLoading: (loading: boolean) => void;
5
+ }
6
+
7
+ export const NowCtx = createPluginCtx<INowCtx>();
@@ -2,14 +2,13 @@ import useSWR from 'swr';
2
2
  import { useEffect, useState } from 'react';
3
3
 
4
4
  import { baseFetcher } from '@/utils';
5
- import { API_ENDPOINT } from '@/constants/routes';
6
5
 
7
- import { useGlobalLoading } from './use_loading';
6
+ import { NowCtx } from '../context';
8
7
 
9
8
  // In dev, use /api prefix to leverage Vite proxy to localhost:8787
10
9
  // const isProd = typeof window !== 'undefined' && /^https:\/\/bbki.ng/.test(window.location.href);
11
10
  const isProd = true;
12
- const API_BASE = !isProd ? '/api' : API_ENDPOINT;
11
+ const API_BASE = !isProd ? '/api' : 'https://cf.bbki.ng';
13
12
 
14
13
  export type StreamingItem = {
15
14
  id: string;
@@ -93,6 +92,8 @@ export function useStreaming(options: UseStreamingOptions = {}) {
93
92
 
94
93
  const isLoading = !data && !error;
95
94
 
95
+ const nowCtx = NowCtx.useCtx();
96
+
96
97
  const [, forceUpdate] = useState(0);
97
98
 
98
99
  // make rerender when customElement defined
@@ -102,7 +103,7 @@ export function useStreaming(options: UseStreamingOptions = {}) {
102
103
  });
103
104
  }, []);
104
105
 
105
- useGlobalLoading('streaming', isLoading);
106
+ nowCtx.setLoading(isLoading);
106
107
 
107
108
  return {
108
109
  streaming: data?.data ?? [],
@@ -0,0 +1,37 @@
1
+ import { IHostContext } from '#/types/hostApi';
2
+ import { IPlugin } from '#/types/plugin';
3
+ import { buildEntryCreator } from '#/utils';
4
+
5
+ import { pageNow } from './components';
6
+ import { NowCtx } from './context';
7
+
8
+ export class NowPlugin implements IPlugin {
9
+ id: string = 'now';
10
+ name: string = 'Now';
11
+ description?: string | undefined = 'Manages real-time site status and logs';
12
+ version: string = '1.0.0';
13
+ author?: string | undefined = 'bbki.ng';
14
+
15
+ onInstall = (ctx: IHostContext) => {
16
+ const nowEntryCreator = buildEntryCreator(ctx);
17
+ const { withCtx } = NowCtx;
18
+
19
+ nowEntryCreator(
20
+ {
21
+ path: '/now',
22
+ label: 'cd ./now',
23
+ pageComponent: withCtx(
24
+ {
25
+ setLoading: loading => {
26
+ ctx.api.setLoading('now', loading);
27
+ },
28
+ },
29
+ pageNow
30
+ ),
31
+ },
32
+ this.id
33
+ );
34
+ };
35
+ }
36
+
37
+ export default new NowPlugin();
@@ -1,7 +1,11 @@
1
- import { StreamingItem } from '@/hooks/use_streaming';
1
+ export type StreamingItem = {
2
+ id: string;
3
+ author: string;
4
+ content: string;
5
+ type?: string;
6
+ createdAt: string;
7
+ };
2
8
 
3
- // Format: "author: content" (with optional timestamp)
4
- // Timestamp format: YYYY-mm-dd HH:MM:SS
5
9
  export const formatStreamingData = (items: StreamingItem[]): string => {
6
10
  if (!items || items.length === 0) {
7
11
  return '';
@@ -1,13 +1,17 @@
1
- import { PathObj } from '@bbki.ng/ui';
2
-
3
1
  import { IHostContext } from '#/types/hostApi';
4
2
  import { IPlugin } from '#/types/plugin';
5
- import { TitleListItem } from '#/types/posts';
3
+ import { HookPoint } from '#/types/slots';
6
4
 
7
5
  import { SpecialTitle } from './components/article';
8
6
  import { XwyLogo } from './components/logo';
9
7
  import { FontRules, LOADING_CLASS } from './const';
10
8
  import { loadFont } from './utils';
9
+ import {
10
+ transformBreadcrumbPaths,
11
+ transformTitleList,
12
+ transformPostContent,
13
+ pinTitle,
14
+ } from './transformers';
11
15
 
12
16
  class XwyPlugin implements IPlugin {
13
17
  id = 'fontstyler';
@@ -30,29 +34,7 @@ class XwyPlugin implements IPlugin {
30
34
  }
31
35
  });
32
36
 
33
- // 注册中间件,修改标题列表
34
- ctx.api.registerMiddleware(
35
- 'transformTitleList',
36
- this.transformTitleList,
37
- this.id,
38
- 10 // weight
39
- );
40
-
41
- // 注册中间件,修改面包屑路径显示
42
- ctx.api.registerMiddleware(
43
- 'transformBreadcrumbPath',
44
- this.transformBreadcrumbPaths,
45
- this.id,
46
- 10 // weight
47
- );
48
-
49
- // 注册中间件,修改文章内容
50
- ctx.api.registerMiddleware(
51
- 'transformPostContent',
52
- this.transformPostContent,
53
- this.id,
54
- 10 // weight
55
- );
37
+ this.batchRegisterMiddlewares(ctx);
56
38
 
57
39
  // 注册logo插槽组件
58
40
  ctx.api.registerSlot('logo', XwyLogo, this.id);
@@ -61,6 +43,22 @@ class XwyPlugin implements IPlugin {
61
43
  ctx.api.registerSlot('articleTitle', SpecialTitle, this.id);
62
44
  };
63
45
 
46
+ private trasformerMap: Partial<Record<HookPoint, Array<(baseData: any) => any>>> = {
47
+ transformTitleList: [pinTitle, transformTitleList],
48
+ transformBreadcrumbPath: [transformBreadcrumbPaths],
49
+ transformPostContent: [transformPostContent],
50
+ };
51
+
52
+ private batchRegisterMiddlewares(ctx: IHostContext) {
53
+ for (const [point, fns] of Object.entries(this.trasformerMap) as Array<
54
+ [HookPoint, Array<(baseData: unknown) => unknown>]
55
+ >) {
56
+ fns.forEach(fn => {
57
+ ctx.api.registerMiddleware(point, fn, this.id, 10);
58
+ });
59
+ }
60
+ }
61
+
64
62
  private initFontLoadingListener() {
65
63
  // listen font loading status and add class to body for styling
66
64
  console.log('Initializing font loading listener');
@@ -70,58 +68,6 @@ class XwyPlugin implements IPlugin {
70
68
  });
71
69
  }
72
70
 
73
- private transformPostContent = (content: string = '') => {
74
- // 在文章内容中替换特定关键词为特殊样式
75
- return content
76
- .replace(
77
- /小乌鸦/g,
78
- `<span class="font-xwy text-content-special text-[1.6rem] align-middle leading-[1.09]">小乌鸦</span>`
79
- )
80
- .replace(
81
- /公园/,
82
- `公园 <span class="font-xwy-icon text-[#679867] text-[1.6rem] align-middle leading-[1.09]">&#xE01A&#xE003</span>`
83
- );
84
- };
85
-
86
- private transformBreadcrumbPaths = (paths: PathObj[]) => {
87
- const result = paths.map(p => {
88
- if (p.name === '小乌鸦合集') {
89
- return {
90
- ...p,
91
- name: '小乌鸦合集',
92
- className: 'font-xwy text-content-special text-[1.2rem] align-middle relative top-[1px]',
93
- };
94
- }
95
-
96
- return p;
97
- });
98
-
99
- console.log('Transformed breadcrumb paths:', result);
100
-
101
- return result;
102
- };
103
-
104
- /**
105
- * 转换标题列表,为匹配的标题添加字体类名
106
- */
107
- private transformTitleList = (titleList: TitleListItem[]): TitleListItem[] => {
108
- return titleList.map(item => {
109
- for (const rule of FontRules) {
110
- const match =
111
- typeof rule.match === 'string' ? item.name === rule.match : rule.match.test(item.name);
112
-
113
- if (match) {
114
- return {
115
- ...item,
116
- variant: rule.variant || 'default',
117
- className: `font-${rule.fontFamily} ${rule.extraCls || ''}`.trim(),
118
- };
119
- }
120
- }
121
- return item;
122
- });
123
- };
124
-
125
71
  onDisable = async () => {
126
72
  this.loadedFonts.forEach(fontName => {
127
73
  const styleEl = document.getElementById(`font-${fontName}`);
@@ -0,0 +1,67 @@
1
+ import { PathObj } from '@bbki.ng/ui';
2
+
3
+ import { TitleListItem } from '#/types/posts';
4
+
5
+ import { FontRules } from '../const';
6
+
7
+ export const transformPostContent = (content: string = '') => {
8
+ // 在文章内容中替换特定关键词为特殊样式
9
+ return content
10
+ .replace(
11
+ /小乌鸦/g,
12
+ `<span class="font-xwy text-content-special text-[1.6rem] align-middle leading-[1.09]">小乌鸦</span>`
13
+ )
14
+ .replace(
15
+ /公园/,
16
+ `公园 <span class="font-xwy-icon text-[#679867] text-[1.6rem] align-middle leading-[1.09]">&#xE01A&#xE003</span>`
17
+ );
18
+ };
19
+
20
+ export const pinTitle = (titleList: TitleListItem[]) => {
21
+ const pinnedTitle = '小乌鸦合集';
22
+ const index = titleList.findIndex(item => item.name === pinnedTitle);
23
+ if (index > -1) {
24
+ const [item] = titleList.splice(index, 1);
25
+ titleList.unshift(item);
26
+ }
27
+ return titleList;
28
+ };
29
+
30
+ export const transformBreadcrumbPaths = (paths: PathObj[]) => {
31
+ const result = paths.map(p => {
32
+ if (p.name === '小乌鸦合集') {
33
+ return {
34
+ ...p,
35
+ name: '小乌鸦合集',
36
+ className: 'font-xwy text-content-special text-[1.2rem] align-middle relative top-[1px]',
37
+ };
38
+ }
39
+
40
+ return p;
41
+ });
42
+
43
+ console.log('Transformed breadcrumb paths:', result);
44
+
45
+ return result;
46
+ };
47
+
48
+ /**
49
+ * 转换标题列表,为匹配的标题添加字体类名
50
+ */
51
+ export const transformTitleList = (titleList: TitleListItem[]): TitleListItem[] => {
52
+ return titleList.map(item => {
53
+ for (const rule of FontRules) {
54
+ const match =
55
+ typeof rule.match === 'string' ? item.name === rule.match : rule.match.test(item.name);
56
+
57
+ if (match) {
58
+ return {
59
+ ...item,
60
+ variant: rule.variant || 'default',
61
+ className: `font-${rule.fontFamily} ${rule.extraCls || ''}`.trim(),
62
+ };
63
+ }
64
+ }
65
+ return item;
66
+ });
67
+ };
@@ -20,6 +20,9 @@ export interface IHostApi {
20
20
  pluginId: string,
21
21
  weight?: number
22
22
  ) => void;
23
+
24
+ installPlugin?: (id: PluginID) => void;
25
+ disablePlugin?: (id: PluginID) => void;
23
26
  }
24
27
 
25
28
  export type PluginInitializer = (api: IHostApi) => void;
@@ -16,7 +16,7 @@ export interface IPlugin {
16
16
  onDestroy?: () => void;
17
17
  }
18
18
 
19
- export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry';
19
+ export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry' | 'store' | 'now';
20
20
 
21
21
  export interface IPluginEntry {
22
22
  path: string;
@@ -32,3 +32,13 @@ export interface IPluginLoadingPayload {
32
32
  id: PluginID;
33
33
  loading: boolean;
34
34
  }
35
+
36
+ export type PluginPerm = 'guest' | 'admin';
37
+
38
+ export interface IPluginManifestEntry {
39
+ id: PluginID;
40
+ name: string;
41
+ version: string;
42
+ description?: string;
43
+ perm?: PluginPerm;
44
+ }