@anker-in/campaign-ui 0.4.6 → 0.4.7

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 (37) hide show
  1. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
  2. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  3. package/dist/cjs/components/LiveChatWidget/api/chat.d.ts +1 -1
  4. package/dist/cjs/components/LiveChatWidget/api/chat.js +2 -2
  5. package/dist/cjs/components/LiveChatWidget/api/chat.js.map +3 -3
  6. package/dist/cjs/components/LiveChatWidget/components/MessageList.js +2 -2
  7. package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +3 -3
  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/hooks/useChatState.d.ts +5 -0
  11. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
  12. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  13. package/dist/cjs/components/LiveChatWidget/utils/fetcher.d.ts +3 -1
  14. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js +1 -1
  15. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js.map +3 -3
  16. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  17. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  18. package/dist/esm/components/LiveChatWidget/api/chat.d.ts +1 -1
  19. package/dist/esm/components/LiveChatWidget/api/chat.js +2 -2
  20. package/dist/esm/components/LiveChatWidget/api/chat.js.map +3 -3
  21. package/dist/esm/components/LiveChatWidget/components/MessageList.js +2 -2
  22. package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +3 -3
  23. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  24. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  25. package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +5 -0
  26. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
  27. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  28. package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +3 -1
  29. package/dist/esm/components/LiveChatWidget/utils/fetcher.js +1 -1
  30. package/dist/esm/components/LiveChatWidget/utils/fetcher.js.map +3 -3
  31. package/package.json +1 -1
  32. package/src/components/LiveChatWidget/LiveChatWidget.tsx +6 -4
  33. package/src/components/LiveChatWidget/api/chat.ts +3 -2
  34. package/src/components/LiveChatWidget/components/MessageList.tsx +20 -4
  35. package/src/components/LiveChatWidget/hooks/useChatAPI.ts +2 -3
  36. package/src/components/LiveChatWidget/hooks/useChatState.ts +27 -7
  37. package/src/components/LiveChatWidget/utils/fetcher.ts +15 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/components/LiveChatWidget/hooks/useChatState.ts"],
