@anker-in/campaign-ui 0.2.11-beta.25 → 0.2.11-beta.26

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.
Files changed (41) hide show
  1. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.d.ts +23 -1
  2. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
  3. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  4. package/dist/cjs/components/LiveChatWidget/api/chat.d.ts +21 -2
  5. package/dist/cjs/components/LiveChatWidget/api/chat.js +2 -2
  6. package/dist/cjs/components/LiveChatWidget/api/chat.js.map +3 -3
  7. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.d.ts +5 -0
  8. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  9. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  10. package/dist/cjs/components/LiveChatWidget/types.d.ts +17 -0
  11. package/dist/cjs/components/LiveChatWidget/types.js +1 -1
  12. package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
  13. package/dist/cjs/components/LiveChatWidget/utils/fetcher.d.ts +36 -0
  14. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js +2 -0
  15. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  16. package/dist/cjs/stories/LiveChatWidget.stories.d.ts +8 -0
  17. package/dist/cjs/stories/LiveChatWidget.stories.js +35 -3
  18. package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
  19. package/dist/esm/components/LiveChatWidget/LiveChatWidget.d.ts +23 -1
  20. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  21. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  22. package/dist/esm/components/LiveChatWidget/api/chat.d.ts +21 -2
  23. package/dist/esm/components/LiveChatWidget/api/chat.js +2 -2
  24. package/dist/esm/components/LiveChatWidget/api/chat.js.map +3 -3
  25. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.d.ts +5 -0
  26. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  27. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  28. package/dist/esm/components/LiveChatWidget/types.d.ts +17 -0
  29. package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +36 -0
  30. package/dist/esm/components/LiveChatWidget/utils/fetcher.js +2 -0
  31. package/dist/esm/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  32. package/dist/esm/stories/LiveChatWidget.stories.d.ts +8 -0
  33. package/dist/esm/stories/LiveChatWidget.stories.js +38 -6
  34. package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
  35. package/package.json +3 -3
  36. package/src/components/LiveChatWidget/LiveChatWidget.tsx +31 -1
  37. package/src/components/LiveChatWidget/api/chat.ts +40 -11
  38. package/src/components/LiveChatWidget/hooks/useChatAPI.ts +11 -6
  39. package/src/components/LiveChatWidget/types.ts +20 -0
  40. package/src/components/LiveChatWidget/utils/fetcher.ts +126 -0
  41. package/src/stories/LiveChatWidget.stories.tsx +82 -4
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { useCallback, useRef } from 'react'
8
8
  import type { ChatStreamRequest, SSEEvent, NewSessionRequest, NewSessionResponse } from '../types'
9
- import { sendMessage, createNewSession } from '../api/chat'
9
+ import { sendMessage, createNewSession, type RecaptchaConfig } from '../api/chat'
10
10
 
