@lobehub/chat 0.103.1 → 0.105.0

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.
@@ -59,8 +59,8 @@ jobs:
59
59
  comment: |
60
60
  ❤️ Great PR @${{ github.event.pull_request.user.login }} ❤️
61
61
 
62
- The growth of project is inseparable from user feedback and contribution, thanks for your contribution!\
63
- 项目的成长离不开用户反馈和贡献,感谢您的贡献!
62
+ The growth of project is inseparable from user feedback and contribution, thanks for your contribution! If you are interesting with the lobehub developer community, please join our [discord](https://discord.com/invite/AYFPHvv2jT) and then dm @arvinxx or @canisminor1990. They will invite you to our private developer channel. We are talking about the lobe-chat development or sharing ai newsletter around the world.\
63
+ 项目的成长离不开用户反馈和贡献,感谢您的贡献! 如果您对 LobeHub 开发者社区感兴趣,请加入我们的 [discord](https://discord.com/invite/AYFPHvv2jT),然后私信 @arvinxx 或 @canisminor1990。他们会邀请您加入我们的私密开发者频道。我们将会讨论关于 Lobe Chat 的开发,分享和讨论全球范围内的 AI 消息。
64
64
  emoji: 'hooray'
65
65
  pr-emoji: '+1, heart'
66
66
  - name: Remove inactive
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.105.0](https://github.com/lobehub/lobe-chat/compare/v0.104.0...v0.105.0)
6
+
7
+ <sup>Released on **2023-11-22**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Standalone pluginn can get more arguments on init.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Standalone pluginn can get more arguments on init, closes [#498](https://github.com/lobehub/lobe-chat/issues/498) ([a7624f5](https://github.com/lobehub/lobe-chat/commit/a7624f5))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 0.104.0](https://github.com/lobehub/lobe-chat/compare/v0.103.1...v0.104.0)
31
+
32
+ <sup>Released on **2023-11-21**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **misc**: Support using env variable to set regions for OpenAI Edge Functions..
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's improved
44
+
45
+ - **misc**: Support using env variable to set regions for OpenAI Edge Functions., closes [#473](https://github.com/lobehub/lobe-chat/issues/473) ([de6b79e](https://github.com/lobehub/lobe-chat/commit/de6b79e))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 0.103.1](https://github.com/lobehub/lobe-chat/compare/v0.103.0...v0.103.1)
6
56
 
7
57
  <sup>Released on **2023-11-21**</sup>
package/README.md CHANGED
@@ -390,11 +390,12 @@ $ docker run -d -p 3210:3210 \
390
390
 
391
391
  This project provides some additional configuration items set with environment variables:
392
392
 
393
- | Environment Variable | Required | Description | Example |
394
- | -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
395
- | `OPENAI_API_KEY` | Yes | This is the API key you apply on the OpenAI account page | `sk-xxxxxx...xxxxxx` |
396
- | `OPENAI_PROXY_URL` | No | If you manually configure the OpenAI interface proxy, you can use this configuration item to override the default OpenAI API request base URL | `https://api.chatanywhere.cn/v1`<br/>The default value is<br/>`https://api.openai.com/v1` |
397
- | `ACCESS_CODE` | No | Add a password to access this service; the password should be a 6-digit number or letter | `awCT74` or `e3@09!` |
393
+ | Environment Variable | Required | Description | Example |
394
+ | ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
395
+ | `OPENAI_API_KEY` | Yes | This is the API key you apply on the OpenAI account page | `sk-xxxxxx...xxxxxx` |
396
+ | `OPENAI_PROXY_URL` | No | If you manually configure the OpenAI interface proxy, you can use this configuration item to override the default OpenAI API request base URL | `https://api.chatanywhere.cn/v1`<br/>The default value is<br/>`https://api.openai.com/v1` |
397
+ | `OPENAI_FUNCTION_REGIONS` | No | When you deploy Lobe-Chat using Vercel and need to specify the region for the Edge Function that handles requests to the OpenAI API, you can use this configuration. The value should be a comma-separated array of strings. | `iad1,sfo1` |
398
+ | `ACCESS_CODE` | No | Add a password to access this service; the password should be a 6-digit number or letter | `awCT74` or `e3@09!` |
398
399
 
399
400
  > \[!NOTE]
400
401
  >
package/README.zh-CN.md CHANGED
@@ -364,11 +364,12 @@ $ docker run -d -p 3210:3210 \
364
364
 
365
365
  本项目提供了一些额外的配置项,使用环境变量进行设置:
366
366
 
367
- | 环境变量 | 类型 | 描述 | 示例 |
368
- | ------------------ | ---- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
369
- | `OPENAI_API_KEY` | 必选 | 这是你在 OpenAI 账户页面申请的 API 密钥 | `sk-xxxxxx...xxxxxx` |
370
- | `OPENAI_PROXY_URL` | 可选 | 如果你手动配置了 OpenAI 接口代理,可以使用此配置项来覆盖默认的 OpenAI API 请求基础 URL | `https://api.chatanywhere.cn/v1`<br/>默认值:<br/>`https://api.openai.com/v1` |
371
- | `ACCESS_CODE` | 可选 | 添加访问此服务的密码,密码应为 6 位数字或字母 | `awCT74` 或 `e3@09!` |
367
+ | 环境变量 | 类型 | 描述 | 示例 |
368
+ | ------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
369
+ | `OPENAI_API_KEY` | 必选 | 这是你在 OpenAI 账户页面申请的 API 密钥 | `sk-xxxxxx...xxxxxx` |
370
+ | `OPENAI_PROXY_URL` | 可选 | 如果你手动配置了 OpenAI 接口代理,可以使用此配置项来覆盖默认的 OpenAI API 请求基础 URL | `https://api.chatanywhere.cn/v1`<br/>默认值:<br/>`https://api.openai.com/v1` |
371
+ | `OPENAI_FUNCTION_REGIONS` | 可选 | 当你使用 Vercel 部署 Lobe-Chat,而且有需求指定响应调用 OpenAI 接口的请求的 Edge Function 的 Region 时,可以使用该配置进行配置,该值的类型为逗号分隔的字符串数组 | `iad1,sfo1` |
372
+ | `ACCESS_CODE` | 可选 | 添加访问此服务的密码,密码应为 6 位数字或字母 | `awCT74` 或 `e3@09!` |
372
373
 
373
374
  > \[!NOTE]
374
375
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.103.1",
3
+ "version": "0.105.0",
4
4
  "description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { getPreferredRegion } from './config';
4
+
5
+ // Stub the global process object to safely mock environment variables
6
+ vi.stubGlobal('process', {
7
+ ...process, // Preserve the original process object
8
+ env: { ...process.env }, // Clone the environment variables object for modification
9
+ });
10
+
11
+ describe('getPreferredRegion', () => {
12
+ beforeEach(() => {
13
+ // Reset environment variables before each test case
14
+ vi.restoreAllMocks();
15
+ });
16
+
17
+ it('returns default value when get config error', () => {
18
+ const originalProcess = global.process;
19
+ const originalError = console.error;
20
+ // @ts-ignore
21
+ global.process = undefined;
22
+ console.error = () => {};
23
+
24
+ const preferredRegion = getPreferredRegion();
25
+ expect(preferredRegion).toBe('auto');
26
+
27
+ global.process = originalProcess;
28
+ console.error = originalError;
29
+ });
30
+
31
+ it('return default value when preferredRegion is empty', () => {
32
+ process.env.OPENAI_FUNCTION_REGIONS = '';
33
+ const preferredRegion = getPreferredRegion();
34
+ expect(preferredRegion).toBe('auto');
35
+ });
36
+
37
+ it('return correct list values when preferredRegion is correctly passed', () => {
38
+ process.env.OPENAI_FUNCTION_REGIONS = 'ida1,sfo1';
39
+ const preferredRegion = getPreferredRegion();
40
+ expect(preferredRegion).toStrictEqual(['ida1', 'sfo1']);
41
+ });
42
+ });
@@ -0,0 +1,15 @@
1
+ import { getServerConfig } from '@/config/server';
2
+
3
+ export const getPreferredRegion = () => {
4
+ try {
5
+ const cfg = getServerConfig();
6
+ if (cfg.OPENAI_FUNCTION_REGIONS.length <= 0) {
7
+ return 'auto';
8
+ }
9
+
10
+ return cfg.OPENAI_FUNCTION_REGIONS;
11
+ } catch (error) {
12
+ console.error('get server config failed, error:', error);
13
+ return 'auto';
14
+ }
15
+ };
@@ -1,9 +1,11 @@
1
1
  import { OpenAIChatStreamPayload } from '@/types/openai/chat';
2
2
 
3
+ import { getPreferredRegion } from '../../config';
3
4
  import { createBizOpenAI } from '../createBizOpenAI';
4
5
  import { createChatCompletion } from './createChatCompletion';
5
6
 
6
7
  export const runtime = 'edge';
8
+ export const preferredRegion = getPreferredRegion();
7
9
 
8
10
  export const POST = async (req: Request) => {
9
11
  const payload = (await req.json()) as OpenAIChatStreamPayload;
@@ -1,9 +1,11 @@
1
1
  import { OpenAISTTPayload } from '@lobehub/tts';
2
2
  import { createOpenaiAudioTranscriptions } from '@lobehub/tts/server';
3
3
 
4
+ import { getPreferredRegion } from '../../config';
4
5
  import { createBizOpenAI } from '../createBizOpenAI';
5
6
 
6
7
  export const runtime = 'edge';
8
+ export const preferredRegion = getPreferredRegion();
7
9
 
8
10
  export const POST = async (req: Request) => {
9
11
  const formData = await req.formData();
@@ -1,9 +1,11 @@
1
1
  import { OpenAITTSPayload } from '@lobehub/tts';
2
2
  import { createOpenaiAudioSpeech } from '@lobehub/tts/server';
3
3
 
4
+ import { getPreferredRegion } from '../../config';
4
5
  import { createBizOpenAI } from '../createBizOpenAI';
5
6
 
6
7
  export const runtime = 'edge';
8
+ export const preferredRegion = getPreferredRegion();
7
9
 
8
10
  export const POST = async (req: Request) => {
9
11
  const payload = (await req.json()) as OpenAITTSPayload;
@@ -23,6 +23,11 @@ import {
23
23
  sendPluginStateToPlugin,
24
24
  } from '../utils/postMessage';
25
25
 
26
+ // just to simplify code a little, don't use this pattern everywhere
27
+ const getSettings = (identifier: string) =>
28
+ pluginSelectors.getPluginSettingsById(identifier)(usePluginStore.getState());
29
+ const getMessage = (id: string) => chatSelectors.getMessageById(id)(useSessionStore.getState());
30
+
26
31
  interface IFrameRenderProps {
27
32
  height?: number;
28
33
  id: string;
@@ -39,8 +44,13 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
39
44
  // when payload change,send content to plugin
40
45
  useOnPluginReadyForInteraction(() => {
41
46
  const iframeWin = iframeRef.current?.contentWindow;
47
+
42
48
  if (iframeWin && payload) {
43
- sendPayloadToPlugin(iframeWin, payload);
49
+ const settings = getSettings(payload.identifier);
50
+ const message = getMessage(id);
51
+ const state = message?.pluginState;
52
+
53
+ sendPayloadToPlugin(iframeWin, { payload, settings, state });
44
54
  }
45
55
  }, [payload]);
46
56
 
@@ -74,7 +84,7 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
74
84
  const iframeWin = iframeRef.current?.contentWindow;
75
85
 
76
86
  if (iframeWin) {
77
- const message = chatSelectors.getMessageById(id)(useSessionStore.getState());
87
+ const message = getMessage(id);
78
88
  if (!message) return;
79
89
 
80
90
  sendPluginStateToPlugin(iframeWin, key, message.pluginState?.[key]);
@@ -94,9 +104,7 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
94
104
  if (iframeWin) {
95
105
  if (!payload?.identifier) return;
96
106
 
97
- const settings = pluginSelectors.getPluginSettingsById(payload?.identifier)(
98
- usePluginStore.getState(),
99
- );
107
+ const settings = getSettings(payload.identifier);
100
108
 
101
109
  sendPluginSettingsToPlugin(iframeWin, settings);
102
110
  }
@@ -0,0 +1,75 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { useOnPluginReadyForInteraction } from './iframeOnReady';
6
+
7
+ describe('useOnPluginReadyForInteraction', () => {
8
+ const mockOnReady = vi.fn();
9
+
10
+ afterEach(() => {
11
+ mockOnReady.mockReset();
12
+ window.removeEventListener('message', expect.any(Function));
13
+ });
14
+
15
+ it('sets readyForRender to true when a PluginChannel.pluginReadyForRender message is received', async () => {
16
+ const { result } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
17
+
18
+ expect(result.current).toBe(false); // Initially, readyForRender should be false
19
+
20
+ const event = new MessageEvent('message', {
21
+ data: { type: PluginChannel.pluginReadyForRender },
22
+ });
23
+
24
+ act(() => {
25
+ window.dispatchEvent(event);
26
+ });
27
+
28
+ expect(result.current).toBe(true); // After the event, readyForRender should be true
29
+ });
30
+
31
+ it('sets readyForRender to true when a PluginChannel.pluginReadyForRender message is received', async () => {
32
+ const { result } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
33
+
34
+ expect(result.current).toBe(false); // Initially, readyForRender should be false
35
+
36
+ const event = new MessageEvent('message', {
37
+ data: { type: PluginChannel.pluginReadyForRender },
38
+ });
39
+
40
+ act(() => {
41
+ window.dispatchEvent(event);
42
+ });
43
+
44
+ expect(result.current).toBe(true); // After the event, readyForRender should be true
45
+ expect(mockOnReady).toHaveBeenCalledTimes(1); // onReady should have been called once
46
+ });
47
+
48
+ it('does not call onReady for non-pluginReadyForRender messages', async () => {
49
+ renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
50
+
51
+ const event = new MessageEvent('message', {
52
+ data: { type: 'nonPluginReadyMessage' },
53
+ });
54
+
55
+ act(() => {
56
+ window.dispatchEvent(event);
57
+ });
58
+
59
+ expect(mockOnReady).not.toHaveBeenCalled();
60
+ });
61
+
62
+ it('cleans up message event listener on unmount', () => {
63
+ const { unmount } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
64
+
65
+ unmount();
66
+
67
+ const event = new MessageEvent('message', {
68
+ data: { type: PluginChannel.pluginReadyForRender },
69
+ });
70
+
71
+ window.dispatchEvent(event);
72
+
73
+ expect(mockOnReady).not.toHaveBeenCalled();
74
+ });
75
+ });
@@ -20,4 +20,6 @@ export const useOnPluginReadyForInteraction = (onReady: () => void, deps: any[]
20
20
  onReady();
21
21
  }
22
22
  }, [readyForRender, ...deps]);
23
+
24
+ return readyForRender;
23
25
  };
@@ -0,0 +1,104 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import {
6
+ useOnPluginFetchMessage,
7
+ useOnPluginFetchPluginSettings,
8
+ useOnPluginFetchPluginState,
9
+ useOnPluginFillContent,
10
+ } from './listenToPlugin';
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ describe('useOnPluginFetchMessage', () => {
17
+ it('calls onRequest when a fetchPluginMessage is received', () => {
18
+ const mockOnRequest = vi.fn();
19
+ renderHook(() => useOnPluginFetchMessage(mockOnRequest));
20
+
21
+ const testData = { key: 'testData', type: PluginChannel.fetchPluginMessage };
22
+ const event = new MessageEvent('message', {
23
+ data: testData,
24
+ });
25
+
26
+ window.dispatchEvent(event);
27
+
28
+ expect(mockOnRequest).toHaveBeenCalledWith(testData);
29
+ });
30
+
31
+ it('does not call onRequest for other message types', () => {
32
+ const mockOnRequest = vi.fn();
33
+ renderHook(() => useOnPluginFetchMessage(mockOnRequest));
34
+
35
+ const event = new MessageEvent('message', {
36
+ data: { type: 'otherMessageType' },
37
+ });
38
+
39
+ window.dispatchEvent(event);
40
+
41
+ expect(mockOnRequest).not.toHaveBeenCalled();
42
+ });
43
+ });
44
+
45
+ describe('useOnPluginFetchPluginState', () => {
46
+ it('calls onRequest with the key when a fetchPluginState message is received', () => {
47
+ const mockOnRequest = vi.fn();
48
+ renderHook(() => useOnPluginFetchPluginState(mockOnRequest));
49
+
50
+ const testKey = 'testKey';
51
+ const event = new MessageEvent('message', {
52
+ data: { type: PluginChannel.fetchPluginState, key: testKey },
53
+ });
54
+
55
+ window.dispatchEvent(event);
56
+
57
+ expect(mockOnRequest).toHaveBeenCalledWith(testKey);
58
+ });
59
+ });
60
+
61
+ describe('useOnPluginFillContent', () => {
62
+ it('calls callback with content when a fillStandalonePluginContent message is received', () => {
63
+ const mockCallback = vi.fn();
64
+ renderHook(() => useOnPluginFillContent(mockCallback));
65
+
66
+ const testContent = 'testContent';
67
+ const event = new MessageEvent('message', {
68
+ data: { type: PluginChannel.fillStandalonePluginContent, content: testContent },
69
+ });
70
+
71
+ window.dispatchEvent(event);
72
+
73
+ expect(mockCallback).toHaveBeenCalledWith(testContent);
74
+ });
75
+
76
+ it('calls callback with JSON stringified content if content is not a string', () => {
77
+ const mockCallback = vi.fn();
78
+ renderHook(() => useOnPluginFillContent(mockCallback));
79
+
80
+ const testContent = { some: 'data' };
81
+ const event = new MessageEvent('message', {
82
+ data: { type: PluginChannel.fillStandalonePluginContent, content: testContent },
83
+ });
84
+
85
+ window.dispatchEvent(event);
86
+
87
+ expect(mockCallback).toHaveBeenCalledWith(JSON.stringify(testContent));
88
+ });
89
+ });
90
+
91
+ describe('useOnPluginFetchPluginSettings', () => {
92
+ it('calls onRequest when a fetchPluginSettings message is received', () => {
93
+ const mockOnRequest = vi.fn();
94
+ renderHook(() => useOnPluginFetchPluginSettings(mockOnRequest));
95
+
96
+ const event = new MessageEvent('message', {
97
+ data: { type: PluginChannel.fetchPluginSettings },
98
+ });
99
+
100
+ window.dispatchEvent(event);
101
+
102
+ expect(mockOnRequest).toHaveBeenCalled();
103
+ });
104
+ });
@@ -0,0 +1,53 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { useOnPluginSettingsUpdate } from './pluginSettings';
6
+
7
+ describe('useOnPluginSettingsUpdate', () => {
8
+ const mockCallback = vi.fn();
9
+
10
+ afterEach(() => {
11
+ mockCallback.mockReset();
12
+ window.removeEventListener('message', expect.any(Function));
13
+ });
14
+
15
+ it('calls the callback when a PluginChannel updatePluginSettings message is received', () => {
16
+ renderHook(() => useOnPluginSettingsUpdate(mockCallback));
17
+
18
+ const testSettings = { theme: 'dark', notifications: true };
19
+ const event = new MessageEvent('message', {
20
+ data: { type: PluginChannel.updatePluginSettings, value: testSettings },
21
+ });
22
+
23
+ window.dispatchEvent(event);
24
+
25
+ expect(mockCallback).toHaveBeenCalledWith(testSettings);
26
+ });
27
+
28
+ it('does not call the callback for non-updatePluginSettings messages', () => {
29
+ renderHook(() => useOnPluginSettingsUpdate(mockCallback));
30
+
31
+ const event = new MessageEvent('message', {
32
+ data: { type: 'nonPluginSettingsUpdate', value: { irrelevant: true } },
33
+ });
34
+
35
+ window.dispatchEvent(event);
36
+
37
+ expect(mockCallback).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it('cleans up message event listener on unmount', () => {
41
+ const { unmount } = renderHook(() => useOnPluginSettingsUpdate(mockCallback));
42
+
43
+ unmount();
44
+
45
+ const event = new MessageEvent('message', {
46
+ data: { type: PluginChannel.updatePluginSettings, value: {} },
47
+ });
48
+
49
+ window.dispatchEvent(event);
50
+
51
+ expect(mockCallback).not.toHaveBeenCalled();
52
+ });
53
+ });
@@ -0,0 +1,43 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { useOnPluginStateUpdate } from './pluginState';
6
+
7
+ describe('useOnPluginStateUpdate', () => {
8
+ // Mock for the callback function to be used in tests
9
+ const mockCallback = vi.fn();
10
+
11
+ afterEach(() => {
12
+ // Reset the mock callback after each test
13
+ mockCallback.mockReset();
14
+ // Ensure no event listeners are left hanging after each test
15
+ window.removeEventListener('message', expect.any(Function));
16
+ });
17
+
18
+ it('calls the callback when a PluginChannel update message is received', () => {
19
+ renderHook(() => useOnPluginStateUpdate(mockCallback));
20
+
21
+ const testKey = 'testKey';
22
+ const testValue = 'testValue';
23
+ const event = new MessageEvent('message', {
24
+ data: { type: PluginChannel.updatePluginState, key: testKey, value: testValue },
25
+ });
26
+
27
+ window.dispatchEvent(event);
28
+
29
+ expect(mockCallback).toHaveBeenCalledWith(testKey, testValue);
30
+ });
31
+
32
+ it('does not call the callback for non-PluginChannel messages', () => {
33
+ renderHook(() => useOnPluginStateUpdate(mockCallback));
34
+
35
+ const event = new MessageEvent('message', {
36
+ data: { type: 'nonPluginMessage', key: 'key', value: 'value' },
37
+ });
38
+
39
+ window.dispatchEvent(event);
40
+
41
+ expect(mockCallback).not.toHaveBeenCalled();
42
+ });
43
+ });
@@ -0,0 +1,64 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import {
5
+ sendMessageContentToPlugin,
6
+ sendPayloadToPlugin,
7
+ sendPluginSettingsToPlugin,
8
+ sendPluginStateToPlugin,
9
+ } from './postMessage';
10
+
11
+ // Mock window object with a postMessage spy
12
+ const mockWindow = {
13
+ postMessage: vi.fn(),
14
+ };
15
+
16
+ describe('plugin communication functions', () => {
17
+ it('sendMessageContentToPlugin should call window.postMessage with correct arguments', () => {
18
+ const props = { some: 'data' };
19
+ sendMessageContentToPlugin(mockWindow as unknown as Window, props);
20
+ expect(mockWindow.postMessage).toHaveBeenCalledWith(
21
+ { props, type: PluginChannel.renderPlugin },
22
+ '*',
23
+ );
24
+ });
25
+
26
+ it('sendPayloadToPlugin should call window.postMessage with correct arguments', () => {
27
+ const props = { payload: 'payload', settings: 'settings', state: 'state' };
28
+ sendPayloadToPlugin(mockWindow as unknown as Window, props);
29
+ expect(mockWindow.postMessage).toHaveBeenCalledWith(
30
+ {
31
+ type: PluginChannel.initStandalonePlugin,
32
+ payload: props.payload,
33
+ settings: props.settings,
34
+ state: props.state,
35
+ props: props.payload, // Note: This is due to the TODO in your code
36
+ },
37
+ '*',
38
+ );
39
+ });
40
+
41
+ it('sendPluginStateToPlugin should call window.postMessage with correct arguments', () => {
42
+ const key = 'key';
43
+ const value = 'value';
44
+ sendPluginStateToPlugin(mockWindow as unknown as Window, key, value);
45
+ expect(mockWindow.postMessage).toHaveBeenCalledWith(
46
+ { key, type: PluginChannel.renderPluginState, value },
47
+ '*',
48
+ );
49
+ });
50
+
51
+ it('sendPluginSettingsToPlugin should call window.postMessage with correct arguments', () => {
52
+ const settings = { setting1: 'value1' };
53
+ sendPluginSettingsToPlugin(mockWindow as unknown as Window, settings);
54
+ expect(mockWindow.postMessage).toHaveBeenCalledWith(
55
+ { type: PluginChannel.renderPluginSettings, value: settings },
56
+ '*',
57
+ );
58
+ });
59
+ });
60
+
61
+ // Reset the mock after each test
62
+ afterEach(() => {
63
+ vi.resetAllMocks();
64
+ });
@@ -4,8 +4,19 @@ export const sendMessageContentToPlugin = (window: Window, props: any) => {
4
4
  window.postMessage({ props, type: PluginChannel.renderPlugin }, '*');
5
5
  };
6
6
 
7
- export const sendPayloadToPlugin = (window: Window, props: any) => {
8
- window.postMessage({ props, type: PluginChannel.initStandalonePlugin }, '*');
7
+ export const sendPayloadToPlugin = (
8
+ window: Window,
9
+ props: { payload: any; settings: any; state?: any },
10
+ ) => {
11
+ window.postMessage(
12
+ {
13
+ type: PluginChannel.initStandalonePlugin,
14
+ ...props,
15
+ // TODO: props need to deprecated
16
+ props: props.payload,
17
+ },
18
+ '*',
19
+ );
9
20
  };
10
21
 
11
22
  export const sendPluginStateToPlugin = (window: Window, key: string, value: any) => {
@@ -39,6 +39,12 @@ describe('getServerConfig', () => {
39
39
  expect(config.USE_AZURE_OPENAI).toBe(false);
40
40
  });
41
41
 
42
+ it('correctly handles values for OPENAI_FUNCTION_REGIONS', () => {
43
+ process.env.OPENAI_FUNCTION_REGIONS = 'iad1,sfo1';
44
+ const config = getServerConfig();
45
+ expect(config.OPENAI_FUNCTION_REGIONS).toStrictEqual(['iad1', 'sfo1']);
46
+ });
47
+
42
48
  it('returns default IMGUR_CLIENT_ID when no environment variable is set', () => {
43
49
  const config = getServerConfig();
44
50
  expect(config.IMGUR_CLIENT_ID).toBe('e415f320d6e24f9');
@@ -27,11 +27,18 @@ export const getServerConfig = () => {
27
27
  throw new Error('[Server Config] you are importing a nodejs-only module outside of nodejs');
28
28
  }
29
29
 
30
+ // region format: iad1,sfo1
31
+ let regions: string[] = [];
32
+ if (process.env.OPENAI_FUNCTION_REGIONS) {
33
+ regions = process.env.OPENAI_FUNCTION_REGIONS.split(',');
34
+ }
35
+
30
36
  return {
31
37
  ACCESS_CODE: process.env.ACCESS_CODE,
32
38
 
33
39
  OPENAI_API_KEY: process.env.OPENAI_API_KEY,
34
40
  OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
41
+ OPENAI_FUNCTION_REGIONS: regions,
35
42
 
36
43
  AZURE_API_KEY: process.env.AZURE_API_KEY,
37
44
  AZURE_API_VERSION: process.env.AZURE_API_VERSION,