@lobehub/chat 1.51.2 → 1.51.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/Dockerfile +1 -1
- package/Dockerfile.database +1 -1
- package/changelog/v1.json +12 -0
- package/docs/usage/providers/wenxin.mdx +16 -13
- package/docs/usage/providers/wenxin.zh-CN.mdx +11 -8
- package/package.json +1 -2
- package/src/app/(main)/settings/llm/ProviderList/providers.tsx +2 -4
- package/src/config/aiModels/wenxin.ts +125 -19
- package/src/config/llm.ts +3 -5
- package/src/config/modelProviders/wenxin.ts +100 -23
- package/src/const/auth.ts +0 -3
- package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -3
- package/src/features/Conversation/components/ChatItem/utils.test.ts +284 -0
- package/src/features/Conversation/components/ChatItem/utils.ts +39 -8
- package/src/features/Conversation/components/MarkdownElements/LobeArtifact/rehypePlugin.test.ts +125 -0
- package/src/features/DevPanel/CacheViewer/DataTable/index.tsx +33 -0
- package/src/features/DevPanel/CacheViewer/cacheProvider.tsx +64 -0
- package/src/features/DevPanel/CacheViewer/getCacheEntries.ts +52 -0
- package/src/features/DevPanel/CacheViewer/index.tsx +25 -0
- package/src/features/DevPanel/CacheViewer/schema.ts +49 -0
- package/src/features/DevPanel/FeatureFlagViewer/Form.tsx +93 -0
- package/src/features/DevPanel/FeatureFlagViewer/index.tsx +11 -0
- package/src/features/DevPanel/MetadataViewer/Ld.tsx +25 -0
- package/src/features/DevPanel/MetadataViewer/MetaData.tsx +30 -0
- package/src/features/DevPanel/MetadataViewer/Og.tsx +75 -0
- package/src/features/DevPanel/MetadataViewer/index.tsx +80 -0
- package/src/features/DevPanel/MetadataViewer/useHead.ts +16 -0
- package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +39 -49
- package/src/features/DevPanel/PostgresViewer/{TableColumns.tsx → SchemaSidebar/Columns.tsx} +6 -4
- package/src/features/DevPanel/PostgresViewer/{Schema.tsx → SchemaSidebar/index.tsx} +49 -55
- package/src/features/DevPanel/PostgresViewer/index.tsx +4 -2
- package/src/features/DevPanel/features/FloatPanel.tsx +218 -0
- package/src/features/DevPanel/features/Header.tsx +50 -0
- package/src/features/DevPanel/features/Table/TableCell.tsx +73 -0
- package/src/features/DevPanel/features/Table/TooltipContent.tsx +39 -0
- package/src/features/DevPanel/{PostgresViewer/DataTable/Table.tsx → features/Table/index.tsx} +12 -14
- package/src/features/DevPanel/index.tsx +29 -5
- package/src/libs/agent-runtime/AgentRuntime.test.ts +0 -1
- package/src/libs/agent-runtime/AgentRuntime.ts +7 -0
- package/src/libs/agent-runtime/wenxin/index.ts +10 -107
- package/src/locales/default/modelProvider.ts +0 -20
- package/src/server/modules/AgentRuntime/index.test.ts +0 -21
- package/src/services/_auth.ts +0 -14
- package/src/store/chat/slices/portal/selectors.test.ts +169 -3
- package/src/store/chat/slices/portal/selectors.ts +6 -1
- package/src/store/user/slices/modelList/selectors/keyVaults.ts +0 -2
- package/src/types/aiProvider.ts +0 -1
- package/src/types/user/settings/keyVaults.ts +1 -6
- package/src/app/(backend)/webapi/chat/wenxin/route.test.ts +0 -27
- package/src/app/(backend)/webapi/chat/wenxin/route.ts +0 -30
- package/src/app/(main)/settings/llm/ProviderList/Wenxin/index.tsx +0 -44
- package/src/app/(main)/settings/provider/(detail)/wenxin/page.tsx +0 -61
- package/src/features/Conversation/Error/APIKeyForm/Wenxin.tsx +0 -49
- package/src/features/DevPanel/FloatPanel.tsx +0 -136
- package/src/features/DevPanel/PostgresViewer/DataTable/TableCell.tsx +0 -34
- package/src/libs/agent-runtime/utils/streams/wenxin.test.ts +0 -153
- package/src/libs/agent-runtime/utils/streams/wenxin.ts +0 -38
- package/src/libs/agent-runtime/wenxin/type.ts +0 -84
@@ -1,44 +0,0 @@
|
|
1
|
-
'use client';
|
2
|
-
|
3
|
-
import { Input } from 'antd';
|
4
|
-
import { useTranslation } from 'react-i18next';
|
5
|
-
|
6
|
-
import { WenxinProviderCard } from '@/config/modelProviders';
|
7
|
-
import { GlobalLLMProviderKey } from '@/types/user/settings';
|
8
|
-
|
9
|
-
import { KeyVaultsConfigKey } from '../../const';
|
10
|
-
import { ProviderItem } from '../../type';
|
11
|
-
|
12
|
-
const providerKey: GlobalLLMProviderKey = 'wenxin';
|
13
|
-
|
14
|
-
export const useWenxinProvider = (): ProviderItem => {
|
15
|
-
const { t } = useTranslation('modelProvider');
|
16
|
-
|
17
|
-
return {
|
18
|
-
...WenxinProviderCard,
|
19
|
-
apiKeyItems: [
|
20
|
-
{
|
21
|
-
children: (
|
22
|
-
<Input.Password
|
23
|
-
autoComplete={'new-password'}
|
24
|
-
placeholder={t(`${providerKey}.accessKey.placeholder`)}
|
25
|
-
/>
|
26
|
-
),
|
27
|
-
desc: t(`${providerKey}.accessKey.desc`),
|
28
|
-
label: t(`${providerKey}.accessKey.title`),
|
29
|
-
name: [KeyVaultsConfigKey, providerKey, 'accessKey'],
|
30
|
-
},
|
31
|
-
{
|
32
|
-
children: (
|
33
|
-
<Input.Password
|
34
|
-
autoComplete={'new-password'}
|
35
|
-
placeholder={t(`${providerKey}.secretKey.placeholder`)}
|
36
|
-
/>
|
37
|
-
),
|
38
|
-
desc: t(`${providerKey}.secretKey.desc`),
|
39
|
-
label: t(`${providerKey}.secretKey.title`),
|
40
|
-
name: [KeyVaultsConfigKey, providerKey, 'secretKey'],
|
41
|
-
},
|
42
|
-
],
|
43
|
-
};
|
44
|
-
};
|
@@ -1,61 +0,0 @@
|
|
1
|
-
'use client';
|
2
|
-
|
3
|
-
import { useTranslation } from 'react-i18next';
|
4
|
-
|
5
|
-
import { FormPassword } from '@/components/FormInput';
|
6
|
-
import { WenxinProviderCard } from '@/config/modelProviders';
|
7
|
-
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
8
|
-
import { GlobalLLMProviderKey } from '@/types/user/settings';
|
9
|
-
|
10
|
-
import { KeyVaultsConfigKey } from '../../const';
|
11
|
-
import { SkeletonInput } from '../../features/ProviderConfig';
|
12
|
-
import { ProviderItem } from '../../type';
|
13
|
-
import ProviderDetail from '../[id]';
|
14
|
-
|
15
|
-
const providerKey: GlobalLLMProviderKey = 'wenxin';
|
16
|
-
|
17
|
-
const useProviderCard = (): ProviderItem => {
|
18
|
-
const { t } = useTranslation('modelProvider');
|
19
|
-
|
20
|
-
const isLoading = useAiInfraStore(aiProviderSelectors.isAiProviderConfigLoading(providerKey));
|
21
|
-
|
22
|
-
return {
|
23
|
-
...WenxinProviderCard,
|
24
|
-
apiKeyItems: [
|
25
|
-
{
|
26
|
-
children: isLoading ? (
|
27
|
-
<SkeletonInput />
|
28
|
-
) : (
|
29
|
-
<FormPassword
|
30
|
-
autoComplete={'new-password'}
|
31
|
-
placeholder={t(`${providerKey}.accessKey.placeholder`)}
|
32
|
-
/>
|
33
|
-
),
|
34
|
-
desc: t(`${providerKey}.accessKey.desc`),
|
35
|
-
label: t(`${providerKey}.accessKey.title`),
|
36
|
-
name: [KeyVaultsConfigKey, 'accessKey'],
|
37
|
-
},
|
38
|
-
{
|
39
|
-
children: isLoading ? (
|
40
|
-
<SkeletonInput />
|
41
|
-
) : (
|
42
|
-
<FormPassword
|
43
|
-
autoComplete={'new-password'}
|
44
|
-
placeholder={t(`${providerKey}.secretKey.placeholder`)}
|
45
|
-
/>
|
46
|
-
),
|
47
|
-
desc: t(`${providerKey}.secretKey.desc`),
|
48
|
-
label: t(`${providerKey}.secretKey.title`),
|
49
|
-
name: [KeyVaultsConfigKey, 'secretKey'],
|
50
|
-
},
|
51
|
-
],
|
52
|
-
};
|
53
|
-
};
|
54
|
-
|
55
|
-
const Page = () => {
|
56
|
-
const card = useProviderCard();
|
57
|
-
|
58
|
-
return <ProviderDetail {...card} />;
|
59
|
-
};
|
60
|
-
|
61
|
-
export default Page;
|
@@ -1,49 +0,0 @@
|
|
1
|
-
import { Wenxin } from '@lobehub/icons';
|
2
|
-
import { Input } from 'antd';
|
3
|
-
import { memo } from 'react';
|
4
|
-
import { useTranslation } from 'react-i18next';
|
5
|
-
|
6
|
-
import { ModelProvider } from '@/libs/agent-runtime';
|
7
|
-
import { useUserStore } from '@/store/user';
|
8
|
-
import { keyVaultsConfigSelectors } from '@/store/user/selectors';
|
9
|
-
|
10
|
-
import { FormAction } from '../style';
|
11
|
-
|
12
|
-
const WenxinForm = memo(() => {
|
13
|
-
const { t } = useTranslation('modelProvider');
|
14
|
-
|
15
|
-
const [accessKey, secretKey, setConfig] = useUserStore((s) => [
|
16
|
-
keyVaultsConfigSelectors.wenxinConfig(s).accessKey,
|
17
|
-
keyVaultsConfigSelectors.wenxinConfig(s).secretKey,
|
18
|
-
s.updateKeyVaultConfig,
|
19
|
-
]);
|
20
|
-
|
21
|
-
return (
|
22
|
-
<FormAction
|
23
|
-
avatar={<Wenxin.Color size={56} />}
|
24
|
-
description={t('wenxin.unlock.description')}
|
25
|
-
title={t('wenxin.unlock.title')}
|
26
|
-
>
|
27
|
-
<Input.Password
|
28
|
-
autoComplete={'new-password'}
|
29
|
-
onChange={(e) => {
|
30
|
-
setConfig(ModelProvider.Wenxin, { accessKey: e.target.value });
|
31
|
-
}}
|
32
|
-
placeholder={'Access Key'}
|
33
|
-
type={'block'}
|
34
|
-
value={accessKey}
|
35
|
-
/>
|
36
|
-
<Input.Password
|
37
|
-
autoComplete={'new-password'}
|
38
|
-
onChange={(e) => {
|
39
|
-
setConfig(ModelProvider.Wenxin, { secretKey: e.target.value });
|
40
|
-
}}
|
41
|
-
placeholder={'Secret Key'}
|
42
|
-
type={'block'}
|
43
|
-
value={secretKey}
|
44
|
-
/>
|
45
|
-
</FormAction>
|
46
|
-
);
|
47
|
-
});
|
48
|
-
|
49
|
-
export default WenxinForm;
|
@@ -1,136 +0,0 @@
|
|
1
|
-
import { ActionIcon, Icon } from '@lobehub/ui';
|
2
|
-
import { FloatButton } from 'antd';
|
3
|
-
import { createStyles } from 'antd-style';
|
4
|
-
import { BugIcon, BugOff, XIcon } from 'lucide-react';
|
5
|
-
import React, { PropsWithChildren, useEffect, useState } from 'react';
|
6
|
-
import { Flexbox } from 'react-layout-kit';
|
7
|
-
import { Rnd } from 'react-rnd';
|
8
|
-
|
9
|
-
// 定义样式
|
10
|
-
const useStyles = createStyles(({ token, css }) => {
|
11
|
-
return {
|
12
|
-
collapsed: css`
|
13
|
-
pointer-events: none;
|
14
|
-
transform: scale(0.8);
|
15
|
-
opacity: 0;
|
16
|
-
`,
|
17
|
-
content: css`
|
18
|
-
overflow: auto;
|
19
|
-
flex: 1;
|
20
|
-
height: 100%;
|
21
|
-
color: ${token.colorText};
|
22
|
-
`,
|
23
|
-
|
24
|
-
expanded: css`
|
25
|
-
pointer-events: auto;
|
26
|
-
transform: scale(1);
|
27
|
-
opacity: 1;
|
28
|
-
`,
|
29
|
-
|
30
|
-
header: css`
|
31
|
-
cursor: move;
|
32
|
-
user-select: none;
|
33
|
-
|
34
|
-
padding-block: 8px;
|
35
|
-
padding-inline: 16px;
|
36
|
-
border-block-end: 1px solid ${token.colorBorderSecondary};
|
37
|
-
border-start-start-radius: 12px;
|
38
|
-
border-start-end-radius: 12px;
|
39
|
-
|
40
|
-
font-weight: ${token.fontWeightStrong};
|
41
|
-
color: ${token.colorText};
|
42
|
-
|
43
|
-
background: ${token.colorFillAlter};
|
44
|
-
`,
|
45
|
-
panel: css`
|
46
|
-
position: fixed;
|
47
|
-
z-index: 1000;
|
48
|
-
|
49
|
-
overflow: hidden;
|
50
|
-
display: flex;
|
51
|
-
|
52
|
-
border-radius: 12px;
|
53
|
-
|
54
|
-
background: ${token.colorBgContainer};
|
55
|
-
box-shadow: ${token.boxShadow};
|
56
|
-
|
57
|
-
transition: opacity ${token.motionDurationMid} ${token.motionEaseInOut};
|
58
|
-
`,
|
59
|
-
};
|
60
|
-
});
|
61
|
-
|
62
|
-
const minWidth = 800;
|
63
|
-
const minHeight = 600;
|
64
|
-
|
65
|
-
const CollapsibleFloatPanel = ({ children }: PropsWithChildren) => {
|
66
|
-
const { styles } = useStyles();
|
67
|
-
const [isExpanded, setIsExpanded] = useState(false);
|
68
|
-
const [position, setPosition] = useState({ x: 100, y: 100 });
|
69
|
-
const [size, setSize] = useState({ height: minHeight, width: minWidth });
|
70
|
-
|
71
|
-
useEffect(() => {
|
72
|
-
try {
|
73
|
-
const localStoragePosition = localStorage.getItem('debug-panel-position');
|
74
|
-
if (localStoragePosition && JSON.parse(localStoragePosition)) {
|
75
|
-
setPosition(JSON.parse(localStoragePosition));
|
76
|
-
}
|
77
|
-
} catch {
|
78
|
-
/* empty */
|
79
|
-
}
|
80
|
-
|
81
|
-
try {
|
82
|
-
const localStorageSize = localStorage.getItem('debug-panel-size');
|
83
|
-
if (localStorageSize && JSON.parse(localStorageSize)) {
|
84
|
-
setSize(JSON.parse(localStorageSize));
|
85
|
-
}
|
86
|
-
} catch {
|
87
|
-
/* empty */
|
88
|
-
}
|
89
|
-
}, []);
|
90
|
-
|
91
|
-
return (
|
92
|
-
<>
|
93
|
-
<FloatButton
|
94
|
-
icon={<Icon icon={isExpanded ? BugOff : BugIcon} />}
|
95
|
-
onClick={() => setIsExpanded(!isExpanded)}
|
96
|
-
style={{ bottom: 24, right: 24 }}
|
97
|
-
/>
|
98
|
-
{isExpanded && (
|
99
|
-
<Rnd
|
100
|
-
bounds="window"
|
101
|
-
className={`${styles.panel} ${isExpanded ? styles.expanded : styles.collapsed}`}
|
102
|
-
dragHandleClassName="panel-drag-handle"
|
103
|
-
minHeight={minHeight}
|
104
|
-
minWidth={minWidth}
|
105
|
-
onDragStop={(e, d) => {
|
106
|
-
setPosition({ x: d.x, y: d.y });
|
107
|
-
}}
|
108
|
-
onResizeStop={(e, direction, ref, delta, position) => {
|
109
|
-
setSize({
|
110
|
-
height: Number(ref.style.height),
|
111
|
-
width: Number(ref.style.width),
|
112
|
-
});
|
113
|
-
setPosition(position);
|
114
|
-
}}
|
115
|
-
position={position}
|
116
|
-
size={size}
|
117
|
-
>
|
118
|
-
<Flexbox height={'100%'}>
|
119
|
-
<Flexbox
|
120
|
-
align={'center'}
|
121
|
-
className={`panel-drag-handle ${styles.header}`}
|
122
|
-
horizontal
|
123
|
-
justify={'space-between'}
|
124
|
-
>
|
125
|
-
开发者面板
|
126
|
-
<ActionIcon icon={XIcon} onClick={() => setIsExpanded(false)} />
|
127
|
-
</Flexbox>
|
128
|
-
<Flexbox className={styles.content}>{children}</Flexbox>
|
129
|
-
</Flexbox>
|
130
|
-
</Rnd>
|
131
|
-
)}
|
132
|
-
</>
|
133
|
-
);
|
134
|
-
};
|
135
|
-
|
136
|
-
export default CollapsibleFloatPanel;
|
@@ -1,34 +0,0 @@
|
|
1
|
-
import React, { useMemo } from 'react';
|
2
|
-
|
3
|
-
interface TableCellProps {
|
4
|
-
column: string;
|
5
|
-
dataItem: any;
|
6
|
-
rowIndex: number;
|
7
|
-
}
|
8
|
-
|
9
|
-
const TableCell = ({ dataItem, column, rowIndex }: TableCellProps) => {
|
10
|
-
const data = dataItem[column];
|
11
|
-
const content = useMemo(() => {
|
12
|
-
switch (typeof data) {
|
13
|
-
case 'object': {
|
14
|
-
return JSON.stringify(data);
|
15
|
-
}
|
16
|
-
|
17
|
-
case 'boolean': {
|
18
|
-
return data ? 'True' : 'False';
|
19
|
-
}
|
20
|
-
|
21
|
-
default: {
|
22
|
-
return data;
|
23
|
-
}
|
24
|
-
}
|
25
|
-
}, [data]);
|
26
|
-
|
27
|
-
return (
|
28
|
-
<td key={column} onDoubleClick={() => console.log('Edit cell:', rowIndex, column)}>
|
29
|
-
{content}
|
30
|
-
</td>
|
31
|
-
);
|
32
|
-
};
|
33
|
-
|
34
|
-
export default TableCell;
|
@@ -1,153 +0,0 @@
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
2
|
-
|
3
|
-
import * as uuidModule from '@/utils/uuid';
|
4
|
-
|
5
|
-
import { convertIterableToStream } from '../../utils/streams/protocol';
|
6
|
-
import { ChatResp } from '../../wenxin/type';
|
7
|
-
import { WenxinStream } from './wenxin';
|
8
|
-
|
9
|
-
const dataStream = [
|
10
|
-
{
|
11
|
-
id: 'as-vb0m37ti8y',
|
12
|
-
object: 'chat.completion',
|
13
|
-
created: 1709089502,
|
14
|
-
sentence_id: 0,
|
15
|
-
is_end: false,
|
16
|
-
is_truncated: false,
|
17
|
-
result: '当然可以,',
|
18
|
-
need_clear_history: false,
|
19
|
-
finish_reason: 'normal',
|
20
|
-
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
21
|
-
},
|
22
|
-
{
|
23
|
-
id: 'as-vb0m37ti8y',
|
24
|
-
object: 'chat.completion',
|
25
|
-
created: 1709089504,
|
26
|
-
sentence_id: 1,
|
27
|
-
is_end: false,
|
28
|
-
is_truncated: false,
|
29
|
-
result:
|
30
|
-
'以下是一些建议的自驾游路线,它们涵盖了各种不同的风景和文化体验:\n\n1. **西安-敦煌历史文化之旅**:\n\n\n\t* 路线:西安',
|
31
|
-
need_clear_history: false,
|
32
|
-
finish_reason: 'normal',
|
33
|
-
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
34
|
-
},
|
35
|
-
{
|
36
|
-
id: 'as-vb0m37ti8y',
|
37
|
-
object: 'chat.completion',
|
38
|
-
created: 1709089506,
|
39
|
-
sentence_id: 2,
|
40
|
-
is_end: false,
|
41
|
-
is_truncated: false,
|
42
|
-
result: ' - 天水 - 兰州 - 嘉峪关 - 敦煌\n\t* 特点:此路线让您领略到中国西北的丰富历史文化。',
|
43
|
-
need_clear_history: false,
|
44
|
-
finish_reason: 'normal',
|
45
|
-
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
46
|
-
},
|
47
|
-
{
|
48
|
-
id: 'as-vb0m37ti8y',
|
49
|
-
object: 'chat.completion',
|
50
|
-
created: 1709089508,
|
51
|
-
sentence_id: 3,
|
52
|
-
is_end: false,
|
53
|
-
is_truncated: false,
|
54
|
-
result: '您可以参观西安的兵马俑、大雁塔,体验兰州的黄河风情,以及在敦煌欣赏壮丽的莫高窟。',
|
55
|
-
need_clear_history: false,
|
56
|
-
finish_reason: 'normal',
|
57
|
-
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
58
|
-
},
|
59
|
-
{
|
60
|
-
id: 'as-vb0m37ti8y',
|
61
|
-
object: 'chat.completion',
|
62
|
-
created: 1709089511,
|
63
|
-
sentence_id: 4,
|
64
|
-
is_end: false,
|
65
|
-
is_truncated: false,
|
66
|
-
result: '\n2. **海南环岛热带风情游**:\n\n\n\t* 路线:海口 - 三亚 - 陵水 - 万宁 - 文昌 - 海',
|
67
|
-
need_clear_history: false,
|
68
|
-
finish_reason: 'normal',
|
69
|
-
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
70
|
-
},
|
71
|
-
{
|
72
|
-
id: 'as-vb0m37ti8y',
|
73
|
-
object: 'chat.completion',
|
74
|
-
created: 1709089512,
|
75
|
-
sentence_id: 5,
|
76
|
-
is_end: false,
|
77
|
-
is_truncated: false,
|
78
|
-
result:
|
79
|
-
'口\n\t* 特点:海南岛是中国唯一的黎族聚居区,这里有独特的热带风情、美丽的海滩和丰富的水果。',
|
80
|
-
need_clear_history: false,
|
81
|
-
finish_reason: 'normal',
|
82
|
-
usage: { prompt_tokens: 5, completion_tokens: 153, total_tokens: 158 },
|
83
|
-
},
|
84
|
-
];
|
85
|
-
|
86
|
-
describe('WenxinStream', () => {
|
87
|
-
it('should transform Wenxin stream to protocol stream', async () => {
|
88
|
-
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
|
89
|
-
|
90
|
-
const mockWenxinStream: AsyncIterable<ChatResp> = {
|
91
|
-
// @ts-ignore
|
92
|
-
async *[Symbol.asyncIterator]() {
|
93
|
-
for (const item of dataStream) {
|
94
|
-
yield item;
|
95
|
-
}
|
96
|
-
},
|
97
|
-
};
|
98
|
-
|
99
|
-
const stream = convertIterableToStream(mockWenxinStream);
|
100
|
-
|
101
|
-
const onStartMock = vi.fn();
|
102
|
-
const onTextMock = vi.fn();
|
103
|
-
const onTokenMock = vi.fn();
|
104
|
-
const onCompletionMock = vi.fn();
|
105
|
-
|
106
|
-
const protocolStream = WenxinStream(stream, {
|
107
|
-
onStart: onStartMock,
|
108
|
-
onText: onTextMock,
|
109
|
-
onToken: onTokenMock,
|
110
|
-
onCompletion: onCompletionMock,
|
111
|
-
});
|
112
|
-
|
113
|
-
const decoder = new TextDecoder();
|
114
|
-
const chunks = [];
|
115
|
-
|
116
|
-
// @ts-ignore
|
117
|
-
for await (const chunk of protocolStream) {
|
118
|
-
chunks.push(decoder.decode(chunk, { stream: true }));
|
119
|
-
}
|
120
|
-
|
121
|
-
expect(chunks).toEqual(
|
122
|
-
[
|
123
|
-
'id: as-vb0m37ti8y',
|
124
|
-
'event: text',
|
125
|
-
`data: "当然可以,"\n`,
|
126
|
-
'id: as-vb0m37ti8y',
|
127
|
-
'event: text',
|
128
|
-
`data: "以下是一些建议的自驾游路线,它们涵盖了各种不同的风景和文化体验:\\n\\n1. **西安-敦煌历史文化之旅**:\\n\\n\\n\\t* 路线:西安"\n`,
|
129
|
-
'id: as-vb0m37ti8y',
|
130
|
-
'event: text',
|
131
|
-
`data: " - 天水 - 兰州 - 嘉峪关 - 敦煌\\n\\t* 特点:此路线让您领略到中国西北的丰富历史文化。"\n`,
|
132
|
-
'id: as-vb0m37ti8y',
|
133
|
-
'event: text',
|
134
|
-
`data: "您可以参观西安的兵马俑、大雁塔,体验兰州的黄河风情,以及在敦煌欣赏壮丽的莫高窟。"\n`,
|
135
|
-
'id: as-vb0m37ti8y',
|
136
|
-
'event: text',
|
137
|
-
`data: "\\n2. **海南环岛热带风情游**:\\n\\n\\n\\t* 路线:海口 - 三亚 - 陵水 - 万宁 - 文昌 - 海"\n`,
|
138
|
-
'id: as-vb0m37ti8y',
|
139
|
-
'event: text',
|
140
|
-
`data: "口\\n\\t* 特点:海南岛是中国唯一的黎族聚居区,这里有独特的热带风情、美丽的海滩和丰富的水果。"\n`,
|
141
|
-
].map((item) => `${item}\n`),
|
142
|
-
);
|
143
|
-
|
144
|
-
expect(onStartMock).toHaveBeenCalledTimes(1);
|
145
|
-
expect(onTextMock).toHaveBeenNthCalledWith(1, '"当然可以,"');
|
146
|
-
expect(onTextMock).toHaveBeenNthCalledWith(
|
147
|
-
2,
|
148
|
-
'"以下是一些建议的自驾游路线,它们涵盖了各种不同的风景和文化体验:\\n\\n1. **西安-敦煌历史文化之旅**:\\n\\n\\n\\t* 路线:西安"',
|
149
|
-
);
|
150
|
-
expect(onTokenMock).toHaveBeenCalledTimes(6);
|
151
|
-
expect(onCompletionMock).toHaveBeenCalledTimes(1);
|
152
|
-
});
|
153
|
-
});
|
@@ -1,38 +0,0 @@
|
|
1
|
-
import { ChatStreamCallbacks } from '@/libs/agent-runtime';
|
2
|
-
import { nanoid } from '@/utils/uuid';
|
3
|
-
|
4
|
-
import { ChatResp } from '../../wenxin/type';
|
5
|
-
import {
|
6
|
-
StreamProtocolChunk,
|
7
|
-
StreamStack,
|
8
|
-
createCallbacksTransformer,
|
9
|
-
createSSEProtocolTransformer,
|
10
|
-
} from './protocol';
|
11
|
-
|
12
|
-
const transformERNIEBotStream = (chunk: ChatResp): StreamProtocolChunk => {
|
13
|
-
const finished = chunk.is_end;
|
14
|
-
if (finished) {
|
15
|
-
return { data: chunk.finish_reason || 'stop', id: chunk.id, type: 'stop' };
|
16
|
-
}
|
17
|
-
|
18
|
-
if (chunk.result) {
|
19
|
-
return { data: chunk.result, id: chunk.id, type: 'text' };
|
20
|
-
}
|
21
|
-
|
22
|
-
return {
|
23
|
-
data: chunk,
|
24
|
-
id: chunk.id,
|
25
|
-
type: 'data',
|
26
|
-
};
|
27
|
-
};
|
28
|
-
|
29
|
-
export const WenxinStream = (
|
30
|
-
rawStream: ReadableStream<ChatResp>,
|
31
|
-
callbacks?: ChatStreamCallbacks,
|
32
|
-
) => {
|
33
|
-
const streamStack: StreamStack = { id: 'chat_' + nanoid() };
|
34
|
-
|
35
|
-
return rawStream
|
36
|
-
.pipeThrough(createSSEProtocolTransformer(transformERNIEBotStream, streamStack))
|
37
|
-
.pipeThrough(createCallbacksTransformer(callbacks));
|
38
|
-
};
|
@@ -1,84 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* token 用量基类
|
3
|
-
*/
|
4
|
-
export interface TokenUsage {
|
5
|
-
/**
|
6
|
-
* 回答tokens数
|
7
|
-
*/
|
8
|
-
completion_tokens?: number;
|
9
|
-
/**
|
10
|
-
* 问题tokens数
|
11
|
-
*/
|
12
|
-
prompt_tokens: number;
|
13
|
-
/**
|
14
|
-
* tokens总数
|
15
|
-
*/
|
16
|
-
total_tokens: number;
|
17
|
-
}
|
18
|
-
|
19
|
-
/**
|
20
|
-
* 响应基类
|
21
|
-
*/
|
22
|
-
export interface RespBase {
|
23
|
-
/**
|
24
|
-
* 时间戳
|
25
|
-
*/
|
26
|
-
created: number;
|
27
|
-
/**
|
28
|
-
* 本轮对话的id
|
29
|
-
*/
|
30
|
-
id: string;
|
31
|
-
/**
|
32
|
-
* 表示当前子句是否是最后一句。只有在流式接口模式下会返回该字段
|
33
|
-
*/
|
34
|
-
is_end?: boolean;
|
35
|
-
/**
|
36
|
-
* 1:表示输入内容无安全风险
|
37
|
-
* 0:表示输入内容有安全风险
|
38
|
-
*/
|
39
|
-
is_safe?: number;
|
40
|
-
/**
|
41
|
-
* 回包类型。
|
42
|
-
*
|
43
|
-
* chat.completion:多轮对话返回
|
44
|
-
*/
|
45
|
-
object: string;
|
46
|
-
/**
|
47
|
-
* 对话返回结果
|
48
|
-
*/
|
49
|
-
result: string;
|
50
|
-
/**
|
51
|
-
* 表示当前子句的序号。只有在流式接口模式下会返回该字段
|
52
|
-
*/
|
53
|
-
sentence_id?: number;
|
54
|
-
/**
|
55
|
-
* token统计信息,token数 = 汉字数+单词数*1.3 (仅为估算逻辑)
|
56
|
-
*/
|
57
|
-
usage: TokenUsage;
|
58
|
-
}
|
59
|
-
|
60
|
-
export interface ChatResp extends RespBase {
|
61
|
-
/**
|
62
|
-
* 当 need_clear_history 为 true 时,此字段会告知第几轮对话有敏感信息,如果是当前问题,ban_round=-1
|
63
|
-
*/
|
64
|
-
ban_round: number;
|
65
|
-
/**
|
66
|
-
* 输出内容标识,说明:
|
67
|
-
* · normal:输出内容完全由大模型生成,未触发截断、替换
|
68
|
-
* · stop:输出结果命中入参stop中指定的字段后被截断
|
69
|
-
* · length:达到了最大的token数,根据EB返回结果is_truncated来截断
|
70
|
-
* · content_filter:输出内容被截断、兜底、替换为**等
|
71
|
-
*/
|
72
|
-
finish_reason: string;
|
73
|
-
/**
|
74
|
-
* 当前生成的结果是否被截断
|
75
|
-
*/
|
76
|
-
is_truncated?: boolean;
|
77
|
-
/**
|
78
|
-
* 表示用户输入是否存在安全,是否关闭当前会话,清理历史会话信息
|
79
|
-
*
|
80
|
-
* true:是,表示用户输入存在安全风险,建议关闭当前会话,清理历史会话信息
|
81
|
-
* false:否,表示用户输入无安全风险
|
82
|
-
*/
|
83
|
-
need_clear_history: boolean;
|
84
|
-
}
|