4
- "sourcesContent": ["/**\n * \u804A\u5929\u72B6\u6001\u7BA1\u7406 Hook\n * \u7BA1\u7406\u6D88\u606F\u5217\u8868\u3001\u7A97\u53E3\u72B6\u6001\u3001\u8F93\u5165\u6846\u72B6\u6001\u3001\u6D41\u5F0F\u6D88\u606F\u7D2F\u79EF\n * \u57FA\u4E8E specs/livechat-widget/plan.md \u7684\u72B6\u6001\u7BA1\u7406\u7B56\u7565\n */\n\nimport { useState, useCallback, useRef, useEffect } from 'react'\nimport type { Message, MessageContent, SSEEvent, StatusData, ErrorData, MessageStartData, BackendCartData, Product, TextContent, ProductCardContent, ProductListContent } from '../types'\nimport { getUserId, saveUserId } from '../utils/userId'\nimport { useSession } from './useSession'\nimport { transformProducts } from '../utils/productTransformers'\nimport { transformCartData } from '../utils/cartTransformers'\n\n// ============================================================================\n// \u8F85\u52A9\u51FD\u6570\uFF1A\u6587\u672C\u89E3\u6790\u548C\u6D88\u606F\u91CD\u7EC4\n// ============================================================================\n\n/**\n * \u5B9E\u65F6\u89E3\u6790\u6D41\u5F0F\u6587\u672C\u4E2D\u7684\u4EA7\u54C1\u5360\u4F4D\u7B26\n * \u5904\u7406\u7F13\u51B2\u533A\u4E2D\u7684\u6587\u672C\uFF0C\u68C0\u6D4B\u5B8C\u6574\u7684 {{product:xxx}} \u5360\u4F4D\u7B26\n *\n * @param buffer \u5F53\u524D\u7F13\u51B2\u533A\u5185\u5BB9\uFF08\u5305\u542B\u65B0\u63A5\u6536\u7684\u6587\u672C\uFF09\n * @param productMap handle \u2192 Product \u7684\u6620\u5C04\u8868\n * @param rawProductMap handle \u2192 raw backend product \u7684\u6620\u5C04\u8868\n * @param onAddToCart \u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n * @param productCardRender \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @returns { contents: \u9700\u8981\u6DFB\u52A0\u7684\u5185\u5BB9\u6570\u7EC4, remainingBuffer: \u5269\u4F59\u7F13\u51B2\u533A\u5185\u5BB9 }\n */\nfunction parseStreamingText(\n buffer: string,\n productMap: Map<string, Product>,\n rawProductMap: Map<string, any>,\n onAddToCart?: (product: Product) => void,\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n): { contents: MessageContent[]; remainingBuffer: string } {\n const contents: MessageContent[] = []\n const regex = /\\{\\{(?:product:)?([^}]+)\\}\\}/g\n\n let lastIndex = 0\n let match: RegExpExecArray | null\n let foundMatch = false\n\n // \u67E5\u627E\u6240\u6709\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\n while ((match = regex.exec(buffer)) !== null) {\n foundMatch = true\n\n // \u63D0\u53D6\u5360\u4F4D\u7B26\u524D\u7684\u6587\u672C\n const beforeText = buffer.slice(lastIndex, match.index)\n if (beforeText) {\n contents.push({ type: 'text', text: beforeText } as TextContent)\n }\n\n // \u63D0\u53D6\u4EA7\u54C1 ID \u5E76\u521B\u5EFA\u4EA7\u54C1\u5361\u7247\n const productId = match[1].trim()\n const product = productMap.get(productId)\n const rawProduct = rawProductMap.get(productId)\n\n // \u65E0\u8BBA\u662F\u5426\u627E\u5230\u4EA7\u54C1\u6570\u636E\uFF0C\u90FD\u6E32\u67D3 product_card\uFF0C\u5E94\u7528\u5C42\u53EF\u901A\u8FC7 productHandle \u67E5\u8BE2\u4EA7\u54C1\n if (product) {\n console.log('[useChatState] \uD83C\uDFAF \u5B9E\u65F6\u68C0\u6D4B\u5230\u4EA7\u54C1:', productId, '\u2192', product.title)\n } else {\n console.log('[useChatState] \uD83D\uDCE6 \u5B9E\u65F6\u68C0\u6D4B\u5230\u4EA7\u54C1\u5360\u4F4D\u7B26\uFF0C\u4EA7\u54C1\u6570\u636E\u5F85\u5E94\u7528\u5C42\u67E5\u8BE2:', productId)\n }\n\n contents.push({\n type: 'product_card',\n data: {\n product: product,\n rawProduct: rawProduct,\n productHandle: productId,\n onAddToCart: onAddToCart,\n productCardRender: productCardRender\n }\n } as ProductCardContent)\n\n lastIndex = regex.lastIndex\n }\n\n // \u5982\u679C\u627E\u5230\u4E86\u81F3\u5C11\u4E00\u4E2A\u5B8C\u6574\u5360\u4F4D\u7B26\n if (foundMatch) {\n // \u8FD4\u56DE\u5269\u4F59\u7684\u6587\u672C\u4F5C\u4E3A\u7F13\u51B2\u533A\uFF08\u53EF\u80FD\u5305\u542B\u4E0D\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\uFF09\n const remainingBuffer = buffer.slice(lastIndex)\n return { contents, remainingBuffer }\n } else {\n // \u6CA1\u6709\u627E\u5230\u5B8C\u6574\u5360\u4F4D\u7B26\uFF0C\u68C0\u67E5\u662F\u5426\u6709\u4E0D\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\u5F00\u5934\n // \u4F8B\u5982\uFF1A\u7F13\u51B2\u533A\u662F \"some text {{prod\"\uFF0C\u6211\u4EEC\u9700\u8981\u4FDD\u7559 \"{{prod\" \u7B49\u5F85\u66F4\u591A\u6587\u672C\n const incompleteMatch = buffer.match(/\\{\\{[^}]*$/)\n\n if (incompleteMatch) {\n // \u6709\u4E0D\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\u5F00\u5934\n const completeText = buffer.slice(0, incompleteMatch.index)\n if (completeText) {\n contents.push({ type: 'text', text: completeText } as TextContent)\n }\n return { contents, remainingBuffer: incompleteMatch[0] }\n } else {\n // \u6CA1\u6709\u4E0D\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\uFF0C\u6574\u4E2A\u7F13\u51B2\u533A\u90FD\u662F\u666E\u901A\u6587\u672C\n if (buffer) {\n contents.push({ type: 'text', text: buffer } as TextContent)\n }\n return { contents, remainingBuffer: '' }\n }\n }\n}\n\n/**\n * \u89E3\u6790\u6587\u672C\u4E2D\u7684 {{handle}}\uFF0C\u8FD4\u56DE [text, product_card, text, ...] \u6570\u7EC4\uFF08\u7528\u4E8E\u5386\u53F2\u6D88\u606F\u91CD\u7EC4\uFF09\n *\n * @param text \u5305\u542B {{handle}} \u6807\u8BB0\u7684\u6587\u672C\n * @param productMap handle \u2192 Product \u7684\u6620\u5C04\u8868\n * @param rawProductMap handle \u2192 raw backend product \u7684\u6620\u5C04\u8868\n * @param onAddToCart \u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n * @param productCardRender \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @returns MessageContent \u6570\u7EC4\n *\n * @example\n * \u8F93\u5165\uFF1A\n * text: \"\u6211\u63A8\u8350\u4EE5\u4E0B\u4EA7\u54C1\uFF1A\\n{{product-handle}}\\n\u8FD9\u6B3E\u4EA7\u54C1\u6027\u4EF7\u6BD4\u5F88\u9AD8\u3002\"\n * productMap: Map { 'product-handle' => Product {...} }\n * \u8F93\u51FA\uFF1A\n * [\n * { type: 'text', text: '\u6211\u63A8\u8350\u4EE5\u4E0B\u4EA7\u54C1\uFF1A' },\n * { type: 'product_card', data: { product: {...}, onAddToCart } },\n * { type: 'text', text: '\u8FD9\u6B3E\u4EA7\u54C1\u6027\u4EF7\u6BD4\u5F88\u9AD8\u3002' }\n * ]\n */\nfunction parseTextWithProductIds(\n text: string,\n productMap: Map<string, Product>,\n rawProductMap: Map<string, any>,\n onAddToCart?: (product: Product) => void,\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n): MessageContent[] {\n const result: MessageContent[] = []\n // \u4FEE\u6539\u6B63\u5219\u8868\u8FBE\u5F0F\u4EE5\u5339\u914D {{product:ID}} \u683C\u5F0F\n // \u5339\u914D {{product:xxx}} \u6216 {{xxx}}\uFF08\u517C\u5BB9\u4E24\u79CD\u683C\u5F0F\uFF09\n const regex = /\\{\\{(?:product:)?([^}]+)\\}\\}/g\n\n let lastIndex = 0\n let match: RegExpExecArray | null\n\n while ((match = regex.exec(text)) !== null) {\n const beforeText = text.slice(lastIndex, match.index).trim()\n // match[1] \u662F\u6355\u83B7\u7EC4\u4E2D\u7684\u5185\u5BB9\uFF0C\u5373 product: \u540E\u9762\u7684 ID\n const productId = match[1].trim()\n\n // \u6DFB\u52A0\u524D\u9762\u7684\u6587\u672C\uFF08\u5982\u679C\u6709\uFF09\n if (beforeText) {\n result.push({\n type: 'text',\n text: beforeText,\n } as TextContent)\n }\n\n // \u6DFB\u52A0\u4EA7\u54C1\u5361\u7247\uFF08\u65E0\u8BBA\u662F\u5426\u627E\u5230\u4EA7\u54C1\u6570\u636E\uFF0C\u90FD\u6E32\u67D3 product_card\uFF09\n const product = productMap.get(productId)\n const rawProduct = rawProductMap.get(productId)\n if (product) {\n console.log(`[useChatState] \u2705 \u627E\u5230\u4EA7\u54C1\u5339\u914D: ${productId} \u2192 ${product.title}`)\n } else {\n console.log(`[useChatState] \uD83D\uDCE6 \u4EA7\u54C1\u5360\u4F4D\u7B26\uFF0C\u4EA7\u54C1\u6570\u636E\u5F85\u5E94\u7528\u5C42\u67E5\u8BE2: ${productId}`)\n }\n\n result.push({\n type: 'product_card',\n data: {\n product: product,\n rawProduct: rawProduct,\n productHandle: productId,\n onAddToCart: onAddToCart,\n productCardRender: productCardRender,\n },\n } as ProductCardContent)\n\n lastIndex = regex.lastIndex\n }\n\n // \u6DFB\u52A0\u6700\u540E\u5269\u4F59\u7684\u6587\u672C\n const remainingText = text.slice(lastIndex).trim()\n if (remainingText) {\n result.push({\n type: 'text',\n text: remainingText,\n } as TextContent)\n }\n\n return result\n}\n\n/**\n * \u91CD\u7EC4\u6D88\u606F\u5185\u5BB9\uFF1A\u89E3\u6790\u6587\u672C\u4E2D\u7684 {{handle}}\uFF0C\u66FF\u6362\u4E3A\u4EA7\u54C1\u5361\u7247\n *\n * \u5904\u7406\u903B\u8F91\uFF1A\n * 1. \u904D\u5386\u6D88\u606F\u7684\u6240\u6709 content blocks\n * 2. \u5BF9\u4E8E text \u7C7B\u578B\uFF0C\u89E3\u6790\u5176\u4E2D\u7684 {{handle}} \u5E76\u62C6\u5206\u4E3A\u591A\u4E2A content\n * 3. \u8DF3\u8FC7 product_list \u7C7B\u578B\uFF08\u5DF2\u7ECF\u88AB\u62C6\u5206\u5230\u6587\u672C\u4E2D\uFF09\n * 4. \u5176\u4ED6\u7C7B\u578B\u76F4\u63A5\u4FDD\u7559\n *\n * @param contents \u539F\u59CB\u6D88\u606F\u5185\u5BB9\u6570\u7EC4\n * @param productMap handle \u2192 Product \u7684\u6620\u5C04\u8868\n * @param rawProductMap handle \u2192 raw backend product \u7684\u6620\u5C04\u8868\n * @param onAddToCart \u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n * @param productCardRender \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @returns \u91CD\u7EC4\u540E\u7684\u6D88\u606F\u5185\u5BB9\u6570\u7EC4\n */\nfunction reorganizeMessageContent(\n contents: MessageContent[],\n productMap: Map<string, Product>,\n rawProductMap: Map<string, any>,\n onAddToCart?: (product: Product) => void,\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n): MessageContent[] {\n const result: MessageContent[] = []\n\n for (const content of contents) {\n // \u53EA\u5904\u7406\u6587\u672C\u7C7B\u578B\n if (content.type === 'text') {\n const textContent = content as TextContent\n const segments = parseTextWithProductIds(textContent.text, productMap, rawProductMap, onAddToCart, productCardRender)\n result.push(...segments)\n }\n // \u8DF3\u8FC7 product_list\uFF08\u5DF2\u7ECF\u88AB\u62C6\u5206\u5230\u6587\u672C\u4E2D\uFF09\n else if (content.type === 'product_list') {\n continue\n }\n // \u5176\u4ED6\u7C7B\u578B\u76F4\u63A5\u4FDD\u7559\n else {\n result.push(content)\n }\n }\n\n return result\n}\n\n/**\n * \u5904\u7406\u5355\u6761\u6D88\u606F\u7684\u91CD\u7EC4\uFF08\u7528\u4E8E\u5386\u53F2\u6D88\u606F\u52A0\u8F7D\uFF09\n * \u5982\u679C\u6D88\u606F\u5305\u542B\u5E26\u6709 {{}} \u7684\u6587\u672C\uFF0C\u5219\u8FDB\u884C\u91CD\u7EC4\n *\n * @param message \u539F\u59CB\u6D88\u606F\n * @param onAddToCart \u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n * @param productCardRender \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @returns \u91CD\u7EC4\u540E\u7684\u6D88\u606F\uFF08\u5982\u679C\u9700\u8981\u91CD\u7EC4\uFF09\uFF0C\u5426\u5219\u8FD4\u56DE\u539F\u6D88\u606F\n */\nfunction maybeReorganizeHistoricalMessage(\n message: Message,\n onAddToCart?: (product: Product) => void,\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n): Message {\n // \u68C0\u67E5\u6D88\u606F\u662F\u5426\u5305\u542B\u5E26\u6709 {{}} \u7684\u6587\u672C\n const hasPlaceholder = message.content.some(\n c => c.type === 'text' && /\\{\\{(?:product:)?[^}]+\\}\\}/.test((c as TextContent).text)\n )\n if (!hasPlaceholder) {\n return message // \u6CA1\u6709\u5360\u4F4D\u7B26\uFF0C\u4E0D\u9700\u8981\u91CD\u7EC4\n }\n\n console.log('[useChatState] \u68C0\u6D4B\u5230\u5386\u53F2\u6D88\u606F\u9700\u8981\u91CD\u7EC4, \u6D88\u606FID:', message.id)\n\n // \u6784\u5EFA\u4EA7\u54C1\u6620\u5C04 (handle \u2192 Product)\n const productMap = new Map<string, Product>()\n // \u4ECE structured_content \u4E2D\u63D0\u53D6\u539F\u59CB\u540E\u7AEF\u4EA7\u54C1\u6570\u636E (handle \u2192 rawProduct)\n const rawProductMap = new Map<string, any>()\n\n // \u4F18\u5148\u4ECE structured_content \u83B7\u53D6\u539F\u59CB\u4EA7\u54C1\u6570\u636E\uFF08\u5982\u679C\u5B58\u5728\uFF09\n if (message.structured_content) {\n message.structured_content.forEach(structuredContent => {\n if (structuredContent.type === 'product_list' && Array.isArray(structuredContent.data)) {\n structuredContent.data.forEach((rawProduct: any) => {\n if (rawProduct && rawProduct.handle) {\n rawProductMap.set(rawProduct.handle, rawProduct)\n console.log('[useChatState] \u4ECE structured_content \u63D0\u53D6\u539F\u59CB\u4EA7\u54C1\u6570\u636E, handle:', rawProduct.handle)\n }\n })\n }\n })\n }\n\n // \u6784\u5EFA\u8F6C\u6362\u540E\u7684\u4EA7\u54C1\u6620\u5C04\uFF08\u7528\u4E8E\u9ED8\u8BA4\u6E32\u67D3\uFF09\n message.content.forEach(content => {\n if (content.type === 'product_list') {\n const productListContent = content as ProductListContent\n productListContent.data.products.forEach(product => {\n if (product && product.handle) {\n productMap.set(product.handle, product)\n }\n })\n }\n })\n\n // \u91CD\u7EC4\u6D88\u606F\u5185\u5BB9\n const reorganizedContent = reorganizeMessageContent(message.content, productMap, rawProductMap, onAddToCart, productCardRender)\n\n // \u8FD4\u56DE\u65B0\u6D88\u606F\u5BF9\u8C61\n return {\n ...message,\n content: reorganizedContent,\n }\n}\n\nexport interface UseChatStateOptions {\n /**\n * \u521D\u59CB\u6B22\u8FCE\u6D88\u606F\n */\n welcomeMessage?: string\n\n /**\n * Shopify \u5E97\u94FA\u57DF\u540D\n */\n site?: string\n\n /**\n * \u53D7\u63A7\u6A21\u5F0F\uFF1A\u662F\u5426\u6253\u5F00\u804A\u5929\u7A97\u53E3\n * \u63D0\u4F9B\u6B64\u53C2\u6570\u65F6\uFF0C\u7EC4\u4EF6\u5904\u4E8E\u53D7\u63A7\u6A21\u5F0F\n */\n open?: boolean\n\n /**\n * \u53D7\u63A7\u6A21\u5F0F\uFF1A\u72B6\u6001\u53D8\u5316\u56DE\u8C03\uFF08\u5FC5\u9700\uFF09\n * \u7528\u4E8E\u540C\u6B65\u72B6\u6001\u5230\u7236\u7EC4\u4EF6\n */\n onOpenChange?: (open: boolean) => void\n\n /**\n * \u7A97\u53E3\u6253\u5F00\u4E8B\u4EF6\u76D1\u542C\uFF08\u53EF\u9009\uFF09\n * \u7528\u4E8E\u57CB\u70B9\u3001\u65E5\u5FD7\u7B49\u526F\u4F5C\u7528\n */\n onOpen?: () => void\n\n /**\n * \u7A97\u53E3\u5173\u95ED\u4E8B\u4EF6\u76D1\u542C\uFF08\u53EF\u9009\uFF09\n * \u7528\u4E8E\u57CB\u70B9\u3001\u65E5\u5FD7\u7B49\u526F\u4F5C\u7528\n */\n onClose?: () => void\n\n /**\n * \u6D88\u606F\u53D1\u9001\u56DE\u8C03\n */\n onMessageSend?: (message: string) => void\n\n /**\n * \u9519\u8BEF\u5904\u7406\u56DE\u8C03\n */\n onError?: (error: Error) => void\n\n /**\n * AI \u6D88\u606F\u56DE\u8C03\n */\n /**\n * AI \u56DE\u590D\u6587\u672C\u6D88\u606F\u65F6\u89E6\u53D1\n */\n onTextMessage?: () => void\n\n /**\n * AI \u56DE\u590D\u5546\u54C1\u5217\u8868\u5361\u7247\u65F6\u89E6\u53D1\n */\n onProductList?: () => void\n\n /**\n * AI \u56DE\u590D\u4FC3\u9500\u5361\u7247\u65F6\u89E6\u53D1\n * @param promotions \u4FC3\u9500\u6D3B\u52A8\u6570\u7EC4\u6570\u636E\n */\n onPromotionList?: (promotions: any[]) => void\n\n /**\n * \u5546\u54C1\u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n */\n onAddToCart?: (product: any) => void\n\n /**\n * \u8D2D\u7269\u8F66\u6309\u94AE\u70B9\u51FB\u56DE\u8C03\n */\n onCart?: (cartId: string, checkoutUrl?: string) => void\n\n /**\n * \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @param product \u4EA7\u54C1\u6570\u636E\uFF08\u5982\u679C\u5728 product_list \u4E2D\u627E\u5230\uFF09\uFF0C\u5426\u5219\u4E3A undefined\n * @param productHandle \u6587\u672C\u5360\u4F4D\u7B26\u4E2D\u7684\u4EA7\u54C1 ID\uFF0C\u53EF\u7528\u4E8E\u5E94\u7528\u5C42\u67E5\u8BE2\u4EA7\u54C1\u6570\u636E\n */\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n\n /**\n * \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5BF9\u6BD4\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @param data \u4EA7\u54C1\u5BF9\u6BD4\u6570\u636E\uFF08\u5305\u542B products \u548C dimensions\uFF09\n */\n productComparisonRender?: (data: any) => React.ReactNode\n}\n\nexport interface UseChatStateReturn {\n /**\n * \u6D88\u606F\u5217\u8868\n */\n messages: Message[]\n\n /**\n * \u804A\u5929\u7A97\u53E3\u662F\u5426\u6253\u5F00\n */\n isOpen: boolean\n\n /**\n * \u7528\u6237 ID\uFF08undefined \u8868\u793A\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u7A7A\u5B57\u7B26\u4E32\u8868\u793A\u7531\u540E\u7AEF\u751F\u6210\uFF09\n */\n userId: string | undefined\n\n /**\n * \u4F1A\u8BDD ID\n */\n sessionId: string | null\n\n /**\n * \u8F93\u5165\u6846\u5185\u5BB9\n */\n inputValue: string\n\n /**\n * \u662F\u5426\u6B63\u5728\u63A5\u6536\u6D41\u5F0F\u6D88\u606F\n */\n isStreaming: boolean\n\n /**\n * \u6253\u5F00\u804A\u5929\u7A97\u53E3\n */\n openChat: () => void\n\n /**\n * \u5173\u95ED\u804A\u5929\u7A97\u53E3\n */\n closeChat: () => void\n\n /**\n * \u5207\u6362\u804A\u5929\u7A97\u53E3\u72B6\u6001\n */\n toggleChat: () => void\n\n /**\n * \u8BBE\u7F6E\u8F93\u5165\u6846\u5185\u5BB9\n */\n setInputValue: (value: string) => void\n\n /**\n * \u8BBE\u7F6E\u7528\u6237 ID\uFF08\u7528\u4E8E\u4FDD\u5B58\u540E\u7AEF\u8FD4\u56DE\u7684 userId\uFF09\n */\n setUserId: (id: string) => void\n\n /**\n * \u6DFB\u52A0\u6D88\u606F\u5230\u5217\u8868\n */\n addMessage: (message: Message) => void\n\n /**\n * \u6279\u91CF\u8BBE\u7F6E\u6D88\u606F\u5217\u8868\uFF08\u7528\u4E8E\u52A0\u8F7D\u5386\u53F2\uFF09\n */\n setMessages: (messages: Message[]) => void\n\n /**\n * \u6E05\u7A7A\u6D88\u606F\u5217\u8868\n */\n clearMessages: () => void\n\n /**\n * \u5904\u7406 SSE \u4E8B\u4EF6\n */\n handleSSEEvent: (event: SSEEvent) => void\n\n /**\n * \u4FDD\u5B58\u4F1A\u8BDD ID\n */\n saveSession: (id: string) => void\n\n /**\n * \u6E05\u7A7A\u4F1A\u8BDD\n */\n clearSession: () => void\n}\n\n/**\n * \u804A\u5929\u72B6\u6001\u7BA1\u7406 Hook\n *\n * \u529F\u80FD\uFF1A\n * 1. \u7BA1\u7406\u6D88\u606F\u5217\u8868\uFF08\u6DFB\u52A0\u3001\u6E05\u7A7A\u3001\u6279\u91CF\u8BBE\u7F6E\uFF09\n * 2. \u7BA1\u7406\u7A97\u53E3\u72B6\u6001\uFF08\u6253\u5F00\u3001\u5173\u95ED\u3001\u5207\u6362\uFF09\n * 3. \u7BA1\u7406\u8F93\u5165\u6846\u72B6\u6001\n * 4. \u5904\u7406 SSE \u6D41\u5F0F\u6D88\u606F\u4E8B\u4EF6\n * 5. \u7D2F\u79EF\u6D41\u5F0F\u6587\u672C\u5185\u5BB9\n *\n * @param options Hook \u914D\u7F6E\u9009\u9879\n * @returns \u72B6\u6001\u7BA1\u7406\u5DE5\u5177\u5BF9\u8C61\n */\nexport function useChatState(options: UseChatStateOptions = {}): UseChatStateReturn {\n const {\n welcomeMessage,\n site,\n open: controlledOpen,\n onOpenChange,\n onOpen,\n onClose,\n onMessageSend,\n onError,\n onTextMessage,\n onProductList,\n onPromotionList,\n onAddToCart,\n onCart,\n productCardRender,\n productComparisonRender,\n } = options\n\n // \u4F1A\u8BDD\u7BA1\u7406\n const { sessionId, saveSession, clearSession } = useSession()\n\n // \u7528\u6237 ID (\u521D\u59CB\u5316\u65F6\u5F02\u6B65\u751F\u6210\uFF0Cundefined \u8868\u793A\u5C1A\u672A\u521D\u59CB\u5316)\n const [userId, setUserId] = useState<string | undefined>(undefined)\n\n // \u521D\u59CB\u5316 userId\n useEffect(() => {\n getUserId().then(id => setUserId(id))\n }, [])\n\n // \u6D88\u606F\u5217\u8868\n const [messages, setMessagesState] = useState<Message[]>(() => {\n // \u5982\u679C\u6709\u6B22\u8FCE\u6D88\u606F\uFF0C\u521D\u59CB\u5316\u65F6\u6DFB\u52A0\n if (welcomeMessage) {\n return [\n {\n id: `welcome-${Date.now()}`,\n role: 'assistant',\n content: [{ type: 'text', text: welcomeMessage }],\n timestamp: Date.now(),\n },\n ]\n }\n return []\n })\n\n // \u804A\u5929\u7A97\u53E3\u662F\u5426\u6253\u5F00\uFF08\u652F\u6301\u53D7\u63A7\u548C\u975E\u53D7\u63A7\u4E24\u79CD\u6A21\u5F0F\uFF09\n const [internalOpen, setInternalOpen] = useState(false)\n const isControlled = controlledOpen !== undefined\n const isOpen = isControlled ? controlledOpen : internalOpen\n\n // \u8F93\u5165\u6846\u5185\u5BB9\n const [inputValue, setInputValue] = useState('')\n\n // \u662F\u5426\u6B63\u5728\u63A5\u6536\u6D41\u5F0F\u6D88\u606F\n const [isStreaming, setIsStreaming] = useState(false)\n\n // \u5F53\u524D\u6B63\u5728\u7D2F\u79EF\u7684\u6D41\u5F0F\u6D88\u606F (\u4E34\u65F6\u5B58\u50A8)\n const currentMessageRef = useRef<Message | null>(null)\n\n // \u6807\u8BB0\u5F53\u524D\u6D88\u606F\u662F\u5426\u5DF2\u89E6\u53D1 onTextMessage \u56DE\u8C03\uFF08\u907F\u514D\u91CD\u590D\u89E6\u53D1\uFF09\n const textMessageCallbackTriggeredRef = useRef<boolean>(false)\n\n // \u4EA7\u54C1\u6620\u5C04\u7F13\u5B58 (handle \u2192 Product)\uFF0C\u7528\u4E8E\u5B9E\u65F6\u89E3\u6790\u5360\u4F4D\u7B26\n const productMapRef = useRef<Map<string, Product>>(new Map())\n\n // \u539F\u59CB\u4EA7\u54C1\u6570\u636E\u7F13\u5B58 (handle \u2192 raw backend product)\uFF0C\u7528\u4E8E productCardRender\n const rawProductMapRef = useRef<Map<string, any>>(new Map())\n\n // \u6587\u672C\u7F13\u51B2\u533A\uFF0C\u7528\u4E8E\u5B58\u50A8\u672A\u5B8C\u6210\u7684\u6587\u672C\uFF08\u5904\u7406\u5360\u4F4D\u7B26\u8DE8\u8D8A\u591A\u4E2A delta \u7684\u60C5\u51B5\uFF09\n const textBufferRef = useRef<string>('')\n\n // \u5361\u7247\u7F13\u5B58\u961F\u5217\uFF0C\u7528\u4E8E\u5B58\u50A8\u9700\u8981\u5EF6\u8FDF\u663E\u793A\u7684\u5361\u7247\uFF08\u5728\u6587\u672C\u5B8C\u6210\u540E\u663E\u793A\uFF09\n const pendingCardsRef = useRef<MessageContent[]>([])\n\n /**\n * \u6253\u5F00\u804A\u5929\u7A97\u53E3\n */\n const openChat = useCallback(() => {\n if (!isControlled) {\n setInternalOpen(true)\n }\n onOpenChange?.(true)\n onOpen?.()\n }, [isControlled, onOpenChange, onOpen])\n\n /**\n * \u5173\u95ED\u804A\u5929\u7A97\u53E3\n */\n const closeChat = useCallback(() => {\n if (!isControlled) {\n setInternalOpen(false)\n }\n onOpenChange?.(false)\n onClose?.()\n }, [isControlled, onOpenChange, onClose])\n\n /**\n * \u5207\u6362\u804A\u5929\u7A97\u53E3\u72B6\u6001\n */\n const toggleChat = useCallback(() => {\n const newState = !isOpen\n if (!isControlled) {\n setInternalOpen(newState)\n }\n onOpenChange?.(newState)\n if (newState) {\n onOpen?.()\n } else {\n onClose?.()\n }\n }, [isControlled, isOpen, onOpenChange, onOpen, onClose])\n\n /**\n * \u6DFB\u52A0\u6D88\u606F\u5230\u5217\u8868\n */\n const addMessage = useCallback((message: Message) => {\n // \u9632\u62A4\uFF1A\u5982\u679C\u6D88\u606F\u4E3A null \u6216 undefined\uFF0C\u4E0D\u6DFB\u52A0\n if (!message) {\n console.warn('[useChatState] Attempted to add null/undefined message')\n return\n }\n setMessagesState(prev => [...prev, message])\n }, [])\n\n /**\n * \u6279\u91CF\u8BBE\u7F6E\u6D88\u606F\u5217\u8868\uFF08\u7528\u4E8E\u52A0\u8F7D\u5386\u53F2\uFF09\n */\n const setMessages = useCallback(\n (newMessages: Message[]) => {\n // \u9632\u62A4\uFF1A\u8FC7\u6EE4\u6389 null/undefined \u6D88\u606F\n const validMessages = newMessages.filter(msg => msg != null)\n if (validMessages.length !== newMessages.length) {\n console.warn('[useChatState] Filtered out null/undefined messages from batch set')\n }\n\n // \u5BF9\u6BCF\u6761\u5386\u53F2\u6D88\u606F\u8FDB\u884C\u91CD\u7EC4\uFF08\u5982\u679C\u9700\u8981\uFF09\n const reorganizedMessages = validMessages.map(msg => maybeReorganizeHistoricalMessage(msg, onAddToCart, productCardRender))\n\n setMessagesState(reorganizedMessages)\n },\n [onAddToCart, productCardRender]\n )\n\n /**\n * \u6E05\u7A7A\u6D88\u606F\u5217\u8868\n */\n const clearMessages = useCallback(() => {\n setMessagesState([])\n }, [])\n\n /**\n * \u5904\u7406 SSE \u4E8B\u4EF6\n * \u6839\u636E\u4E8B\u4EF6\u7C7B\u578B\u8FDB\u884C\u4E0D\u540C\u7684\u5904\u7406\n */\n const handleSSEEvent = useCallback(\n (event: SSEEvent) => {\n const { event: eventType, data } = event\n\n switch (eventType) {\n case 'message_start': {\n // \u5F00\u59CB\u63A5\u6536\u65B0\u6D88\u606F\n setIsStreaming(true)\n\n // \u91CD\u7F6E\u6587\u672C\u6D88\u606F\u56DE\u8C03\u6807\u8BB0\n textMessageCallbackTriggeredRef.current = false\n\n // \u91CD\u7F6E\u6587\u672C\u7F13\u51B2\u533A\n textBufferRef.current = ''\n\n // \u91CD\u7F6E\u5361\u7247\u7F13\u5B58\u961F\u5217\n pendingCardsRef.current = []\n\n // T039: \u4FDD\u5B58 sessionId \u548C userId\uFF08\u5982\u679C\u540E\u7AEF\u8FD4\u56DE\uFF09\n const messageStartData = data as MessageStartData & { userId?: string }\n if (messageStartData.sessionId && messageStartData.sessionId !== sessionId) {\n saveSession(messageStartData.sessionId)\n }\n // \u4FDD\u5B58\u540E\u7AEF\u8FD4\u56DE\u7684 userId\uFF08\u5982\u679C\u6709\uFF09\n if (messageStartData.userId) {\n saveUserId(messageStartData.userId)\n setUserId(messageStartData.userId)\n }\n\n // \u68C0\u67E5\u6700\u540E\u4E00\u6761\u6D88\u606F\u662F\u5426\u662F thinking \u6D88\u606F\uFF08\u7528\u6237\u53D1\u9001\u6D88\u606F\u65F6\u5DF2\u6DFB\u52A0\uFF09\n setMessagesState(prev => {\n const lastMessage = prev[prev.length - 1]\n const hasThinking =\n lastMessage &&\n lastMessage.role === 'assistant' &&\n lastMessage.content.length === 1 &&\n lastMessage.content[0].type === 'thinking'\n\n if (hasThinking) {\n // \u590D\u7528\u5DF2\u5B58\u5728\u7684 thinking \u6D88\u606F\n currentMessageRef.current = lastMessage\n return prev // \u4E0D\u9700\u8981\u6DFB\u52A0\u65B0\u6D88\u606F\n } else {\n // \u6CA1\u6709 thinking \u6D88\u606F\uFF0C\u521B\u5EFA\u65B0\u7684\uFF08\u517C\u5BB9\u5176\u4ED6\u573A\u666F\uFF09\n const messageId = `msg-${Date.now()}`\n currentMessageRef.current = {\n id: messageId,\n role: 'assistant',\n content: [{ type: 'thinking', data: { status: 'thinking' } }],\n timestamp: Date.now(),\n }\n return [...prev, currentMessageRef.current!]\n }\n })\n break\n }\n\n case 'content_delta': {\n // \u7D2F\u79EF\u6D41\u5F0F\u6587\u672C\u5185\u5BB9\uFF0C\u5E76\u5B9E\u65F6\u68C0\u6D4B\u4EA7\u54C1\u5360\u4F4D\u7B26\n const deltaData = data as any\n const deltaText = deltaData.delta || deltaData.text || ''\n\n if (currentMessageRef.current && deltaText) {\n // \u89E6\u53D1\u6587\u672C\u6D88\u606F\u56DE\u8C03\uFF08\u4EC5\u89E6\u53D1\u4E00\u6B21\uFF09\n if (!textMessageCallbackTriggeredRef.current) {\n textMessageCallbackTriggeredRef.current = true\n onTextMessage?.()\n }\n\n // \u79FB\u9664\u601D\u8003\u6C14\u6CE1\uFF08\u5982\u679C\u5B58\u5728\uFF09\n const hasThinking = currentMessageRef.current.content.some(c => c.type === 'thinking')\n if (hasThinking) {\n currentMessageRef.current.content = currentMessageRef.current.content.filter(c => c.type !== 'thinking')\n }\n\n // \u5C06\u65B0\u6587\u672C\u6DFB\u52A0\u5230\u7F13\u51B2\u533A\n textBufferRef.current += deltaText\n\n // \u5B9E\u65F6\u89E3\u6790\u7F13\u51B2\u533A\u4E2D\u7684\u5360\u4F4D\u7B26\n const { contents, remainingBuffer } = parseStreamingText(\n textBufferRef.current,\n productMapRef.current,\n rawProductMapRef.current,\n onAddToCart,\n productCardRender\n )\n\n // \u66F4\u65B0\u7F13\u51B2\u533A\u4E3A\u5269\u4F59\u5185\u5BB9\n textBufferRef.current = remainingBuffer\n\n // \u5C06\u89E3\u6790\u51FA\u7684\u5185\u5BB9\u6DFB\u52A0\u5230\u6D88\u606F\u4E2D\n if (contents.length > 0) {\n contents.forEach(content => {\n const lastContent = currentMessageRef.current!.content[\n currentMessageRef.current!.content.length - 1\n ] as MessageContent | undefined\n\n // \u5982\u679C\u662F\u6587\u672C\u5185\u5BB9\u4E14\u6700\u540E\u4E00\u4E2A\u4E5F\u662F\u6587\u672C\uFF0C\u5219\u5408\u5E76\n if (content.type === 'text' && lastContent && lastContent.type === 'text') {\n lastContent.text += content.text\n } else {\n // \u5426\u5219\u6DFB\u52A0\u65B0\u5185\u5BB9\n currentMessageRef.current!.content.push(content)\n }\n })\n\n // \u66F4\u65B0\u6D88\u606F\u5217\u8868\u4EE5\u89E6\u53D1\u6E32\u67D3\n setMessagesState(prev => {\n if (!currentMessageRef.current) return prev\n\n const updated = [...prev]\n const existingIndex = updated.findIndex(m => m && m.id === currentMessageRef.current!.id)\n\n if (existingIndex >= 0) {\n updated[existingIndex] = { ...currentMessageRef.current! }\n } else {\n updated.push({ ...currentMessageRef.current! })\n }\n\n return updated\n })\n }\n }\n break\n }\n\n case 'content_block': {\n // \u63A5\u6536\u7ED3\u6784\u5316\u5185\u5BB9\u5757\uFF08\u5546\u54C1\u3001\u653F\u7B56\u7B49\uFF09\n // API \u8FD4\u56DE\u683C\u5F0F\u53D8\u66F4:\n // \u65B0\u683C\u5F0F: {index: number, type: string, data: {...}} <- type \u5728\u5916\u5C42\n // \u65E7\u683C\u5F0F: {index: number, data: {type: string, ...}} <- type \u5728 data \u5185\n const blockData = data as any\n if (currentMessageRef.current) {\n // \u83B7\u53D6 type \u548C data\n // \u4F18\u5148\u4ECE\u5916\u5C42\u83B7\u53D6 type\uFF0C\u517C\u5BB9\u65E7\u683C\u5F0F\u4ECE data \u5185\u83B7\u53D6\n const contentType = blockData.type || blockData.data?.type\n const contentData = blockData.data\n\n if (!contentType || !contentData) {\n console.warn('[useChatState] Invalid content_block:', blockData)\n break\n }\n\n // ============================================================\n // \u8F6C\u6362\u6570\u636E\u7ED3\u6784\u4EE5\u5339\u914D\u7C7B\u578B\u5B9A\u4E49\n // \u6839\u636E\u540E\u7AEF\u6570\u636E\u7ED3\u6784\u89C4\u8303\uFF08\u98DE\u4E66\u6587\u6863\uFF09\u8FDB\u884C\u8F6C\u6362\n // ============================================================\n let messageContent: MessageContent\n\n // ========== 1. \u4EA7\u54C1\u5217\u8868\u5361\u7247 (Product List) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"product_list\", data: [product1, product2, ...]}\n // data \u76F4\u63A5\u662F\u4EA7\u54C1\u6570\u7EC4\uFF0C\u4E0D\u662F {products: [...]}\n if (contentType === 'product_list' && Array.isArray(contentData)) {\n // \u89E6\u53D1\u5546\u54C1\u5217\u8868\u56DE\u8C03\n onProductList?.()\n\n // \u7F13\u5B58\u539F\u59CB\u540E\u7AEF\u4EA7\u54C1\u6570\u636E\uFF08\u7528\u4E8E productCardRender\uFF09\n // \u4F7F\u7528 handle \u4F5C\u4E3A key \u8FDB\u884C\u5339\u914D\n contentData.forEach((rawProduct: any) => {\n if (rawProduct && rawProduct.handle) {\n rawProductMapRef.current.set(rawProduct.handle, rawProduct)\n console.log('[useChatState] \u7F13\u5B58\u539F\u59CB\u4EA7\u54C1\u6570\u636E, handle:', rawProduct.handle)\n }\n })\n\n // \u4F7F\u7528\u7EDF\u4E00\u7684\u4EA7\u54C1\u8F6C\u6362\u5DE5\u5177\u51FD\u6570\n const transformedProducts = transformProducts(contentData, site)\n\n // \u5EFA\u7ACB\u4EA7\u54C1\u6620\u5C04\u7F13\u5B58 (handle \u2192 Product)\n // \u7528\u4E8E\u540E\u7EED\u5728 message_end \u65F6\u89E3\u6790\u6587\u672C\u4E2D\u7684 {{handle}}\n transformedProducts.forEach(product => {\n if (product && product.handle) {\n productMapRef.current.set(product.handle, product)\n\n console.log('[useChatState] \u5EFA\u7ACB\u4EA7\u54C1\u6620\u5C04:', {\n handle: product.handle,\n title: product.title,\n })\n }\n })\n\n // \u26A0\uFE0F \u4E0D\u8981\u628A product_list \u6DFB\u52A0\u5230\u6D88\u606F\u4E2D\uFF0C\u907F\u514D\u95EA\u70C1\n // \u7B49\u5230 message_end \u65F6\uFF0C\u901A\u8FC7\u6587\u672C\u89E3\u6790\u521B\u5EFA product_card\uFF0C\u76F4\u63A5\u663E\u793A\u6700\u7EC8\u7684\u4EA4\u66FF\u683C\u5F0F\n console.log('[useChatState] \u2705 \u4EA7\u54C1\u5217\u8868\u5DF2\u7F13\u5B58\uFF0C\u4E0D\u6DFB\u52A0\u5230\u6D88\u606F\u4E2D\uFF08\u907F\u514D\u95EA\u70C1\uFF09')\n break // \u76F4\u63A5\u8DF3\u51FA\uFF0C\u4E0D\u6267\u884C\u540E\u7EED\u7684 push \u64CD\u4F5C\n }\n // ========== 2. \u4EA7\u54C1\u5BF9\u6BD4\u5361\u7247 (Product Comparison) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"product_comparison\", data: {products: [...], dimensions: {...}}}\n else if (contentType === 'product_comparison' && contentData.products) {\n // \u4F7F\u7528\u7EDF\u4E00\u7684\u4EA7\u54C1\u8F6C\u6362\u5DE5\u5177\u51FD\u6570\n const transformedProducts = transformProducts(contentData.products, site)\n\n messageContent = {\n type: 'product_comparison',\n data: {\n products: transformedProducts,\n dimensions: contentData.dimensions || {},\n productComparisonRender: productComparisonRender,\n },\n } as MessageContent\n }\n // ========== 3. FAQ \u5217\u8868\u5361\u7247 (FAQ List) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"faq_list\", data: {found, count, total, results: [...]}}\n else if (contentType === 'faq_list' && contentData.found !== undefined) {\n messageContent = {\n type: 'faq_list',\n data: contentData, // \u76F4\u63A5\u4F7F\u7528\uFF0C\u7ED3\u6784\u5DF2\u5339\u914D\n } as MessageContent\n }\n // ========== 4. \u5FEB\u6377\u56DE\u590D (Quick Replies) ==========\n else if (contentType === 'quick_replies' && contentData.replies) {\n messageContent = {\n type: 'quick_replies',\n data: {\n replies: contentData.replies,\n },\n } as MessageContent\n }\n // ========== 5. \u653F\u7B56\u5185\u5BB9 (Policy) ==========\n else if (contentType === 'policy' && contentData.title && contentData.content) {\n messageContent = {\n type: 'policy',\n data: {\n title: contentData.title,\n content: contentData.content,\n },\n } as MessageContent\n }\n // ========== 6. \u4FC3\u9500\u6D3B\u52A8\u5217\u8868 (Promotion List) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"promotion_list\", data: {found, count, total, results: [...]}}\n else if (contentType === 'promotion_list' && contentData.found !== undefined) {\n // \u89E6\u53D1\u4FC3\u9500\u5361\u7247\u56DE\u8C03\uFF0C\u4F20\u9012\u4FC3\u9500\u6D3B\u52A8\u6570\u7EC4\n onPromotionList?.(contentData.results || [])\n\n messageContent = {\n type: 'promotion_list',\n data: contentData, // \u76F4\u63A5\u4F7F\u7528\uFF0C\u7ED3\u6784\u5DF2\u5339\u914D\n } as MessageContent\n }\n // ========== 7. \u8D2D\u7269\u8F66 (Cart) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"cart\", data: {id, lines: {edges: [...]}, cost, ...}} (Shopify GraphQL)\n // \u9700\u8981\u8F6C\u6362\u4E3A\u524D\u7AEF\u683C\u5F0F: {cartId, lines: [...], cost, ...}\n else if (contentType === 'cart' && contentData.id !== undefined) {\n // \u8F6C\u6362\u540E\u7AEF Shopify GraphQL \u683C\u5F0F\u4E3A\u524D\u7AEF\u6807\u51C6\u683C\u5F0F\n const transformedData = transformCartData(contentData as BackendCartData)\n messageContent = {\n type: 'cart',\n data: {\n ...transformedData,\n onCart: onCart, // \u6CE8\u5165\u8D2D\u7269\u8F66\u6309\u94AE\u56DE\u8C03\n },\n } as MessageContent\n }\n // ========== 8. \u5176\u4ED6\u7C7B\u578B\uFF08\u901A\u7528\u5904\u7406\uFF09 ==========\n else {\n messageContent = {\n type: contentType,\n data: contentData,\n } as MessageContent\n }\n\n // \u26A0\uFE0F \u91CD\u8981\u4FEE\u6539\uFF1A\u5C06\u5361\u7247\u7F13\u5B58\u8D77\u6765\uFF0C\u4E0D\u7ACB\u5373\u6DFB\u52A0\u5230\u6D88\u606F\u4E2D\n // \u7B49\u5F85 message_end \u65F6\uFF0C\u5728\u6587\u672C\u5B8C\u6210\u540E\u518D\u7EDF\u4E00\u6DFB\u52A0\u5361\u7247\n console.log('[useChatState] \u2705 \u5361\u7247\u5DF2\u7F13\u5B58\uFF0C\u7B49\u5F85\u6587\u672C\u5B8C\u6210\u540E\u663E\u793A:', contentType)\n pendingCardsRef.current.push(messageContent)\n\n // \u4E0D\u518D\u7ACB\u5373\u66F4\u65B0\u6D88\u606F\u5217\u8868\uFF0C\u907F\u514D\u5361\u7247\u5728\u6587\u672C\u4E4B\u524D\u663E\u793A\n // \u539F\u6765\u7684\u4EE3\u7801\uFF1A\n // currentMessageRef.current.content.push(messageContent)\n // setMessagesState(prev => { ... })\n }\n break\n }\n\n case 'tool_start':\n case 'tool_end': {\n // \u5DE5\u5177\u8C03\u7528\u4E8B\u4EF6\uFF0C\u6682\u65F6\u5FFD\u7565\n // \u53EF\u4EE5\u5728\u672A\u6765\u7528\u4E8E\u663E\u793A\u5DE5\u5177\u8C03\u7528\u72B6\u6001\n break\n }\n\n case 'message_end': {\n // \u6D88\u606F\u63A5\u6536\u5B8C\u6210\n setIsStreaming(false)\n\n // \u5904\u7406\u7F13\u51B2\u533A\u4E2D\u5269\u4F59\u7684\u6587\u672C\uFF08\u5982\u679C\u6709\uFF09\n if (currentMessageRef.current && textBufferRef.current) {\n console.log('[useChatState] \u5904\u7406\u5269\u4F59\u7F13\u51B2\u533A:', textBufferRef.current)\n\n const lastContent = currentMessageRef.current.content[\n currentMessageRef.current.content.length - 1\n ] as MessageContent | undefined\n\n // \u5982\u679C\u6700\u540E\u4E00\u4E2A\u5185\u5BB9\u662F\u6587\u672C\uFF0C\u5219\u5408\u5E76\n if (lastContent && lastContent.type === 'text') {\n lastContent.text += textBufferRef.current\n } else {\n // \u5426\u5219\u6DFB\u52A0\u4E3A\u65B0\u7684\u6587\u672C\u5757\n currentMessageRef.current.content.push({\n type: 'text',\n text: textBufferRef.current,\n })\n }\n }\n\n // \u26A0\uFE0F \u91CD\u8981\u4FEE\u6539\uFF1A\u5728\u6587\u672C\u5B8C\u6210\u540E\uFF0C\u6DFB\u52A0\u6240\u6709\u7F13\u5B58\u7684\u5361\u7247\n if (currentMessageRef.current && pendingCardsRef.current.length > 0) {\n console.log('[useChatState] \uD83D\uDCCB \u6587\u672C\u5DF2\u5B8C\u6210\uFF0C\u73B0\u5728\u6DFB\u52A0', pendingCardsRef.current.length, '\u4E2A\u7F13\u5B58\u7684\u5361\u7247')\n\n // \u5C06\u6240\u6709\u7F13\u5B58\u7684\u5361\u7247\u6DFB\u52A0\u5230\u6D88\u606F\u5185\u5BB9\u4E2D\n currentMessageRef.current.content.push(...pendingCardsRef.current)\n }\n\n // \u26A0\uFE0F \u8D85\u65F6\u68C0\u6D4B\uFF1A\u5982\u679C message_end \u65F6\u4ECD\u5B58\u5728 thinking block\uFF0C\u89C6\u4E3A\u8D85\u65F6/\u5F02\u5E38\n // \u6CE8\u610F\uFF1A\u5728\u6DFB\u52A0\u5361\u7247\u4E4B\u540E\u68C0\u6D4B\uFF0C\u8FD9\u6837\u5982\u679C\u6709\u5361\u7247\u5C31\u4E0D\u4F1A\u6DFB\u52A0\u8D85\u65F6\u63D0\u793A\n if (currentMessageRef.current) {\n const hasThinking = currentMessageRef.current.content.some(c => c.type === 'thinking')\n\n if (hasThinking) {\n console.log('[useChatState] \u26A0\uFE0F message_end \u65F6\u4ECD\u5B58\u5728 thinking block\uFF0C\u7ACB\u5373\u79FB\u9664\uFF08\u89C6\u4E3A\u8D85\u65F6\uFF09')\n\n // \u79FB\u9664 thinking block\n currentMessageRef.current.content = currentMessageRef.current.content.filter(c => c.type !== 'thinking')\n\n // \u5982\u679C\u6CA1\u6709\u5176\u4ED6\u5185\u5BB9\uFF08\u5305\u62EC\u7F13\u5B58\u7684\u5361\u7247\uFF09\uFF0C\u6DFB\u52A0\u8D85\u65F6\u63D0\u793A\n if (currentMessageRef.current.content.length === 0) {\n currentMessageRef.current.content.push({\n type: 'text',\n text: 'Response timed out, please try again.',\n } as TextContent)\n }\n }\n }\n\n // \u66F4\u65B0\u6D88\u606F\u5217\u8868\uFF08\u7EDF\u4E00\u66F4\u65B0\uFF0C\u5305\u542B\u6587\u672C\u548C\u5361\u7247\uFF09\n if (currentMessageRef.current) {\n setMessagesState(prev => {\n if (!currentMessageRef.current) return prev\n\n const updated = [...prev]\n const existingIndex = updated.findIndex(m => m && m.id === currentMessageRef.current!.id)\n\n if (existingIndex >= 0) {\n updated[existingIndex] = { ...currentMessageRef.current! }\n }\n\n return updated\n })\n }\n\n // \u6E05\u7A7A\u7F13\u51B2\u533A\u3001\u4EA7\u54C1\u6620\u5C04\u548C\u5361\u7247\u7F13\u5B58\n textBufferRef.current = ''\n pendingCardsRef.current = []\n productMapRef.current.clear()\n rawProductMapRef.current.clear()\n currentMessageRef.current = null\n break\n }\n\n case 'status': {\n // T040: \u72B6\u6001\u66F4\u65B0\uFF08\u5982\u4F1A\u8BDD\u8FC7\u671F\uFF09\n const statusData = data as StatusData\n if (statusData.type === 'session_expired') {\n // \u4F1A\u8BDD\u8FC7\u671F\uFF0C\u6E05\u7A7A\u6D88\u606F\u5217\u8868\u548C\u4F1A\u8BDD\n clearMessages()\n clearSession()\n if (welcomeMessage) {\n addMessage({\n id: `welcome-${Date.now()}`,\n role: 'assistant',\n content: [{ type: 'text', text: welcomeMessage }],\n timestamp: Date.now(),\n })\n }\n }\n break\n }\n\n case 'error': {\n // \u9519\u8BEF\u5904\u7406\n const errorData = data as ErrorData\n setIsStreaming(false)\n\n // \u6E05\u7406\u7F13\u5B58\uFF08\u9632\u6B62\u6CC4\u6F0F\u5230\u4E0B\u6B21\u6D88\u606F\uFF09\n textBufferRef.current = ''\n pendingCardsRef.current = []\n productMapRef.current.clear()\n rawProductMapRef.current.clear()\n currentMessageRef.current = null\n\n // \u6DFB\u52A0\u9519\u8BEF\u6D88\u606F\u5230\u754C\u9762\n addMessage({\n id: `error-${Date.now()}`,\n role: 'system',\n content: [\n {\n type: 'error',\n data: {\n message: errorData.message,\n code: errorData.code,\n },\n },\n ],\n timestamp: Date.now(),\n })\n\n onError?.(new Error(errorData.message))\n break\n }\n\n case 'done': {\n // \u6D41\u7ED3\u675F\n setIsStreaming(false)\n\n // \u6E05\u7406\u7F13\u5B58\uFF08\u9632\u6B62\u6CC4\u6F0F\u5230\u4E0B\u6B21\u6D88\u606F\uFF09\n textBufferRef.current = ''\n pendingCardsRef.current = []\n productMapRef.current.clear()\n rawProductMapRef.current.clear()\n\n // \u6E05\u7406\u5F53\u524D\u6D88\u606F\u5F15\u7528\n currentMessageRef.current = null\n break\n }\n\n default:\n // \u5176\u4ED6\u4E8B\u4EF6\u7C7B\u578B\uFF08tool_start, tool_end \u7B49\uFF09\n break\n }\n },\n [\n welcomeMessage,\n site,\n addMessage,\n clearMessages,\n clearSession,\n saveSession,\n sessionId,\n onError,\n onTextMessage,\n onProductList,\n onPromotionList,\n onAddToCart,\n onCart,\n ]\n )\n\n\n return {\n messages,\n isOpen,\n userId,\n sessionId,\n inputValue,\n isStreaming,\n openChat,\n closeChat,\n toggleChat,\n setInputValue,\n setUserId,\n addMessage,\n setMessages,\n clearMessages,\n handleSSEEvent,\n saveSession,\n clearSession,\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["useState", "useCallback", "useRef", "useEffect", "getUserId", "saveUserId", "useSession", "transformProducts", "transformCartData", "parseStreamingText", "buffer", "productMap", "rawProductMap", "onAddToCart", "productCardRender", "contents", "regex", "lastIndex", "match", "foundMatch", "beforeText", "productId", "product", "rawProduct", "remainingBuffer", "incompleteMatch", "completeText", "parseTextWithProductIds", "text", "result", "remainingText", "reorganizeMessageContent", "content", "segments", "maybeReorganizeHistoricalMessage", "message", "structuredContent", "reorganizedContent", "useChatState", "options", "welcomeMessage", "site", "controlledOpen", "onOpenChange", "onOpen", "onClose", "onMessageSend", "onError", "onTextMessage", "onProductList", "onPromotionList", "onCart", "productComparisonRender", "sessionId", "saveSession", "clearSession", "userId", "setUserId", "id", "messages", "setMessagesState", "internalOpen", "setInternalOpen", "isControlled", "isOpen", "inputValue", "setInputValue", "isStreaming", "setIsStreaming", "currentMessageRef", "textMessageCallbackTriggeredRef", "productMapRef", "rawProductMapRef", "textBufferRef", "pendingCardsRef", "openChat", "closeChat", "toggleChat", "newState", "addMessage", "prev", "setMessages", "newMessages", "validMessages", "msg", "reorganizedMessages", "clearMessages", "handleSSEEvent", "event", "eventType", "data", "messageStartData", "lastMessage", "messageId", "deltaData", "deltaText", "c", "lastContent", "updated", "existingIndex", "m", "blockData", "contentType", "contentData", "messageContent", "errorData"]
4
+ "sourcesContent": ["/**\n * \u804A\u5929\u72B6\u6001\u7BA1\u7406 Hook\n * \u7BA1\u7406\u6D88\u606F\u5217\u8868\u3001\u7A97\u53E3\u72B6\u6001\u3001\u8F93\u5165\u6846\u72B6\u6001\u3001\u6D41\u5F0F\u6D88\u606F\u7D2F\u79EF\n * \u57FA\u4E8E specs/livechat-widget/plan.md \u7684\u72B6\u6001\u7BA1\u7406\u7B56\u7565\n */\n\nimport { useState, useCallback, useRef, useEffect } from 'react'\nimport type { Message, MessageContent, SSEEvent, StatusData, ErrorData, MessageStartData, BackendCartData, Product, TextContent, ProductCardContent, ProductListContent } from '../types'\nimport { getUserId, saveUserId } from '../utils/userId'\nimport { useSession } from './useSession'\nimport { transformProducts } from '../utils/productTransformers'\nimport { transformCartData } from '../utils/cartTransformers'\n\n// ============================================================================\n// \u8F85\u52A9\u51FD\u6570\uFF1A\u6587\u672C\u89E3\u6790\u548C\u6D88\u606F\u91CD\u7EC4\n// ============================================================================\n\n/**\n * \u5B9E\u65F6\u89E3\u6790\u6D41\u5F0F\u6587\u672C\u4E2D\u7684\u4EA7\u54C1\u5360\u4F4D\u7B26\n * \u5904\u7406\u7F13\u51B2\u533A\u4E2D\u7684\u6587\u672C\uFF0C\u68C0\u6D4B\u5B8C\u6574\u7684 {{product:xxx}} \u5360\u4F4D\u7B26\n *\n * @param buffer \u5F53\u524D\u7F13\u51B2\u533A\u5185\u5BB9\uFF08\u5305\u542B\u65B0\u63A5\u6536\u7684\u6587\u672C\uFF09\n * @param productMap handle \u2192 Product \u7684\u6620\u5C04\u8868\n * @param rawProductMap handle \u2192 raw backend product \u7684\u6620\u5C04\u8868\n * @param onAddToCart \u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n * @param productCardRender \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @returns { contents: \u9700\u8981\u6DFB\u52A0\u7684\u5185\u5BB9\u6570\u7EC4, remainingBuffer: \u5269\u4F59\u7F13\u51B2\u533A\u5185\u5BB9 }\n */\nfunction parseStreamingText(\n buffer: string,\n productMap: Map<string, Product>,\n rawProductMap: Map<string, any>,\n onAddToCart?: (product: Product) => void,\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n): { contents: MessageContent[]; remainingBuffer: string } {\n const contents: MessageContent[] = []\n const regex = /\\{\\{(?:product:)?([^}]+)\\}\\}/g\n\n let lastIndex = 0\n let match: RegExpExecArray | null\n let foundMatch = false\n\n // \u67E5\u627E\u6240\u6709\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\n while ((match = regex.exec(buffer)) !== null) {\n foundMatch = true\n\n // \u63D0\u53D6\u5360\u4F4D\u7B26\u524D\u7684\u6587\u672C\n const beforeText = buffer.slice(lastIndex, match.index)\n if (beforeText) {\n contents.push({ type: 'text', text: beforeText } as TextContent)\n }\n\n // \u63D0\u53D6\u4EA7\u54C1 ID \u5E76\u521B\u5EFA\u4EA7\u54C1\u5361\u7247\n const productId = match[1].trim()\n const product = productMap.get(productId)\n const rawProduct = rawProductMap.get(productId)\n\n // \u65E0\u8BBA\u662F\u5426\u627E\u5230\u4EA7\u54C1\u6570\u636E\uFF0C\u90FD\u6E32\u67D3 product_card\uFF0C\u5E94\u7528\u5C42\u53EF\u901A\u8FC7 productHandle \u67E5\u8BE2\u4EA7\u54C1\n if (product) {\n console.log('[useChatState] \uD83C\uDFAF \u5B9E\u65F6\u68C0\u6D4B\u5230\u4EA7\u54C1:', productId, '\u2192', product.title)\n } else {\n console.log('[useChatState] \uD83D\uDCE6 \u5B9E\u65F6\u68C0\u6D4B\u5230\u4EA7\u54C1\u5360\u4F4D\u7B26\uFF0C\u4EA7\u54C1\u6570\u636E\u5F85\u5E94\u7528\u5C42\u67E5\u8BE2:', productId)\n }\n\n contents.push({\n type: 'product_card',\n data: {\n product: product,\n rawProduct: rawProduct,\n productHandle: productId,\n onAddToCart: onAddToCart,\n productCardRender: productCardRender\n }\n } as ProductCardContent)\n\n lastIndex = regex.lastIndex\n }\n\n // \u5982\u679C\u627E\u5230\u4E86\u81F3\u5C11\u4E00\u4E2A\u5B8C\u6574\u5360\u4F4D\u7B26\n if (foundMatch) {\n // \u8FD4\u56DE\u5269\u4F59\u7684\u6587\u672C\u4F5C\u4E3A\u7F13\u51B2\u533A\uFF08\u53EF\u80FD\u5305\u542B\u4E0D\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\uFF09\n const remainingBuffer = buffer.slice(lastIndex)\n return { contents, remainingBuffer }\n } else {\n // \u6CA1\u6709\u627E\u5230\u5B8C\u6574\u5360\u4F4D\u7B26\uFF0C\u68C0\u67E5\u662F\u5426\u6709\u4E0D\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\u5F00\u5934\n // \u4F8B\u5982\uFF1A\u7F13\u51B2\u533A\u662F \"some text {{prod\"\uFF0C\u6211\u4EEC\u9700\u8981\u4FDD\u7559 \"{{prod\" \u7B49\u5F85\u66F4\u591A\u6587\u672C\n const incompleteMatch = buffer.match(/\\{\\{[^}]*$/)\n\n if (incompleteMatch) {\n // \u6709\u4E0D\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\u5F00\u5934\n const completeText = buffer.slice(0, incompleteMatch.index)\n if (completeText) {\n contents.push({ type: 'text', text: completeText } as TextContent)\n }\n return { contents, remainingBuffer: incompleteMatch[0] }\n } else {\n // \u6CA1\u6709\u4E0D\u5B8C\u6574\u7684\u5360\u4F4D\u7B26\uFF0C\u6574\u4E2A\u7F13\u51B2\u533A\u90FD\u662F\u666E\u901A\u6587\u672C\n if (buffer) {\n contents.push({ type: 'text', text: buffer } as TextContent)\n }\n return { contents, remainingBuffer: '' }\n }\n }\n}\n\n/**\n * \u89E3\u6790\u6587\u672C\u4E2D\u7684 {{handle}}\uFF0C\u8FD4\u56DE [text, product_card, text, ...] \u6570\u7EC4\uFF08\u7528\u4E8E\u5386\u53F2\u6D88\u606F\u91CD\u7EC4\uFF09\n *\n * @param text \u5305\u542B {{handle}} \u6807\u8BB0\u7684\u6587\u672C\n * @param productMap handle \u2192 Product \u7684\u6620\u5C04\u8868\n * @param rawProductMap handle \u2192 raw backend product \u7684\u6620\u5C04\u8868\n * @param onAddToCart \u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n * @param productCardRender \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @returns MessageContent \u6570\u7EC4\n *\n * @example\n * \u8F93\u5165\uFF1A\n * text: \"\u6211\u63A8\u8350\u4EE5\u4E0B\u4EA7\u54C1\uFF1A\\n{{product-handle}}\\n\u8FD9\u6B3E\u4EA7\u54C1\u6027\u4EF7\u6BD4\u5F88\u9AD8\u3002\"\n * productMap: Map { 'product-handle' => Product {...} }\n * \u8F93\u51FA\uFF1A\n * [\n * { type: 'text', text: '\u6211\u63A8\u8350\u4EE5\u4E0B\u4EA7\u54C1\uFF1A' },\n * { type: 'product_card', data: { product: {...}, onAddToCart } },\n * { type: 'text', text: '\u8FD9\u6B3E\u4EA7\u54C1\u6027\u4EF7\u6BD4\u5F88\u9AD8\u3002' }\n * ]\n */\nfunction parseTextWithProductIds(\n text: string,\n productMap: Map<string, Product>,\n rawProductMap: Map<string, any>,\n onAddToCart?: (product: Product) => void,\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n): MessageContent[] {\n const result: MessageContent[] = []\n // \u4FEE\u6539\u6B63\u5219\u8868\u8FBE\u5F0F\u4EE5\u5339\u914D {{product:ID}} \u683C\u5F0F\n // \u5339\u914D {{product:xxx}} \u6216 {{xxx}}\uFF08\u517C\u5BB9\u4E24\u79CD\u683C\u5F0F\uFF09\n const regex = /\\{\\{(?:product:)?([^}]+)\\}\\}/g\n\n let lastIndex = 0\n let match: RegExpExecArray | null\n\n while ((match = regex.exec(text)) !== null) {\n const beforeText = text.slice(lastIndex, match.index).trim()\n // match[1] \u662F\u6355\u83B7\u7EC4\u4E2D\u7684\u5185\u5BB9\uFF0C\u5373 product: \u540E\u9762\u7684 ID\n const productId = match[1].trim()\n\n // \u6DFB\u52A0\u524D\u9762\u7684\u6587\u672C\uFF08\u5982\u679C\u6709\uFF09\n if (beforeText) {\n result.push({\n type: 'text',\n text: beforeText,\n } as TextContent)\n }\n\n // \u6DFB\u52A0\u4EA7\u54C1\u5361\u7247\uFF08\u65E0\u8BBA\u662F\u5426\u627E\u5230\u4EA7\u54C1\u6570\u636E\uFF0C\u90FD\u6E32\u67D3 product_card\uFF09\n const product = productMap.get(productId)\n const rawProduct = rawProductMap.get(productId)\n if (product) {\n console.log(`[useChatState] \u2705 \u627E\u5230\u4EA7\u54C1\u5339\u914D: ${productId} \u2192 ${product.title}`)\n } else {\n console.log(`[useChatState] \uD83D\uDCE6 \u4EA7\u54C1\u5360\u4F4D\u7B26\uFF0C\u4EA7\u54C1\u6570\u636E\u5F85\u5E94\u7528\u5C42\u67E5\u8BE2: ${productId}`)\n }\n\n result.push({\n type: 'product_card',\n data: {\n product: product,\n rawProduct: rawProduct,\n productHandle: productId,\n onAddToCart: onAddToCart,\n productCardRender: productCardRender,\n },\n } as ProductCardContent)\n\n lastIndex = regex.lastIndex\n }\n\n // \u6DFB\u52A0\u6700\u540E\u5269\u4F59\u7684\u6587\u672C\n const remainingText = text.slice(lastIndex).trim()\n if (remainingText) {\n result.push({\n type: 'text',\n text: remainingText,\n } as TextContent)\n }\n\n return result\n}\n\n/**\n * \u91CD\u7EC4\u6D88\u606F\u5185\u5BB9\uFF1A\u89E3\u6790\u6587\u672C\u4E2D\u7684 {{handle}}\uFF0C\u66FF\u6362\u4E3A\u4EA7\u54C1\u5361\u7247\n *\n * \u5904\u7406\u903B\u8F91\uFF1A\n * 1. \u904D\u5386\u6D88\u606F\u7684\u6240\u6709 content blocks\n * 2. \u5BF9\u4E8E text \u7C7B\u578B\uFF0C\u89E3\u6790\u5176\u4E2D\u7684 {{handle}} \u5E76\u62C6\u5206\u4E3A\u591A\u4E2A content\n * 3. \u8DF3\u8FC7 product_list \u7C7B\u578B\uFF08\u5DF2\u7ECF\u88AB\u62C6\u5206\u5230\u6587\u672C\u4E2D\uFF09\n * 4. \u5176\u4ED6\u7C7B\u578B\u76F4\u63A5\u4FDD\u7559\n *\n * @param contents \u539F\u59CB\u6D88\u606F\u5185\u5BB9\u6570\u7EC4\n * @param productMap handle \u2192 Product \u7684\u6620\u5C04\u8868\n * @param rawProductMap handle \u2192 raw backend product \u7684\u6620\u5C04\u8868\n * @param onAddToCart \u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n * @param productCardRender \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @returns \u91CD\u7EC4\u540E\u7684\u6D88\u606F\u5185\u5BB9\u6570\u7EC4\n */\nfunction reorganizeMessageContent(\n contents: MessageContent[],\n productMap: Map<string, Product>,\n rawProductMap: Map<string, any>,\n onAddToCart?: (product: Product) => void,\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n): MessageContent[] {\n const result: MessageContent[] = []\n\n for (const content of contents) {\n // \u53EA\u5904\u7406\u6587\u672C\u7C7B\u578B\n if (content.type === 'text') {\n const textContent = content as TextContent\n const segments = parseTextWithProductIds(textContent.text, productMap, rawProductMap, onAddToCart, productCardRender)\n result.push(...segments)\n }\n // \u8DF3\u8FC7 product_list\uFF08\u5DF2\u7ECF\u88AB\u62C6\u5206\u5230\u6587\u672C\u4E2D\uFF09\n else if (content.type === 'product_list') {\n continue\n }\n // \u5176\u4ED6\u7C7B\u578B\u76F4\u63A5\u4FDD\u7559\n else {\n result.push(content)\n }\n }\n\n return result\n}\n\n/**\n * \u5904\u7406\u5355\u6761\u6D88\u606F\u7684\u91CD\u7EC4\uFF08\u7528\u4E8E\u5386\u53F2\u6D88\u606F\u52A0\u8F7D\uFF09\n * \u5982\u679C\u6D88\u606F\u5305\u542B\u5E26\u6709 {{}} \u7684\u6587\u672C\uFF0C\u5219\u8FDB\u884C\u91CD\u7EC4\n *\n * @param message \u539F\u59CB\u6D88\u606F\n * @param onAddToCart \u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n * @param productCardRender \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @returns \u91CD\u7EC4\u540E\u7684\u6D88\u606F\uFF08\u5982\u679C\u9700\u8981\u91CD\u7EC4\uFF09\uFF0C\u5426\u5219\u8FD4\u56DE\u539F\u6D88\u606F\n */\nfunction maybeReorganizeHistoricalMessage(\n message: Message,\n onAddToCart?: (product: Product) => void,\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n): Message {\n // \u68C0\u67E5\u6D88\u606F\u662F\u5426\u5305\u542B\u5E26\u6709 {{}} \u7684\u6587\u672C\n const hasPlaceholder = message.content.some(\n c => c.type === 'text' && /\\{\\{(?:product:)?[^}]+\\}\\}/.test((c as TextContent).text)\n )\n if (!hasPlaceholder) {\n return message // \u6CA1\u6709\u5360\u4F4D\u7B26\uFF0C\u4E0D\u9700\u8981\u91CD\u7EC4\n }\n\n console.log('[useChatState] \u68C0\u6D4B\u5230\u5386\u53F2\u6D88\u606F\u9700\u8981\u91CD\u7EC4, \u6D88\u606FID:', message.id)\n\n // \u6784\u5EFA\u4EA7\u54C1\u6620\u5C04 (handle \u2192 Product)\n const productMap = new Map<string, Product>()\n // \u4ECE structured_content \u4E2D\u63D0\u53D6\u539F\u59CB\u540E\u7AEF\u4EA7\u54C1\u6570\u636E (handle \u2192 rawProduct)\n const rawProductMap = new Map<string, any>()\n\n // \u4F18\u5148\u4ECE structured_content \u83B7\u53D6\u539F\u59CB\u4EA7\u54C1\u6570\u636E\uFF08\u5982\u679C\u5B58\u5728\uFF09\n if (message.structured_content) {\n message.structured_content.forEach(structuredContent => {\n if (structuredContent.type === 'product_list' && Array.isArray(structuredContent.data)) {\n structuredContent.data.forEach((rawProduct: any) => {\n if (rawProduct && rawProduct.handle) {\n rawProductMap.set(rawProduct.handle, rawProduct)\n console.log('[useChatState] \u4ECE structured_content \u63D0\u53D6\u539F\u59CB\u4EA7\u54C1\u6570\u636E, handle:', rawProduct.handle)\n }\n })\n }\n })\n }\n\n // \u6784\u5EFA\u8F6C\u6362\u540E\u7684\u4EA7\u54C1\u6620\u5C04\uFF08\u7528\u4E8E\u9ED8\u8BA4\u6E32\u67D3\uFF09\n message.content.forEach(content => {\n if (content.type === 'product_list') {\n const productListContent = content as ProductListContent\n productListContent.data.products.forEach(product => {\n if (product && product.handle) {\n productMap.set(product.handle, product)\n }\n })\n }\n })\n\n // \u91CD\u7EC4\u6D88\u606F\u5185\u5BB9\n const reorganizedContent = reorganizeMessageContent(message.content, productMap, rawProductMap, onAddToCart, productCardRender)\n\n // \u8FD4\u56DE\u65B0\u6D88\u606F\u5BF9\u8C61\n return {\n ...message,\n content: reorganizedContent,\n }\n}\n\nexport interface UseChatStateOptions {\n /**\n * \u521D\u59CB\u6B22\u8FCE\u6D88\u606F\n */\n welcomeMessage?: string\n\n /**\n * Shopify \u5E97\u94FA\u57DF\u540D\n */\n site?: string\n\n /**\n * \u53D7\u63A7\u6A21\u5F0F\uFF1A\u662F\u5426\u6253\u5F00\u804A\u5929\u7A97\u53E3\n * \u63D0\u4F9B\u6B64\u53C2\u6570\u65F6\uFF0C\u7EC4\u4EF6\u5904\u4E8E\u53D7\u63A7\u6A21\u5F0F\n */\n open?: boolean\n\n /**\n * \u53D7\u63A7\u6A21\u5F0F\uFF1A\u72B6\u6001\u53D8\u5316\u56DE\u8C03\uFF08\u5FC5\u9700\uFF09\n * \u7528\u4E8E\u540C\u6B65\u72B6\u6001\u5230\u7236\u7EC4\u4EF6\n */\n onOpenChange?: (open: boolean) => void\n\n /**\n * \u7A97\u53E3\u6253\u5F00\u4E8B\u4EF6\u76D1\u542C\uFF08\u53EF\u9009\uFF09\n * \u7528\u4E8E\u57CB\u70B9\u3001\u65E5\u5FD7\u7B49\u526F\u4F5C\u7528\n */\n onOpen?: () => void\n\n /**\n * \u7A97\u53E3\u5173\u95ED\u4E8B\u4EF6\u76D1\u542C\uFF08\u53EF\u9009\uFF09\n * \u7528\u4E8E\u57CB\u70B9\u3001\u65E5\u5FD7\u7B49\u526F\u4F5C\u7528\n */\n onClose?: () => void\n\n /**\n * \u6D88\u606F\u53D1\u9001\u56DE\u8C03\n */\n onMessageSend?: (message: string) => void\n\n /**\n * \u9519\u8BEF\u5904\u7406\u56DE\u8C03\n */\n onError?: (error: Error) => void\n\n /**\n * AI \u6D88\u606F\u56DE\u8C03\n */\n /**\n * AI \u56DE\u590D\u6587\u672C\u6D88\u606F\u65F6\u89E6\u53D1\n */\n onTextMessage?: () => void\n\n /**\n * AI \u56DE\u590D\u5546\u54C1\u5217\u8868\u5361\u7247\u65F6\u89E6\u53D1\n */\n onProductList?: () => void\n\n /**\n * AI \u56DE\u590D\u4FC3\u9500\u5361\u7247\u65F6\u89E6\u53D1\n * @param promotions \u4FC3\u9500\u6D3B\u52A8\u6570\u7EC4\u6570\u636E\n */\n onPromotionList?: (promotions: any[]) => void\n\n /**\n * \u5546\u54C1\u6DFB\u52A0\u5230\u8D2D\u7269\u8F66\u56DE\u8C03\n */\n onAddToCart?: (product: any) => void\n\n /**\n * \u8D2D\u7269\u8F66\u6309\u94AE\u70B9\u51FB\u56DE\u8C03\n */\n onCart?: (cartId: string, checkoutUrl?: string) => void\n\n /**\n * \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @param product \u4EA7\u54C1\u6570\u636E\uFF08\u5982\u679C\u5728 product_list \u4E2D\u627E\u5230\uFF09\uFF0C\u5426\u5219\u4E3A undefined\n * @param productHandle \u6587\u672C\u5360\u4F4D\u7B26\u4E2D\u7684\u4EA7\u54C1 ID\uFF0C\u53EF\u7528\u4E8E\u5E94\u7528\u5C42\u67E5\u8BE2\u4EA7\u54C1\u6570\u636E\n */\n productCardRender?: (product: any, productHandle: string) => React.ReactNode\n\n /**\n * \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5BF9\u6BD4\u5361\u7247\u6E32\u67D3\u51FD\u6570\n * @param data \u4EA7\u54C1\u5BF9\u6BD4\u6570\u636E\uFF08\u5305\u542B products \u548C dimensions\uFF09\n */\n productComparisonRender?: (data: any) => React.ReactNode\n}\n\nexport interface UseChatStateReturn {\n /**\n * \u6D88\u606F\u5217\u8868\n */\n messages: Message[]\n\n /**\n * \u804A\u5929\u7A97\u53E3\u662F\u5426\u6253\u5F00\n */\n isOpen: boolean\n\n /**\n * \u7528\u6237 ID\uFF08undefined \u8868\u793A\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u7A7A\u5B57\u7B26\u4E32\u8868\u793A\u7531\u540E\u7AEF\u751F\u6210\uFF09\n */\n userId: string | undefined\n\n /**\n * \u4F1A\u8BDD ID\n */\n sessionId: string | null\n\n /**\n * \u8F93\u5165\u6846\u5185\u5BB9\n */\n inputValue: string\n\n /**\n * \u662F\u5426\u6B63\u5728\u63A5\u6536\u6D41\u5F0F\u6D88\u606F\n */\n isStreaming: boolean\n\n /**\n * \u6253\u5F00\u804A\u5929\u7A97\u53E3\n */\n openChat: () => void\n\n /**\n * \u5173\u95ED\u804A\u5929\u7A97\u53E3\n */\n closeChat: () => void\n\n /**\n * \u5207\u6362\u804A\u5929\u7A97\u53E3\u72B6\u6001\n */\n toggleChat: () => void\n\n /**\n * \u8BBE\u7F6E\u8F93\u5165\u6846\u5185\u5BB9\n */\n setInputValue: (value: string) => void\n\n /**\n * \u8BBE\u7F6E\u7528\u6237 ID\uFF08\u7528\u4E8E\u4FDD\u5B58\u540E\u7AEF\u8FD4\u56DE\u7684 userId\uFF09\n */\n setUserId: (id: string) => void\n\n /**\n * \u6DFB\u52A0\u6D88\u606F\u5230\u5217\u8868\n */\n addMessage: (message: Message) => void\n\n /**\n * \u6279\u91CF\u8BBE\u7F6E\u6D88\u606F\u5217\u8868\uFF08\u7528\u4E8E\u52A0\u8F7D\u5386\u53F2\uFF09\n */\n setMessages: (messages: Message[]) => void\n\n /**\n * \u6E05\u7A7A\u6D88\u606F\u5217\u8868\n */\n clearMessages: () => void\n\n /**\n * \u5904\u7406 SSE \u4E8B\u4EF6\n */\n handleSSEEvent: (event: SSEEvent) => void\n\n /**\n * \u5F00\u59CB\u65B0\u7684 stream \u4F1A\u8BDD\uFF0C\u8FD4\u56DE\u7ED1\u5B9A\u4E86\u5F53\u524D streamId \u7684\u4E8B\u4EF6\u5904\u7406\u5668\u3002\n * \u8C03\u7528\u6B64\u65B9\u6CD5\u4F1A\u4F7F\u4E4B\u524D\u6240\u6709 stream \u7684\u4E8B\u4EF6\u5904\u7406\u5668\u5931\u6548\uFF0C\u9632\u6B62\u5E76\u53D1 stream \u6C61\u67D3\u5171\u4EAB buffer\u3002\n */\n startStream: () => (event: SSEEvent) => void\n\n /**\n * \u4FDD\u5B58\u4F1A\u8BDD ID\n */\n saveSession: (id: string) => void\n\n /**\n * \u6E05\u7A7A\u4F1A\u8BDD\n */\n clearSession: () => void\n}\n\n/**\n * \u804A\u5929\u72B6\u6001\u7BA1\u7406 Hook\n *\n * \u529F\u80FD\uFF1A\n * 1. \u7BA1\u7406\u6D88\u606F\u5217\u8868\uFF08\u6DFB\u52A0\u3001\u6E05\u7A7A\u3001\u6279\u91CF\u8BBE\u7F6E\uFF09\n * 2. \u7BA1\u7406\u7A97\u53E3\u72B6\u6001\uFF08\u6253\u5F00\u3001\u5173\u95ED\u3001\u5207\u6362\uFF09\n * 3. \u7BA1\u7406\u8F93\u5165\u6846\u72B6\u6001\n * 4. \u5904\u7406 SSE \u6D41\u5F0F\u6D88\u606F\u4E8B\u4EF6\n * 5. \u7D2F\u79EF\u6D41\u5F0F\u6587\u672C\u5185\u5BB9\n *\n * @param options Hook \u914D\u7F6E\u9009\u9879\n * @returns \u72B6\u6001\u7BA1\u7406\u5DE5\u5177\u5BF9\u8C61\n */\nexport function useChatState(options: UseChatStateOptions = {}): UseChatStateReturn {\n const {\n welcomeMessage,\n site,\n open: controlledOpen,\n onOpenChange,\n onOpen,\n onClose,\n onMessageSend,\n onError,\n onTextMessage,\n onProductList,\n onPromotionList,\n onAddToCart,\n onCart,\n productCardRender,\n productComparisonRender,\n } = options\n\n // \u4F1A\u8BDD\u7BA1\u7406\n const { sessionId, saveSession, clearSession } = useSession()\n\n // \u7528\u6237 ID (\u521D\u59CB\u5316\u65F6\u5F02\u6B65\u751F\u6210\uFF0Cundefined \u8868\u793A\u5C1A\u672A\u521D\u59CB\u5316)\n const [userId, setUserId] = useState<string | undefined>(undefined)\n\n // \u521D\u59CB\u5316 userId\n useEffect(() => {\n getUserId().then(id => setUserId(id))\n }, [])\n\n // \u6D88\u606F\u5217\u8868\n const [messages, setMessagesState] = useState<Message[]>(() => {\n // \u5982\u679C\u6709\u6B22\u8FCE\u6D88\u606F\uFF0C\u521D\u59CB\u5316\u65F6\u6DFB\u52A0\n if (welcomeMessage) {\n return [\n {\n id: `welcome-${Date.now()}`,\n role: 'assistant',\n content: [{ type: 'text', text: welcomeMessage }],\n timestamp: Date.now(),\n },\n ]\n }\n return []\n })\n\n // \u804A\u5929\u7A97\u53E3\u662F\u5426\u6253\u5F00\uFF08\u652F\u6301\u53D7\u63A7\u548C\u975E\u53D7\u63A7\u4E24\u79CD\u6A21\u5F0F\uFF09\n const [internalOpen, setInternalOpen] = useState(false)\n const isControlled = controlledOpen !== undefined\n const isOpen = isControlled ? controlledOpen : internalOpen\n\n // \u8F93\u5165\u6846\u5185\u5BB9\n const [inputValue, setInputValue] = useState('')\n\n // \u662F\u5426\u6B63\u5728\u63A5\u6536\u6D41\u5F0F\u6D88\u606F\n const [isStreaming, setIsStreaming] = useState(false)\n\n // \u5F53\u524D\u6B63\u5728\u7D2F\u79EF\u7684\u6D41\u5F0F\u6D88\u606F (\u4E34\u65F6\u5B58\u50A8)\n const currentMessageRef = useRef<Message | null>(null)\n\n // \u6807\u8BB0\u5F53\u524D\u6D88\u606F\u662F\u5426\u5DF2\u89E6\u53D1 onTextMessage \u56DE\u8C03\uFF08\u907F\u514D\u91CD\u590D\u89E6\u53D1\uFF09\n const textMessageCallbackTriggeredRef = useRef<boolean>(false)\n\n // \u4EA7\u54C1\u6620\u5C04\u7F13\u5B58 (handle \u2192 Product)\uFF0C\u7528\u4E8E\u5B9E\u65F6\u89E3\u6790\u5360\u4F4D\u7B26\n const productMapRef = useRef<Map<string, Product>>(new Map())\n\n // \u539F\u59CB\u4EA7\u54C1\u6570\u636E\u7F13\u5B58 (handle \u2192 raw backend product)\uFF0C\u7528\u4E8E productCardRender\n const rawProductMapRef = useRef<Map<string, any>>(new Map())\n\n // \u6587\u672C\u7F13\u51B2\u533A\uFF0C\u7528\u4E8E\u5B58\u50A8\u672A\u5B8C\u6210\u7684\u6587\u672C\uFF08\u5904\u7406\u5360\u4F4D\u7B26\u8DE8\u8D8A\u591A\u4E2A delta \u7684\u60C5\u51B5\uFF09\n const textBufferRef = useRef<string>('')\n\n // \u5361\u7247\u7F13\u5B58\u961F\u5217\uFF0C\u7528\u4E8E\u5B58\u50A8\u9700\u8981\u5EF6\u8FDF\u663E\u793A\u7684\u5361\u7247\uFF08\u5728\u6587\u672C\u5B8C\u6210\u540E\u663E\u793A\uFF09\n const pendingCardsRef = useRef<MessageContent[]>([])\n\n // \u5F53\u524D\u6D3B\u8DC3 stream \u7684\u552F\u4E00 ID\uFF0C\u7528\u4E8E\u4E22\u5F03\u8FC7\u671F stream \u7684\u4E8B\u4EF6\uFF0C\u9632\u6B62\u5E76\u53D1 stream \u5171\u4EAB buffer\n const activeStreamIdRef = useRef<string | null>(null)\n\n /**\n * \u6253\u5F00\u804A\u5929\u7A97\u53E3\n */\n const openChat = useCallback(() => {\n if (!isControlled) {\n setInternalOpen(true)\n }\n onOpenChange?.(true)\n onOpen?.()\n }, [isControlled, onOpenChange, onOpen])\n\n /**\n * \u5173\u95ED\u804A\u5929\u7A97\u53E3\n */\n const closeChat = useCallback(() => {\n if (!isControlled) {\n setInternalOpen(false)\n }\n onOpenChange?.(false)\n onClose?.()\n }, [isControlled, onOpenChange, onClose])\n\n /**\n * \u5207\u6362\u804A\u5929\u7A97\u53E3\u72B6\u6001\n */\n const toggleChat = useCallback(() => {\n const newState = !isOpen\n if (!isControlled) {\n setInternalOpen(newState)\n }\n onOpenChange?.(newState)\n if (newState) {\n onOpen?.()\n } else {\n onClose?.()\n }\n }, [isControlled, isOpen, onOpenChange, onOpen, onClose])\n\n /**\n * \u6DFB\u52A0\u6D88\u606F\u5230\u5217\u8868\n */\n const addMessage = useCallback((message: Message) => {\n // \u9632\u62A4\uFF1A\u5982\u679C\u6D88\u606F\u4E3A null \u6216 undefined\uFF0C\u4E0D\u6DFB\u52A0\n if (!message) {\n console.warn('[useChatState] Attempted to add null/undefined message')\n return\n }\n setMessagesState(prev => [...prev, message])\n }, [])\n\n /**\n * \u6279\u91CF\u8BBE\u7F6E\u6D88\u606F\u5217\u8868\uFF08\u7528\u4E8E\u52A0\u8F7D\u5386\u53F2\uFF09\n */\n const setMessages = useCallback(\n (newMessages: Message[]) => {\n // \u9632\u62A4\uFF1A\u8FC7\u6EE4\u6389 null/undefined \u6D88\u606F\n const validMessages = newMessages.filter(msg => msg != null)\n if (validMessages.length !== newMessages.length) {\n console.warn('[useChatState] Filtered out null/undefined messages from batch set')\n }\n\n // \u5BF9\u6BCF\u6761\u5386\u53F2\u6D88\u606F\u8FDB\u884C\u91CD\u7EC4\uFF08\u5982\u679C\u9700\u8981\uFF09\n const reorganizedMessages = validMessages.map(msg => maybeReorganizeHistoricalMessage(msg, onAddToCart, productCardRender))\n\n setMessagesState(reorganizedMessages)\n },\n [onAddToCart, productCardRender]\n )\n\n /**\n * \u6E05\u7A7A\u6D88\u606F\u5217\u8868\n */\n const clearMessages = useCallback(() => {\n setMessagesState([])\n }, [])\n\n /**\n * \u5904\u7406 SSE \u4E8B\u4EF6\n * \u6839\u636E\u4E8B\u4EF6\u7C7B\u578B\u8FDB\u884C\u4E0D\u540C\u7684\u5904\u7406\n */\n const handleSSEEvent = useCallback(\n (event: SSEEvent, streamId?: string) => {\n const { event: eventType, data } = event\n\n // \u5E76\u53D1 stream \u9632\u62A4\uFF1A\u9664 message_start \u5916\u7684\u6240\u6709\u4E8B\u4EF6\u5FC5\u987B\u5339\u914D\u5F53\u524D\u6D3B\u8DC3 streamId\n if (eventType !== 'message_start' && streamId !== activeStreamIdRef.current) {\n return\n }\n\n switch (eventType) {\n case 'message_start': {\n setIsStreaming(true)\n\n textMessageCallbackTriggeredRef.current = false\n textBufferRef.current = ''\n pendingCardsRef.current = []\n\n // T039: \u4FDD\u5B58 sessionId \u548C userId\uFF08\u5982\u679C\u540E\u7AEF\u8FD4\u56DE\uFF09\n const messageStartData = data as MessageStartData & { userId?: string }\n if (messageStartData.sessionId && messageStartData.sessionId !== sessionId) {\n saveSession(messageStartData.sessionId)\n }\n // \u4FDD\u5B58\u540E\u7AEF\u8FD4\u56DE\u7684 userId\uFF08\u5982\u679C\u6709\uFF09\n if (messageStartData.userId) {\n saveUserId(messageStartData.userId)\n setUserId(messageStartData.userId)\n }\n\n // \u68C0\u67E5\u6700\u540E\u4E00\u6761\u6D88\u606F\u662F\u5426\u662F thinking \u6D88\u606F\uFF08\u7528\u6237\u53D1\u9001\u6D88\u606F\u65F6\u5DF2\u6DFB\u52A0\uFF09\n setMessagesState(prev => {\n const lastMessage = prev[prev.length - 1]\n const hasThinking =\n lastMessage &&\n lastMessage.role === 'assistant' &&\n lastMessage.content.length === 1 &&\n lastMessage.content[0].type === 'thinking'\n\n if (hasThinking) {\n // \u590D\u7528\u5DF2\u5B58\u5728\u7684 thinking \u6D88\u606F\n currentMessageRef.current = lastMessage\n return prev // \u4E0D\u9700\u8981\u6DFB\u52A0\u65B0\u6D88\u606F\n } else {\n // \u6CA1\u6709 thinking \u6D88\u606F\uFF0C\u521B\u5EFA\u65B0\u7684\uFF08\u517C\u5BB9\u5176\u4ED6\u573A\u666F\uFF09\n const messageId = `msg-${Date.now()}`\n currentMessageRef.current = {\n id: messageId,\n role: 'assistant',\n content: [{ type: 'thinking', data: { status: 'thinking' } }],\n timestamp: Date.now(),\n }\n return [...prev, currentMessageRef.current!]\n }\n })\n break\n }\n\n case 'content_delta': {\n // \u7D2F\u79EF\u6D41\u5F0F\u6587\u672C\u5185\u5BB9\uFF0C\u5E76\u5B9E\u65F6\u68C0\u6D4B\u4EA7\u54C1\u5360\u4F4D\u7B26\n const deltaData = data as any\n const deltaText = deltaData.delta || deltaData.text || ''\n\n if (currentMessageRef.current && deltaText) {\n // \u89E6\u53D1\u6587\u672C\u6D88\u606F\u56DE\u8C03\uFF08\u4EC5\u89E6\u53D1\u4E00\u6B21\uFF09\n if (!textMessageCallbackTriggeredRef.current) {\n textMessageCallbackTriggeredRef.current = true\n onTextMessage?.()\n }\n\n // \u79FB\u9664\u601D\u8003\u6C14\u6CE1\uFF08\u5982\u679C\u5B58\u5728\uFF09\n const hasThinking = currentMessageRef.current.content.some(c => c.type === 'thinking')\n if (hasThinking) {\n currentMessageRef.current.content = currentMessageRef.current.content.filter(c => c.type !== 'thinking')\n }\n\n // \u5C06\u65B0\u6587\u672C\u6DFB\u52A0\u5230\u7F13\u51B2\u533A\n textBufferRef.current += deltaText\n\n // \u5B9E\u65F6\u89E3\u6790\u7F13\u51B2\u533A\u4E2D\u7684\u5360\u4F4D\u7B26\n const { contents, remainingBuffer } = parseStreamingText(\n textBufferRef.current,\n productMapRef.current,\n rawProductMapRef.current,\n onAddToCart,\n productCardRender\n )\n\n // \u66F4\u65B0\u7F13\u51B2\u533A\u4E3A\u5269\u4F59\u5185\u5BB9\n textBufferRef.current = remainingBuffer\n\n // \u5C06\u89E3\u6790\u51FA\u7684\u5185\u5BB9\u6DFB\u52A0\u5230\u6D88\u606F\u4E2D\n if (contents.length > 0) {\n contents.forEach(content => {\n const lastContent = currentMessageRef.current!.content[\n currentMessageRef.current!.content.length - 1\n ] as MessageContent | undefined\n\n // \u5982\u679C\u662F\u6587\u672C\u5185\u5BB9\u4E14\u6700\u540E\u4E00\u4E2A\u4E5F\u662F\u6587\u672C\uFF0C\u5219\u5408\u5E76\n if (content.type === 'text' && lastContent && lastContent.type === 'text') {\n lastContent.text += content.text\n } else {\n // \u5426\u5219\u6DFB\u52A0\u65B0\u5185\u5BB9\n currentMessageRef.current!.content.push(content)\n }\n })\n\n // \u66F4\u65B0\u6D88\u606F\u5217\u8868\u4EE5\u89E6\u53D1\u6E32\u67D3\n setMessagesState(prev => {\n if (!currentMessageRef.current) return prev\n\n const updated = [...prev]\n const existingIndex = updated.findIndex(m => m && m.id === currentMessageRef.current!.id)\n\n if (existingIndex >= 0) {\n updated[existingIndex] = { ...currentMessageRef.current! }\n } else {\n updated.push({ ...currentMessageRef.current! })\n }\n\n return updated\n })\n }\n }\n break\n }\n\n case 'content_block': {\n // \u63A5\u6536\u7ED3\u6784\u5316\u5185\u5BB9\u5757\uFF08\u5546\u54C1\u3001\u653F\u7B56\u7B49\uFF09\n // API \u8FD4\u56DE\u683C\u5F0F\u53D8\u66F4:\n // \u65B0\u683C\u5F0F: {index: number, type: string, data: {...}} <- type \u5728\u5916\u5C42\n // \u65E7\u683C\u5F0F: {index: number, data: {type: string, ...}} <- type \u5728 data \u5185\n const blockData = data as any\n if (currentMessageRef.current) {\n // \u83B7\u53D6 type \u548C data\n // \u4F18\u5148\u4ECE\u5916\u5C42\u83B7\u53D6 type\uFF0C\u517C\u5BB9\u65E7\u683C\u5F0F\u4ECE data \u5185\u83B7\u53D6\n const contentType = blockData.type || blockData.data?.type\n const contentData = blockData.data\n\n if (!contentType || !contentData) {\n console.warn('[useChatState] Invalid content_block:', blockData)\n break\n }\n\n // ============================================================\n // \u8F6C\u6362\u6570\u636E\u7ED3\u6784\u4EE5\u5339\u914D\u7C7B\u578B\u5B9A\u4E49\n // \u6839\u636E\u540E\u7AEF\u6570\u636E\u7ED3\u6784\u89C4\u8303\uFF08\u98DE\u4E66\u6587\u6863\uFF09\u8FDB\u884C\u8F6C\u6362\n // ============================================================\n let messageContent: MessageContent\n\n // ========== 1. \u4EA7\u54C1\u5217\u8868\u5361\u7247 (Product List) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"product_list\", data: [product1, product2, ...]}\n // data \u76F4\u63A5\u662F\u4EA7\u54C1\u6570\u7EC4\uFF0C\u4E0D\u662F {products: [...]}\n if (contentType === 'product_list' && Array.isArray(contentData)) {\n // \u89E6\u53D1\u5546\u54C1\u5217\u8868\u56DE\u8C03\n onProductList?.()\n\n // \u7F13\u5B58\u539F\u59CB\u540E\u7AEF\u4EA7\u54C1\u6570\u636E\uFF08\u7528\u4E8E productCardRender\uFF09\n // \u4F7F\u7528 handle \u4F5C\u4E3A key \u8FDB\u884C\u5339\u914D\n contentData.forEach((rawProduct: any) => {\n if (rawProduct && rawProduct.handle) {\n rawProductMapRef.current.set(rawProduct.handle, rawProduct)\n console.log('[useChatState] \u7F13\u5B58\u539F\u59CB\u4EA7\u54C1\u6570\u636E, handle:', rawProduct.handle)\n }\n })\n\n // \u4F7F\u7528\u7EDF\u4E00\u7684\u4EA7\u54C1\u8F6C\u6362\u5DE5\u5177\u51FD\u6570\n const transformedProducts = transformProducts(contentData, site)\n\n // \u5EFA\u7ACB\u4EA7\u54C1\u6620\u5C04\u7F13\u5B58 (handle \u2192 Product)\n // \u7528\u4E8E\u540E\u7EED\u5728 message_end \u65F6\u89E3\u6790\u6587\u672C\u4E2D\u7684 {{handle}}\n transformedProducts.forEach(product => {\n if (product && product.handle) {\n productMapRef.current.set(product.handle, product)\n\n console.log('[useChatState] \u5EFA\u7ACB\u4EA7\u54C1\u6620\u5C04:', {\n handle: product.handle,\n title: product.title,\n })\n }\n })\n\n // \u26A0\uFE0F \u4E0D\u8981\u628A product_list \u6DFB\u52A0\u5230\u6D88\u606F\u4E2D\uFF0C\u907F\u514D\u95EA\u70C1\n // \u7B49\u5230 message_end \u65F6\uFF0C\u901A\u8FC7\u6587\u672C\u89E3\u6790\u521B\u5EFA product_card\uFF0C\u76F4\u63A5\u663E\u793A\u6700\u7EC8\u7684\u4EA4\u66FF\u683C\u5F0F\n console.log('[useChatState] \u2705 \u4EA7\u54C1\u5217\u8868\u5DF2\u7F13\u5B58\uFF0C\u4E0D\u6DFB\u52A0\u5230\u6D88\u606F\u4E2D\uFF08\u907F\u514D\u95EA\u70C1\uFF09')\n break // \u76F4\u63A5\u8DF3\u51FA\uFF0C\u4E0D\u6267\u884C\u540E\u7EED\u7684 push \u64CD\u4F5C\n }\n // ========== 2. \u4EA7\u54C1\u5BF9\u6BD4\u5361\u7247 (Product Comparison) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"product_comparison\", data: {products: [...], dimensions: {...}}}\n else if (contentType === 'product_comparison' && contentData.products) {\n // \u4F7F\u7528\u7EDF\u4E00\u7684\u4EA7\u54C1\u8F6C\u6362\u5DE5\u5177\u51FD\u6570\n const transformedProducts = transformProducts(contentData.products, site)\n\n messageContent = {\n type: 'product_comparison',\n data: {\n products: transformedProducts,\n dimensions: contentData.dimensions || {},\n productComparisonRender: productComparisonRender,\n },\n } as MessageContent\n }\n // ========== 3. FAQ \u5217\u8868\u5361\u7247 (FAQ List) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"faq_list\", data: {found, count, total, results: [...]}}\n else if (contentType === 'faq_list' && contentData.found !== undefined) {\n messageContent = {\n type: 'faq_list',\n data: contentData, // \u76F4\u63A5\u4F7F\u7528\uFF0C\u7ED3\u6784\u5DF2\u5339\u914D\n } as MessageContent\n }\n // ========== 4. \u5FEB\u6377\u56DE\u590D (Quick Replies) ==========\n else if (contentType === 'quick_replies' && contentData.replies) {\n messageContent = {\n type: 'quick_replies',\n data: {\n replies: contentData.replies,\n },\n } as MessageContent\n }\n // ========== 5. \u653F\u7B56\u5185\u5BB9 (Policy) ==========\n else if (contentType === 'policy' && contentData.title && contentData.content) {\n messageContent = {\n type: 'policy',\n data: {\n title: contentData.title,\n content: contentData.content,\n },\n } as MessageContent\n }\n // ========== 6. \u4FC3\u9500\u6D3B\u52A8\u5217\u8868 (Promotion List) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"promotion_list\", data: {found, count, total, results: [...]}}\n else if (contentType === 'promotion_list' && contentData.found !== undefined) {\n // \u89E6\u53D1\u4FC3\u9500\u5361\u7247\u56DE\u8C03\uFF0C\u4F20\u9012\u4FC3\u9500\u6D3B\u52A8\u6570\u7EC4\n onPromotionList?.(contentData.results || [])\n\n messageContent = {\n type: 'promotion_list',\n data: contentData, // \u76F4\u63A5\u4F7F\u7528\uFF0C\u7ED3\u6784\u5DF2\u5339\u914D\n } as MessageContent\n }\n // ========== 7. \u8D2D\u7269\u8F66 (Cart) ==========\n // \u540E\u7AEF\u683C\u5F0F: {type: \"cart\", data: {id, lines: {edges: [...]}, cost, ...}} (Shopify GraphQL)\n // \u9700\u8981\u8F6C\u6362\u4E3A\u524D\u7AEF\u683C\u5F0F: {cartId, lines: [...], cost, ...}\n else if (contentType === 'cart' && contentData.id !== undefined) {\n // \u8F6C\u6362\u540E\u7AEF Shopify GraphQL \u683C\u5F0F\u4E3A\u524D\u7AEF\u6807\u51C6\u683C\u5F0F\n const transformedData = transformCartData(contentData as BackendCartData)\n messageContent = {\n type: 'cart',\n data: {\n ...transformedData,\n onCart: onCart, // \u6CE8\u5165\u8D2D\u7269\u8F66\u6309\u94AE\u56DE\u8C03\n },\n } as MessageContent\n }\n // ========== 8. \u5176\u4ED6\u7C7B\u578B\uFF08\u901A\u7528\u5904\u7406\uFF09 ==========\n else {\n messageContent = {\n type: contentType,\n data: contentData,\n } as MessageContent\n }\n\n // \u26A0\uFE0F \u91CD\u8981\u4FEE\u6539\uFF1A\u5C06\u5361\u7247\u7F13\u5B58\u8D77\u6765\uFF0C\u4E0D\u7ACB\u5373\u6DFB\u52A0\u5230\u6D88\u606F\u4E2D\n // \u7B49\u5F85 message_end \u65F6\uFF0C\u5728\u6587\u672C\u5B8C\u6210\u540E\u518D\u7EDF\u4E00\u6DFB\u52A0\u5361\u7247\n console.log('[useChatState] \u2705 \u5361\u7247\u5DF2\u7F13\u5B58\uFF0C\u7B49\u5F85\u6587\u672C\u5B8C\u6210\u540E\u663E\u793A:', contentType)\n pendingCardsRef.current.push(messageContent)\n\n // \u4E0D\u518D\u7ACB\u5373\u66F4\u65B0\u6D88\u606F\u5217\u8868\uFF0C\u907F\u514D\u5361\u7247\u5728\u6587\u672C\u4E4B\u524D\u663E\u793A\n // \u539F\u6765\u7684\u4EE3\u7801\uFF1A\n // currentMessageRef.current.content.push(messageContent)\n // setMessagesState(prev => { ... })\n }\n break\n }\n\n case 'tool_start':\n case 'tool_end': {\n // \u5DE5\u5177\u8C03\u7528\u4E8B\u4EF6\uFF0C\u6682\u65F6\u5FFD\u7565\n // \u53EF\u4EE5\u5728\u672A\u6765\u7528\u4E8E\u663E\u793A\u5DE5\u5177\u8C03\u7528\u72B6\u6001\n break\n }\n\n case 'message_end': {\n // \u6D88\u606F\u63A5\u6536\u5B8C\u6210\n setIsStreaming(false)\n\n // \u5904\u7406\u7F13\u51B2\u533A\u4E2D\u5269\u4F59\u7684\u6587\u672C\uFF08\u5982\u679C\u6709\uFF09\n if (currentMessageRef.current && textBufferRef.current) {\n console.log('[useChatState] \u5904\u7406\u5269\u4F59\u7F13\u51B2\u533A:', textBufferRef.current)\n\n const lastContent = currentMessageRef.current.content[\n currentMessageRef.current.content.length - 1\n ] as MessageContent | undefined\n\n // \u5982\u679C\u6700\u540E\u4E00\u4E2A\u5185\u5BB9\u662F\u6587\u672C\uFF0C\u5219\u5408\u5E76\n if (lastContent && lastContent.type === 'text') {\n lastContent.text += textBufferRef.current\n } else {\n // \u5426\u5219\u6DFB\u52A0\u4E3A\u65B0\u7684\u6587\u672C\u5757\n currentMessageRef.current.content.push({\n type: 'text',\n text: textBufferRef.current,\n })\n }\n }\n\n // \u26A0\uFE0F \u91CD\u8981\u4FEE\u6539\uFF1A\u5728\u6587\u672C\u5B8C\u6210\u540E\uFF0C\u6DFB\u52A0\u6240\u6709\u7F13\u5B58\u7684\u5361\u7247\n if (currentMessageRef.current && pendingCardsRef.current.length > 0) {\n console.log('[useChatState] \uD83D\uDCCB \u6587\u672C\u5DF2\u5B8C\u6210\uFF0C\u73B0\u5728\u6DFB\u52A0', pendingCardsRef.current.length, '\u4E2A\u7F13\u5B58\u7684\u5361\u7247')\n\n // \u5C06\u6240\u6709\u7F13\u5B58\u7684\u5361\u7247\u6DFB\u52A0\u5230\u6D88\u606F\u5185\u5BB9\u4E2D\n currentMessageRef.current.content.push(...pendingCardsRef.current)\n }\n\n // \u26A0\uFE0F \u8D85\u65F6\u68C0\u6D4B\uFF1A\u5982\u679C message_end \u65F6\u4ECD\u5B58\u5728 thinking block\uFF0C\u89C6\u4E3A\u8D85\u65F6/\u5F02\u5E38\n // \u6CE8\u610F\uFF1A\u5728\u6DFB\u52A0\u5361\u7247\u4E4B\u540E\u68C0\u6D4B\uFF0C\u8FD9\u6837\u5982\u679C\u6709\u5361\u7247\u5C31\u4E0D\u4F1A\u6DFB\u52A0\u8D85\u65F6\u63D0\u793A\n if (currentMessageRef.current) {\n const hasThinking = currentMessageRef.current.content.some(c => c.type === 'thinking')\n\n if (hasThinking) {\n console.log('[useChatState] \u26A0\uFE0F message_end \u65F6\u4ECD\u5B58\u5728 thinking block\uFF0C\u7ACB\u5373\u79FB\u9664\uFF08\u89C6\u4E3A\u8D85\u65F6\uFF09')\n\n // \u79FB\u9664 thinking block\n currentMessageRef.current.content = currentMessageRef.current.content.filter(c => c.type !== 'thinking')\n\n // \u5982\u679C\u6CA1\u6709\u5176\u4ED6\u5185\u5BB9\uFF08\u5305\u62EC\u7F13\u5B58\u7684\u5361\u7247\uFF09\uFF0C\u6DFB\u52A0\u8D85\u65F6\u63D0\u793A\n if (currentMessageRef.current.content.length === 0) {\n currentMessageRef.current.content.push({\n type: 'text',\n text: 'Response timed out, please try again.',\n } as TextContent)\n }\n }\n }\n\n // \u66F4\u65B0\u6D88\u606F\u5217\u8868\uFF08\u7EDF\u4E00\u66F4\u65B0\uFF0C\u5305\u542B\u6587\u672C\u548C\u5361\u7247\uFF09\n if (currentMessageRef.current) {\n setMessagesState(prev => {\n if (!currentMessageRef.current) return prev\n\n const updated = [...prev]\n const existingIndex = updated.findIndex(m => m && m.id === currentMessageRef.current!.id)\n\n if (existingIndex >= 0) {\n updated[existingIndex] = { ...currentMessageRef.current! }\n }\n\n return updated\n })\n }\n\n // \u6E05\u7A7A\u7F13\u51B2\u533A\u3001\u4EA7\u54C1\u6620\u5C04\u548C\u5361\u7247\u7F13\u5B58\n textBufferRef.current = ''\n pendingCardsRef.current = []\n productMapRef.current.clear()\n rawProductMapRef.current.clear()\n currentMessageRef.current = null\n break\n }\n\n case 'status': {\n // T040: \u72B6\u6001\u66F4\u65B0\uFF08\u5982\u4F1A\u8BDD\u8FC7\u671F\uFF09\n const statusData = data as StatusData\n if (statusData.type === 'session_expired') {\n // \u4F1A\u8BDD\u8FC7\u671F\uFF0C\u6E05\u7A7A\u6D88\u606F\u5217\u8868\u548C\u4F1A\u8BDD\n clearMessages()\n clearSession()\n if (welcomeMessage) {\n addMessage({\n id: `welcome-${Date.now()}`,\n role: 'assistant',\n content: [{ type: 'text', text: welcomeMessage }],\n timestamp: Date.now(),\n })\n }\n }\n break\n }\n\n case 'error': {\n // \u9519\u8BEF\u5904\u7406\n const errorData = data as ErrorData\n setIsStreaming(false)\n\n // \u6E05\u7406\u7F13\u5B58\uFF08\u9632\u6B62\u6CC4\u6F0F\u5230\u4E0B\u6B21\u6D88\u606F\uFF09\n textBufferRef.current = ''\n pendingCardsRef.current = []\n productMapRef.current.clear()\n rawProductMapRef.current.clear()\n currentMessageRef.current = null\n\n // \u6DFB\u52A0\u9519\u8BEF\u6D88\u606F\u5230\u754C\u9762\n addMessage({\n id: `error-${Date.now()}`,\n role: 'system',\n content: [\n {\n type: 'error',\n data: {\n message: errorData.message,\n code: errorData.code,\n },\n },\n ],\n timestamp: Date.now(),\n })\n\n onError?.(new Error(errorData.message))\n break\n }\n\n case 'done': {\n // \u6D41\u7ED3\u675F\n setIsStreaming(false)\n\n // \u6E05\u7406\u7F13\u5B58\uFF08\u9632\u6B62\u6CC4\u6F0F\u5230\u4E0B\u6B21\u6D88\u606F\uFF09\n textBufferRef.current = ''\n pendingCardsRef.current = []\n productMapRef.current.clear()\n rawProductMapRef.current.clear()\n\n // \u6E05\u7406\u5F53\u524D\u6D88\u606F\u5F15\u7528\n currentMessageRef.current = null\n break\n }\n\n default:\n // \u5176\u4ED6\u4E8B\u4EF6\u7C7B\u578B\uFF08tool_start, tool_end \u7B49\uFF09\n break\n }\n },\n [\n welcomeMessage,\n site,\n addMessage,\n clearMessages,\n clearSession,\n saveSession,\n sessionId,\n onError,\n onTextMessage,\n onProductList,\n onPromotionList,\n onAddToCart,\n onCart,\n ]\n )\n\n\n const startStream = useCallback((): ((event: SSEEvent) => void) => {\n const streamId = `stream-${Date.now()}-${Math.random().toString(36).slice(2)}`\n activeStreamIdRef.current = streamId\n textBufferRef.current = ''\n pendingCardsRef.current = []\n productMapRef.current.clear()\n rawProductMapRef.current.clear()\n currentMessageRef.current = null\n return (event: SSEEvent) => handleSSEEvent(event, streamId)\n }, [handleSSEEvent])\n\n return {\n messages,\n isOpen,\n userId,\n sessionId,\n inputValue,\n isStreaming,\n openChat,\n closeChat,\n toggleChat,\n setInputValue,\n setUserId,\n addMessage,\n setMessages,\n clearMessages,\n handleSSEEvent,\n startStream,\n saveSession,\n clearSession,\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["useState", "useCallback", "useRef", "useEffect", "getUserId", "saveUserId", "useSession", "transformProducts", "transformCartData", "parseStreamingText", "buffer", "productMap", "rawProductMap", "onAddToCart", "productCardRender", "contents", "regex", "lastIndex", "match", "foundMatch", "beforeText", "productId", "product", "rawProduct", "remainingBuffer", "incompleteMatch", "completeText", "parseTextWithProductIds", "text", "result", "remainingText", "reorganizeMessageContent", "content", "segments", "maybeReorganizeHistoricalMessage", "message", "structuredContent", "reorganizedContent", "useChatState", "options", "welcomeMessage", "site", "controlledOpen", "onOpenChange", "onOpen", "onClose", "onMessageSend", "onError", "onTextMessage", "onProductList", "onPromotionList", "onCart", "productComparisonRender", "sessionId", "saveSession", "clearSession", "userId", "setUserId", "id", "messages", "setMessagesState", "internalOpen", "setInternalOpen", "isControlled", "isOpen", "inputValue", "setInputValue", "isStreaming", "setIsStreaming", "currentMessageRef", "textMessageCallbackTriggeredRef", "productMapRef", "rawProductMapRef", "textBufferRef", "pendingCardsRef", "activeStreamIdRef", "openChat", "closeChat", "toggleChat", "newState", "addMessage", "prev", "setMessages", "newMessages", "validMessages", "msg", "reorganizedMessages", "clearMessages", "handleSSEEvent", "event", "streamId", "eventType", "data", "messageStartData", "lastMessage", "messageId", "deltaData", "deltaText", "c", "lastContent", "updated", "existingIndex", "m", "blockData", "contentType", "contentData", "messageContent", "errorData", "startStream"]
7
7
  }
@@ -15,12 +15,14 @@ export interface FetcherOptions {
15
15
  recaptchaSitekey?: string;
16
16
  recaptchaAction?: string;
17
17
  recaptchaHeaderKey?: string;
18
+ /** 外部传入的 AbortSignal,用于提前中止请求 */
19
+ signal?: AbortSignal;
18
20
  }
19
21
  /**
20
22
  * Fetcher 函数
21
23
  * 支持 reCAPTCHA 和自定义 headers
22
24
  */
23
- export declare const fetcher: ({ url, method, headers, body, timeout, needRecaptcha, recaptchaSitekey, recaptchaAction, recaptchaHeaderKey, }: FetcherOptions) => Promise<Response>;
25
+ export declare const fetcher: ({ url, method, headers, body, timeout, needRecaptcha, recaptchaSitekey, recaptchaAction, recaptchaHeaderKey, signal: externalSignal, }: FetcherOptions) => Promise<Response>;
24
26
  /**
25
27
  * 扩展 Window 接口以支持 grecaptcha
26
28
  */
@@ -1,2 +1,2 @@
1
- const u=async(n,t)=>{if(typeof window>"u")return console.warn("[LiveChat Fetcher] reCAPTCHA not available in non-browser environment"),!1;const r=window.grecaptcha?.enterprise?.execute??window.grecaptcha?.execute;if(!r)return console.warn("[LiveChat Fetcher] reCAPTCHA not loaded (neither enterprise nor standard v3)"),!1;try{return await r(t,{action:n})}catch(e){return console.error("[LiveChat Fetcher] reCAPTCHA execution failed:",e),!1}};async function f(n,t,r="X-Recaptcha-Token"){const e=await u(n,t);return e?{[r]:e}:{}}const T=async({url:n,method:t="POST",headers:r={},body:e,timeout:o=9e4,needRecaptcha:h=!1,recaptchaSitekey:c,recaptchaAction:g="",recaptchaHeaderKey:p="X-Recaptcha-Token"})=>{let s={};h&&(c?s=await f(g,c,p):console.warn("[LiveChat Fetcher] needRecaptcha=true but recaptchaSitekey is missing"));const d=e?JSON.stringify(e):void 0,l=new AbortController;let i;o&&(i=setTimeout(()=>l.abort(),o));try{const a=await fetch(n,{method:t,mode:"cors",headers:{"Content-Type":"application/json",...r,...s},signal:l.signal,...t!=="GET"&&d&&{body:d}});return i&&clearTimeout(i),a}catch(a){throw i&&clearTimeout(i),a}};export{T as fetcher};
1
+ const b=async(n,t)=>{if(typeof window>"u")return console.warn("[LiveChat Fetcher] reCAPTCHA not available in non-browser environment"),!1;const r=window.grecaptcha?.enterprise?.execute??window.grecaptcha?.execute;if(!r)return console.warn("[LiveChat Fetcher] reCAPTCHA not loaded (neither enterprise nor standard v3)"),!1;try{return await r(t,{action:n})}catch(e){return console.error("[LiveChat Fetcher] reCAPTCHA execution failed:",e),!1}};async function T(n,t,r="X-Recaptcha-Token"){const e=await b(n,t);return e?{[r]:e}:{}}const y=async({url:n,method:t="POST",headers:r={},body:e,timeout:s=9e4,needRecaptcha:p=!1,recaptchaSitekey:d,recaptchaAction:u="",recaptchaHeaderKey:f="X-Recaptcha-Token",signal:i})=>{let l={};p&&(d?l=await T(u,d,f):console.warn("[LiveChat Fetcher] needRecaptcha=true but recaptchaSitekey is missing"));const h=e?JSON.stringify(e):void 0,a=new AbortController;let o;s&&(o=setTimeout(()=>a.abort(),s));const g=()=>a.abort();i&&(i.aborted?a.abort():i.addEventListener("abort",g,{once:!0}));try{const c=await fetch(n,{method:t,mode:"cors",headers:{"Content-Type":"application/json",...r,...l},signal:a.signal,...t!=="GET"&&h&&{body:h}});return o&&clearTimeout(o),c}catch(c){throw o&&clearTimeout(o),c}finally{i?.removeEventListener("abort",g)}};export{y as fetcher};
2
2
  //# sourceMappingURL=fetcher.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/components/LiveChatWidget/utils/fetcher.ts"],
4
- "sourcesContent": ["/**\n * LiveChat Fetcher\n * \u53C2\u7167 storefront \u7684\u5B9E\u73B0\uFF0C\u652F\u6301 reCAPTCHA\n */\n\n/**\n * \u6267\u884C Google reCAPTCHA \u9A8C\u8BC1\n * \u517C\u5BB9 Enterprise \u7248 (recaptcha/enterprise.js) \u548C\u6807\u51C6\u7248 v3 (recaptcha/api.js)\n * \u4F18\u5148\u4F7F\u7528 Enterprise \u7248\uFF0C\u56DE\u9000\u5230\u6807\u51C6\u7248\n */\nconst executeRecaptcha = async (action: string, sitekey: string): Promise<string | false> => {\n if (typeof window === 'undefined') {\n console.warn('[LiveChat Fetcher] reCAPTCHA not available in non-browser environment')\n return false\n }\n\n const executor = window.grecaptcha?.enterprise?.execute ?? window.grecaptcha?.execute\n\n if (!executor) {\n console.warn('[LiveChat Fetcher] reCAPTCHA not loaded (neither enterprise nor standard v3)')\n return false\n }\n\n try {\n const token = await executor(sitekey, { action })\n return token\n } catch (error) {\n console.error('[LiveChat Fetcher] reCAPTCHA execution failed:', error)\n return false\n }\n}\n\n/**\n * \u83B7\u53D6 reCAPTCHA headers\n */\nasync function getRecaptchaHeaders(\n action: string,\n sitekey: string,\n headerKey = 'X-Recaptcha-Token'\n): Promise<Record<string, string>> {\n const recaptchaToken = await executeRecaptcha(action, sitekey)\n if (!recaptchaToken) {\n return {}\n }\n return {\n [headerKey]: recaptchaToken,\n }\n}\n\n/**\n * Fetcher \u53C2\u6570\n */\nexport interface FetcherOptions {\n url: string\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n headers?: Record<string, string>\n body?: any\n timeout?: number\n needRecaptcha?: boolean\n recaptchaSitekey?: string\n recaptchaAction?: string\n recaptchaHeaderKey?: string\n}\n\n/**\n * Fetcher \u51FD\u6570\n * \u652F\u6301 reCAPTCHA \u548C\u81EA\u5B9A\u4E49 headers\n */\nexport const fetcher = async ({\n url,\n method = 'POST',\n headers = {},\n body,\n timeout = 90000,\n needRecaptcha = false,\n recaptchaSitekey,\n recaptchaAction = '',\n recaptchaHeaderKey = 'X-Recaptcha-Token',\n}: FetcherOptions): Promise<Response> => {\n // \u83B7\u53D6 reCAPTCHA headers\uFF08\u5982\u679C\u9700\u8981\uFF09\n let recaptchaHeaders: Record<string, string> = {}\n if (needRecaptcha) {\n if (!recaptchaSitekey) {\n console.warn('[LiveChat Fetcher] needRecaptcha=true but recaptchaSitekey is missing')\n } else {\n recaptchaHeaders = await getRecaptchaHeaders(recaptchaAction, recaptchaSitekey, recaptchaHeaderKey)\n }\n }\n\n // \u51C6\u5907\u8BF7\u6C42\u4F53\n const bodyData = body ? JSON.stringify(body) : undefined\n\n const controller = new AbortController()\n let timeoutTimer: NodeJS.Timeout | undefined\n if (timeout) {\n timeoutTimer = setTimeout(() => controller.abort(), timeout)\n }\n\n try {\n const response = await fetch(url, {\n method,\n mode: 'cors',\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n ...recaptchaHeaders,\n },\n signal: controller.signal,\n ...(method !== 'GET' && bodyData && { body: bodyData }),\n })\n\n if (timeoutTimer) {\n clearTimeout(timeoutTimer)\n }\n\n return response\n } catch (error) {\n if (timeoutTimer) {\n clearTimeout(timeoutTimer)\n }\n throw error\n }\n}\n\n/**\n * \u6269\u5C55 Window \u63A5\u53E3\u4EE5\u652F\u6301 grecaptcha\n */\n declare global {\n interface Window {\n grecaptcha?: {\n execute: (sitekey: string, options: { action: string }) => Promise<string>\n ready: (callback: () => void) => void\n enterprise?: {\n execute: (sitekey: string, options: { action: string }) =>\n Promise<string>\n ready: (callback: () => void) => void\n }\n }\n }\n }\n"],
5
- "mappings": "AAUA,MAAMA,EAAmB,MAAOC,EAAgBC,IAA6C,CAC3F,GAAI,OAAO,OAAW,IACpB,eAAQ,KAAK,uEAAuE,EAC7E,GAGT,MAAMC,EAAW,OAAO,YAAY,YAAY,SAAW,OAAO,YAAY,QAE9E,GAAI,CAACA,EACH,eAAQ,KAAK,8EAA8E,EACpF,GAGT,GAAI,CAEF,OADc,MAAMA,EAASD,EAAS,CAAE,OAAAD,CAAO,CAAC,CAElD,OAASG,EAAO,CACd,eAAQ,MAAM,iDAAkDA,CAAK,EAC9D,EACT,CACF,EAKA,eAAeC,EACbJ,EACAC,EACAI,EAAY,oBACqB,CACjC,MAAMC,EAAiB,MAAMP,EAAiBC,EAAQC,CAAO,EAC7D,OAAKK,EAGE,CACL,CAACD,CAAS,EAAGC,CACf,EAJS,CAAC,CAKZ,CAqBO,MAAMC,EAAU,MAAO,CAC5B,IAAAC,EACA,OAAAC,EAAS,OACT,QAAAC,EAAU,CAAC,EACX,KAAAC,EACA,QAAAC,EAAU,IACV,cAAAC,EAAgB,GAChB,iBAAAC,EACA,gBAAAC,EAAkB,GAClB,mBAAAC,EAAqB,mBACvB,IAAyC,CAEvC,IAAIC,EAA2C,CAAC,EAC5CJ,IACGC,EAGHG,EAAmB,MAAMb,EAAoBW,EAAiBD,EAAkBE,CAAkB,EAFlG,QAAQ,KAAK,uEAAuE,GAOxF,MAAME,EAAWP,EAAO,KAAK,UAAUA,CAAI,EAAI,OAEzCQ,EAAa,IAAI,gBACvB,IAAIC,EACAR,IACFQ,EAAe,WAAW,IAAMD,EAAW,MAAM,EAAGP,CAAO,GAG7D,GAAI,CACF,MAAMS,EAAW,MAAM,MAAMb,EAAK,CAChC,OAAAC,EACA,KAAM,OACN,QAAS,CACP,eAAgB,mBAChB,GAAGC,EACH,GAAGO,CACL,EACA,OAAQE,EAAW,OACnB,GAAIV,IAAW,OAASS,GAAY,CAAE,KAAMA,CAAS,CACvD,CAAC,EAED,OAAIE,GACF,aAAaA,CAAY,EAGpBC,CACT,OAASlB,EAAO,CACd,MAAIiB,GACF,aAAaA,CAAY,EAErBjB,CACR,CACF",
6
- "names": ["executeRecaptcha", "action", "sitekey", "executor", "error", "getRecaptchaHeaders", "headerKey", "recaptchaToken", "fetcher", "url", "method", "headers", "body", "timeout", "needRecaptcha", "recaptchaSitekey", "recaptchaAction", "recaptchaHeaderKey", "recaptchaHeaders", "bodyData", "controller", "timeoutTimer", "response"]
4
+ "sourcesContent": ["/**\n * LiveChat Fetcher\n * \u53C2\u7167 storefront \u7684\u5B9E\u73B0\uFF0C\u652F\u6301 reCAPTCHA\n */\n\n/**\n * \u6267\u884C Google reCAPTCHA \u9A8C\u8BC1\n * \u517C\u5BB9 Enterprise \u7248 (recaptcha/enterprise.js) \u548C\u6807\u51C6\u7248 v3 (recaptcha/api.js)\n * \u4F18\u5148\u4F7F\u7528 Enterprise \u7248\uFF0C\u56DE\u9000\u5230\u6807\u51C6\u7248\n */\nconst executeRecaptcha = async (action: string, sitekey: string): Promise<string | false> => {\n if (typeof window === 'undefined') {\n console.warn('[LiveChat Fetcher] reCAPTCHA not available in non-browser environment')\n return false\n }\n\n const executor = window.grecaptcha?.enterprise?.execute ?? window.grecaptcha?.execute\n\n if (!executor) {\n console.warn('[LiveChat Fetcher] reCAPTCHA not loaded (neither enterprise nor standard v3)')\n return false\n }\n\n try {\n const token = await executor(sitekey, { action })\n return token\n } catch (error) {\n console.error('[LiveChat Fetcher] reCAPTCHA execution failed:', error)\n return false\n }\n}\n\n/**\n * \u83B7\u53D6 reCAPTCHA headers\n */\nasync function getRecaptchaHeaders(\n action: string,\n sitekey: string,\n headerKey = 'X-Recaptcha-Token'\n): Promise<Record<string, string>> {\n const recaptchaToken = await executeRecaptcha(action, sitekey)\n if (!recaptchaToken) {\n return {}\n }\n return {\n [headerKey]: recaptchaToken,\n }\n}\n\n/**\n * Fetcher \u53C2\u6570\n */\nexport interface FetcherOptions {\n url: string\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n headers?: Record<string, string>\n body?: any\n timeout?: number\n needRecaptcha?: boolean\n recaptchaSitekey?: string\n recaptchaAction?: string\n recaptchaHeaderKey?: string\n /** \u5916\u90E8\u4F20\u5165\u7684 AbortSignal\uFF0C\u7528\u4E8E\u63D0\u524D\u4E2D\u6B62\u8BF7\u6C42 */\n signal?: AbortSignal\n}\n\n/**\n * Fetcher \u51FD\u6570\n * \u652F\u6301 reCAPTCHA \u548C\u81EA\u5B9A\u4E49 headers\n */\nexport const fetcher = async ({\n url,\n method = 'POST',\n headers = {},\n body,\n timeout = 90000,\n needRecaptcha = false,\n recaptchaSitekey,\n recaptchaAction = '',\n recaptchaHeaderKey = 'X-Recaptcha-Token',\n signal: externalSignal,\n}: FetcherOptions): Promise<Response> => {\n let recaptchaHeaders: Record<string, string> = {}\n if (needRecaptcha) {\n if (!recaptchaSitekey) {\n console.warn('[LiveChat Fetcher] needRecaptcha=true but recaptchaSitekey is missing')\n } else {\n recaptchaHeaders = await getRecaptchaHeaders(recaptchaAction, recaptchaSitekey, recaptchaHeaderKey)\n }\n }\n\n const bodyData = body ? JSON.stringify(body) : undefined\n\n // \u5408\u5E76\u5916\u90E8 signal\uFF08\u7528\u4E8E\u624B\u52A8\u4E2D\u6B62\uFF09\u548C\u8D85\u65F6 signal\n const controller = new AbortController()\n let timeoutTimer: NodeJS.Timeout | undefined\n if (timeout) {\n timeoutTimer = setTimeout(() => controller.abort(), timeout)\n }\n // \u5982\u679C\u5916\u90E8 signal \u5DF2\u4E2D\u6B62\u6216\u5728\u8BF7\u6C42\u671F\u95F4\u4E2D\u6B62\uFF0C\u540C\u6B65\u5230\u5185\u90E8 controller\n const onExternalAbort = () => controller.abort()\n if (externalSignal) {\n if (externalSignal.aborted) {\n controller.abort()\n } else {\n externalSignal.addEventListener('abort', onExternalAbort, { once: true })\n }\n }\n\n try {\n const response = await fetch(url, {\n method,\n mode: 'cors',\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n ...recaptchaHeaders,\n },\n signal: controller.signal,\n ...(method !== 'GET' && bodyData && { body: bodyData }),\n })\n\n if (timeoutTimer) {\n clearTimeout(timeoutTimer)\n }\n\n return response\n } catch (error) {\n if (timeoutTimer) {\n clearTimeout(timeoutTimer)\n }\n throw error\n } finally {\n externalSignal?.removeEventListener('abort', onExternalAbort)\n }\n}\n\n/**\n * \u6269\u5C55 Window \u63A5\u53E3\u4EE5\u652F\u6301 grecaptcha\n */\n declare global {\n interface Window {\n grecaptcha?: {\n execute: (sitekey: string, options: { action: string }) => Promise<string>\n ready: (callback: () => void) => void\n enterprise?: {\n execute: (sitekey: string, options: { action: string }) =>\n Promise<string>\n ready: (callback: () => void) => void\n }\n }\n }\n }\n"],
5
+ "mappings": "AAUA,MAAMA,EAAmB,MAAOC,EAAgBC,IAA6C,CAC3F,GAAI,OAAO,OAAW,IACpB,eAAQ,KAAK,uEAAuE,EAC7E,GAGT,MAAMC,EAAW,OAAO,YAAY,YAAY,SAAW,OAAO,YAAY,QAE9E,GAAI,CAACA,EACH,eAAQ,KAAK,8EAA8E,EACpF,GAGT,GAAI,CAEF,OADc,MAAMA,EAASD,EAAS,CAAE,OAAAD,CAAO,CAAC,CAElD,OAASG,EAAO,CACd,eAAQ,MAAM,iDAAkDA,CAAK,EAC9D,EACT,CACF,EAKA,eAAeC,EACbJ,EACAC,EACAI,EAAY,oBACqB,CACjC,MAAMC,EAAiB,MAAMP,EAAiBC,EAAQC,CAAO,EAC7D,OAAKK,EAGE,CACL,CAACD,CAAS,EAAGC,CACf,EAJS,CAAC,CAKZ,CAuBO,MAAMC,EAAU,MAAO,CAC5B,IAAAC,EACA,OAAAC,EAAS,OACT,QAAAC,EAAU,CAAC,EACX,KAAAC,EACA,QAAAC,EAAU,IACV,cAAAC,EAAgB,GAChB,iBAAAC,EACA,gBAAAC,EAAkB,GAClB,mBAAAC,EAAqB,oBACrB,OAAQC,CACV,IAAyC,CACvC,IAAIC,EAA2C,CAAC,EAC5CL,IACGC,EAGHI,EAAmB,MAAMd,EAAoBW,EAAiBD,EAAkBE,CAAkB,EAFlG,QAAQ,KAAK,uEAAuE,GAMxF,MAAMG,EAAWR,EAAO,KAAK,UAAUA,CAAI,EAAI,OAGzCS,EAAa,IAAI,gBACvB,IAAIC,EACAT,IACFS,EAAe,WAAW,IAAMD,EAAW,MAAM,EAAGR,CAAO,GAG7D,MAAMU,EAAkB,IAAMF,EAAW,MAAM,EAC3CH,IACEA,EAAe,QACjBG,EAAW,MAAM,EAEjBH,EAAe,iBAAiB,QAASK,EAAiB,CAAE,KAAM,EAAK,CAAC,GAI5E,GAAI,CACF,MAAMC,EAAW,MAAM,MAAMf,EAAK,CAChC,OAAAC,EACA,KAAM,OACN,QAAS,CACP,eAAgB,mBAChB,GAAGC,EACH,GAAGQ,CACL,EACA,OAAQE,EAAW,OACnB,GAAIX,IAAW,OAASU,GAAY,CAAE,KAAMA,CAAS,CACvD,CAAC,EAED,OAAIE,GACF,aAAaA,CAAY,EAGpBE,CACT,OAASpB,EAAO,CACd,MAAIkB,GACF,aAAaA,CAAY,EAErBlB,CACR,QAAE,CACAc,GAAgB,oBAAoB,QAASK,CAAe,CAC9D,CACF",
6
+ "names": ["executeRecaptcha", "action", "sitekey", "executor", "error", "getRecaptchaHeaders", "headerKey", "recaptchaToken", "fetcher", "url", "method", "headers", "body", "timeout", "needRecaptcha", "recaptchaSitekey", "recaptchaAction", "recaptchaHeaderKey", "externalSignal", "recaptchaHeaders", "bodyData", "controller", "timeoutTimer", "onExternalAbort", "response"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anker-in/campaign-ui",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Campaign UI components and utilities for Anker projects",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -182,7 +182,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
182
182
  addMessage,
183
183
  setMessages,
184
184
  clearMessages,
185
- handleSSEEvent,
185
+ startStream,
186
186
  saveSession,
187
187
  clearSession,
188
188
  } = chatState
@@ -743,10 +743,12 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
743
743
  },
