@lark-apaas/client-toolkit 1.2.0-alpha.0 → 1.2.0-alpha.11
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/lib/apis/components/Avatar.d.ts +4 -0
- package/lib/apis/components/Avatar.js +2 -0
- package/lib/apis/components/NavLink.d.ts +9 -0
- package/lib/apis/components/NavLink.js +50 -0
- package/lib/apis/components/TruncatedTitle.d.ts +5 -0
- package/lib/apis/components/TruncatedTitle.js +2 -0
- package/lib/apis/components/UniversalLink.d.ts +12 -0
- package/lib/apis/components/UniversalLink.js +26 -0
- package/lib/components/AppContainer/index.js +26 -12
- package/lib/components/AppContainer/utils/observable.d.ts +1 -0
- package/lib/components/AppContainer/utils/observable.js +16 -0
- package/lib/components/PagePlaceholder/index.js +1 -1
- package/lib/hooks/useAppInfo.js +5 -3
- package/lib/hooks/useCurrentUserProfile.js +15 -8
- package/lib/index.d.ts +1 -1
- package/lib/index.js +12 -1
- package/lib/logger/logger.js +20 -0
- package/lib/server-log/index.d.ts +9 -3
- package/lib/server-log/index.js +2 -1
- package/lib/server-log/sse-client.d.ts +108 -0
- package/lib/server-log/sse-client.js +209 -0
- package/lib/server-log/types.d.ts +9 -0
- package/lib/utils/axiosConfig.js +6 -4
- package/lib/utils/getAxiosForBackend.js +55 -1
- package/lib/utils/safeStringify.d.ts +4 -0
- package/lib/utils/safeStringify.js +42 -0
- package/package.json +6 -5
- package/lib/logger/__tests__/batch-logger.test.d.ts +0 -1
- package/lib/logger/__tests__/batch-logger.test.js +0 -367
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type NavLinkProps } from 'react-router-dom';
|
|
3
|
+
/**
|
|
4
|
+
* Enhanced NavLink component that extends react-router-dom's NavLink
|
|
5
|
+
* with support for hash links (anchor navigation) and smooth scrolling.
|
|
6
|
+
*/
|
|
7
|
+
declare const NavLink: React.ForwardRefExoticComponent<NavLinkProps & React.RefAttributes<HTMLAnchorElement>>;
|
|
8
|
+
export { NavLink };
|
|
9
|
+
export type { NavLinkProps };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { NavLink, useLocation, useNavigate } from "react-router-dom";
|
|
4
|
+
const NavLink_NavLink = /*#__PURE__*/ forwardRef(({ to, children, className, style, ...props }, ref)=>{
|
|
5
|
+
const isHashLink = 'string' == typeof to && to.startsWith('#');
|
|
6
|
+
const location = useLocation();
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
if (isHashLink) {
|
|
9
|
+
const handleClick = (e)=>{
|
|
10
|
+
e.preventDefault();
|
|
11
|
+
const element = document.querySelector(to);
|
|
12
|
+
if (element) element.scrollIntoView({
|
|
13
|
+
behavior: 'smooth'
|
|
14
|
+
});
|
|
15
|
+
navigate(to);
|
|
16
|
+
};
|
|
17
|
+
const isActive = location.hash === to;
|
|
18
|
+
const renderProps = {
|
|
19
|
+
isActive,
|
|
20
|
+
isPending: false,
|
|
21
|
+
isTransitioning: false
|
|
22
|
+
};
|
|
23
|
+
const resolvedClassName = 'function' == typeof className ? className(renderProps) : className;
|
|
24
|
+
const resolvedStyle = 'function' == typeof style ? style(renderProps) : style;
|
|
25
|
+
const { caseSensitive, end, replace, state, preventScrollReset, relative, viewTransition, ...restProps } = props;
|
|
26
|
+
return /*#__PURE__*/ jsx("a", {
|
|
27
|
+
href: to,
|
|
28
|
+
onClick: handleClick,
|
|
29
|
+
ref: ref,
|
|
30
|
+
className: resolvedClassName,
|
|
31
|
+
style: resolvedStyle,
|
|
32
|
+
...restProps,
|
|
33
|
+
children: 'function' == typeof children ? children(renderProps) : children
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return /*#__PURE__*/ jsx(NavLink, {
|
|
37
|
+
to: to,
|
|
38
|
+
ref: ref,
|
|
39
|
+
className: className,
|
|
40
|
+
style: style,
|
|
41
|
+
onClick: ()=>(document.getElementById('rootContainer') || window)?.scrollTo({
|
|
42
|
+
top: 0,
|
|
43
|
+
behavior: 'smooth'
|
|
44
|
+
}),
|
|
45
|
+
...props,
|
|
46
|
+
children: children
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
NavLink_NavLink.displayName = 'NavLink';
|
|
50
|
+
export { NavLink_NavLink as NavLink };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TruncatedTitle component - displays truncated text with a tooltip when overflowed.
|
|
3
|
+
* This is an alias for OverflowTooltipText for semantic naming in layout components.
|
|
4
|
+
*/
|
|
5
|
+
export { OverflowTooltipText, OverflowTooltipText as TruncatedTitle, type OverflowTooltipTextProps, type OverflowTooltipTextProps as TruncatedTitleProps, } from '../../components/ui/overflow-tooltip-text';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface UniversalLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
3
|
+
/** 链接目标:内部路由、hash 锚点或外部 URL */
|
|
4
|
+
to: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 统一的链接组件,用于替换原生 <a> 标签
|
|
8
|
+
* - 内部路由(/dashboard)→ react-router Link
|
|
9
|
+
* - Hash 锚点(#section)→ <a>
|
|
10
|
+
* - 外链(https://...)→ <a target="_blank">
|
|
11
|
+
*/
|
|
12
|
+
export declare const UniversalLink: React.ForwardRefExoticComponent<UniversalLinkProps & React.RefAttributes<HTMLAnchorElement>>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import react from "react";
|
|
3
|
+
import { Link } from "react-router-dom";
|
|
4
|
+
function isInternalRoute(to) {
|
|
5
|
+
return !to.startsWith('#') && !to.startsWith('http://') && !to.startsWith('https://') && !to.startsWith('//');
|
|
6
|
+
}
|
|
7
|
+
function isExternalLink(to) {
|
|
8
|
+
return to.startsWith('http://') || to.startsWith('https://') || to.startsWith('//');
|
|
9
|
+
}
|
|
10
|
+
const UniversalLink_UniversalLink = /*#__PURE__*/ react.forwardRef(function({ to, ...props }, ref) {
|
|
11
|
+
if (isInternalRoute(to)) return /*#__PURE__*/ jsx(Link, {
|
|
12
|
+
to: to,
|
|
13
|
+
ref: ref,
|
|
14
|
+
...props
|
|
15
|
+
});
|
|
16
|
+
return /*#__PURE__*/ jsx("a", {
|
|
17
|
+
href: to,
|
|
18
|
+
ref: ref,
|
|
19
|
+
...props,
|
|
20
|
+
...isExternalLink(to) && {
|
|
21
|
+
target: props.target ?? '_blank',
|
|
22
|
+
rel: props.rel ?? 'noopener noreferrer'
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
export { UniversalLink_UniversalLink as UniversalLink };
|
|
@@ -2,6 +2,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { ConfigProvider } from "antd";
|
|
4
4
|
import { MiaodaInspector } from "@lark-apaas/miaoda-inspector";
|
|
5
|
+
import zh_CN from "antd/locale/zh_CN";
|
|
5
6
|
import IframeBridge from "./IframeBridge.js";
|
|
6
7
|
import { defaultUIConfig } from "../theme/ui-config.js";
|
|
7
8
|
import { Toaster } from "./sonner.js";
|
|
@@ -15,10 +16,12 @@ import { useAppInfo } from "../../hooks/index.js";
|
|
|
15
16
|
import { TrackKey } from "../../types/tea.js";
|
|
16
17
|
import safety from "./safety.js";
|
|
17
18
|
import { getAppId } from "../../utils/getAppId.js";
|
|
18
|
-
import {
|
|
19
|
+
import { ServerLogSSEClient } from "../../server-log/index.js";
|
|
19
20
|
import QueryProvider from "../QueryProvider/index.js";
|
|
21
|
+
import { initObservable } from "./utils/observable.js";
|
|
20
22
|
registerDayjsPlugins();
|
|
21
23
|
initAxiosConfig();
|
|
24
|
+
initObservable();
|
|
22
25
|
const isMiaodaPreview = window.IS_MIAODA_PREVIEW;
|
|
23
26
|
const readCssVarColor = (varName, fallback)=>{
|
|
24
27
|
try {
|
|
@@ -32,7 +35,7 @@ const readCssVarColor = (varName, fallback)=>{
|
|
|
32
35
|
const App = (props)=>{
|
|
33
36
|
const { themeMeta = {} } = props;
|
|
34
37
|
useAppInfo();
|
|
35
|
-
const
|
|
38
|
+
const serverLogClientRef = useRef(null);
|
|
36
39
|
const { rem } = findValueByPixel(themeMetaOptions.themeRadius, themeMeta.borderRadius) || {
|
|
37
40
|
rem: '0.625'
|
|
38
41
|
};
|
|
@@ -48,22 +51,32 @@ const App = (props)=>{
|
|
|
48
51
|
if ('production' !== process.env.NODE_ENV && window.parent !== window) {
|
|
49
52
|
try {
|
|
50
53
|
const backendUrl = window.location.origin;
|
|
51
|
-
|
|
54
|
+
serverLogClientRef.current = new ServerLogSSEClient({
|
|
52
55
|
serverUrl: backendUrl,
|
|
53
|
-
|
|
54
|
-
pollInterval: 2000,
|
|
55
|
-
limit: 100,
|
|
56
|
+
sseEndpoint: '/dev/logs/server-logs/stream',
|
|
56
57
|
debug: true
|
|
57
58
|
});
|
|
58
|
-
|
|
59
|
-
console.log('[AppContainer] Server log
|
|
59
|
+
serverLogClientRef.current.start();
|
|
60
|
+
console.log('[AppContainer] Server log SSE client started');
|
|
60
61
|
} catch (error) {
|
|
61
|
-
console.error('[AppContainer] Failed to start server log
|
|
62
|
+
console.error('[AppContainer] Failed to start server log SSE client:', error);
|
|
62
63
|
}
|
|
64
|
+
const handleVisibilityChange = ()=>{
|
|
65
|
+
if (!serverLogClientRef.current) return;
|
|
66
|
+
if (document.hidden) {
|
|
67
|
+
serverLogClientRef.current.pause();
|
|
68
|
+
console.log('[AppContainer] Tab hidden, SSE paused');
|
|
69
|
+
} else {
|
|
70
|
+
serverLogClientRef.current.resume();
|
|
71
|
+
console.log('[AppContainer] Tab visible, SSE resumed');
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
63
75
|
return ()=>{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
77
|
+
if (serverLogClientRef.current) {
|
|
78
|
+
serverLogClientRef.current.stop();
|
|
79
|
+
console.log('[AppContainer] Server log SSE client stopped');
|
|
67
80
|
}
|
|
68
81
|
};
|
|
69
82
|
}
|
|
@@ -224,6 +237,7 @@ const AppContainer_AppContainer = (props)=>{
|
|
|
224
237
|
/*#__PURE__*/ jsx(safety, {}),
|
|
225
238
|
/*#__PURE__*/ jsx(QueryProvider, {
|
|
226
239
|
children: /*#__PURE__*/ jsx(ConfigProvider, {
|
|
240
|
+
locale: zh_CN,
|
|
227
241
|
theme: {
|
|
228
242
|
token: antdThemeToken,
|
|
229
243
|
components: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const initObservable: () => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AppEnv, observable } from "@lark-apaas/observable-web";
|
|
2
|
+
const initObservable = ()=>{
|
|
3
|
+
try {
|
|
4
|
+
observable.start({
|
|
5
|
+
serviceName: "app",
|
|
6
|
+
env: 'development' === process.env.NODE_ENV ? AppEnv.Dev : AppEnv.Prod,
|
|
7
|
+
collectorUrl: {
|
|
8
|
+
log: `/spark/app/${window.appId}/runtime/api/v1/observability/logs/collect`,
|
|
9
|
+
metric: `/spark/app/${window.appId}/runtime/api/v1/observability/metrics/collect`
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Failed to start WebObservableSdk:', error);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export { initObservable };
|
|
@@ -15,7 +15,7 @@ const PagePlaceholder = ({ title = '页面待开发', description = '页面暂
|
|
|
15
15
|
children: title
|
|
16
16
|
}),
|
|
17
17
|
/*#__PURE__*/ jsx("div", {
|
|
18
|
-
className: "text-center text-muted-foreground text-base leading-6",
|
|
18
|
+
className: "text-center text-muted-foreground text-base leading-6 line-clamp-2 sm:max-w-[480px]",
|
|
19
19
|
children: description
|
|
20
20
|
})
|
|
21
21
|
]
|
package/lib/hooks/useAppInfo.js
CHANGED
|
@@ -23,17 +23,19 @@ const useAppInfo = ()=>{
|
|
|
23
23
|
if (info.description) {
|
|
24
24
|
const meta = document.querySelector("meta[property='og:description']");
|
|
25
25
|
if (meta) meta.content = info.description;
|
|
26
|
+
const metaDom = document.querySelector("meta[name='description']");
|
|
27
|
+
if (metaDom) metaDom.content = info.description;
|
|
26
28
|
}
|
|
27
29
|
};
|
|
28
|
-
const handleMetaInfoChanged = async (info)=>{
|
|
29
|
-
if (!info) info = await getAppInfo(
|
|
30
|
+
const handleMetaInfoChanged = async (info, refresh = true)=>{
|
|
31
|
+
if (!info) info = await getAppInfo(refresh);
|
|
30
32
|
updateDomInfo(info);
|
|
31
33
|
setAppInfo((prev)=>({
|
|
32
34
|
...prev,
|
|
33
35
|
...info
|
|
34
36
|
}));
|
|
35
37
|
};
|
|
36
|
-
handleMetaInfoChanged();
|
|
38
|
+
handleMetaInfoChanged(null, false);
|
|
37
39
|
const onUpdate = (e)=>{
|
|
38
40
|
const info = e.detail;
|
|
39
41
|
handleMetaInfoChanged(info);
|
|
@@ -3,6 +3,11 @@ import { logger } from "../logger/index.js";
|
|
|
3
3
|
import { getCurrentUserProfile } from "../integrations/getCurrentUserProfile.js";
|
|
4
4
|
import { getDataloom } from "../integrations/dataloom.js";
|
|
5
5
|
import { isSparkRuntime } from "../utils/utils.js";
|
|
6
|
+
function getNameFromArray(nameArray) {
|
|
7
|
+
if (!nameArray || 0 === nameArray.length) return;
|
|
8
|
+
const chineseName = nameArray.find((item)=>2052 === item.language_code);
|
|
9
|
+
return chineseName?.text ?? nameArray[0]?.text;
|
|
10
|
+
}
|
|
6
11
|
function getCompatibilityUserProfile() {
|
|
7
12
|
const userInfo = getCurrentUserProfile();
|
|
8
13
|
return {
|
|
@@ -18,34 +23,36 @@ const useCurrentUserProfile = ()=>{
|
|
|
18
23
|
if (isSparkRuntime()) {
|
|
19
24
|
(async ()=>{
|
|
20
25
|
const dataloom = await getDataloom();
|
|
21
|
-
const result = await dataloom
|
|
26
|
+
const result = await dataloom?.service?.session?.getUserInfo();
|
|
22
27
|
const userInfo = result?.data?.user_info;
|
|
28
|
+
const userName = getNameFromArray(userInfo?.name);
|
|
23
29
|
setUserInfo({
|
|
24
30
|
user_id: userInfo?.user_id?.toString(),
|
|
25
31
|
email: userInfo?.email,
|
|
26
|
-
name:
|
|
32
|
+
name: userName,
|
|
27
33
|
avatar: userInfo?.avatar?.image?.large,
|
|
28
|
-
userName:
|
|
34
|
+
userName: userName,
|
|
29
35
|
userAvatar: userInfo?.avatar?.image?.large
|
|
30
36
|
});
|
|
31
37
|
})();
|
|
32
38
|
handleMetaInfoChanged = async ()=>{
|
|
33
39
|
const dataloom = await getDataloom();
|
|
34
|
-
const result = await dataloom
|
|
40
|
+
const result = await dataloom?.service?.session?.getUserInfo();
|
|
35
41
|
const userInfo = result?.data?.user_info;
|
|
42
|
+
const userName = getNameFromArray(userInfo?.name);
|
|
36
43
|
const newUserInfo = {
|
|
37
44
|
user_id: userInfo?.user_id?.toString(),
|
|
38
45
|
email: userInfo?.email,
|
|
39
|
-
name:
|
|
46
|
+
name: userName,
|
|
40
47
|
avatar: userInfo?.avatar?.image?.large,
|
|
41
|
-
userName:
|
|
48
|
+
userName: userName,
|
|
42
49
|
userAvatar: userInfo?.avatar?.image?.large
|
|
43
50
|
};
|
|
44
|
-
logger.info('MiaoDaMetaInfoChanged', newUserInfo);
|
|
51
|
+
if ('development' === process.env.NODE_ENV) logger.info('MiaoDaMetaInfoChanged', newUserInfo);
|
|
45
52
|
setUserInfo(newUserInfo);
|
|
46
53
|
};
|
|
47
54
|
} else handleMetaInfoChanged = ()=>{
|
|
48
|
-
logger.info('MiaoDaMetaInfoChanged', getCompatibilityUserProfile());
|
|
55
|
+
if ('development' === process.env.NODE_ENV) logger.info('MiaoDaMetaInfoChanged', getCompatibilityUserProfile());
|
|
49
56
|
setUserInfo(getCompatibilityUserProfile());
|
|
50
57
|
};
|
|
51
58
|
window.addEventListener('MiaoDaMetaInfoChanged', handleMetaInfoChanged);
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { createClient } from "@lark-apaas/client-capability";
|
|
2
|
+
import { normalizeBasePath } from "./utils/utils.js";
|
|
3
|
+
import { logger } from "./logger/index.js";
|
|
1
4
|
import { version } from "../package.json";
|
|
2
|
-
|
|
5
|
+
const capabilityClient = createClient({
|
|
6
|
+
baseURL: normalizeBasePath(process.env.CLIENT_BASE_PATH),
|
|
7
|
+
fetchOptions: {
|
|
8
|
+
headers: {
|
|
9
|
+
'X-Suda-Csrf-Token': window.csrfToken ?? ''
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
logger: logger
|
|
13
|
+
});
|
|
3
14
|
const src = {
|
|
4
15
|
version: version
|
|
5
16
|
};
|
package/lib/logger/logger.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { observable } from "@lark-apaas/observable-web";
|
|
1
2
|
import { interceptors } from "./selected-logs.js";
|
|
2
3
|
import { interceptErrors } from "./intercept-global-error.js";
|
|
4
|
+
import { mapLogLevel, processLogParams } from "../utils/safeStringify.js";
|
|
5
|
+
const shouldReportToObservable = 'production' === process.env.NODE_ENV;
|
|
3
6
|
const LOG_LEVELS = [
|
|
4
7
|
'debug',
|
|
5
8
|
'info',
|
|
@@ -45,18 +48,35 @@ let logger = {
|
|
|
45
48
|
},
|
|
46
49
|
info (message, ...args) {
|
|
47
50
|
if (shouldLog('info')) console.log(...getFormattedPrefix('info'), message, ...args);
|
|
51
|
+
if (shouldReportToObservable) observable.log('INFO', processLogParams([
|
|
52
|
+
message,
|
|
53
|
+
...args
|
|
54
|
+
]));
|
|
48
55
|
},
|
|
49
56
|
warn (message, ...args) {
|
|
50
57
|
if (shouldLog('warn')) console.log(...getFormattedPrefix('warn'), message, ...args);
|
|
58
|
+
if (shouldReportToObservable) observable.log('WARN', processLogParams([
|
|
59
|
+
message,
|
|
60
|
+
...args
|
|
61
|
+
]));
|
|
51
62
|
},
|
|
52
63
|
error (message, ...args) {
|
|
53
64
|
if (shouldLog('error')) console.error(...getFormattedPrefix('error'), message, ...args);
|
|
65
|
+
if (shouldReportToObservable) observable.log('ERROR', processLogParams([
|
|
66
|
+
message,
|
|
67
|
+
...args
|
|
68
|
+
]));
|
|
54
69
|
},
|
|
55
70
|
success (message, ...args) {
|
|
56
71
|
if (shouldLog('success')) console.log(...getFormattedPrefix('success'), message, ...args);
|
|
72
|
+
if (shouldReportToObservable) observable.log('INFO', processLogParams([
|
|
73
|
+
message,
|
|
74
|
+
...args
|
|
75
|
+
]));
|
|
57
76
|
},
|
|
58
77
|
log ({ level, args }) {
|
|
59
78
|
if (shouldLog(level)) console.log(...getFormattedPrefix(level), ...args);
|
|
79
|
+
if (shouldReportToObservable && "debug" !== level) observable.log(mapLogLevel(level), processLogParams(args));
|
|
60
80
|
}
|
|
61
81
|
};
|
|
62
82
|
if ('production' !== process.env.NODE_ENV) window.__RUNTIME_LOGGER__ = {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server Log 模块
|
|
3
3
|
*
|
|
4
|
-
* 通过
|
|
4
|
+
* 通过 SSE (Server-Sent Events) 方式获取服务端日志并转发给父窗口
|
|
5
|
+
*
|
|
6
|
+
* 变更说明 (2026-01-04):
|
|
7
|
+
* - 从 HTTP 轮询改为 SSE,降低延迟和网络压力
|
|
8
|
+
* - ServerLogPoller 已弃用,使用 ServerLogSSEClient 替代
|
|
5
9
|
*/
|
|
10
|
+
export { ServerLogSSEClient } from './sse-client';
|
|
11
|
+
export type { ServerLogSSEClientOptions } from './sse-client';
|
|
6
12
|
export { ServerLogPoller } from './poller';
|
|
7
13
|
export type { ServerLogPollerOptions } from './poller';
|
|
8
|
-
export type { ServerLog, ServerLogLevel, ServerLogSource, ServerLogMeta, ServerLogPostMessage, ClientToServerMessage, ServerToClientMessage, } from './types';
|
|
9
|
-
export {
|
|
14
|
+
export type { ServerLog, ServerLogLevel, ServerLogSource, ServerLogMeta, ServerLogPostMessage, ServerLogControlMessage, ClientToServerMessage, ServerToClientMessage, } from './types';
|
|
15
|
+
export { ServerLogSSEClient as default } from './sse-client';
|
package/lib/server-log/index.js
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Log SSE Client (SSE 版本)
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 通过 EventSource 连接后端 SSE 端点 (/dev/logs/server-logs/stream)
|
|
6
|
+
* 2. 实时接收服务端日志
|
|
7
|
+
* 3. 将接收到的日志通过 postMessage 转发给父窗口 (miaoda)
|
|
8
|
+
* 4. 管理连接状态并通知父窗口
|
|
9
|
+
* 5. 支持通过 postMessage 从父窗口控制 (pause/resume/stop)
|
|
10
|
+
*
|
|
11
|
+
* 优势:
|
|
12
|
+
* - 相比 HTTP 轮询,延迟更低(~10ms vs 0-2000ms)
|
|
13
|
+
* - 无需持续请求,减少网络压力
|
|
14
|
+
* - EventSource 自带自动重连机制
|
|
15
|
+
* - 支持暂停/恢复,可按需关闭
|
|
16
|
+
*/
|
|
17
|
+
export interface ServerLogSSEClientOptions {
|
|
18
|
+
/**
|
|
19
|
+
* 后端服务器 URL
|
|
20
|
+
* @example 'http://localhost:3000'
|
|
21
|
+
*/
|
|
22
|
+
serverUrl: string;
|
|
23
|
+
/**
|
|
24
|
+
* SSE 端点路径
|
|
25
|
+
* @default '/dev/logs/server-logs/stream'
|
|
26
|
+
*/
|
|
27
|
+
sseEndpoint?: string;
|
|
28
|
+
/**
|
|
29
|
+
* 是否启用调试日志
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
debug?: boolean;
|
|
33
|
+
}
|
|
34
|
+
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
35
|
+
export declare class ServerLogSSEClient {
|
|
36
|
+
private eventSource;
|
|
37
|
+
private status;
|
|
38
|
+
private isPaused;
|
|
39
|
+
private options;
|
|
40
|
+
private messageListener;
|
|
41
|
+
private reconnectCount;
|
|
42
|
+
constructor(options: ServerLogSSEClientOptions);
|
|
43
|
+
/**
|
|
44
|
+
* 启动 SSE 连接
|
|
45
|
+
*/
|
|
46
|
+
start(): void;
|
|
47
|
+
/**
|
|
48
|
+
* 停止 SSE 连接
|
|
49
|
+
*/
|
|
50
|
+
stop(): void;
|
|
51
|
+
/**
|
|
52
|
+
* 暂停接收日志(断开 SSE 连接)
|
|
53
|
+
*/
|
|
54
|
+
pause(): void;
|
|
55
|
+
/**
|
|
56
|
+
* 恢复接收日志(重新建立 SSE 连接)
|
|
57
|
+
*/
|
|
58
|
+
resume(): void;
|
|
59
|
+
/**
|
|
60
|
+
* 重新连接(用于断开后手动重连)
|
|
61
|
+
*/
|
|
62
|
+
reconnect(): void;
|
|
63
|
+
/**
|
|
64
|
+
* 获取当前连接状态
|
|
65
|
+
*/
|
|
66
|
+
getStatus(): ConnectionStatus;
|
|
67
|
+
/**
|
|
68
|
+
* 是否暂停中
|
|
69
|
+
*/
|
|
70
|
+
getIsPaused(): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* 设置 EventSource 连接
|
|
73
|
+
*/
|
|
74
|
+
private setupEventSource;
|
|
75
|
+
/**
|
|
76
|
+
* 清理 EventSource
|
|
77
|
+
*/
|
|
78
|
+
private cleanupEventSource;
|
|
79
|
+
/**
|
|
80
|
+
* 设置父窗口控制消息监听器
|
|
81
|
+
*/
|
|
82
|
+
private setupParentMessageListener;
|
|
83
|
+
/**
|
|
84
|
+
* 清理消息监听器
|
|
85
|
+
*/
|
|
86
|
+
private cleanupMessageListener;
|
|
87
|
+
/**
|
|
88
|
+
* 转发日志到父窗口
|
|
89
|
+
*/
|
|
90
|
+
private forwardLog;
|
|
91
|
+
/**
|
|
92
|
+
* 更新连接状态并通知父窗口
|
|
93
|
+
*/
|
|
94
|
+
private updateStatus;
|
|
95
|
+
/**
|
|
96
|
+
* 发送消息到父窗口
|
|
97
|
+
*/
|
|
98
|
+
private postToParent;
|
|
99
|
+
/**
|
|
100
|
+
* 调试日志
|
|
101
|
+
*/
|
|
102
|
+
private log;
|
|
103
|
+
/**
|
|
104
|
+
* 错误日志
|
|
105
|
+
*/
|
|
106
|
+
private error;
|
|
107
|
+
}
|
|
108
|
+
export {};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
class ServerLogSSEClient {
|
|
2
|
+
eventSource = null;
|
|
3
|
+
status = 'disconnected';
|
|
4
|
+
isPaused = false;
|
|
5
|
+
options;
|
|
6
|
+
messageListener = null;
|
|
7
|
+
reconnectCount = 0;
|
|
8
|
+
constructor(options){
|
|
9
|
+
this.options = {
|
|
10
|
+
serverUrl: options.serverUrl,
|
|
11
|
+
sseEndpoint: (process.env.CLIENT_BASE_PATH || '') + (options.sseEndpoint || '/dev/logs/server-logs/stream'),
|
|
12
|
+
debug: options.debug ?? false
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
start() {
|
|
16
|
+
if (this.eventSource) return void this.log('SSE client already started');
|
|
17
|
+
this.log('Starting SSE client...', {
|
|
18
|
+
serverUrl: this.options.serverUrl,
|
|
19
|
+
sseEndpoint: this.options.sseEndpoint
|
|
20
|
+
});
|
|
21
|
+
this.updateStatus('connecting');
|
|
22
|
+
this.setupEventSource();
|
|
23
|
+
this.setupParentMessageListener();
|
|
24
|
+
}
|
|
25
|
+
stop() {
|
|
26
|
+
this.log('Stopping SSE client...');
|
|
27
|
+
this.cleanupEventSource();
|
|
28
|
+
this.cleanupMessageListener();
|
|
29
|
+
this.updateStatus('disconnected');
|
|
30
|
+
}
|
|
31
|
+
pause() {
|
|
32
|
+
if (this.isPaused) return;
|
|
33
|
+
this.isPaused = true;
|
|
34
|
+
this.cleanupEventSource();
|
|
35
|
+
this.log('Paused - SSE connection closed');
|
|
36
|
+
}
|
|
37
|
+
resume() {
|
|
38
|
+
if (!this.isPaused) return;
|
|
39
|
+
this.isPaused = false;
|
|
40
|
+
this.setupEventSource();
|
|
41
|
+
this.log('Resumed - SSE connection reopened');
|
|
42
|
+
}
|
|
43
|
+
reconnect() {
|
|
44
|
+
this.log('Reconnecting...');
|
|
45
|
+
this.cleanupEventSource();
|
|
46
|
+
this.isPaused = false;
|
|
47
|
+
this.setupEventSource();
|
|
48
|
+
}
|
|
49
|
+
getStatus() {
|
|
50
|
+
return this.status;
|
|
51
|
+
}
|
|
52
|
+
getIsPaused() {
|
|
53
|
+
return this.isPaused;
|
|
54
|
+
}
|
|
55
|
+
setupEventSource() {
|
|
56
|
+
this.reconnectCount = 0;
|
|
57
|
+
const url = `${this.options.serverUrl}${this.options.sseEndpoint}`;
|
|
58
|
+
this.log('Connecting to SSE endpoint:', url);
|
|
59
|
+
try {
|
|
60
|
+
this.eventSource = new EventSource(url);
|
|
61
|
+
this.eventSource.addEventListener('connected', (event)=>{
|
|
62
|
+
try {
|
|
63
|
+
const data = JSON.parse(event.data);
|
|
64
|
+
this.log('Connected to SSE server', data);
|
|
65
|
+
this.updateStatus('connected');
|
|
66
|
+
} catch (e) {
|
|
67
|
+
this.error('Failed to parse connected event', e);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
this.eventSource.addEventListener('log', (event)=>{
|
|
71
|
+
if (this.isPaused) return;
|
|
72
|
+
try {
|
|
73
|
+
const log = JSON.parse(event.data);
|
|
74
|
+
this.forwardLog(log);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
this.error('Failed to parse log event', e);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
this.eventSource.addEventListener('heartbeat', (event)=>{
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(event.data);
|
|
82
|
+
this.log('Heartbeat received', data);
|
|
83
|
+
} catch (e) {}
|
|
84
|
+
});
|
|
85
|
+
this.eventSource.onmessage = (event)=>{
|
|
86
|
+
this.log('Received generic message:', event.data);
|
|
87
|
+
};
|
|
88
|
+
this.eventSource.onerror = (event)=>{
|
|
89
|
+
this.error('SSE connection error', event);
|
|
90
|
+
this.reconnectCount++;
|
|
91
|
+
if (this.reconnectCount > 1) {
|
|
92
|
+
this.log('Max reconnect attempts reached, closing connection');
|
|
93
|
+
this.cleanupEventSource();
|
|
94
|
+
this.updateStatus('error');
|
|
95
|
+
this.postToParent({
|
|
96
|
+
type: 'SERVER_LOG_CONNECTION',
|
|
97
|
+
status: 'error',
|
|
98
|
+
error: 'SSE connection failed after retry'
|
|
99
|
+
});
|
|
100
|
+
} else if (this.eventSource?.readyState === EventSource.CONNECTING) {
|
|
101
|
+
this.log(`Reconnecting... attempt ${this.reconnectCount}`);
|
|
102
|
+
this.updateStatus('connecting');
|
|
103
|
+
} else if (this.eventSource?.readyState === EventSource.CLOSED) {
|
|
104
|
+
this.updateStatus('error');
|
|
105
|
+
this.postToParent({
|
|
106
|
+
type: 'SERVER_LOG_CONNECTION',
|
|
107
|
+
status: 'error',
|
|
108
|
+
error: 'SSE connection closed'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
this.eventSource.onopen = ()=>{
|
|
113
|
+
this.log('SSE connection opened');
|
|
114
|
+
this.reconnectCount = 0;
|
|
115
|
+
};
|
|
116
|
+
} catch (e) {
|
|
117
|
+
this.error('Failed to create EventSource', e);
|
|
118
|
+
this.updateStatus('error');
|
|
119
|
+
this.postToParent({
|
|
120
|
+
type: 'SERVER_LOG_CONNECTION',
|
|
121
|
+
status: 'error',
|
|
122
|
+
error: e instanceof Error ? e.message : String(e)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
cleanupEventSource() {
|
|
127
|
+
if (this.eventSource) {
|
|
128
|
+
this.eventSource.close();
|
|
129
|
+
this.eventSource = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
setupParentMessageListener() {
|
|
133
|
+
this.messageListener = (event)=>{
|
|
134
|
+
const data = event.data;
|
|
135
|
+
if (data?.type !== 'SERVER_LOG_CONTROL') return;
|
|
136
|
+
this.log('Received control message:', data);
|
|
137
|
+
switch(data.action){
|
|
138
|
+
case 'pause':
|
|
139
|
+
this.pause();
|
|
140
|
+
break;
|
|
141
|
+
case 'resume':
|
|
142
|
+
this.resume();
|
|
143
|
+
break;
|
|
144
|
+
case 'stop':
|
|
145
|
+
this.stop();
|
|
146
|
+
break;
|
|
147
|
+
case 'reconnect':
|
|
148
|
+
this.reconnect();
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
this.log('Unknown control action:', data.action);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
window.addEventListener('message', this.messageListener);
|
|
155
|
+
}
|
|
156
|
+
cleanupMessageListener() {
|
|
157
|
+
if (this.messageListener) {
|
|
158
|
+
window.removeEventListener('message', this.messageListener);
|
|
159
|
+
this.messageListener = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
forwardLog(log) {
|
|
163
|
+
try {
|
|
164
|
+
this.log('Forwarding log to parent window', {
|
|
165
|
+
type: 'SERVER_LOG',
|
|
166
|
+
logId: log.id,
|
|
167
|
+
level: log.level,
|
|
168
|
+
tags: log.tags
|
|
169
|
+
});
|
|
170
|
+
this.postToParent({
|
|
171
|
+
type: 'SERVER_LOG',
|
|
172
|
+
payload: JSON.stringify(log)
|
|
173
|
+
});
|
|
174
|
+
this.log('Log forwarded successfully');
|
|
175
|
+
} catch (e) {
|
|
176
|
+
this.error('Failed to forward log', e);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
updateStatus(status) {
|
|
180
|
+
const previousStatus = this.status;
|
|
181
|
+
this.status = status;
|
|
182
|
+
if (previousStatus !== status) {
|
|
183
|
+
this.log(`Status changed: ${previousStatus} → ${status}`);
|
|
184
|
+
this.postToParent({
|
|
185
|
+
type: 'SERVER_LOG_CONNECTION',
|
|
186
|
+
status
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
postToParent(message) {
|
|
191
|
+
if (window.parent === window) return;
|
|
192
|
+
try {
|
|
193
|
+
window.parent.postMessage(message, '*');
|
|
194
|
+
} catch (e) {
|
|
195
|
+
this.error('postMessage error', e);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
log(message, data) {
|
|
199
|
+
const enableLog = 'true' === localStorage.getItem('debug_server_log_sse');
|
|
200
|
+
if (this.options.debug && enableLog) if (data) console.log(`[ServerLogSSEClient] ${message}`, data);
|
|
201
|
+
else console.log(`[ServerLogSSEClient] ${message}`);
|
|
202
|
+
}
|
|
203
|
+
error(message, error) {
|
|
204
|
+
const enableLog = 'true' === localStorage.getItem('debug_server_log_sse');
|
|
205
|
+
if (enableLog) if (error) console.error(`[ServerLogSSEClient] ${message}`, error);
|
|
206
|
+
else console.error(`[ServerLogSSEClient] ${message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export { ServerLogSSEClient };
|
|
@@ -164,3 +164,12 @@ export type ServerLogPostMessage = {
|
|
|
164
164
|
} | {
|
|
165
165
|
type: 'SERVER_LOG_CLEARED';
|
|
166
166
|
};
|
|
167
|
+
/**
|
|
168
|
+
* PostMessage 控制类型(parent → iframe)
|
|
169
|
+
*
|
|
170
|
+
* 父窗口可以通过 postMessage 控制 iframe 内的 SSE 客户端
|
|
171
|
+
*/
|
|
172
|
+
export type ServerLogControlMessage = {
|
|
173
|
+
type: 'SERVER_LOG_CONTROL';
|
|
174
|
+
action: 'pause' | 'resume' | 'stop' | 'reconnect';
|
|
175
|
+
};
|
package/lib/utils/axiosConfig.js
CHANGED
|
@@ -52,7 +52,7 @@ async function logResponse(ok, responseOrError) {
|
|
|
52
52
|
logTraceID
|
|
53
53
|
};
|
|
54
54
|
if (stacktrace) logMeta.stacktrace = stacktrace;
|
|
55
|
-
logger.log({
|
|
55
|
+
if ('development' === process.env.NODE_ENV) logger.log({
|
|
56
56
|
level: ok,
|
|
57
57
|
args: [
|
|
58
58
|
parts.join(''),
|
|
@@ -131,15 +131,17 @@ function initAxiosConfig(axiosInstance) {
|
|
|
131
131
|
if (!axiosInstance) axiosInstance = axios;
|
|
132
132
|
axiosInstance.interceptors.request.use((config)=>{
|
|
133
133
|
const requestUUID = crypto.randomUUID();
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
if ('production' !== process.env.NODE_ENV) {
|
|
135
|
+
const stacktrace = getStacktrace();
|
|
136
|
+
requestStacktraceMap.set(requestUUID, stacktrace);
|
|
137
|
+
}
|
|
136
138
|
config._requestUUID = requestUUID;
|
|
137
139
|
config._startTime = Date.now();
|
|
138
140
|
const csrfToken = window.csrfToken;
|
|
139
141
|
if (csrfToken) config.headers['X-Suda-Csrf-Token'] = csrfToken;
|
|
140
142
|
return config;
|
|
141
143
|
}, (error)=>Promise.reject(error));
|
|
142
|
-
axiosInstance.interceptors.response.use((response)=>{
|
|
144
|
+
'production' !== process.env.NODE_ENV && axiosInstance.interceptors.response.use((response)=>{
|
|
143
145
|
logResponse('success', response);
|
|
144
146
|
return response;
|
|
145
147
|
}, (error)=>{
|
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import { initAxiosConfig } from "./axiosConfig.js";
|
|
3
3
|
let axiosInstance;
|
|
4
|
+
function showToast(message, duration = 3000) {
|
|
5
|
+
return new Promise((resolve)=>{
|
|
6
|
+
const toast = document.createElement('div');
|
|
7
|
+
const iconSvg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
8
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1.333a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 8 1.333ZM8 10a.667.667 0 1 0 0 1.333A.667.667 0 0 0 8 10Zm0-5.333a.667.667 0 0 0-.667.666v3.334a.667.667 0 0 0 1.334 0V5.333A.667.667 0 0 0 8 4.667Z" fill="#ff811a"/>
|
|
9
|
+
</svg>`;
|
|
10
|
+
const iconWrapper = document.createElement('span');
|
|
11
|
+
iconWrapper.innerHTML = iconSvg;
|
|
12
|
+
Object.assign(iconWrapper.style, {
|
|
13
|
+
display: 'flex',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
marginRight: '8px',
|
|
16
|
+
flexShrink: '0'
|
|
17
|
+
});
|
|
18
|
+
const textWrapper = document.createElement('span');
|
|
19
|
+
textWrapper.textContent = message;
|
|
20
|
+
toast.appendChild(iconWrapper);
|
|
21
|
+
toast.appendChild(textWrapper);
|
|
22
|
+
Object.assign(toast.style, {
|
|
23
|
+
position: 'fixed',
|
|
24
|
+
top: '20%',
|
|
25
|
+
left: '50%',
|
|
26
|
+
transform: 'translate(-50%, -50%)',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
padding: '12px 16px',
|
|
30
|
+
backgroundColor: '#fff',
|
|
31
|
+
color: '#1f2329',
|
|
32
|
+
border: '1px solid #dee0e3',
|
|
33
|
+
borderRadius: '6px',
|
|
34
|
+
fontSize: '14px',
|
|
35
|
+
lineHeight: '1.5',
|
|
36
|
+
zIndex: '99999',
|
|
37
|
+
maxWidth: '80vw',
|
|
38
|
+
wordBreak: 'break-word',
|
|
39
|
+
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.03), 0 3px 6px rgba(0, 0, 0, 0.05), 0 6px 18px rgba(0, 0, 0, 0.03)',
|
|
40
|
+
opacity: '0',
|
|
41
|
+
transition: 'opacity 0.3s ease'
|
|
42
|
+
});
|
|
43
|
+
document.body.appendChild(toast);
|
|
44
|
+
requestAnimationFrame(()=>{
|
|
45
|
+
toast.style.opacity = '1';
|
|
46
|
+
});
|
|
47
|
+
setTimeout(()=>{
|
|
48
|
+
toast.style.opacity = '0';
|
|
49
|
+
setTimeout(()=>{
|
|
50
|
+
document.body.removeChild(toast);
|
|
51
|
+
resolve();
|
|
52
|
+
}, 300);
|
|
53
|
+
}, duration);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
4
56
|
function getAxiosForBackend() {
|
|
5
57
|
if (!axiosInstance) {
|
|
6
58
|
axiosInstance = axios.create({
|
|
@@ -9,7 +61,9 @@ function getAxiosForBackend() {
|
|
|
9
61
|
axiosInstance.interceptors.response.use(null, (err)=>{
|
|
10
62
|
if (err.config.meta?.autoJumpToLogin !== false) {
|
|
11
63
|
const loginUrl = err.response?.headers?.['x-login-url'];
|
|
12
|
-
if (loginUrl)
|
|
64
|
+
if (loginUrl) showToast('需要登录后才能执行操作,将自动跳转登录页', 3000).then(()=>{
|
|
65
|
+
window.location.href = loginUrl;
|
|
66
|
+
});
|
|
13
67
|
}
|
|
14
68
|
return Promise.reject(err);
|
|
15
69
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function safeStringify(obj) {
|
|
2
|
+
const seen = new Set();
|
|
3
|
+
try {
|
|
4
|
+
return JSON.stringify(obj, (key, value)=>{
|
|
5
|
+
if ('object' == typeof value && null !== value) {
|
|
6
|
+
if (seen.has(value)) return '[Circular]';
|
|
7
|
+
seen.add(value);
|
|
8
|
+
}
|
|
9
|
+
if ('bigint' == typeof value) return value.toString();
|
|
10
|
+
if (value instanceof Date) return value.toISOString();
|
|
11
|
+
if (value instanceof Map) return Object.fromEntries(value);
|
|
12
|
+
if (value instanceof Set) return Array.from(value);
|
|
13
|
+
if (void 0 === value) return 'undefined';
|
|
14
|
+
if ('symbol' == typeof value) return value.toString();
|
|
15
|
+
return value;
|
|
16
|
+
});
|
|
17
|
+
} catch {
|
|
18
|
+
return '';
|
|
19
|
+
} finally{
|
|
20
|
+
seen.clear();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function processLogParams(args) {
|
|
24
|
+
if (1 === args.length) return safeStringify(args[0]);
|
|
25
|
+
const obj = {};
|
|
26
|
+
for(let i = 0; i < args.length; i++)obj[i.toString()] = args[i];
|
|
27
|
+
return safeStringify(obj);
|
|
28
|
+
}
|
|
29
|
+
function mapLogLevel(level) {
|
|
30
|
+
switch(level){
|
|
31
|
+
case "error":
|
|
32
|
+
return "ERROR";
|
|
33
|
+
case "info":
|
|
34
|
+
case "success":
|
|
35
|
+
return "INFO";
|
|
36
|
+
case "warn":
|
|
37
|
+
return "WARN";
|
|
38
|
+
default:
|
|
39
|
+
return "INFO";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export { mapLogLevel, processLogParams, safeStringify };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/client-toolkit",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.11",
|
|
4
4
|
"types": "./lib/index.d.ts",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"files": [
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"dev": "rslib build --watch",
|
|
73
73
|
"format": "biome format --write",
|
|
74
74
|
"storybook": "storybook dev",
|
|
75
|
-
"test": "
|
|
75
|
+
"test": "vitest",
|
|
76
76
|
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
|
|
77
77
|
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
|
|
78
78
|
"prepublishOnly": "npm run build && node scripts/replace-workspace-alias.js"
|
|
@@ -80,9 +80,10 @@
|
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@ant-design/colors": "^7.2.1",
|
|
82
82
|
"@ant-design/cssinjs": "^1.24.0",
|
|
83
|
-
"@data-loom/js": "
|
|
84
|
-
"@lark-apaas/client-capability": "0.
|
|
85
|
-
"@lark-apaas/miaoda-inspector": "^1.0.
|
|
83
|
+
"@data-loom/js": "0.4.4-alpha.6",
|
|
84
|
+
"@lark-apaas/client-capability": "^0.1.2",
|
|
85
|
+
"@lark-apaas/miaoda-inspector": "^1.0.8",
|
|
86
|
+
"@lark-apaas/observable-web": "^1.0.0",
|
|
86
87
|
"@radix-ui/react-avatar": "^1.1.10",
|
|
87
88
|
"@radix-ui/react-popover": "^1.1.15",
|
|
88
89
|
"@radix-ui/react-slot": "^1.2.3",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { BatchLogger, batchLogInfo, initBatchLogger } from "../batch-logger.js";
|
|
3
|
-
var __webpack_require__ = {};
|
|
4
|
-
(()=>{
|
|
5
|
-
__webpack_require__.g = (()=>{
|
|
6
|
-
if ('object' == typeof globalThis) return globalThis;
|
|
7
|
-
try {
|
|
8
|
-
return this || new Function('return this')();
|
|
9
|
-
} catch (e) {
|
|
10
|
-
if ('object' == typeof window) return window;
|
|
11
|
-
}
|
|
12
|
-
})();
|
|
13
|
-
})();
|
|
14
|
-
vi.mock('node-fetch', ()=>({
|
|
15
|
-
default: vi.fn()
|
|
16
|
-
}));
|
|
17
|
-
const mockConsole = {
|
|
18
|
-
debug: vi.fn(),
|
|
19
|
-
info: vi.fn(),
|
|
20
|
-
warn: vi.fn(),
|
|
21
|
-
error: vi.fn(),
|
|
22
|
-
log: vi.fn()
|
|
23
|
-
};
|
|
24
|
-
describe('BatchLogger', ()=>{
|
|
25
|
-
let batchLogger;
|
|
26
|
-
let mockFetch;
|
|
27
|
-
beforeEach(()=>{
|
|
28
|
-
vi.clearAllMocks();
|
|
29
|
-
mockFetch = vi.fn();
|
|
30
|
-
__webpack_require__.g.fetch = mockFetch;
|
|
31
|
-
const mockResponse = new Response(JSON.stringify({
|
|
32
|
-
success: true
|
|
33
|
-
}), {
|
|
34
|
-
status: 200,
|
|
35
|
-
statusText: 'OK',
|
|
36
|
-
headers: {
|
|
37
|
-
'Content-Type': 'application/json'
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
mockFetch.mockResolvedValue(mockResponse);
|
|
41
|
-
if ('undefined' == typeof window) __webpack_require__.g.window = void 0;
|
|
42
|
-
});
|
|
43
|
-
afterEach(async ()=>{
|
|
44
|
-
if (batchLogger) await batchLogger.destroy();
|
|
45
|
-
});
|
|
46
|
-
afterAll(()=>{
|
|
47
|
-
vi.restoreAllMocks();
|
|
48
|
-
});
|
|
49
|
-
describe('Basic Functionality', ()=>{
|
|
50
|
-
it('should create a BatchLogger instance', ()=>{
|
|
51
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
52
|
-
userId: 'user123',
|
|
53
|
-
tenantId: 'tenant456',
|
|
54
|
-
appId: 'app789'
|
|
55
|
-
});
|
|
56
|
-
expect(batchLogger).toBeInstanceOf(BatchLogger);
|
|
57
|
-
});
|
|
58
|
-
it('should add logs to queue', ()=>{
|
|
59
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
60
|
-
userId: 'user123',
|
|
61
|
-
tenantId: 'tenant456',
|
|
62
|
-
appId: 'app789'
|
|
63
|
-
});
|
|
64
|
-
batchLogger.batchLog('info', 'Test message', 'test-source');
|
|
65
|
-
expect(batchLogger.getQueueSize()).toBe(1);
|
|
66
|
-
});
|
|
67
|
-
it('should respect batch size limit', async ()=>{
|
|
68
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
69
|
-
userId: 'user123',
|
|
70
|
-
tenantId: 'tenant456',
|
|
71
|
-
appId: 'app789',
|
|
72
|
-
sizeThreshold: 3,
|
|
73
|
-
flushInterval: 10000
|
|
74
|
-
});
|
|
75
|
-
for(let i = 0; i < 5; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
76
|
-
await new Promise((resolve)=>setTimeout(resolve, 200));
|
|
77
|
-
expect(mockFetch).toHaveBeenCalled();
|
|
78
|
-
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
79
|
-
expect(batchLogger.getQueueSize()).toBeLessThanOrEqual(2);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
describe('Log Preservation', ()=>{
|
|
83
|
-
it('should preserve all logs without merging', ()=>{
|
|
84
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
85
|
-
userId: 'user123',
|
|
86
|
-
tenantId: 'tenant456',
|
|
87
|
-
appId: 'app789'
|
|
88
|
-
});
|
|
89
|
-
for(let i = 0; i < 5; i++)batchLogger.batchLog('error', 'Same error message');
|
|
90
|
-
expect(batchLogger.getQueueSize()).toBe(5);
|
|
91
|
-
});
|
|
92
|
-
it('should handle different log levels independently', ()=>{
|
|
93
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
94
|
-
userId: 'user123',
|
|
95
|
-
tenantId: 'tenant456',
|
|
96
|
-
appId: 'app789'
|
|
97
|
-
});
|
|
98
|
-
batchLogger.batchLog('info', 'Test message');
|
|
99
|
-
batchLogger.batchLog('info', 'Test message');
|
|
100
|
-
batchLogger.batchLog('warn', 'Test message');
|
|
101
|
-
batchLogger.batchLog('warn', 'Test message');
|
|
102
|
-
expect(batchLogger.getQueueSize()).toBe(4);
|
|
103
|
-
});
|
|
104
|
-
it('should handle large volumes of logs efficiently', ()=>{
|
|
105
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
106
|
-
userId: 'user123',
|
|
107
|
-
tenantId: 'tenant456',
|
|
108
|
-
appId: 'app789',
|
|
109
|
-
sizeThreshold: 1000,
|
|
110
|
-
flushInterval: 10000
|
|
111
|
-
});
|
|
112
|
-
for(let i = 0; i < 200; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
113
|
-
expect(batchLogger.getQueueSize()).toBe(200);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('Retry Mechanism', ()=>{
|
|
117
|
-
it('should retry failed requests', async ()=>{
|
|
118
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
119
|
-
userId: 'user123',
|
|
120
|
-
tenantId: 'tenant456',
|
|
121
|
-
appId: 'app789',
|
|
122
|
-
maxRetries: 2,
|
|
123
|
-
retryDelay: 100,
|
|
124
|
-
flushInterval: 10000
|
|
125
|
-
});
|
|
126
|
-
const successResponse = new Response(JSON.stringify({
|
|
127
|
-
success: true
|
|
128
|
-
}), {
|
|
129
|
-
status: 200,
|
|
130
|
-
statusText: 'OK',
|
|
131
|
-
headers: {
|
|
132
|
-
'Content-Type': 'application/json'
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
mockFetch.mockRejectedValueOnce(new Error('Network error')).mockRejectedValueOnce(new Error('Network error')).mockResolvedValueOnce(successResponse);
|
|
136
|
-
batchLogger.batchLog('info', 'Test message');
|
|
137
|
-
await batchLogger.flush();
|
|
138
|
-
expect(mockFetch).toHaveBeenCalled();
|
|
139
|
-
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(3);
|
|
140
|
-
});
|
|
141
|
-
it('should handle permanent failures', async ()=>{
|
|
142
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
143
|
-
userId: 'user123',
|
|
144
|
-
tenantId: 'tenant456',
|
|
145
|
-
appId: 'app789',
|
|
146
|
-
maxRetries: 1,
|
|
147
|
-
retryDelay: 100,
|
|
148
|
-
flushInterval: 10000
|
|
149
|
-
});
|
|
150
|
-
mockFetch.mockRejectedValue(new Error('Permanent error'));
|
|
151
|
-
batchLogger.batchLog('info', 'Test message');
|
|
152
|
-
await batchLogger.flush();
|
|
153
|
-
expect(mockConsole.warn).toHaveBeenCalled();
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
describe('Auto Flush', ()=>{
|
|
157
|
-
it('should auto flush based on interval', async ()=>{
|
|
158
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
159
|
-
userId: 'user123',
|
|
160
|
-
tenantId: 'tenant456',
|
|
161
|
-
appId: 'app789',
|
|
162
|
-
flushInterval: 1000,
|
|
163
|
-
sizeThreshold: 100
|
|
164
|
-
});
|
|
165
|
-
for(let i = 0; i < 3; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
166
|
-
await new Promise((resolve)=>setTimeout(resolve, 1200));
|
|
167
|
-
expect(mockFetch).toHaveBeenCalled();
|
|
168
|
-
expect(batchLogger.getQueueSize()).toBe(0);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
describe('Request Configuration', ()=>{
|
|
172
|
-
it('should include custom headers', async ()=>{
|
|
173
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
174
|
-
userId: 'user123',
|
|
175
|
-
tenantId: 'tenant456',
|
|
176
|
-
appId: 'app789',
|
|
177
|
-
endpoint: 'https://api.example.com/logs',
|
|
178
|
-
headers: {
|
|
179
|
-
Authorization: 'Bearer token123',
|
|
180
|
-
'X-Custom-Header': 'custom-value'
|
|
181
|
-
},
|
|
182
|
-
flushInterval: 1000
|
|
183
|
-
});
|
|
184
|
-
batchLogger.batchLog('info', 'Test message');
|
|
185
|
-
await batchLogger.flush();
|
|
186
|
-
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/logs', expect.objectContaining({
|
|
187
|
-
headers: expect.objectContaining({
|
|
188
|
-
Authorization: 'Bearer token123',
|
|
189
|
-
'X-Custom-Header': 'custom-value'
|
|
190
|
-
})
|
|
191
|
-
}));
|
|
192
|
-
});
|
|
193
|
-
it('should send correct batch format', async ()=>{
|
|
194
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
195
|
-
userId: 'user123',
|
|
196
|
-
tenantId: 'tenant456',
|
|
197
|
-
appId: 'app789',
|
|
198
|
-
flushInterval: 10000
|
|
199
|
-
});
|
|
200
|
-
batchLogger.batchLog('info', 'Test message', 'test-source');
|
|
201
|
-
await batchLogger.flush();
|
|
202
|
-
expect(mockFetch).toHaveBeenCalledWith('/dev/logs/collect-batch', expect.objectContaining({
|
|
203
|
-
method: 'POST',
|
|
204
|
-
headers: {
|
|
205
|
-
'Content-Type': 'application/json'
|
|
206
|
-
}
|
|
207
|
-
}));
|
|
208
|
-
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
209
|
-
expect(Array.isArray(callBody)).toBe(true);
|
|
210
|
-
expect(callBody).toHaveLength(1);
|
|
211
|
-
expect(callBody[0]).toMatchObject({
|
|
212
|
-
level: 'info',
|
|
213
|
-
message: 'Test message',
|
|
214
|
-
source: 'test-source',
|
|
215
|
-
user_id: 'user123',
|
|
216
|
-
tenant_id: 'tenant456',
|
|
217
|
-
app_id: 'app789'
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
describe('Global Instance Management', ()=>{
|
|
222
|
-
it('should initialize global batch logger instance', ()=>{
|
|
223
|
-
initBatchLogger(mockConsole, {
|
|
224
|
-
userId: 'user123',
|
|
225
|
-
tenantId: 'tenant456',
|
|
226
|
-
appId: 'app789'
|
|
227
|
-
});
|
|
228
|
-
expect(()=>batchLogInfo('info', 'Test message')).not.toThrow();
|
|
229
|
-
});
|
|
230
|
-
it('should handle batchLogInfo when no instance exists', ()=>{
|
|
231
|
-
expect(()=>batchLogInfo('info', 'Test message')).not.toThrow();
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
describe('Configuration Updates', ()=>{
|
|
235
|
-
it('should update configuration', ()=>{
|
|
236
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
237
|
-
userId: 'user123',
|
|
238
|
-
tenantId: 'tenant456',
|
|
239
|
-
appId: 'app789',
|
|
240
|
-
sizeThreshold: 10
|
|
241
|
-
});
|
|
242
|
-
batchLogger.updateConfig({
|
|
243
|
-
sizeThreshold: 20,
|
|
244
|
-
maxRetries: 5
|
|
245
|
-
});
|
|
246
|
-
for(let i = 0; i < 21; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
247
|
-
expect(batchLogger.getQueueSize()).toBe(1);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
describe('High Volume Log Processing', ()=>{
|
|
251
|
-
it('should handle large batch of logs without crashing', async ()=>{
|
|
252
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
253
|
-
userId: 'user123',
|
|
254
|
-
tenantId: 'tenant456',
|
|
255
|
-
appId: 'app789',
|
|
256
|
-
sizeThreshold: 50,
|
|
257
|
-
flushInterval: 1000
|
|
258
|
-
});
|
|
259
|
-
const largeBatchSize = 1000;
|
|
260
|
-
const startTime = Date.now();
|
|
261
|
-
for(let i = 0; i < largeBatchSize; i++)batchLogger.batchLog('info', `High volume log message ${i % 100}`, 'stress-test');
|
|
262
|
-
const endTime = Date.now();
|
|
263
|
-
expect(endTime - startTime).toBeLessThan(5000);
|
|
264
|
-
await new Promise((resolve)=>setTimeout(resolve, 2000));
|
|
265
|
-
expect(batchLogger.getQueueSize()).toBe(0);
|
|
266
|
-
expect(mockFetch).toHaveBeenCalled();
|
|
267
|
-
const callCount = mockFetch.mock.calls.length;
|
|
268
|
-
expect(callCount).toBeGreaterThan(0);
|
|
269
|
-
expect(callCount).toBeLessThanOrEqual(25);
|
|
270
|
-
});
|
|
271
|
-
it('should control request frequency under high load', async ()=>{
|
|
272
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
273
|
-
userId: 'user123',
|
|
274
|
-
tenantId: 'tenant456',
|
|
275
|
-
appId: 'app789',
|
|
276
|
-
sizeThreshold: 30,
|
|
277
|
-
flushInterval: 500
|
|
278
|
-
});
|
|
279
|
-
const requestsBefore = mockFetch.mock.calls.length;
|
|
280
|
-
for(let batch = 0; batch < 5; batch++){
|
|
281
|
-
for(let i = 0; i < 25; i++)batchLogger.batchLog('error', `Error message ${batch}`, 'high-frequency-test');
|
|
282
|
-
await new Promise((resolve)=>setTimeout(resolve, 50));
|
|
283
|
-
}
|
|
284
|
-
await new Promise((resolve)=>setTimeout(resolve, 1000));
|
|
285
|
-
const totalRequests = mockFetch.mock.calls.length - requestsBefore;
|
|
286
|
-
expect(totalRequests).toBeGreaterThan(0);
|
|
287
|
-
expect(totalRequests).toBeLessThanOrEqual(10);
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
describe('Edge Cases', ()=>{
|
|
291
|
-
it('should handle empty flush', async ()=>{
|
|
292
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
293
|
-
userId: 'user123',
|
|
294
|
-
tenantId: 'tenant456',
|
|
295
|
-
appId: 'app789'
|
|
296
|
-
});
|
|
297
|
-
await batchLogger.flush();
|
|
298
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
299
|
-
});
|
|
300
|
-
it('should handle concurrent flush calls safely', async ()=>{
|
|
301
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
302
|
-
userId: 'user123',
|
|
303
|
-
tenantId: 'tenant456',
|
|
304
|
-
appId: 'app789',
|
|
305
|
-
flushInterval: 10000
|
|
306
|
-
});
|
|
307
|
-
for(let i = 0; i < 20; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
308
|
-
const flushPromises = [];
|
|
309
|
-
for(let i = 0; i < 10; i++)flushPromises.push(batchLogger.flush());
|
|
310
|
-
await Promise.all(flushPromises);
|
|
311
|
-
expect(batchLogger.getQueueSize()).toBe(0);
|
|
312
|
-
expect(mockFetch.mock.calls.length).toBeGreaterThan(0);
|
|
313
|
-
expect(mockFetch.mock.calls.length).toBeLessThanOrEqual(3);
|
|
314
|
-
});
|
|
315
|
-
it('should ensure no logs remain after destroy', async ()=>{
|
|
316
|
-
mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
317
|
-
success: true
|
|
318
|
-
}), {
|
|
319
|
-
status: 200,
|
|
320
|
-
statusText: 'OK',
|
|
321
|
-
headers: {
|
|
322
|
-
'Content-Type': 'application/json'
|
|
323
|
-
}
|
|
324
|
-
}));
|
|
325
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
326
|
-
userId: 'user123',
|
|
327
|
-
tenantId: 'tenant456',
|
|
328
|
-
appId: 'app789',
|
|
329
|
-
flushInterval: 10000,
|
|
330
|
-
sizeThreshold: 1000
|
|
331
|
-
});
|
|
332
|
-
for(let i = 0; i < 50; i++){
|
|
333
|
-
batchLogger.batchLog('info', `Test message info ${i}`);
|
|
334
|
-
batchLogger.batchLog('warn', `Warning message warn ${i}`);
|
|
335
|
-
}
|
|
336
|
-
const queueSize = batchLogger.getQueueSize();
|
|
337
|
-
expect(queueSize).toBe(100);
|
|
338
|
-
const fetchCallsBefore = mockFetch.mock.calls.length;
|
|
339
|
-
await batchLogger.destroy();
|
|
340
|
-
const queueSizeAfter = batchLogger.getQueueSize();
|
|
341
|
-
const fetchCallsAfter = mockFetch.mock.calls.length;
|
|
342
|
-
expect(queueSizeAfter).toBe(0);
|
|
343
|
-
expect(fetchCallsAfter).toBeGreaterThan(fetchCallsBefore);
|
|
344
|
-
});
|
|
345
|
-
it('should handle environment detection without browser APIs', ()=>{
|
|
346
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
347
|
-
userId: 'user123',
|
|
348
|
-
tenantId: 'tenant456',
|
|
349
|
-
appId: 'app789'
|
|
350
|
-
});
|
|
351
|
-
batchLogger.batchLog('info', 'Test message');
|
|
352
|
-
expect(batchLogger.getQueueSize()).toBe(1);
|
|
353
|
-
});
|
|
354
|
-
it('should handle destroy method with pending logs', async ()=>{
|
|
355
|
-
batchLogger = new BatchLogger(mockConsole, {
|
|
356
|
-
userId: 'user123',
|
|
357
|
-
tenantId: 'tenant456',
|
|
358
|
-
appId: 'app789'
|
|
359
|
-
});
|
|
360
|
-
batchLogger.batchLog('info', 'Test message');
|
|
361
|
-
batchLogger.batchLog('warn', 'Warning message');
|
|
362
|
-
await batchLogger.destroy();
|
|
363
|
-
expect(batchLogger.getQueueSize()).toBe(0);
|
|
364
|
-
expect(mockFetch).toHaveBeenCalled();
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
});
|