@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
|
-
}
|