744
744
  }
745
745
 
746
+ // 创建绑定了当前 streamId 的事件处理器,防止并发 stream 污染共享 buffer
747
+ const streamEventHandler = startStream()
748
+
746
749
  // 发送消息到后端
747
750
  await sendMessageStream(requestPayload, event => {
748
- // 处理 SSE 事件
749
- handleSSEEvent(event)
751
+ streamEventHandler(event)
750
752
 
751
753
  // 特殊处理:会话过期(error 事件且 type 为 validation_error)
752
754
  if (event.event === 'error' && event.data.type === 'validation_error') {
@@ -838,7 +840,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
838
840
  setMessages,
839
841
  createSession,
840
842
  sendMessageStream,
841
- handleSSEEvent,
843
+ startStream,
842
844
  saveSession,
843
845
  clearSession,
844
846
  onMessageSend,
@@ -42,9 +42,9 @@ export async function sendMessage(
42
42
  request: ChatStreamRequest,
43
43
  onEvent: (event: SSEEvent) => void,
44
44
  customHeaders?: Record<string, string>,
45
- recaptchaConfig?: RecaptchaConfig
45
+ recaptchaConfig?: RecaptchaConfig,
46
+ signal?: AbortSignal
46
47
  ): Promise<void> {
47
- // 使用 fetcher 处理 reCAPTCHA 和自定义 headers
48
48
  const response = await fetcher({
49
49
  url: `${apiBaseUrl}/api/chat/stream`,
50
50
  method: 'POST',
@@ -56,6 +56,7 @@ export async function sendMessage(
56
56
  needRecaptcha: recaptchaConfig?.needRecaptcha,
57
57
  recaptchaSitekey: recaptchaConfig?.recaptchaSitekey,
58
58
  recaptchaAction: recaptchaConfig?.recaptchaAction || 'send_message',
59
+ signal,
59
60
  })
60
61
 
61
62
  if (!response.ok) {
@@ -124,6 +124,8 @@ export const MessageList: React.FC<MessageListProps> = ({
124
124
  }, [])
125
125
  const [showScrollButton, setShowScrollButton] = useState(false)
126
126
 
127
+ const [showEmpty, setShowEmpty] = useState(false)
128
+
127
129
  // 检查是否接近底部
128
130
  const isNearBottom = useCallback(
129
131
  (threshold = 100) => {
@@ -187,17 +189,31 @@ export const MessageList: React.FC<MessageListProps> = ({
187
189
  return () => clearTimeout(timeoutId)
188
190
  }, [messages, autoScroll, listElement])
189
191
 
190
- // 空状态 + 加载中
192
+ useEffect(() => {
193
+ if (!isLoadingHistory && messages.length === 0) {
194
+ const timer = setTimeout(() => setShowEmpty(true), 150)
195
+ return () => clearTimeout(timer)
196
+ } else {
197
+ setShowEmpty(false)
198
+ }
199
+ }, [isLoadingHistory, messages.length])
200
+
201
+ if (isLoadingHistory) {
202
+ return (
203
+ <div className={`flex flex-1 items-center justify-center overflow-hidden ${className}`}>
204
+ <LoadingIndicator />
205
+ </div>
206
+ )
207
+ }
208
+
191
209
  if (messages.length === 0) {
192
- if (isLoadingHistory) {
193
- // 加载中,显示 loading
210
+ if (!showEmpty) {
194
211
  return (
195
212
  <div className={`flex flex-1 items-center justify-center overflow-hidden ${className}`}>
196
213
  <LoadingIndicator />
197
214
  </div>
198
215
  )
199
216
  }
200
- // 空状态
201
217
  return (
202
218
  <div className={`flex-1 overflow-hidden ${className}`}>{emptyPlaceholder || <DefaultEmptyPlaceholder />}</div>
203
219
  )
@@ -82,17 +82,16 @@ export function useChatAPI(options: UseChatAPIOptions): UseChatAPIReturn {
82
82
  */
83
83
  const sendMessageStream = useCallback(
84
84
  async (request: ChatStreamRequest, onEvent: (event: SSEEvent) => void) => {
85
- // 如果正在发送,先中断之前的请求
86
85
  if (abortControllerRef.current) {
87
86
  abortControllerRef.current.abort()
88
87
  }
89
88
 
90
- // 创建新的 AbortController
91
89
  abortControllerRef.current = new AbortController()
92
90
  isSendingRef.current = true
91
+ const { signal } = abortControllerRef.current
93
92
 
94
93
  try {
95
- await sendMessage(apiBaseUrl, request, onEvent, headers, recaptchaConfig)
94
+ await sendMessage(apiBaseUrl, request, onEvent, headers, recaptchaConfig, signal)
96
95
  } catch (error) {
97
96
  // 忽略手动中断的错误
98
97
  if (error instanceof Error && error.name === 'AbortError') {
@@ -461,6 +461,12 @@ export interface UseChatStateReturn {
461
461
  */
462
462
  handleSSEEvent: (event: SSEEvent) => void
463
463
 
464
+ /**
465
+ * 开始新的 stream 会话,返回绑定了当前 streamId 的事件处理器。
466
+ * 调用此方法会使之前所有 stream 的事件处理器失效,防止并发 stream 污染共享 buffer。
467
+ */
468
+ startStream: () => (event: SSEEvent) => void
469
+
464
470
  /**
465
471
  * 保存会话 ID
466
472
  */
@@ -560,6 +566,9 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
560
566
  // 卡片缓存队列,用于存储需要延迟显示的卡片(在文本完成后显示)
561
567
  const pendingCardsRef = useRef<MessageContent[]>([])
562
568
 
569
+ // 当前活跃 stream 的唯一 ID,用于丢弃过期 stream 的事件,防止并发 stream 共享 buffer
570
+ const activeStreamIdRef = useRef<string | null>(null)
571
+
563
572
  /**
564
573
  * 打开聊天窗口
565
574
  */
@@ -641,21 +650,20 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
641
650
  * 根据事件类型进行不同的处理
642
651
  */
643
652
  const handleSSEEvent = useCallback(
644
- (event: SSEEvent) => {
653
+ (event: SSEEvent, streamId?: string) => {
645
654
  const { event: eventType, data } = event
646
655
 
656
+ // 并发 stream 防护:除 message_start 外的所有事件必须匹配当前活跃 streamId
657
+ if (eventType !== 'message_start' && streamId !== activeStreamIdRef.current) {
658
+ return
659
+ }
660
+
647
661
  switch (eventType) {
648
662
  case 'message_start': {
649
- // 开始接收新消息
650
663
  setIsStreaming(true)
651
664
 
652
- // 重置文本消息回调标记
653
665
  textMessageCallbackTriggeredRef.current = false
654
-
655
- // 重置文本缓冲区
656
666
  textBufferRef.current = ''
657
-
658
- // 重置卡片缓存队列
659
667
  pendingCardsRef.current = []
660
668
 
661
669
  // T039: 保存 sessionId 和 userId(如果后端返回)
@@ -1088,6 +1096,17 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
1088
1096
  )
1089
1097
 
1090
1098
 
1099
+ const startStream = useCallback((): ((event: SSEEvent) => void) => {
1100
+ const streamId = `stream-${Date.now()}-${Math.random().toString(36).slice(2)}`
1101
+ activeStreamIdRef.current = streamId
1102
+ textBufferRef.current = ''
1103
+ pendingCardsRef.current = []
1104
+ productMapRef.current.clear()
1105
+ rawProductMapRef.current.clear()
1106
+ currentMessageRef.current = null
1107
+ return (event: SSEEvent) => handleSSEEvent(event, streamId)
1108
+ }, [handleSSEEvent])
1109
+
1091
1110
  return {
1092
1111
  messages,
1093
1112
  isOpen,
@@ -1104,6 +1123,7 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
1104
1123
  setMessages,
1105
1124
  clearMessages,
1106
1125
  handleSSEEvent,
1126
+ startStream,
1107
1127
  saveSession,
1108
1128
  clearSession,
1109
1129
  }
@@ -60,6 +60,8 @@ export interface FetcherOptions {
60
60
  recaptchaSitekey?: string
61
61
  recaptchaAction?: string
62
62
  recaptchaHeaderKey?: string
63
+ /** 外部传入的 AbortSignal,用于提前中止请求 */
64
+ signal?: AbortSignal
63
65
  }
64
66
 
65
67
  /**
@@ -76,8 +78,8 @@ export const fetcher = async ({
76
78
  recaptchaSitekey,
77
79
  recaptchaAction = '',
78
80
  recaptchaHeaderKey = 'X-Recaptcha-Token',
81
+ signal: externalSignal,
79
82
  }: FetcherOptions): Promise<Response> => {
80
- // 获取 reCAPTCHA headers(如果需要)
81
83
  let recaptchaHeaders: Record<string, string> = {}
82
84
  if (needRecaptcha) {
83
85
  if (!recaptchaSitekey) {
@@ -87,14 +89,23 @@ export const fetcher = async ({
87
89
  }
88
90
  }
89
91
 
90
- // 准备请求体
91
92
  const bodyData = body ? JSON.stringify(body) : undefined
92
93
 
94
+ // 合并外部 signal(用于手动中止)和超时 signal
93
95
  const controller = new AbortController()
94
96
  let timeoutTimer: NodeJS.Timeout | undefined
95
97
  if (timeout) {
96
98
  timeoutTimer = setTimeout(() => controller.abort(), timeout)
97
99
  }
100
+ // 如果外部 signal 已中止或在请求期间中止,同步到内部 controller
101
+ const onExternalAbort = () => controller.abort()
102
+ if (externalSignal) {
103
+ if (externalSignal.aborted) {
104
+ controller.abort()
105
+ } else {
106
+ externalSignal.addEventListener('abort', onExternalAbort, { once: true })
107
+ }
108
+ }
98
109
 
99
110
  try {
100
111
  const response = await fetch(url, {
@@ -119,6 +130,8 @@ export const fetcher = async ({
119
130
  clearTimeout(timeoutTimer)
120
131
  }
121
132
  throw error
133
+ } finally {
134
+ externalSignal?.removeEventListener('abort', onExternalAbort)
122
135
  }
123
136
  }
124
137