@lark-apaas/client-toolkit 1.2.53-alpha.2 → 1.2.53-alpha.3

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.
@@ -8,8 +8,5 @@ export interface UniversalLinkProps extends Omit<React.AnchorHTMLAttributes<HTML
8
8
  * - 内部路由(/dashboard)→ react-router Link
9
9
  * - Hash 锚点(#section)→ <a>
10
10
  * - 外链(https://...)→ <a target="_blank">
11
- *
12
- * 预览态(iframe 内)时,通过 postMessage 通知父页面处理链接跳转,
13
- * 避免浏览器安全策略限制导致链接无法正常打开。
14
11
  */
15
12
  export declare const UniversalLink: React.ForwardRefExoticComponent<UniversalLinkProps & React.RefAttributes<HTMLAnchorElement>>;
@@ -1,62 +1,23 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import react from "react";
3
3
  import { Link } from "react-router-dom";
4
- import { isPreview } from "../../utils/utils.js";
5
- import { getParentOriginFromParams, submitPostMessage } from "../../utils/postMessage.js";
6
4
  function isInternalRoute(to) {
7
5
  return !to.startsWith('#') && !to.startsWith('http://') && !to.startsWith('https://') && !to.startsWith('//');
8
6
  }
9
7
  function isExternalLink(to) {
10
8
  return to.startsWith('http://') || to.startsWith('https://') || to.startsWith('//');
11
9
  }
12
- function isIframeUnderMainHost() {
13
- if ('undefined' == typeof window) return false;
14
- if (window.parent === window) return false;
15
- if (getParentOriginFromParams()) return true;
16
- const envOrigin = process.env.FORCE_FRAMEWORK_DOMAIN_MAIN;
17
- if (!envOrigin || !document.referrer) return false;
18
- try {
19
- return new URL(document.referrer).origin === envOrigin;
20
- } catch {
21
- return false;
22
- }
23
- }
24
- const UniversalLink_UniversalLink = /*#__PURE__*/ react.forwardRef(function({ to, onClick, ...props }, ref) {
25
- const isExternal = isExternalLink(to);
26
- const preview = (isPreview() || isIframeUnderMainHost()) && isExternal;
27
- if (preview) {
28
- const handlePreviewClick = (e)=>{
29
- e.preventDefault();
30
- onClick?.(e);
31
- submitPostMessage({
32
- type: 'OpenIframeLink',
33
- data: {
34
- href: to,
35
- external: true
36
- }
37
- });
38
- };
39
- return /*#__PURE__*/ jsx("a", {
40
- href: to,
41
- ref: ref,
42
- ...props,
43
- onClick: handlePreviewClick,
44
- target: props.target ?? '_blank',
45
- rel: props.rel ?? 'noopener noreferrer'
46
- });
47
- }
10
+ const UniversalLink_UniversalLink = /*#__PURE__*/ react.forwardRef(function({ to, ...props }, ref) {
48
11
  if (isInternalRoute(to)) return /*#__PURE__*/ jsx(Link, {
49
12
  to: to,
50
13
  ref: ref,
51
- ...props,
52
- onClick: onClick
14
+ ...props
53
15
  });
54
16
  return /*#__PURE__*/ jsx("a", {
55
17
  href: to,
56
18
  ref: ref,
57
19
  ...props,
58
- onClick: onClick,
59
- ...isExternal && {
20
+ ...isExternalLink(to) && {
60
21
  target: props.target ?? '_blank',
61
22
  rel: props.rel ?? 'noopener noreferrer'
62
23
  }
package/lib/auth.d.ts CHANGED
@@ -1,4 +1,2 @@
1
1
  export { AuthProvider, useAuth, Can, CanRole, useCan, AbilityContext, useUserPermissions, PermissionClient, ROLE_SUBJECT, } from '@lark-apaas/auth-sdk';
2
2
  export type { AuthProviderProps, PermissionApiResponse, PermissionApiConfig, PermissionPointData, AuthSdkConfig, } from '@lark-apaas/auth-sdk';
3
- export { authClient, AccountClient } from '@lark-apaas/auth-sdk';
4
- export type { AccountClient as AuthClient, AccountServiceResponse, AccountServiceError, SearchParams, User, UserResponse, SignInRedirectionOptions, UserBaseInfo, UserInfoResponse, Avatar, } from '@lark-apaas/auth-sdk';
package/lib/auth.js CHANGED
@@ -1,2 +1,2 @@
1
- import { AbilityContext, AccountClient, AuthProvider, Can, CanRole, PermissionClient, ROLE_SUBJECT, authClient, useAuth, useCan, useUserPermissions } from "@lark-apaas/auth-sdk";
2
- export { AbilityContext, AccountClient, AuthProvider, Can, CanRole, PermissionClient, ROLE_SUBJECT, authClient, useAuth, useCan, useUserPermissions };
1
+ import { AbilityContext, AuthProvider, Can, CanRole, PermissionClient, ROLE_SUBJECT, useAuth, useCan, useUserPermissions } from "@lark-apaas/auth-sdk";
2
+ export { AbilityContext, AuthProvider, Can, CanRole, PermissionClient, ROLE_SUBJECT, useAuth, useCan, useUserPermissions };
@@ -7,11 +7,9 @@ import IframeBridge from "./IframeBridge.js";
7
7
  import { Toaster } from "./sonner.js";
8
8
  import { PageHoc } from "./PageHoc.js";
9
9
  import { reportTeaEvent } from "./utils/tea.js";
10
- import { SafetyErrorBoundary } from "./safety-error-boundary.js";
11
10
  import { useAppInfo } from "../../hooks/index.js";
12
11
  import { TrackKey } from "../../types/tea.js";
13
12
  import { slardar } from "@lark-apaas/internal-slardar";
14
- import Watermark, { isDoubaoSource } from "./Watermark.js";
15
13
  import { getAppId } from "../../utils/getAppId.js";
16
14
  import { isNewPathEnabled } from "../../utils/apiPath.js";
17
15
  import QueryProvider from "../QueryProvider/index.js";
@@ -198,11 +196,9 @@ const AppContainer_AppContainer = (props)=>{
198
196
  };
199
197
  return /*#__PURE__*/ jsxs(Fragment, {
200
198
  children: [
201
- /*#__PURE__*/ jsx(SafetyErrorBoundary, {
202
- children: isDoubaoSource() ? /*#__PURE__*/ jsx(Watermark, {}) : /*#__PURE__*/ jsx(Suspense, {
203
- fallback: null,
204
- children: /*#__PURE__*/ jsx(Safety, {})
205
- })
199
+ /*#__PURE__*/ jsx(Suspense, {
200
+ fallback: null,
201
+ children: /*#__PURE__*/ jsx(Safety, {})
206
202
  }),
207
203
  /*#__PURE__*/ jsx(QueryProvider, {
208
204
  children: /*#__PURE__*/ jsx(ConfigProvider, {
@@ -2,32 +2,12 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
4
4
  import { getAppId } from "../../utils/getAppId.js";
5
- import { getEnv } from "../../utils/getParentOrigin.js";
6
5
  import { getCsrfToken } from "../../utils/getCsrfToken.js";
7
6
  import { isNewPathEnabled } from "../../utils/apiPath.js";
8
7
  import { useIsMobile } from "../../hooks/index.js";
9
8
  import { X } from "lucide-react";
10
9
  import { Sheet, SheetContent, SheetTrigger } from "../ui/drawer.js";
11
10
  import { t } from "../../locales/index.js";
12
- const ICON_FEEDBACK_URL = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/LMfspH/ljhwZthlaukjlkulzlp/miaoda-ui/icon_feedback_outlined.png';
13
- const REPORT_DOMAIN = {
14
- BOE: 'tns.feishu-boe.cn',
15
- PRE: 'tns.feishu-pre.cn',
16
- ONLINE: 'tns.feishu.cn'
17
- };
18
- const openReport = ()=>{
19
- const appId = getAppId();
20
- if (!appId) return;
21
- const params = JSON.stringify({
22
- scene: 'miaoda_app_report',
23
- entity_id: appId,
24
- entity_type: 'miaoda_app',
25
- extra: ''
26
- });
27
- const domain = REPORT_DOMAIN[getEnv()] ?? REPORT_DOMAIN.ONLINE;
28
- const url = `https://${domain}/cust/lark_report/?type=common&params=${encodeURIComponent(params)}&lang=zh-CN`;
29
- window.open(url, '_blank', 'noopener,noreferrer');
30
- };
31
11
  const Component = ()=>{
32
12
  const HasClosedKey = `miaoda-creatByMiaoda-has-closed-${getAppId()}`;
33
13
  const [visible, setVisible] = useState(!window.localStorage?.getItem(HasClosedKey));
@@ -149,21 +129,6 @@ const Component = ()=>{
149
129
  children: t('safety.ai.disclaimer')
150
130
  })
151
131
  ]
152
- }),
153
- /*#__PURE__*/ jsxs("div", {
154
- className: "self-stretch shrink-0 flex items-center gap-x-[6px] cursor-pointer",
155
- "data-custom-element": "safety-report",
156
- onClick: openReport,
157
- children: [
158
- /*#__PURE__*/ jsx("img", {
159
- src: ICON_FEEDBACK_URL,
160
- className: "shrink-0 w-[14px] h-[14px]"
161
- }),
162
- /*#__PURE__*/ jsx("p", {
163
- className: "shrink-0 m-0! text-[#646A73] text-sm underline underline-offset-2",
164
- children: t('safety.report')
165
- })
166
- ]
167
132
  })
168
133
  ]
169
134
  }),
@@ -283,21 +248,6 @@ const Component = ()=>{
283
248
  children: t('safety.ai.disclaimer')
284
249
  })
285
250
  ]
286
- }),
287
- /*#__PURE__*/ jsxs("div", {
288
- className: "self-stretch shrink-0 flex items-center gap-x-[6px] cursor-pointer",
289
- "data-custom-element": "safety-report",
290
- onClick: openReport,
291
- children: [
292
- /*#__PURE__*/ jsx("img", {
293
- src: ICON_FEEDBACK_URL,
294
- className: "shrink-0 w-[12px] h-[12px]"
295
- }),
296
- /*#__PURE__*/ jsx("p", {
297
- className: "shrink-0 m-0! text-[#a6a6a6] underline underline-offset-2",
298
- children: t('safety.report')
299
- })
300
- ]
301
251
  })
302
252
  ]
303
253
  }),
@@ -1,6 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import react from "react";
3
- import { authClient } from "@lark-apaas/auth-sdk";
3
+ import { getDataloom } from "../../integrations/dataloom.js";
4
4
  import { UserSelectUI } from "./UserSelectUI/index.js";
5
5
  const UserSelect = ({ mode = 'single', defaultValue, value, onChange, placeholder, fetchUsers })=>{
6
6
  const normalizedIds = react.useMemo(()=>{
@@ -67,7 +67,8 @@ const UserSelect = ({ mode = 'single', defaultValue, value, onChange, placeholde
67
67
  if (!normalizedIds.length) return void setUiValue(void 0);
68
68
  const fetchProfiles = async ()=>{
69
69
  try {
70
- const response = await authClient.user.getByIds(normalizedIds);
70
+ const dataloom = await getDataloom();
71
+ const response = await dataloom.service.user.getByIds(normalizedIds);
71
72
  const fetchedList = Array.isArray(response?.data) ? response?.data : Array.isArray(response?.data?.user_list) ? response?.data?.user_list : [];
72
73
  const fetchedMap = new Map();
73
74
  fetchedList.forEach((profile)=>{
@@ -114,7 +115,8 @@ const UserSelect = ({ mode = 'single', defaultValue, value, onChange, placeholde
114
115
  const fetchUsersImpl = react.useCallback(async (search)=>{
115
116
  if (fetchUsers) return fetchUsers(search);
116
117
  try {
117
- const { data } = await authClient.user.search({
118
+ const dataloom = await getDataloom();
119
+ const { data } = await dataloom.service.user.search({
118
120
  name: search,
119
121
  pageSize: 20
120
122
  });
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
- import { authClient } from "@lark-apaas/auth-sdk";
3
2
  import { logger } from "../logger/index.js";
4
3
  import { getCurrentUserProfile } from "../integrations/getCurrentUserProfile.js";
4
+ import { getDataloom } from "../integrations/dataloom.js";
5
5
  import { isSparkRuntime } from "../utils/utils.js";
6
6
  import { getAxiosForBackend } from "../utils/getAxiosForBackend.js";
7
7
  function getNameFromArray(nameArray) {
@@ -35,7 +35,8 @@ const useCurrentUserProfile = ()=>{
35
35
  useEffect(()=>{
36
36
  let cancelled = false;
37
37
  const fetchAndSetUserInfo = async ()=>{
38
- const result = await authClient.session.getUserInfo();
38
+ const dataloom = await getDataloom();
39
+ const result = await dataloom?.service?.session?.getUserInfo();
39
40
  if (cancelled) return;
40
41
  const info = result?.data?.user_info;
41
42
  const userName = getNameFromArray(info?.name);
@@ -1,12 +1,13 @@
1
1
  import { useState } from "react";
2
- import { authClient } from "@lark-apaas/auth-sdk";
2
+ import { getDataloom } from "../integrations/dataloom.js";
3
3
  function useLogout() {
4
4
  const [isLoading, setIsLoading] = useState(false);
5
5
  async function handlerLogout() {
6
6
  if ('production' !== process.env.NODE_ENV) return void console.log('只有生产环境才执行登出');
7
7
  setIsLoading(true);
8
8
  try {
9
- await authClient.session.signOut();
9
+ const dataloom = await getDataloom();
10
+ await dataloom.service.session.signOut();
10
11
  } catch (error) {
11
12
  console.error('登出失败', error);
12
13
  } finally{
@@ -1,6 +1,5 @@
1
1
  import { splitWorkspaceUrl } from "../utils/url.js";
2
2
  import { createClient } from "@lark-apaas/dataloom";
3
- import { authClient } from "@lark-apaas/auth-sdk";
4
3
  import { getAppId } from "../utils/getAppId.js";
5
4
  import { getAppPublished } from "../utils/getInitialInfo.js";
6
5
  const createDataLoomClient = (url, pat)=>{
@@ -13,11 +12,7 @@ const createDataLoomClient = (url, pat)=>{
13
12
  enableDataloomLog: 'production' !== process.env.NODE_ENV,
14
13
  requestRateLimit: 'production' !== process.env.NODE_ENV ? 100 : void 0,
15
14
  brandName: 'miaoda',
16
- appId,
17
- accountServices: {
18
- user: authClient.user,
19
- session: authClient.session
20
- }
15
+ appId
21
16
  }
22
17
  });
23
18
  };
@@ -6,6 +6,15 @@ export type I18nText = {
6
6
  export type DepartmentBasic = {
7
7
  departmentID: string;
8
8
  name: I18nText;
9
+ /** 飞书部门 open id(od- 开头),调飞书部门能力时使用 */
10
+ openDepartmentID?: string;
11
+ };
12
+ /** 直属上级 / 虚线上级,仅在 needFullFields 时返回 */
13
+ export type LeaderUser = {
14
+ /** 飞书企业内 user_id */
15
+ employeeID?: string;
16
+ /** 妙搭用户全局唯一 ID */
17
+ miaodaUserID?: string;
9
18
  };
10
19
  export type UserInfo = {
11
20
  userID: string;
@@ -16,11 +25,32 @@ export type UserInfo = {
16
25
  department: DepartmentBasic;
17
26
  email?: string;
18
27
  tenantName?: string;
28
+ /** 妙搭用户全局唯一 ID(来源同 userID,向前兼容别名) */
29
+ miaodaUserID?: string;
30
+ /** 飞书企业内 user_id,仅内部飞书用户;调飞书开放平台能力时使用 */
31
+ employeeID?: string;
32
+ /** 飞书用户全局唯一 ID(来源同 larkUserID 别名) */
33
+ larkID?: string;
34
+ nickname?: string;
35
+ mobile?: string;
36
+ gender?: number;
37
+ country?: string;
38
+ workStation?: string;
39
+ employeeNo?: string;
40
+ city?: string;
41
+ jobTitle?: string;
42
+ employeeType?: number;
43
+ /** 直属上级 */
44
+ leader?: LeaderUser;
45
+ /** 虚线上级 */
46
+ dottedLineLeaders?: LeaderUser[];
19
47
  };
20
48
  export type DepartmentInfo = {
21
49
  departmentID: string;
22
50
  larkDepartmentID: string;
23
51
  name: I18nText;
52
+ /** 飞书部门 open id(od- 开头) */
53
+ openDepartmentID?: string;
24
54
  };
25
55
  export type AccountType = 'apaas' | 'lark';
26
56
  export type SearchAvatar = {
@@ -35,6 +65,8 @@ export type SearchUsersParams = {
35
65
  offset?: number;
36
66
  pageSize?: number;
37
67
  searchExternalContact?: boolean;
68
+ /** 是否返回 full 字段(手机号/职位/上级等),默认 false */
69
+ needFullFields?: boolean;
38
70
  };
39
71
  export type SearchUsersResponse = {
40
72
  data: {
@@ -75,6 +107,8 @@ export type SearchDepartmentsResponse = {
75
107
  export type ChatInfo = {
76
108
  /** 群组 ID */
77
109
  chatID: string;
110
+ /** 飞书群组 open id(oc_ 开头),调飞书群组能力时使用 */
111
+ openChatID?: string;
78
112
  /** 群组名称(国际化文本) */
79
113
  name: I18nText;
80
114
  /** 头像:URL 或 16 进制 RGB 颜色 */
@@ -23,10 +23,6 @@ const messages_messages = {
23
23
  zh: '了解更多',
24
24
  en: 'Learn more'
25
25
  },
26
- 'safety.report': {
27
- zh: '投诉与举报',
28
- en: 'Report'
29
- },
30
26
  'safety.cover.pc': {
31
27
  zh: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/LMfspH/ljhwZthlaukjlkulzlp/logo/miaodacover.png',
32
28
  en: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/LMfspH/ljhwZthlaukjlkulzlp/logo/miaodacover-weben.png'
@@ -46,18 +46,7 @@ export interface DevServerMessage extends IframeMessage<{
46
46
  }> {
47
47
  type: 'DevServerMessage';
48
48
  }
49
- /**
50
- * 预览态下链接点击通知父页面处理。
51
- * iframe 内直接跳转可能被浏览器安全策略拦截,
52
- * 通过 postMessage 让父页面在新标签页或外壳中打开链接。
53
- */
54
- export interface OpenIframeLinkMessage extends IframeMessage<{
55
- href: string;
56
- external: boolean;
57
- }> {
58
- type: 'OpenIframeLink';
59
- }
60
- export type OutgoingMessage = PreviewReadyMessage | HmrMessage | ConsoleMessage | ChildLocationChangeMessage | CreatePageMessage | RenderErrorMessage | RenderErrorRepairMessage | PageScreenshotMessage | DevServerMessage | UpdateRoutesMessage | OpenIframeLinkMessage;
49
+ export type OutgoingMessage = PreviewReadyMessage | HmrMessage | ConsoleMessage | ChildLocationChangeMessage | CreatePageMessage | RenderErrorMessage | RenderErrorRepairMessage | PageScreenshotMessage | DevServerMessage | UpdateRoutesMessage;
61
50
  export interface GetRoutesMessage extends IframeMessage<Record<string, never>> {
62
51
  type: 'GetRoutes';
63
52
  }
@@ -1,5 +1,4 @@
1
1
  import type { IncomingMessage, OutgoingMessage } from '../types/iframe-events';
2
- export declare function getParentOriginFromParams(): string | undefined;
3
2
  export declare function resolveParentOrigin(): string;
4
3
  export declare function submitPostMessage<T extends OutgoingMessage>(message: T, targetOrigin?: string): void;
5
4
  export declare function isOutgoingMessage<T extends OutgoingMessage['type']>(msg: OutgoingMessage, type: T): msg is Extract<OutgoingMessage, {
@@ -50,4 +50,4 @@ function isOutgoingMessage(msg, type) {
50
50
  function isIncomingMessage(msg, type) {
51
51
  return msg.type === type;
52
52
  }
53
- export { getParentOriginFromParams, isIncomingMessage, isOutgoingMessage, resolveParentOrigin, submitPostMessage };
53
+ export { isIncomingMessage, isOutgoingMessage, resolveParentOrigin, submitPostMessage };
@@ -1,8 +1,10 @@
1
- import { authClient } from "@lark-apaas/auth-sdk";
1
+ import { getDataloom } from "../integrations/dataloom.js";
2
2
  const pendingRequests = new Map();
3
3
  async function fetchUserProfilesByIds(ids) {
4
4
  try {
5
- const response = await authClient.user.getByIds(ids);
5
+ const dataloom = await getDataloom();
6
+ if (!dataloom) throw new Error('Dataloom client is unavailable');
7
+ const response = await dataloom.service.user.getByIds(ids);
6
8
  return Array.isArray(response?.data) ? response.data : response?.data?.user_list || [];
7
9
  } catch (error) {
8
10
  console.error(`Failed to fetch profiles for user IDs ${ids.join(', ')}:`, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/client-toolkit",
3
- "version": "1.2.53-alpha.2",
3
+ "version": "1.2.53-alpha.3",
4
4
  "types": "./lib/index.d.ts",
5
5
  "main": "./lib/index.js",
6
6
  "files": [
@@ -99,9 +99,9 @@
99
99
  "@ant-design/colors": "^7.2.1",
100
100
  "@ant-design/cssinjs": "^1.24.0",
101
101
  "@lark-apaas/aily-web-sdk": "^0.0.11",
102
- "@lark-apaas/auth-sdk": "^0.1.5",
103
- "@lark-apaas/client-capability": "^0.1.7",
104
- "@lark-apaas/dataloom": "^0.1.3",
102
+ "@lark-apaas/auth-sdk": "^0.1.4",
103
+ "@lark-apaas/client-capability": "^0.1.6",
104
+ "@lark-apaas/dataloom": "0.1.4-alpha.2",
105
105
  "@lark-apaas/internal-slardar": "^0.0.3",
106
106
  "@lark-apaas/miaoda-inspector": "^1.0.23",
107
107
  "@lark-apaas/observable-web": "^1.0.6",
@@ -1,60 +0,0 @@
1
- import React from 'react';
2
- /**
3
- * 是否豆包来源应用。来源由 coding/canvas 构建链编译期把 MIAODA_APP_SOURCE 注入 process.env
4
- * (define 整体替换 `process.env.MIAODA_APP_SOURCE` 这个 key)。但完整版 toolkit 同时被其它
5
- * 全栈构建链(fullstack-vite/rspack-preset)消费——它们不注入该 key,且浏览器没有 process 全局,
6
- * 此时裸读会抛 `ReferenceError: process is not defined`,把整个宿主应用白屏。故 try/catch 兜底:
7
- * 未注入 → 返回 false(非豆包,回落 Safety),绝不让水印判定拖垮宿主应用。
8
- *
9
- * 注意:只能裸引用完整 key `process.env.MIAODA_APP_SOURCE`(define 替换的就是它);不能写
10
- * `typeof process` 之类——那段不会被 define 替换,注入构建里 process 仍可能未定义,反而误判。
11
- */
12
- export declare const isDoubaoSource: () => boolean;
13
- /**
14
- * 打开「投诉与举报」页:scene/entity_type 固定,entity_id 取当前妙搭 appId,域名按环境选,
15
- * params 整体 encode。appId 为空(异常环境)时直接不跳转,避免带空 entity_id 打开举报中心
16
- * (与 safety.tsx openReport 同口径)。
17
- */
18
- export declare const openDoubaoReport: () => void;
19
- export declare const FeedbackIcon: React.FC;
20
- export declare const CloseIcon: React.FC<{
21
- size: number;
22
- }>;
23
- export declare const Avatar: React.FC<{
24
- size: number;
25
- }>;
26
- export declare const Pill: React.FC<{
27
- variant: 'desktop' | 'mobile';
28
- onClose?: () => void;
29
- showClose?: boolean;
30
- }>;
31
- export declare const MenuRow: React.FC<{
32
- icon: 'feedback' | 'close';
33
- label: string;
34
- fontSize: number;
35
- onClick?: () => void;
36
- }>;
37
- type MenuItem = {
38
- icon: 'feedback' | 'close';
39
- label: string;
40
- onClick?: () => void;
41
- };
42
- export declare const MenuCard: React.FC<{
43
- items: MenuItem[];
44
- fontSize: number;
45
- footerFontSize: number;
46
- minWidth: number;
47
- style?: React.CSSProperties;
48
- }>;
49
- /**
50
- * 豆包水印:仅当应用来源为豆包时,在视口右下角展示收起态药丸(头像 +「豆包AI」)。
51
- *
52
- * 设计稿:Figma 1582:41560(桌面)/ 1582:41777(移动)。
53
- * - 药丸 / 卡片本体用代码画(投影走 CSS box-shadow、图标用无滤镜小 SVG),高分屏清晰。
54
- * - 桌面端点关闭叉 → 隐藏整个水印;hover 徽标 → 上方弹白色卡片(投诉与举报 + 由飞书妙搭提供支持)。
55
- * - 移动端 tap 徽标 → 上方弹卡片(投诉与举报 / 不再展示 / 由飞书妙搭提供支持)。
56
- * - 「投诉与举报」点击跳飞书风控举报中心(tns,域名随环境);后端链接未就绪时由 REPORT_ENABLED 整体隐藏。
57
- * - 不加 NODE_ENV 判断——预览态与运行态都要展示。
58
- */
59
- declare const Watermark: React.FC;
60
- export default Watermark;
@@ -1,364 +0,0 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from "react";
3
- import { Content, Portal, Root, Trigger } from "@radix-ui/react-popover";
4
- import { DOUBAO_LOGO_SRC, DOUBAO_PILL_MOBILE_PNG } from "./doubao-watermark-asset.js";
5
- import { getAppId } from "../../utils/getAppId.js";
6
- import { getEnv } from "../../utils/getParentOrigin.js";
7
- import { useIsMobile } from "../../hooks/index.js";
8
- const DOUBAO_SOURCE = '5';
9
- const isDoubaoSource = ()=>{
10
- try {
11
- return String(process.env.MIAODA_APP_SOURCE) === DOUBAO_SOURCE;
12
- } catch {
13
- return false;
14
- }
15
- };
16
- const REPORT_ENABLED = true;
17
- const BRAND_TEXT = '豆包 AI 生成';
18
- const POWERED_BY_TEXT = '由飞书妙搭提供支持';
19
- const FONT_FAMILY = "'PingFang SC', -apple-system, BlinkMacSystemFont, sans-serif";
20
- const PILL_SHADOW = '0px 8px 32px rgba(0,0,0,0.08), 0px 4px 16px rgba(0,0,0,0.04), 0px 2px 4px rgba(0,0,0,0.02)';
21
- const CARD_SHADOW = '0px 4px 16px rgba(0,0,0,0.03), 0px 4px 10px rgba(0,0,0,0.03), 0px 2px 4px rgba(0,0,0,0.02)';
22
- const REPORT_DOMAIN = {
23
- BOE: 'tns.feishu-boe.cn',
24
- PRE: 'tns.feishu-pre.cn',
25
- ONLINE: 'tns.feishu.cn'
26
- };
27
- const openDoubaoReport = ()=>{
28
- const appId = getAppId();
29
- if (!appId) return;
30
- const params = JSON.stringify({
31
- scene: 'miaoda_app_report',
32
- entity_id: appId,
33
- entity_type: 'miaoda_app',
34
- extra: ''
35
- });
36
- const domain = REPORT_DOMAIN[getEnv()] ?? REPORT_DOMAIN.ONLINE;
37
- const url = `https://${domain}/cust/lark_report/?type=common&params=${encodeURIComponent(params)}&lang=zh-CN`;
38
- window.open(url, '_blank', 'noopener,noreferrer');
39
- };
40
- const FeedbackIcon = ()=>/*#__PURE__*/ jsxs("svg", {
41
- width: 16,
42
- height: 16,
43
- viewBox: "0 0 16 16",
44
- fill: "none",
45
- xmlns: "http://www.w3.org/2000/svg",
46
- style: {
47
- display: 'block',
48
- flexShrink: 0
49
- },
50
- children: [
51
- /*#__PURE__*/ jsx("path", {
52
- d: "M12.6667 2H3.33333V14H6.66667V15.3334H3.33333C2.59695 15.3334 2 14.7364 2 14V2C2 1.26364 2.59695 0.666687 3.33333 0.666687H12.6667C13.403 0.666687 14 1.26364 14 2V7.33335H12.6667V2Z",
53
- fill: "currentColor"
54
- }),
55
- /*#__PURE__*/ jsx("path", {
56
- d: "M5.33333 4.66669C4.96514 4.66669 4.66667 4.96516 4.66667 5.33335C4.66667 5.70154 4.96514 6 5.33333 6H10.6667C11.0349 6 11.3333 5.70154 11.3333 5.33335C11.3333 4.96516 11.0349 4.66669 10.6667 4.66669H5.33333Z",
57
- fill: "currentColor"
58
- }),
59
- /*#__PURE__*/ jsx("path", {
60
- d: "M4.66667 8.33335C4.66667 7.96516 4.96514 7.66669 5.33333 7.66669H8C8.36819 7.66669 8.66667 7.96516 8.66667 8.33335C8.66667 8.70154 8.36819 9 8 9H5.33333C4.96514 9 4.66667 8.70154 4.66667 8.33335Z",
61
- fill: "currentColor"
62
- }),
63
- /*#__PURE__*/ jsx("path", {
64
- d: "M13.5523 8.70017C13.9428 8.30964 14.576 8.30964 14.9665 8.70017C15.357 9.09069 15.357 9.72386 14.9665 10.1144L14.2594 10.8215L12.8452 9.40728L13.5523 8.70017Z",
65
- fill: "currentColor"
66
- }),
67
- /*#__PURE__*/ jsx("path", {
68
- d: "M12.1381 10.1144L13.5523 11.5286L10.2309 14.85C10.0916 14.9892 9.91229 15.0815 9.71798 15.1138L8.45737 15.3234C8.42136 15.3294 8.38467 15.3177 8.35885 15.2918C8.33271 15.2657 8.32099 15.2284 8.32749 15.192L8.55128 13.9374C8.58465 13.7502 8.6746 13.5779 8.80901 13.4435L12.1381 10.1144Z",
69
- fill: "currentColor"
70
- })
71
- ]
72
- });
73
- const CloseIcon = ({ size })=>/*#__PURE__*/ jsx("svg", {
74
- width: size,
75
- height: size,
76
- viewBox: "0 0 10 10",
77
- fill: "none",
78
- xmlns: "http://www.w3.org/2000/svg",
79
- style: {
80
- display: 'block',
81
- flexShrink: 0
82
- },
83
- children: /*#__PURE__*/ jsx("path", {
84
- d: "M8.08312 8.08298C8.2384 7.92769 8.23953 7.67705 8.08424 7.52176L5.3625 4.80002L8.08422 2.07829C8.23951 1.92301 8.23839 1.67236 8.0831 1.51708C7.92781 1.36179 7.67717 1.36066 7.52188 1.51595C7.36659 1.67124 4.80016 4.23767 4.80016 4.23767L2.07841 1.51593C1.92313 1.36064 1.67248 1.36177 1.5172 1.51705C1.36191 1.67234 1.36078 1.92298 1.51607 2.07827L4.23782 4.80002L1.51605 7.52178C1.36076 7.67707 1.36189 7.92771 1.51718 8.083C1.67246 8.23829 1.92311 8.23941 2.07839 8.08412L4.80016 5.36236L7.5219 8.0841C7.67719 8.23939 7.92783 8.23826 8.08312 8.08298Z",
85
- fill: "currentColor"
86
- })
87
- });
88
- const Avatar = ({ size })=>/*#__PURE__*/ jsx("img", {
89
- src: DOUBAO_LOGO_SRC,
90
- width: size,
91
- height: size,
92
- alt: "",
93
- style: {
94
- width: size,
95
- height: size,
96
- maxWidth: 'none',
97
- maxHeight: 'none',
98
- borderRadius: 100,
99
- flexShrink: 0,
100
- display: 'block'
101
- }
102
- });
103
- const Pill = ({ variant, onClose, showClose = false })=>{
104
- const isDesktop = 'desktop' === variant;
105
- return /*#__PURE__*/ jsxs("div", {
106
- style: {
107
- boxSizing: 'border-box',
108
- display: 'inline-flex',
109
- alignItems: 'center',
110
- gap: isDesktop ? 4 : 6,
111
- padding: isDesktop ? '6px 12px 6px 8px' : '4px 12px 4px 8px',
112
- background: '#363636',
113
- border: '0.5px solid rgba(255,255,255,0.1)',
114
- borderRadius: 100,
115
- boxShadow: PILL_SHADOW,
116
- cursor: 'pointer'
117
- },
118
- children: [
119
- /*#__PURE__*/ jsx(Avatar, {
120
- size: isDesktop ? 16 : 18
121
- }),
122
- /*#__PURE__*/ jsx("span", {
123
- style: {
124
- fontFamily: FONT_FAMILY,
125
- fontSize: isDesktop ? 12 : 14,
126
- lineHeight: isDesktop ? '18px' : '22px',
127
- color: 'rgba(255,255,255,0.85)',
128
- whiteSpace: 'nowrap'
129
- },
130
- children: BRAND_TEXT
131
- }),
132
- isDesktop && onClose && showClose && /*#__PURE__*/ jsx("span", {
133
- role: "button",
134
- "aria-label": "关闭",
135
- onClick: (e)=>{
136
- e.stopPropagation();
137
- onClose();
138
- },
139
- style: {
140
- display: 'inline-flex',
141
- alignItems: 'center',
142
- cursor: 'pointer',
143
- flexShrink: 0,
144
- color: 'rgba(255,255,255,0.5)'
145
- },
146
- children: /*#__PURE__*/ jsx(CloseIcon, {
147
- size: 10
148
- })
149
- })
150
- ]
151
- });
152
- };
153
- const MenuRow = ({ icon, label, fontSize, onClick })=>{
154
- const [hover, setHover] = useState(false);
155
- return /*#__PURE__*/ jsxs("div", {
156
- role: "menuitem",
157
- "data-label": label,
158
- onClick: onClick,
159
- onMouseEnter: ()=>setHover(true),
160
- onMouseLeave: ()=>setHover(false),
161
- style: {
162
- boxSizing: 'border-box',
163
- display: 'flex',
164
- alignItems: 'center',
165
- gap: 8,
166
- padding: '4px 10px',
167
- borderRadius: 6,
168
- cursor: 'pointer',
169
- color: '#000000',
170
- background: hover ? 'rgba(0,0,0,0.04)' : 'transparent'
171
- },
172
- children: [
173
- /*#__PURE__*/ jsx("span", {
174
- style: {
175
- display: 'inline-flex',
176
- width: 16,
177
- height: 16,
178
- flexShrink: 0,
179
- color: '#000000'
180
- },
181
- children: 'feedback' === icon ? /*#__PURE__*/ jsx(FeedbackIcon, {}) : /*#__PURE__*/ jsx(CloseIcon, {
182
- size: 16
183
- })
184
- }),
185
- /*#__PURE__*/ jsx("span", {
186
- style: {
187
- fontFamily: FONT_FAMILY,
188
- fontSize,
189
- lineHeight: `${fontSize + 8}px`,
190
- color: '#000000',
191
- whiteSpace: 'nowrap'
192
- },
193
- children: label
194
- })
195
- ]
196
- });
197
- };
198
- const MenuCard = ({ items, fontSize, footerFontSize, minWidth, style })=>/*#__PURE__*/ jsxs("div", {
199
- style: {
200
- boxSizing: 'border-box',
201
- display: 'flex',
202
- flexDirection: 'column',
203
- minWidth,
204
- background: '#ffffff',
205
- border: '0.5px solid rgba(0,0,0,0.1)',
206
- borderRadius: 10,
207
- boxShadow: CARD_SHADOW,
208
- overflow: 'hidden',
209
- outline: 'none',
210
- ...style
211
- },
212
- children: [
213
- items.length > 0 && /*#__PURE__*/ jsx("div", {
214
- style: {
215
- boxSizing: 'border-box',
216
- padding: 4
217
- },
218
- children: items.map((it)=>/*#__PURE__*/ jsx(MenuRow, {
219
- icon: it.icon,
220
- label: it.label,
221
- fontSize: fontSize,
222
- onClick: it.onClick
223
- }, it.label))
224
- }),
225
- /*#__PURE__*/ jsx("div", {
226
- style: {
227
- boxSizing: 'border-box',
228
- background: '#f9f9f9',
229
- padding: '8px 14px',
230
- fontFamily: FONT_FAMILY,
231
- fontSize: footerFontSize,
232
- lineHeight: `${footerFontSize + 8}px`,
233
- color: 'rgba(0,0,0,0.35)',
234
- whiteSpace: 'nowrap'
235
- },
236
- children: POWERED_BY_TEXT
237
- })
238
- ]
239
- });
240
- const MobileWatermark = ({ onDontShow })=>{
241
- const [open, setOpen] = useState(false);
242
- const items = [];
243
- if (REPORT_ENABLED) items.push({
244
- icon: 'feedback',
245
- label: '投诉与举报',
246
- onClick: openDoubaoReport
247
- });
248
- items.push({
249
- icon: 'close',
250
- label: '不再展示',
251
- onClick: ()=>{
252
- setOpen(false);
253
- onDontShow();
254
- }
255
- });
256
- return /*#__PURE__*/ jsxs(Root, {
257
- open: open,
258
- onOpenChange: setOpen,
259
- children: [
260
- /*#__PURE__*/ jsx(Trigger, {
261
- asChild: true,
262
- children: /*#__PURE__*/ jsx("div", {
263
- "data-custom-element": "doubao-watermark-mobile",
264
- style: {
265
- position: 'fixed',
266
- right: 12,
267
- bottom: 12,
268
- width: 122,
269
- height: 30,
270
- zIndex: 9999,
271
- cursor: 'pointer'
272
- },
273
- children: /*#__PURE__*/ jsx("img", {
274
- src: DOUBAO_PILL_MOBILE_PNG,
275
- width: 166,
276
- height: 94,
277
- alt: "豆包 AI 生成",
278
- style: {
279
- position: 'absolute',
280
- right: -12,
281
- bottom: -40,
282
- width: 166,
283
- height: 94,
284
- maxWidth: 'none',
285
- maxHeight: 'none',
286
- display: 'block',
287
- pointerEvents: 'none'
288
- }
289
- })
290
- })
291
- }),
292
- /*#__PURE__*/ jsx(Portal, {
293
- children: /*#__PURE__*/ jsx(Content, {
294
- side: "top",
295
- align: "end",
296
- sideOffset: 8,
297
- style: {
298
- outline: 'none'
299
- },
300
- children: /*#__PURE__*/ jsx(MenuCard, {
301
- items: items,
302
- fontSize: 16,
303
- footerFontSize: 14,
304
- minWidth: 160
305
- })
306
- })
307
- })
308
- ]
309
- });
310
- };
311
- const Watermark_Watermark = ()=>{
312
- const [closed, setClosed] = useState(false);
313
- const [open, setOpen] = useState(false);
314
- const timer = useRef(null);
315
- const isMobile = useIsMobile();
316
- if (!isDoubaoSource()) return null;
317
- if (closed) return null;
318
- if (isMobile) return /*#__PURE__*/ jsx(MobileWatermark, {
319
- onDontShow: ()=>setClosed(true)
320
- });
321
- const enter = ()=>{
322
- if (timer.current) clearTimeout(timer.current);
323
- setOpen(true);
324
- };
325
- const leave = ()=>{
326
- timer.current = setTimeout(()=>setOpen(false), 100);
327
- };
328
- const desktopItems = [];
329
- if (REPORT_ENABLED) desktopItems.push({
330
- icon: 'feedback',
331
- label: '投诉与举报',
332
- onClick: openDoubaoReport
333
- });
334
- return /*#__PURE__*/ jsxs("div", {
335
- "data-custom-element": "doubao-watermark",
336
- onMouseEnter: enter,
337
- onMouseLeave: leave,
338
- style: {
339
- position: 'fixed',
340
- right: 12,
341
- bottom: 12,
342
- zIndex: 9999,
343
- display: 'flex',
344
- flexDirection: 'column',
345
- alignItems: 'flex-end',
346
- gap: 8
347
- },
348
- children: [
349
- open && /*#__PURE__*/ jsx(MenuCard, {
350
- items: desktopItems,
351
- fontSize: 14,
352
- footerFontSize: 12,
353
- minWidth: 144
354
- }),
355
- /*#__PURE__*/ jsx(Pill, {
356
- variant: "desktop",
357
- onClose: ()=>setClosed(true),
358
- showClose: open
359
- })
360
- ]
361
- });
362
- };
363
- const Watermark = Watermark_Watermark;
364
- export { Avatar, CloseIcon, FeedbackIcon, MenuCard, MenuRow, Pill, Watermark as default, isDoubaoSource, openDoubaoReport };
@@ -1,121 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import react from "react";
3
- import { renderToStaticMarkup } from "react-dom/server";
4
- import Watermark, { Avatar, CloseIcon, FeedbackIcon, MenuCard, Pill, openDoubaoReport } from "../Watermark.js";
5
- import { DOUBAO_LOGO_SRC, DOUBAO_PILL_MOBILE_PNG } from "../doubao-watermark-asset.js";
6
- const html = ()=>renderToStaticMarkup(/*#__PURE__*/ react.createElement(Watermark));
7
- const setViewportWidth = (w)=>Object.defineProperty(window, 'innerWidth', {
8
- value: w,
9
- configurable: true
10
- });
11
- afterEach(()=>{
12
- vi.unstubAllEnvs();
13
- vi.restoreAllMocks();
14
- delete globalThis.ENVIRONMENT;
15
- delete window.appId;
16
- setViewportWidth(1024);
17
- });
18
- describe('Watermark(豆包来源水印)', ()=>{
19
- it('豆包来源(MIAODA_APP_SOURCE=5)时展示收起态药丸(头像 + 豆包 AI 生成)', ()=>{
20
- vi.stubEnv('MIAODA_APP_SOURCE', '5');
21
- const out = html();
22
- expect(out).toContain('data-custom-element="doubao-watermark"');
23
- expect(out).toContain('豆包 AI 生成');
24
- expect(out).toContain(DOUBAO_LOGO_SRC);
25
- expect(out).not.toContain('aria-label="关闭"');
26
- expect(out).not.toContain('投诉与举报');
27
- expect(out).not.toContain('由飞书妙搭提供支持');
28
- });
29
- it('移动端视口(<768)时展示移动端药丸(doubao-watermark-mobile)而非桌面药丸', ()=>{
30
- vi.stubEnv('MIAODA_APP_SOURCE', '5');
31
- setViewportWidth(375);
32
- const out = html();
33
- expect(out).toContain('data-custom-element="doubao-watermark-mobile"');
34
- expect(out).toContain(DOUBAO_PILL_MOBILE_PNG);
35
- expect(out).toContain('豆包 AI 生成');
36
- expect(out).not.toContain('aria-label="关闭"');
37
- });
38
- it('非豆包来源(其它 enum)时不渲染', ()=>{
39
- vi.stubEnv('MIAODA_APP_SOURCE', '3');
40
- expect(html()).toBe('');
41
- });
42
- it('来源未注入时不渲染(String(undefined) !== "5")', ()=>{
43
- expect(html()).toBe('');
44
- });
45
- describe('展开态展示组件(SSR 覆盖)', ()=>{
46
- it('Pill 桌面态:头像 + 豆包 AI 生成;关闭叉仅 showClose 时显示', ()=>{
47
- const out = renderToStaticMarkup(/*#__PURE__*/ react.createElement(Pill, {
48
- variant: 'desktop',
49
- onClose: ()=>{}
50
- }));
51
- expect(out).toContain('豆包 AI 生成');
52
- expect(out).toContain(DOUBAO_LOGO_SRC);
53
- expect(out).not.toContain('aria-label="关闭"');
54
- const hovered = renderToStaticMarkup(/*#__PURE__*/ react.createElement(Pill, {
55
- variant: 'desktop',
56
- onClose: ()=>{},
57
- showClose: true
58
- }));
59
- expect(hovered).toContain('aria-label="关闭"');
60
- });
61
- it('Pill 移动态:头像 + 豆包 AI 生成,无关闭叉', ()=>{
62
- const out = renderToStaticMarkup(/*#__PURE__*/ react.createElement(Pill, {
63
- variant: 'mobile'
64
- }));
65
- expect(out).toContain('豆包 AI 生成');
66
- expect(out).not.toContain('aria-label="关闭"');
67
- });
68
- it('MenuCard:投诉与举报 + 不再展示 行 + 由飞书妙搭提供支持 页脚', ()=>{
69
- const out = renderToStaticMarkup(/*#__PURE__*/ react.createElement(MenuCard, {
70
- items: [
71
- {
72
- icon: 'feedback',
73
- label: '投诉与举报',
74
- onClick: ()=>{}
75
- },
76
- {
77
- icon: 'close',
78
- label: '不再展示',
79
- onClick: ()=>{}
80
- }
81
- ],
82
- fontSize: 16,
83
- footerFontSize: 14,
84
- minWidth: 160
85
- }));
86
- expect(out).toContain('data-label="投诉与举报"');
87
- expect(out).toContain('data-label="不再展示"');
88
- expect(out).toContain('由飞书妙搭提供支持');
89
- });
90
- it('图标:FeedbackIcon / CloseIcon / Avatar 渲染', ()=>{
91
- expect(renderToStaticMarkup(/*#__PURE__*/ react.createElement(FeedbackIcon))).toContain('<svg');
92
- expect(renderToStaticMarkup(/*#__PURE__*/ react.createElement(CloseIcon, {
93
- size: 16
94
- }))).toContain('<svg');
95
- expect(renderToStaticMarkup(/*#__PURE__*/ react.createElement(Avatar, {
96
- size: 18
97
- }))).toContain(DOUBAO_LOGO_SRC);
98
- });
99
- });
100
- describe('openDoubaoReport(举报跳转,后端就绪后启用)', ()=>{
101
- it('按当前环境(getEnv)拼出飞书风控举报中心(tns) URL 并以新窗口打开', ()=>{
102
- const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
103
- window.appId = 'app_abc';
104
- globalThis.ENVIRONMENT = 'staging';
105
- openDoubaoReport();
106
- expect(openSpy).toHaveBeenCalledTimes(1);
107
- const [url, target, features] = openSpy.mock.calls[0];
108
- expect(String(url)).toContain('tns.feishu-boe.cn/cust/lark_report/');
109
- expect(String(url)).toContain(encodeURIComponent('miaoda_app_report'));
110
- expect(String(url)).toContain('app_abc');
111
- expect(target).toBe('_blank');
112
- expect(features).toBe('noopener,noreferrer');
113
- });
114
- it('无 appId 时不跳转(避免带空 entity_id 打开举报中心)', ()=>{
115
- const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
116
- globalThis.ENVIRONMENT = 'online';
117
- openDoubaoReport();
118
- expect(openSpy).not.toHaveBeenCalled();
119
- });
120
- });
121
- });
@@ -1,53 +0,0 @@
1
- import { jsx } from "react/jsx-runtime";
2
- import "react";
3
- import { render, screen } from "@testing-library/react";
4
- import { describe, expect, it, vi } from "vitest";
5
- import { slardar } from "@lark-apaas/internal-slardar";
6
- import { SafetyErrorBoundary } from "../safety-error-boundary.js";
7
- vi.mock('@lark-apaas/internal-slardar', ()=>({
8
- slardar: {
9
- sendLog: vi.fn()
10
- }
11
- }));
12
- describe('SafetyErrorBoundary', ()=>{
13
- it('renders children when no error', ()=>{
14
- render(/*#__PURE__*/ jsx(SafetyErrorBoundary, {
15
- children: /*#__PURE__*/ jsx("div", {
16
- "data-testid": "child",
17
- children: "child content"
18
- })
19
- }));
20
- expect(screen.getByTestId('child').textContent).toBe('child content');
21
- });
22
- it('renders null fallback when child throws', ()=>{
23
- const Throwing = ()=>{
24
- throw new Error('Failed to fetch dynamically imported module: safety-XXX.js');
25
- };
26
- const errSpy = vi.spyOn(console, 'error').mockImplementation(()=>{});
27
- const { container } = render(/*#__PURE__*/ jsx(SafetyErrorBoundary, {
28
- children: /*#__PURE__*/ jsx(Throwing, {})
29
- }));
30
- expect(container.innerHTML).toBe('');
31
- errSpy.mockRestore();
32
- });
33
- it('logs error via slardar when child throws', ()=>{
34
- const Throwing = ()=>{
35
- throw new Error('chunk load failed');
36
- };
37
- const errSpy = vi.spyOn(console, 'error').mockImplementation(()=>{});
38
- slardar.sendLog.mockClear();
39
- render(/*#__PURE__*/ jsx(SafetyErrorBoundary, {
40
- children: /*#__PURE__*/ jsx(Throwing, {})
41
- }));
42
- expect(slardar.sendLog).toHaveBeenCalledTimes(1);
43
- expect(slardar.sendLog).toHaveBeenCalledWith(expect.objectContaining({
44
- content: expect.stringContaining('chunk load failed'),
45
- level: 'error',
46
- extra: expect.objectContaining({
47
- stack: expect.any(String),
48
- componentStack: expect.any(String)
49
- })
50
- }));
51
- errSpy.mockRestore();
52
- });
53
- });
@@ -1,2 +0,0 @@
1
- export declare const DOUBAO_LOGO_SRC = "https://lf3-static.bytednsdoc.com/obj/eden-cn/LMfspH/ljhwZthlaukjlkulzlp/miaoda-ui/setting_voice.svg";
2
- export declare const DOUBAO_PILL_MOBILE_PNG = "https://lf3-static.bytednsdoc.com/obj/eden-cn/LMfspH/ljhwZthlaukjlkulzlp/miaoda-ui/Frame%202147223991%20(3).png";
@@ -1,4 +0,0 @@
1
- const CDN = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/LMfspH/ljhwZthlaukjlkulzlp/miaoda-ui';
2
- const DOUBAO_LOGO_SRC = `${CDN}/setting_voice.svg`;
3
- const DOUBAO_PILL_MOBILE_PNG = `${CDN}/Frame%202147223991%20(3).png`;
4
- export { DOUBAO_LOGO_SRC, DOUBAO_PILL_MOBILE_PNG };
@@ -1,23 +0,0 @@
1
- import React from 'react';
2
- interface Props {
3
- children: React.ReactNode;
4
- }
5
- interface State {
6
- hasError: boolean;
7
- }
8
- /**
9
- * SafetyErrorBoundary - 包裹 Safety 徽章的 lazy import 失败兜底
10
- *
11
- * Why: AppContainer 内 `<Suspense fallback={null}><Safety /></Suspense>` 中 Safety 是 React.lazy
12
- * 拆出的非核心徽章 chunk。chunk 加载失败(CDN 缓存窗 / 部署漂移 / dev preview hash 漂移 / 网络抖动)
13
- * 时 rejected Promise 越过 Suspense 上抛,会冒到 React root 让整应用 unmount = 白屏。
14
- *
15
- * ErrorBoundary 把失败局限在徽章自身:徽章不显示,应用继续跑。同时上报 slardar 便于事后归因。
16
- */
17
- export declare class SafetyErrorBoundary extends React.Component<Props, State> {
18
- state: State;
19
- static getDerivedStateFromError(): State;
20
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
21
- render(): React.ReactNode;
22
- }
23
- export {};
@@ -1,29 +0,0 @@
1
- import react from "react";
2
- import { slardar } from "@lark-apaas/internal-slardar";
3
- class SafetyErrorBoundary extends react.Component {
4
- state = {
5
- hasError: false
6
- };
7
- static getDerivedStateFromError() {
8
- return {
9
- hasError: true
10
- };
11
- }
12
- componentDidCatch(error, errorInfo) {
13
- try {
14
- slardar.sendLog({
15
- content: `safety_chunk_load_error: ${error.message}`,
16
- level: 'error',
17
- extra: {
18
- stack: error.stack ?? '',
19
- componentStack: errorInfo.componentStack ?? ''
20
- }
21
- });
22
- } catch {}
23
- }
24
- render() {
25
- if (this.state.hasError) return null;
26
- return this.props.children;
27
- }
28
- }
29
- export { SafetyErrorBoundary };