@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.
- package/dist/cjs/components/LiveChatWidget/LiveChatWidget.d.ts +23 -1
- package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
- package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/api/chat.d.ts +21 -2
- package/dist/cjs/components/LiveChatWidget/api/chat.js +2 -2
- package/dist/cjs/components/LiveChatWidget/api/chat.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.d.ts +5 -0
- package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
- package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/types.d.ts +17 -0
- package/dist/cjs/components/LiveChatWidget/types.js +1 -1
- package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
- package/dist/cjs/components/LiveChatWidget/utils/fetcher.d.ts +36 -0
- package/dist/cjs/components/LiveChatWidget/utils/fetcher.js +2 -0
- package/dist/cjs/components/LiveChatWidget/utils/fetcher.js.map +7 -0
- package/dist/cjs/stories/LiveChatWidget.stories.d.ts +8 -0
- package/dist/cjs/stories/LiveChatWidget.stories.js +35 -3
- package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.d.ts +23 -1
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/api/chat.d.ts +21 -2
- package/dist/esm/components/LiveChatWidget/api/chat.js +2 -2
- package/dist/esm/components/LiveChatWidget/api/chat.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.d.ts +5 -0
- package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
- package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/types.d.ts +17 -0
- package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +36 -0
- package/dist/esm/components/LiveChatWidget/utils/fetcher.js +2 -0
- package/dist/esm/components/LiveChatWidget/utils/fetcher.js.map +7 -0
- package/dist/esm/stories/LiveChatWidget.stories.d.ts +8 -0
- package/dist/esm/stories/LiveChatWidget.stories.js +38 -6
- package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
- package/package.json +3 -3
- package/src/components/LiveChatWidget/LiveChatWidget.tsx +31 -1
- package/src/components/LiveChatWidget/api/chat.ts +40 -11
- package/src/components/LiveChatWidget/hooks/useChatAPI.ts +11 -6
- package/src/components/LiveChatWidget/types.ts +20 -0
- package/src/components/LiveChatWidget/utils/fetcher.ts +126 -0
- 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
|
-
|
|
143
|
-
|
|
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
|
+
}
|