@anker-in/campaign-ui 0.3.3 → 0.3.5
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 +21 -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 +23 -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/components/ChatHeader.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
- package/dist/cjs/components/LiveChatWidget/components/ChatInput.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/ChatInput.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js +2 -2
- package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
- package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
- package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
- package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageList.js +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/constants.d.ts +5 -0
- package/dist/cjs/components/LiveChatWidget/constants.js +1 -1
- package/dist/cjs/components/LiveChatWidget/constants.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -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/hooks/useChatState.d.ts +36 -2
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/index.d.ts +1 -1
- package/dist/cjs/components/LiveChatWidget/index.js +1 -1
- package/dist/cjs/components/LiveChatWidget/index.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/types.d.ts +213 -3
- 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 +42 -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/components/chat/markdown.js +1 -1
- package/dist/cjs/components/chat/markdown.js.map +2 -2
- package/dist/cjs/components/index.d.ts +2 -0
- package/dist/cjs/components/index.js +1 -1
- package/dist/cjs/components/index.js.map +3 -3
- package/dist/cjs/stories/LiveChatWidget.stories.d.ts +1 -79
- package/dist/cjs/stories/LiveChatWidget.stories.js +3 -49
- package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.d.ts +21 -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 +23 -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/components/ChatHeader.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
- package/dist/esm/components/LiveChatWidget/components/ChatInput.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/ChatInput.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ChatMessage.js +2 -2
- package/dist/esm/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
- package/dist/esm/components/LiveChatWidget/components/ChatWindow.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
- package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
- package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
- package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageList.js +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/constants.d.ts +5 -0
- package/dist/esm/components/LiveChatWidget/constants.js +1 -1
- package/dist/esm/components/LiveChatWidget/constants.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -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/hooks/useChatState.d.ts +36 -2
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/index.d.ts +1 -1
- package/dist/esm/components/LiveChatWidget/index.js +1 -1
- package/dist/esm/components/LiveChatWidget/index.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/types.d.ts +213 -3
- package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +42 -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/components/chat/markdown.js +1 -1
- package/dist/esm/components/chat/markdown.js.map +2 -2
- package/dist/esm/components/index.d.ts +2 -0
- package/dist/esm/components/index.js +1 -1
- package/dist/esm/components/index.js.map +3 -3
- package/dist/esm/stories/LiveChatWidget.stories.d.ts +1 -79
- package/dist/esm/stories/LiveChatWidget.stories.js +3 -49
- package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
- package/dist/index.d.mts +1305 -0
- package/dist/index.d.ts +1305 -0
- package/dist/index.js +26656 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +26641 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +8 -1
- package/src/components/LiveChatWidget/LiveChatWidget.tsx +907 -0
- package/src/components/LiveChatWidget/api/chat.ts +175 -0
- package/src/components/LiveChatWidget/components/ChatBubble.tsx +152 -0
- package/src/components/LiveChatWidget/components/ChatHeader.tsx +150 -0
- package/src/components/LiveChatWidget/components/ChatInput.tsx +253 -0
- package/src/components/LiveChatWidget/components/ChatMessage.tsx +190 -0
- package/src/components/LiveChatWidget/components/ChatWindow.tsx +363 -0
- package/src/components/LiveChatWidget/components/ComplianceDialog.tsx +216 -0
- package/src/components/LiveChatWidget/components/MessageContent/CartCard.tsx +202 -0
- package/src/components/LiveChatWidget/components/MessageContent/ErrorBlock.tsx +75 -0
- package/src/components/LiveChatWidget/components/MessageContent/FAQList.tsx +128 -0
- package/src/components/LiveChatWidget/components/MessageContent/PolicyBlock.tsx +152 -0
- package/src/components/LiveChatWidget/components/MessageContent/ProductCard.tsx +227 -0
- package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +377 -0
- package/src/components/LiveChatWidget/components/MessageContent/ProductList.tsx +293 -0
- package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +170 -0
- package/src/components/LiveChatWidget/components/MessageContent/QuickReplies.tsx +91 -0
- package/src/components/LiveChatWidget/components/MessageContent/TextBlock.tsx +110 -0
- package/src/components/LiveChatWidget/components/MessageContent/ThinkingBlock.tsx +53 -0
- package/src/components/LiveChatWidget/components/MessageContent/index.ts +16 -0
- package/src/components/LiveChatWidget/components/MessageContent.tsx +113 -0
- package/src/components/LiveChatWidget/components/MessageList.tsx +256 -0
- package/src/components/LiveChatWidget/components/ScrollAnchor.tsx +75 -0
- package/src/components/LiveChatWidget/constants.ts +36 -0
- package/src/components/LiveChatWidget/hooks/useChatAPI.ts +146 -0
- package/src/components/LiveChatWidget/hooks/useChatState.ts +1091 -0
- package/src/components/LiveChatWidget/hooks/useSession.ts +123 -0
- package/src/components/LiveChatWidget/index.tsx +63 -0
- package/src/components/LiveChatWidget/types.ts +1012 -0
- package/src/components/LiveChatWidget/utils/cartTransformers.ts +72 -0
- package/src/components/LiveChatWidget/utils/fetcher.ts +131 -0
- package/src/components/LiveChatWidget/utils/messageRenderers.ts +120 -0
- package/src/components/LiveChatWidget/utils/productTransformers.ts +149 -0
- package/src/components/LiveChatWidget/utils/userId.ts +140 -0
- package/src/components/LiveChatWidget/utils/validation.ts +99 -0
- package/src/components/chat/markdown.tsx +1 -1
- package/src/components/index.ts +23 -0
- package/src/stories/LiveChatWidget.stories.tsx +317 -0
- package/src/styles/livechat.css +346 -0
- package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
- package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
- package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/atobID.d.ts +0 -1
- package/dist/cjs/components/credits/context/utils/atobID.js +0 -2
- package/dist/cjs/components/credits/context/utils/atobID.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
- package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js +0 -2
- package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
- package/dist/cjs/components/credits/context/utils/variantGetCoupon.js +0 -2
- package/dist/cjs/components/credits/context/utils/variantGetCoupon.js.map +0 -7
- package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
- package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
- package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
- package/dist/esm/components/credits/context/utils/atobID.d.ts +0 -1
- package/dist/esm/components/credits/context/utils/atobID.js +0 -2
- package/dist/esm/components/credits/context/utils/atobID.js.map +0 -7
- package/dist/esm/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
- package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js +0 -2
- package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
- package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
- package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
- package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
- package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
- package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
- package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
- package/dist/esm/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
- package/dist/esm/components/credits/context/utils/variantGetCoupon.js +0 -2
- package/dist/esm/components/credits/context/utils/variantGetCoupon.js.map +0 -7
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 购物车数据转换工具
|
|
3
|
+
* 将后端返回的 Shopify GraphQL 格式转换为前端标准格式
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BackendCartData, CartData, CartLine } from '../types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 转换后端购物车数据为前端格式
|
|
10
|
+
*
|
|
11
|
+
* @param backendData 后端返回的 Shopify GraphQL 格式数据
|
|
12
|
+
* @returns 前端标准化的购物车数据
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const backendData = {
|
|
17
|
+
* id: "gid://shopify/Cart/xxx",
|
|
18
|
+
* lines: { edges: [{ node: {...} }] },
|
|
19
|
+
* cost: { totalAmount: {...}, subtotalAmount: {...} },
|
|
20
|
+
* ...
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const cartData = transformCartData(backendData)
|
|
24
|
+
* // => { cartId: "gid://shopify/Cart/xxx", lines: [...], ... }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function transformCartData(backendData: BackendCartData): CartData {
|
|
28
|
+
// 转换商品行数据:从 edges[].node 结构转为扁平数组
|
|
29
|
+
const lines: CartLine[] = backendData.lines.edges.map(edge => ({
|
|
30
|
+
id: edge.node.id,
|
|
31
|
+
quantity: edge.node.quantity,
|
|
32
|
+
cost: {
|
|
33
|
+
totalAmount: edge.node.cost.totalAmount,
|
|
34
|
+
amountPerQuantity: edge.node.cost.amountPerQuantity,
|
|
35
|
+
subtotalAmount: edge.node.cost.subtotalAmount,
|
|
36
|
+
},
|
|
37
|
+
merchandise: {
|
|
38
|
+
id: edge.node.merchandise.id,
|
|
39
|
+
title: edge.node.merchandise.title,
|
|
40
|
+
price: edge.node.merchandise.price,
|
|
41
|
+
image: edge.node.merchandise.image
|
|
42
|
+
? {
|
|
43
|
+
url: edge.node.merchandise.image.url,
|
|
44
|
+
altText: edge.node.merchandise.image.altText || undefined,
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
47
|
+
product: {
|
|
48
|
+
id: edge.node.merchandise.product.id,
|
|
49
|
+
title: edge.node.merchandise.product.title,
|
|
50
|
+
handle: edge.node.merchandise.product.handle,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
attributes: edge.node.attributes,
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
// 返回标准化的购物车数据
|
|
57
|
+
return {
|
|
58
|
+
isEmpty: backendData.totalQuantity === 0 || lines.length === 0,
|
|
59
|
+
cartId: backendData.id,
|
|
60
|
+
totalQuantity: backendData.totalQuantity,
|
|
61
|
+
lines,
|
|
62
|
+
cost: {
|
|
63
|
+
totalAmount: backendData.cost.totalAmount,
|
|
64
|
+
subtotalAmount: backendData.cost.subtotalAmount,
|
|
65
|
+
},
|
|
66
|
+
discountCodes: backendData.discountCodes?.map((code: any) => ({
|
|
67
|
+
code: code.code || code,
|
|
68
|
+
applicable: code.applicable !== false,
|
|
69
|
+
})),
|
|
70
|
+
checkoutUrl: backendData.checkoutUrl,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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?.enterprise?.execute) {
|
|
11
|
+
console.warn('[LiveChat Fetcher] reCAPTCHA not loaded')
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const token = await window.grecaptcha.enterprise.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 = '',
|
|
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
|
+
enterprise?: {
|
|
125
|
+
execute: (sitekey: string, options: { action: string }) =>
|
|
126
|
+
Promise<string>
|
|
127
|
+
ready: (callback: () => void) => void
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息渲染器注册表
|
|
3
|
+
* 支持自定义消息类型的扩展机制
|
|
4
|
+
* 基于 specs/livechat-widget/plan.md 的扩展机制设计
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MessageContent, MessageRenderer } from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 消息渲染器注册表
|
|
11
|
+
* 允许注册自定义的消息类型渲染器
|
|
12
|
+
*/
|
|
13
|
+
export class MessageRendererRegistry {
|
|
14
|
+
private renderers = new Map<string, MessageRenderer>()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 注册自定义渲染器
|
|
18
|
+
* @param type 消息内容类型
|
|
19
|
+
* @param renderer 渲染器实现
|
|
20
|
+
*/
|
|
21
|
+
register(type: string, renderer: MessageRenderer): void {
|
|
22
|
+
if (!type || typeof type !== 'string') {
|
|
23
|
+
throw new Error('Message type must be a non-empty string')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!renderer) {
|
|
27
|
+
throw new Error('Renderer is required')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 检查 renderer 是否有 render 属性
|
|
31
|
+
if (!('render' in renderer) || typeof renderer.render !== 'function') {
|
|
32
|
+
throw new Error('Renderer must have a render function')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.renderers.set(type, renderer)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 批量注册渲染器
|
|
40
|
+
* @param renderers 渲染器映射表
|
|
41
|
+
*/
|
|
42
|
+
registerMany(renderers: Record<string, MessageRenderer>): void {
|
|
43
|
+
Object.entries(renderers).forEach(([type, renderer]) => {
|
|
44
|
+
this.register(type, renderer)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取指定类型的渲染器
|
|
50
|
+
* @param type 消息内容类型
|
|
51
|
+
* @returns 渲染器实现,如果未注册则返回 undefined
|
|
52
|
+
*/
|
|
53
|
+
get(type: string): MessageRenderer | undefined {
|
|
54
|
+
return this.renderers.get(type)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 检查是否已注册指定类型的渲染器
|
|
59
|
+
* @param type 消息内容类型
|
|
60
|
+
* @returns 是否已注册
|
|
61
|
+
*/
|
|
62
|
+
has(type: string): boolean {
|
|
63
|
+
return this.renderers.has(type)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 移除指定类型的渲染器
|
|
68
|
+
* @param type 消息内容类型
|
|
69
|
+
* @returns 是否成功移除
|
|
70
|
+
*/
|
|
71
|
+
unregister(type: string): boolean {
|
|
72
|
+
return this.renderers.delete(type)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 清空所有已注册的渲染器
|
|
77
|
+
*/
|
|
78
|
+
clear(): void {
|
|
79
|
+
this.renderers.clear()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 渲染消息内容
|
|
84
|
+
* @param content 消息内容
|
|
85
|
+
* @param isUser 是否为用户消息
|
|
86
|
+
* @param isSystem 是否为系统消息
|
|
87
|
+
* @returns 渲染结果,如果未找到对应渲染器则返回 null
|
|
88
|
+
*/
|
|
89
|
+
render(content: MessageContent, isUser: boolean, isSystem: boolean): React.ReactNode {
|
|
90
|
+
const renderer = this.renderers.get(content.type)
|
|
91
|
+
|
|
92
|
+
if (!renderer) {
|
|
93
|
+
console.warn(`[MessageRendererRegistry] No renderer found for type: ${content.type}`)
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
return renderer.render(content, isUser, isSystem)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(`[MessageRendererRegistry] Error rendering ${content.type}:`, error)
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 获取所有已注册的消息类型
|
|
107
|
+
* @returns 消息类型数组
|
|
108
|
+
*/
|
|
109
|
+
getRegisteredTypes(): string[] {
|
|
110
|
+
return Array.from(this.renderers.keys())
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 创建默认的渲染器注册表实例
|
|
116
|
+
* @returns 新的注册表实例
|
|
117
|
+
*/
|
|
118
|
+
export function createRendererRegistry(): MessageRendererRegistry {
|
|
119
|
+
return new MessageRendererRegistry()
|
|
120
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 产品数据转换工具函数
|
|
3
|
+
* 将后端返回的产品数据转换为前端使用的格式
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Product, Variant } from '../types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 转换单个变体数据
|
|
10
|
+
*/
|
|
11
|
+
function transformVariant(variant: any): Variant {
|
|
12
|
+
return {
|
|
13
|
+
id: variant.shopify_variant_id || variant.id,
|
|
14
|
+
title: variant.title || variant.option1,
|
|
15
|
+
sku: variant.sku,
|
|
16
|
+
price: variant.price
|
|
17
|
+
? {
|
|
18
|
+
amount: variant.price,
|
|
19
|
+
currency: variant.currency || 'USD',
|
|
20
|
+
}
|
|
21
|
+
: undefined,
|
|
22
|
+
availableForSale: variant.available,
|
|
23
|
+
discount: variant.discount
|
|
24
|
+
? {
|
|
25
|
+
has_discount: variant.discount.has_discount,
|
|
26
|
+
discount_price: variant.discount.discount_price,
|
|
27
|
+
discount_code: variant.discount.discount_code,
|
|
28
|
+
discount_percentage: variant.discount.discount_percentage,
|
|
29
|
+
discount_type: variant.discount.discount_type,
|
|
30
|
+
discount_value: variant.discount.discount_value,
|
|
31
|
+
}
|
|
32
|
+
: undefined,
|
|
33
|
+
memberPrice: variant.member_price
|
|
34
|
+
? {
|
|
35
|
+
has_member_price: variant.member_price.has_member_price,
|
|
36
|
+
price: variant.member_price.price,
|
|
37
|
+
}
|
|
38
|
+
: undefined,
|
|
39
|
+
inventoryQuantity: variant.inventory_quantity,
|
|
40
|
+
option1: variant.option1,
|
|
41
|
+
option2: variant.option2,
|
|
42
|
+
option3: variant.option3,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 转换产品数据
|
|
48
|
+
* 将后端返回的产品对象转换为前端使用的 Product 类型
|
|
49
|
+
*
|
|
50
|
+
* @param product - 后端返回的产品数据
|
|
51
|
+
* @param site - 站点域名,用于构建产品 URL(可选,默认为空字符串)
|
|
52
|
+
* @returns 转换后的 Product 对象
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const backendProduct = {
|
|
57
|
+
* shopify_product_id: "gid://shopify/Product/123",
|
|
58
|
+
* handle: "product-handle",
|
|
59
|
+
* title: "Product Title",
|
|
60
|
+
* price_range: { min: 99.99, max: 149.99, currency: "USD" },
|
|
61
|
+
* // ... 其他字段
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* const product = transformProduct(backendProduct, "www.example.com")
|
|
65
|
+
* // product.shopifyId === "gid://shopify/Product/123"
|
|
66
|
+
* // product.productUrl === "https://www.example.com/products/product-handle"
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function transformProduct(product: any, site: string = ''): Product {
|
|
70
|
+
return {
|
|
71
|
+
// 基本信息
|
|
72
|
+
shopifyId: product.shopify_product_id || product.id,
|
|
73
|
+
sku: product.variants?.[0]?.sku || '',
|
|
74
|
+
handle: product.handle,
|
|
75
|
+
title: product.title,
|
|
76
|
+
description: product.description,
|
|
77
|
+
vendor: product.vendor,
|
|
78
|
+
|
|
79
|
+
// 价格信息
|
|
80
|
+
price: {
|
|
81
|
+
amount: product.price_range?.min || product.variants?.[0]?.price || 0,
|
|
82
|
+
currency: product.price_range?.currency || product.variants?.[0]?.currency || 'USD',
|
|
83
|
+
},
|
|
84
|
+
priceRange: product.price_range
|
|
85
|
+
? {
|
|
86
|
+
min: product.price_range.min,
|
|
87
|
+
max: product.price_range.max,
|
|
88
|
+
currency: product.price_range.currency,
|
|
89
|
+
}
|
|
90
|
+
: undefined,
|
|
91
|
+
memberPriceRange: product.member_price_range
|
|
92
|
+
? {
|
|
93
|
+
min: product.member_price_range.min,
|
|
94
|
+
max: product.member_price_range.max,
|
|
95
|
+
currency: product.member_price_range.currency,
|
|
96
|
+
}
|
|
97
|
+
: undefined,
|
|
98
|
+
|
|
99
|
+
// 媒体和链接
|
|
100
|
+
imageUrl: product.featured_image || '',
|
|
101
|
+
productUrl: site ? `https://${site}/products/${product.handle}` : `/products/${product.handle}`,
|
|
102
|
+
|
|
103
|
+
// 库存和状态
|
|
104
|
+
stockStatus: product.variants?.[0]?.available ? 'in_stock' : 'out_of_stock',
|
|
105
|
+
|
|
106
|
+
// 评分和热度
|
|
107
|
+
hotScore: product.popularity_score,
|
|
108
|
+
averageRating: product.average_rating,
|
|
109
|
+
reviewCount: product.review_count,
|
|
110
|
+
|
|
111
|
+
// 变体信息
|
|
112
|
+
variantCount: product.variant_count,
|
|
113
|
+
availableCount: product.available_count,
|
|
114
|
+
variants: product.variants?.map(transformVariant),
|
|
115
|
+
|
|
116
|
+
// 特性和标签
|
|
117
|
+
features: product.features
|
|
118
|
+
? {
|
|
119
|
+
is_new: product.features.is_new,
|
|
120
|
+
has_rental: product.features.has_rental,
|
|
121
|
+
has_presale: product.features.has_presale,
|
|
122
|
+
has_member_price: product.features.has_member_price,
|
|
123
|
+
has_discount: product.features.has_discount,
|
|
124
|
+
}
|
|
125
|
+
: undefined,
|
|
126
|
+
tags: product.tags,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 批量转换产品列表
|
|
132
|
+
*
|
|
133
|
+
* @param products - 后端返回的产品数组
|
|
134
|
+
* @param site - 站点域名(可选)
|
|
135
|
+
* @returns 转换后的 Product 数组
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* const backendProducts = [product1, product2, product3]
|
|
140
|
+
* const products = transformProducts(backendProducts, "www.example.com")
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function transformProducts(products: any[], site?: string): Product[] {
|
|
144
|
+
if (!Array.isArray(products)) {
|
|
145
|
+
return []
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return products.map(product => transformProduct(product, site))
|
|
149
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* userId 生成和管理工具
|
|
3
|
+
* 基于 specs/livechat-widget/research.md 的决策
|
|
4
|
+
*
|
|
5
|
+
* 策略:
|
|
6
|
+
* 1. 优先从 localStorage 读取
|
|
7
|
+
* 2. 尝试获取 Google Analytics ID (GAID)
|
|
8
|
+
* 3. 兜底:时间戳 + 随机数哈希
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'livechat_user_id'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取用户唯一标识符(异步版本)
|
|
15
|
+
* @returns userId (GAID 或哈希值)
|
|
16
|
+
*/
|
|
17
|
+
export async function getUserId(): Promise<string> {
|
|
18
|
+
// 1. 尝试从 localStorage 读取
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
21
|
+
if (stored) return stored
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. 尝试获取 GAID
|
|
25
|
+
const gaid = getGAID()
|
|
26
|
+
if (gaid) {
|
|
27
|
+
if (typeof window !== 'undefined') {
|
|
28
|
+
localStorage.setItem(STORAGE_KEY, gaid)
|
|
29
|
+
}
|
|
30
|
+
return gaid
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. 兜底:使用 SHA-256 生成哈希
|
|
34
|
+
try {
|
|
35
|
+
const fallback = await generateHashedUserId()
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
localStorage.setItem(STORAGE_KEY, fallback)
|
|
38
|
+
}
|
|
39
|
+
return fallback
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// 如果 SHA-256 失败,使用同步兜底方案
|
|
42
|
+
console.warn('[LiveChat userId] SHA-256 failed, using sync fallback:', error)
|
|
43
|
+
const fallback = generateHashedUserIdSync()
|
|
44
|
+
if (typeof window !== 'undefined') {
|
|
45
|
+
localStorage.setItem(STORAGE_KEY, fallback)
|
|
46
|
+
}
|
|
47
|
+
return fallback
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 从 Google Analytics 获取 Client ID (GAID)
|
|
53
|
+
* @returns GAID 或 null
|
|
54
|
+
*/
|
|
55
|
+
function getGAID(): string | null {
|
|
56
|
+
if (typeof window === 'undefined') return null
|
|
57
|
+
|
|
58
|
+
// 检查 gtag 是否可用
|
|
59
|
+
if (typeof (window as any).gtag !== 'function') {
|
|
60
|
+
console.warn('[LiveChat userId] Google Analytics gtag is not available')
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Google Analytics 4 方法
|
|
66
|
+
// 注意:gtag 的 'get' 命令是异步的,这里使用同步 fallback
|
|
67
|
+
// 在实际项目中,如果 GA 已初始化,可以从 dataLayer 读取
|
|
68
|
+
const dataLayer = (window as any).dataLayer || []
|
|
69
|
+
|
|
70
|
+
// 尝试从 dataLayer 中查找已存在的 client_id
|
|
71
|
+
for (const item of dataLayer) {
|
|
72
|
+
if (item && item[1] && typeof item[1] === 'object') {
|
|
73
|
+
const clientId = item[1].client_id || item[1].clientId
|
|
74
|
+
if (clientId && typeof clientId === 'string') {
|
|
75
|
+
return `G-${clientId}`
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 如果没有找到,返回 null,使用兜底方案
|
|
81
|
+
return null
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('[LiveChat userId] Failed to get GAID:', error)
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 生成哈希用户 ID(兜底方案)
|
|
90
|
+
* @returns 格式为 user-{hash} 的用户 ID,hash 由 timestamp + random 通过 SHA-256 生成
|
|
91
|
+
*/
|
|
92
|
+
async function generateHashedUserId(): Promise<string> {
|
|
93
|
+
const timestamp = Date.now()
|
|
94
|
+
const random = Math.random().toString(36).substring(2, 15) // 生成随机字符串
|
|
95
|
+
|
|
96
|
+
// 将 timestamp 和 random 组合成字符串
|
|
97
|
+
const rawString = `${timestamp}${random}`
|
|
98
|
+
|
|
99
|
+
// 使用 Web Crypto API 生成 SHA-256 哈希
|
|
100
|
+
const encoder = new TextEncoder()
|
|
101
|
+
const data = encoder.encode(rawString)
|
|
102
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
103
|
+
|
|
104
|
+
// 将 ArrayBuffer 转换为 16 进制字符串
|
|
105
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
106
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
|
107
|
+
|
|
108
|
+
// 取前 16 位作为用户 ID(保持简洁)
|
|
109
|
+
return `user-${hashHex.substring(0, 16)}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 同步版本的哈希用户 ID 生成(兜底的兜底)
|
|
114
|
+
* 当 Web Crypto API 不可用时使用
|
|
115
|
+
*/
|
|
116
|
+
function generateHashedUserIdSync(): string {
|
|
117
|
+
const timestamp = Date.now()
|
|
118
|
+
const random = Math.random().toString(36).substring(2, 15)
|
|
119
|
+
const rawString = `${timestamp}${random}`
|
|
120
|
+
|
|
121
|
+
// 使用简单的哈希算法作为兜底
|
|
122
|
+
let hash = 0
|
|
123
|
+
for (let i = 0; i < rawString.length; i++) {
|
|
124
|
+
const char = rawString.charCodeAt(i)
|
|
125
|
+
hash = (hash << 5) - hash + char
|
|
126
|
+
hash = hash & hash
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hashString = Math.abs(hash).toString(16).padStart(8, '0')
|
|
130
|
+
return `user-${hashString}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 清除保存的 userId(用于测试或重置)
|
|
135
|
+
*/
|
|
136
|
+
export function clearUserId(): void {
|
|
137
|
+
if (typeof window !== 'undefined') {
|
|
138
|
+
localStorage.removeItem(STORAGE_KEY)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 输入验证工具
|
|
3
|
+
* 基于 specs/livechat-widget/research.md 的 XSS 防护策略
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 清理用户输入
|
|
8
|
+
* - 移除控制字符
|
|
9
|
+
* - 限制长度
|
|
10
|
+
* @param input 用户输入的字符串
|
|
11
|
+
* @param maxLength 最大长度
|
|
12
|
+
* @returns 清理后的字符串
|
|
13
|
+
*/
|
|
14
|
+
export function sanitizeInput(input: string, maxLength: number = 5000): string {
|
|
15
|
+
if (!input || typeof input !== 'string') {
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 移除控制字符(ASCII 0-31 和 127)
|
|
20
|
+
let sanitized = input.replace(/[\x00-\x1F\x7F]/g, '')
|
|
21
|
+
|
|
22
|
+
// 限制长度
|
|
23
|
+
if (sanitized.length > maxLength) {
|
|
24
|
+
sanitized = sanitized.substring(0, maxLength)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 去除首尾空白
|
|
28
|
+
return sanitized.trim()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 验证 URL 是否安全
|
|
33
|
+
* @param url URL 字符串
|
|
34
|
+
* @returns 是否为安全的 URL
|
|
35
|
+
*/
|
|
36
|
+
export function isValidUrl(url: string): boolean {
|
|
37
|
+
if (!url || typeof url !== 'string') {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const parsed = new URL(url)
|
|
43
|
+
// 只允许 http 和 https 协议
|
|
44
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
|
|
45
|
+
} catch {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 验证 sessionId 是否为有效的 UUID
|
|
52
|
+
* @param sessionId 会话 ID
|
|
53
|
+
* @returns 是否为有效的 UUID
|
|
54
|
+
*/
|
|
55
|
+
export function isValidUUID(sessionId: string): boolean {
|
|
56
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// UUID v4 格式:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
61
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
62
|
+
return uuidRegex.test(sessionId)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 验证消息内容是否有效
|
|
67
|
+
* @param content 消息内容
|
|
68
|
+
* @returns 是否有效
|
|
69
|
+
*/
|
|
70
|
+
export function isValidMessageContent(content: string): boolean {
|
|
71
|
+
if (!content || typeof content !== 'string') {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 去除空白后不能为空
|
|
76
|
+
const trimmed = content.trim()
|
|
77
|
+
return trimmed.length > 0 && trimmed.length <= 5000
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 转义 HTML 特殊字符
|
|
82
|
+
* @param text 原始文本
|
|
83
|
+
* @returns 转义后的文本
|
|
84
|
+
*/
|
|
85
|
+
export function escapeHtml(text: string): string {
|
|
86
|
+
if (!text || typeof text !== 'string') {
|
|
87
|
+
return ''
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const map: Record<string, string> = {
|
|
91
|
+
'&': '&',
|
|
92
|
+
'<': '<',
|
|
93
|
+
'>': '>',
|
|
94
|
+
'"': '"',
|
|
95
|
+
"'": ''',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return text.replace(/[&<>"']/g, char => map[char] || char)
|
|
99
|
+
}
|