11
11
  export interface UseChatAPIOptions {
12
12
  /**
@@ -19,6 +19,11 @@ export interface UseChatAPIOptions {
19
19
  */
20
20
  headers?: Record<string, string>
21
21
 
22
+ /**
23
+ * reCAPTCHA 配置
24
+ */
25
+ recaptchaConfig?: RecaptchaConfig
26
+
22
27
  /**
23
28
  * 错误处理回调
24
29
  */
@@ -64,7 +69,7 @@ export interface UseChatAPIReturn {
64
69
  * @returns API 调用工具对象
65
70
  */
66
71
  export function useChatAPI(options: UseChatAPIOptions): UseChatAPIReturn {
67
- const { apiBaseUrl, headers, onError } = options
72
+ const { apiBaseUrl, headers, recaptchaConfig, onError } = options
68
73
 
69
74
  // 用于中断当前请求的 AbortController
70
75
  const abortControllerRef = useRef<AbortController | null>(null)
@@ -87,7 +92,7 @@ export function useChatAPI(options: UseChatAPIOptions): UseChatAPIReturn {
87
92
  isSendingRef.current = true
88
93
 
89
94
  try {
90
- await sendMessage(apiBaseUrl, request, onEvent, headers)
95
+ await sendMessage(apiBaseUrl, request, onEvent, headers, recaptchaConfig)
91
96
  } catch (error) {
92
97
  // 忽略手动中断的错误
93
98
  if (error instanceof Error && error.name === 'AbortError') {
@@ -102,7 +107,7 @@ export function useChatAPI(options: UseChatAPIOptions): UseChatAPIReturn {
102
107
  abortControllerRef.current = null
103
108
  }
104
109
  },
105
- [apiBaseUrl, headers, onError]
110
+ [apiBaseUrl, headers, recaptchaConfig, onError]
106
111
  )
107
112
 
108
113
  /**
@@ -111,14 +116,14 @@ export function useChatAPI(options: UseChatAPIOptions): UseChatAPIReturn {
111
116
  const createSession = useCallback(
112
117
  async (request: NewSessionRequest): Promise<NewSessionResponse> => {
113
118
  try {
114
- return await createNewSession(apiBaseUrl, request, headers)
119
+ return await createNewSession(apiBaseUrl, request, headers, recaptchaConfig)
115
120
  } catch (error) {
116
121
  console.error('[useChatAPI] Create session error:', error)
117
122
  onError?.(error as Error)
118
123
  throw error
119
124
  }
120
125
  },
121
- [apiBaseUrl, headers, onError]
126
+ [apiBaseUrl, headers, recaptchaConfig, onError]
122
127
  )
123
128
 
124
129
  /**
@@ -705,6 +705,26 @@ export interface LiveChatWidgetProps {
705
705
  */
706
706
  headers?: Record<string, string>
707
707
 
708
+ /**
709
+ * 是否启用 reCAPTCHA 验证
710
+ * @default false
711
+ */
712
+ needRecaptcha?: boolean
713
+
714
+ /**
715
+ * reCAPTCHA site key
716
+ * 当 needRecaptcha=true 时必填
717
+ * @example "6LfS4J4pAAAAACX1e_WrxutmxxzCK7FU4WzVqL14"
718
+ */
719
+ recaptchaSitekey?: string
720
+
721
+ /**
722
+ * reCAPTCHA action 前缀
723
+ * 实际使用时会根据不同接口添加后缀(如 chat_stream, new_session)
724
+ * @default "livechat"
725
+ */
726
+ recaptchaAction?: string
727
+
708
728
  /**
709
729
  * Shopify 店铺域名
710
730
  * @example "www.eufy.com"
@@ -0,0 +1,126 @@
1
+ /**
2
+ * LiveChat Fetcher
3
+ * 参照 storefront 的实现,支持 reCAPTCHA
4
+ */
5
+
6
+ /**
7
+ * 执行 Google reCAPTCHA 验证
8
+ */
9
+ const executeRecaptcha = async (action: string, sitekey: string): Promise<string | false> => {
10
+ if (typeof window === 'undefined' || !window.grecaptcha?.execute) {
11
+ console.warn('[LiveChat Fetcher] reCAPTCHA not loaded')
12
+ return false
13
+ }
14
+
15
+ try {
16
+ const token = await window.grecaptcha.execute(sitekey, { action })
17
+ return token
18
+ } catch (error) {
19
+ console.error('[LiveChat Fetcher] reCAPTCHA execution failed:', error)
20
+ return false
21
+ }
22
+ }
23
+
24
+ /**
25
+ * 获取 reCAPTCHA headers
26
+ */
27
+ async function getRecaptchaHeaders(
28
+ action: string,
29
+ sitekey: string,
30
+ headerKey = 'X-Recaptcha-Token'
31
+ ): Promise<Record<string, string>> {
32
+ const recaptchaToken = await executeRecaptcha(action, sitekey)
33
+ if (!recaptchaToken) {
34
+ return {}
35
+ }
36
+ return {
37
+ [headerKey]: recaptchaToken,
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Fetcher 参数
43
+ */
44
+ export interface FetcherOptions {
45
+ url: string
46
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
47
+ headers?: Record<string, string>
48
+ body?: any
49
+ timeout?: number
50
+ needRecaptcha?: boolean
51
+ recaptchaSitekey?: string
52
+ recaptchaAction?: string
53
+ recaptchaHeaderKey?: string
54
+ }
55
+
56
+ /**
57
+ * Fetcher 函数
58
+ * 支持 reCAPTCHA 和自定义 headers
59
+ */
60
+ export const fetcher = async ({
61
+ url,
62
+ method = 'POST',
63
+ headers = {},
64
+ body,
65
+ timeout = 90000,
66
+ needRecaptcha = false,
67
+ recaptchaSitekey,
68
+ recaptchaAction = 'activity',
69
+ recaptchaHeaderKey = 'X-Recaptcha-Token',
70
+ }: FetcherOptions): Promise<Response> => {
71
+ // 获取 reCAPTCHA headers(如果需要)
72
+ let recaptchaHeaders: Record<string, string> = {}
73
+ if (needRecaptcha) {
74
+ if (!recaptchaSitekey) {
75
+ console.warn('[LiveChat Fetcher] needRecaptcha=true but recaptchaSitekey is missing')
76
+ } else {
77
+ recaptchaHeaders = await getRecaptchaHeaders(recaptchaAction, recaptchaSitekey, recaptchaHeaderKey)
78
+ }
79
+ }
80
+
81
+ // 准备请求体
82
+ const bodyData = body ? JSON.stringify(body) : undefined
83
+
84
+ const controller = new AbortController()
85
+ let timeoutTimer: NodeJS.Timeout | undefined
86
+ if (timeout) {
87
+ timeoutTimer = setTimeout(() => controller.abort(), timeout)
88
+ }
89
+
90
+ try {
91
+ const response = await fetch(url, {
92
+ method,
93
+ mode: 'cors',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ ...headers,
97
+ ...recaptchaHeaders,
98
+ },
99
+ signal: controller.signal,
100
+ ...(method !== 'GET' && bodyData && { body: bodyData }),
101
+ })
102
+
103
+ if (timeoutTimer) {
104
+ clearTimeout(timeoutTimer)
105
+ }
106
+
107
+ return response
108
+ } catch (error) {
109
+ if (timeoutTimer) {
110
+ clearTimeout(timeoutTimer)
111
+ }
112
+ throw error
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 扩展 Window 接口以支持 grecaptcha
118
+ */
119
+ declare global {
120
+ interface Window {
121
+ grecaptcha?: {
122
+ execute: (sitekey: string, options: { action: string }) => Promise<string>
123
+ ready: (callback: () => void) => void
124
+ }
125
+ }
126
+ }
@@ -85,6 +85,27 @@ const customRenderers = {
85
85
  defaultValue: { summary: 'undefined' },
86
86
  },
87
87
  },
88
+ needRecaptcha: {
89
+ control: 'boolean',
90
+ description: '是否启用 Google reCAPTCHA v3 验证',
91
+ table: {
92
+ defaultValue: { summary: 'false' },
93
+ },
94
+ },
95
+ recaptchaSitekey: {
96
+ control: 'text',
97
+ description: 'Google reCAPTCHA v3 site key',
98
+ table: {
99
+ defaultValue: { summary: 'undefined' },
100
+ },
101
+ },
102
+ recaptchaAction: {
103
+ control: 'text',
104
+ description: 'reCAPTCHA action 名称,用于区分不同的验证场景',
105
+ table: {
106
+ defaultValue: { summary: '"activity"' },
107
+ },
108
+ },
88
109
  site: {
89
110
  control: 'text',
90
111
  description: 'Shopify 店铺 URL',
@@ -138,10 +159,10 @@ export const Default: Story = {
138
159
 
139
160
  site: 'beta.eufy.com',
140
161
 
141
- headers: {
142
- 'X-Recaptcha-Sitekey': '6LfS4J4pAAAAACX1e_WrxutmxxzCK7FU4WzVqL14',
143
- recaptcha: '',
144
- },
162
+ // reCAPTCHA 配置(使用组件 props,不是 headers
163
+ needRecaptcha: true,
164
+ recaptchaSitekey: '6LfS4J4pAAAAACX1e_WrxutmxxzCK7FU4WzVqL14 999testtest',
165
+ recaptchaAction: 'chat',
145
166
  },
146
167
  }
147
168
 
@@ -492,3 +513,60 @@ FAQ 列表消息支持:
492
513
  },
493
514
  },
494
515
  }
516
+
517
+ /**
518
+ * 带 reCAPTCHA 配置
519
+ *
520
+ * 展示如何配置 Google reCAPTCHA v3 验证,保护 API 请求免受滥用。
521
+ *
522
+ * reCAPTCHA 会在后台自动验证用户行为,无需用户手动操作。
523
+ */
524
+ export const WithRecaptcha: Story = {
525
+ args: {
526
+ // 启用 reCAPTCHA
527
+ needRecaptcha: true,
528
+ // 配置你的 reCAPTCHA site key
529
+ recaptchaSitekey: '6LfS4J4pAAAAACX1e_WrxutmxxzCK7FU4WzVqL14',
530
+ // 可选:自定义 action 名称
531
+ recaptchaAction: 'livechat',
532
+ },
533
+ parameters: {
534
+ docs: {
535
+ description: {
536
+ story: `
537
+ ### reCAPTCHA v3 配置
538
+
539
+ 启用 reCAPTCHA v3 可以保护你的 API 免受机器人和滥用行为的攻击。
540
+
541
+ **配置步骤:**
542
+
543
+ 1. 在 [Google reCAPTCHA](https://www.google.com/recaptcha/admin) 创建 v3 密钥
544
+ 2. 在页面中加载 reCAPTCHA 脚本:
545
+ \`\`\`html
546
+ <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
547
+ \`\`\`
548
+ 3. 配置 LiveChatWidget 组件:
549
+ \`\`\`tsx
550
+ <LiveChatWidget
551
+ needRecaptcha={true}
552
+ recaptchaSitekey="YOUR_SITE_KEY"
553
+ recaptchaAction="livechat"
554
+ />
555
+ \`\`\`
556
+
557
+ **工作原理:**
558
+
559
+ - 每次发送消息或创建会话时,会自动获取 reCAPTCHA token
560
+ - token 会通过 \`X-Recaptcha-Token\` header 发送到后端
561
+ - 后端需要验证 token 的有效性
562
+
563
+ **注意事项:**
564
+
565
+ - reCAPTCHA v3 在后台运行,不会打断用户体验
566
+ - 建议为不同的操作使用不同的 \`recaptchaAction\` 以便分析
567
+ - 确保后端正确验证 reCAPTCHA token
568
+ `,
569
+ },
570
+ },
571
+ },
572
+ }