@unif/react-native-chat-sdk 0.1.0 → 0.1.1
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/README.md +252 -0
- package/package.json +1 -1
- package/src/hooks/ChatProvider.tsx +4 -1
- package/src/hooks/useXChat.ts +36 -14
- package/src/index.ts +2 -1
- package/src/tools/XRequest.ts +12 -3
- package/src/types/provider.ts +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# @unif/react-native-chat-sdk
|
|
2
|
+
|
|
3
|
+
AI 聊天 SDK — 流式通信、Provider 模式、状态管理,参考 [Ant Design X SDK](https://ant-design-x.antgroup.com/x-sdks/introduce) 架构设计。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @unif/react-native-chat-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### peerDependencies
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
yarn add react react-native react-native-sse
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 架构
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌──────────────────────────────────────────┐
|
|
21
|
+
│ useXChat (Hook) │
|
|
22
|
+
│ 组合 Provider + store,输出 messages │
|
|
23
|
+
├──────────────────────────────────────────┤
|
|
24
|
+
│ ChatProvider 层 │
|
|
25
|
+
│ AbstractChatProvider → OpenAI/DeepSeek │
|
|
26
|
+
├──────────────────────────────────────────┤
|
|
27
|
+
│ 工具层 │
|
|
28
|
+
│ XRequest + XStream + SSEParser │
|
|
29
|
+
└──────────────────────────────────────────┘
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 快速开始
|
|
33
|
+
|
|
34
|
+
### 方式一:ChatProvider 全局模式
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import {
|
|
38
|
+
ChatProvider,
|
|
39
|
+
useChatContext,
|
|
40
|
+
OpenAIChatProvider,
|
|
41
|
+
XRequest,
|
|
42
|
+
} from '@unif/react-native-chat-sdk';
|
|
43
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
44
|
+
|
|
45
|
+
const request = new XRequest({
|
|
46
|
+
baseURL: 'https://api.openai.com',
|
|
47
|
+
getToken: async () => 'sk-...',
|
|
48
|
+
});
|
|
49
|
+
const provider = new OpenAIChatProvider({request, model: 'gpt-4o'});
|
|
50
|
+
|
|
51
|
+
function App() {
|
|
52
|
+
return (
|
|
53
|
+
<ChatProvider provider={provider} storage={AsyncStorage}>
|
|
54
|
+
<ChatScreen />
|
|
55
|
+
</ChatProvider>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ChatScreen() {
|
|
60
|
+
const {messages, onRequest, requesting, abort} = useChatContext();
|
|
61
|
+
// 使用 messages 渲染列表,onRequest 发送消息...
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 方式二:直接使用 Hooks
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import {useXChat, useXConversations, useXModel} from '@unif/react-native-chat-sdk';
|
|
69
|
+
|
|
70
|
+
function ChatScreen() {
|
|
71
|
+
const {messages, onRequest, abort, requesting} = useXChat({provider});
|
|
72
|
+
const {sessions, switchSession, newSession} = useXConversations({storage: AsyncStorage});
|
|
73
|
+
const {selectedModel, setSelectedModel} = useXModel({models: MODELS});
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
### Hooks
|
|
80
|
+
|
|
81
|
+
#### useXChat
|
|
82
|
+
|
|
83
|
+
聊天操作核心 hook,消费 Provider 输出可渲染 messages。
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const {
|
|
87
|
+
messages, // MessageInfo[] — 可渲染消息列表
|
|
88
|
+
requesting, // boolean — 是否请求中
|
|
89
|
+
suggestions, // SuggestionItem[] — 建议提示
|
|
90
|
+
error, // string | null — 错误信息
|
|
91
|
+
onRequest, // (input) => void — 发起请求
|
|
92
|
+
abort, // () => void — 中止请求
|
|
93
|
+
resetChat, // () => void — 重置聊天
|
|
94
|
+
loadSession, // (messages) => void — 加载历史会话
|
|
95
|
+
clearError, // () => void — 清除错误
|
|
96
|
+
} = useXChat({provider});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### useXConversations
|
|
100
|
+
|
|
101
|
+
会话管理 hook,组合 session + history。
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const {
|
|
105
|
+
sessions, // SessionSummary[]
|
|
106
|
+
activeId, // string
|
|
107
|
+
switchSession, // (id) => Promise<ChatMessage[]>
|
|
108
|
+
newSession, // () => void
|
|
109
|
+
deleteSession, // (id) => Promise<void>
|
|
110
|
+
archiveSession, // (id, title, messages) => Promise<void>
|
|
111
|
+
} = useXConversations({storage: AsyncStorage});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### useXModel
|
|
115
|
+
|
|
116
|
+
模型选择 hook。
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const {
|
|
120
|
+
selectedModel, // string
|
|
121
|
+
models, // ModelInfo[]
|
|
122
|
+
setSelectedModel, // (id) => void
|
|
123
|
+
} = useXModel({models: AVAILABLE_MODELS, defaultModel: 'gpt-4o'});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Providers
|
|
127
|
+
|
|
128
|
+
#### AbstractChatProvider
|
|
129
|
+
|
|
130
|
+
抽象基类,自定义 Provider 需继承并实现 3 个方法:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
class MyProvider extends AbstractChatProvider {
|
|
134
|
+
// 将 useXChat.onRequest() 参数转为 HTTP 请求体
|
|
135
|
+
transformParams(input: RequestParams): Record<string, unknown>;
|
|
136
|
+
|
|
137
|
+
// 创建用户本地消息(立即显示在列表中)
|
|
138
|
+
transformLocalMessage(input: RequestParams): ChatMessage;
|
|
139
|
+
|
|
140
|
+
// 将 SSE 流数据转为 assistant 消息(流式累积)
|
|
141
|
+
transformMessage(output: SSEOutput, current?: ChatMessage): ChatMessage;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### OpenAIChatProvider
|
|
146
|
+
|
|
147
|
+
OpenAI 兼容 Provider,支持所有 OpenAI 标准 API。
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const provider = new OpenAIChatProvider({
|
|
151
|
+
baseURL: 'https://api.openai.com',
|
|
152
|
+
model: 'gpt-4o',
|
|
153
|
+
getToken: async () => apiKey,
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### DeepSeekChatProvider
|
|
158
|
+
|
|
159
|
+
DeepSeek Provider,继承 OpenAI,额外处理 `reasoning_content`。
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const provider = new DeepSeekChatProvider({
|
|
163
|
+
model: 'deepseek-chat',
|
|
164
|
+
getToken: async () => apiKey,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Tools
|
|
169
|
+
|
|
170
|
+
#### XStream
|
|
171
|
+
|
|
172
|
+
SSE 流适配器,基于 `react-native-sse` 的 RN 原生实现。
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const stream = new XStream({
|
|
176
|
+
url: 'https://api.example.com/stream',
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: {message: 'hello'},
|
|
179
|
+
timeout: 120000,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
stream.connect({
|
|
183
|
+
onMessage: (output) => console.log(output.data),
|
|
184
|
+
onError: (err) => console.error(err),
|
|
185
|
+
onComplete: () => console.log('done'),
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### XRequest
|
|
190
|
+
|
|
191
|
+
面向 LLM 的 HTTP 请求工具,组合 XStream + SSEParser。
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const request = new XRequest({
|
|
195
|
+
baseURL: 'https://api.example.com',
|
|
196
|
+
endpoint: '/v1/chat/completions',
|
|
197
|
+
getToken: async () => token,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await request.create(params, {
|
|
201
|
+
onStream: (chunk) => handleChunk(chunk),
|
|
202
|
+
onSuccess: () => handleDone(),
|
|
203
|
+
onError: (err) => handleError(err),
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### SSEParser
|
|
208
|
+
|
|
209
|
+
SSE 解析器,event_id 幂等去重 + seq 严格排序。
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const parser = new SSEParser();
|
|
213
|
+
parser.reset(); // 每轮对话重置
|
|
214
|
+
|
|
215
|
+
const envelopes = parser.process(rawData); // 返回排序后的事件数组
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Types
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// 消息类型
|
|
222
|
+
interface ChatMessage {
|
|
223
|
+
id: string;
|
|
224
|
+
text: string;
|
|
225
|
+
role: 'user' | 'assistant' | 'system';
|
|
226
|
+
status: 'local' | 'loading' | 'updating' | 'success' | 'error' | 'abort';
|
|
227
|
+
messageType: 'text' | 'card' | 'system';
|
|
228
|
+
createdAt: Date;
|
|
229
|
+
turnId: string;
|
|
230
|
+
cardType?: string;
|
|
231
|
+
cardData?: { data: Record<string, unknown>; actions: string[] };
|
|
232
|
+
extra?: Record<string, unknown>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 存储抽象
|
|
236
|
+
interface ChatStorage {
|
|
237
|
+
getItem: (key: string) => Promise<string | null>;
|
|
238
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
239
|
+
removeItem: (key: string) => Promise<void>;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## 兼容性
|
|
244
|
+
|
|
245
|
+
| @unif/react-native-chat-sdk | React Native | React | 状态 |
|
|
246
|
+
|-----------------------------|-------------|-------|------|
|
|
247
|
+
| 0.1.x | >= 0.71 | >= 18 | ✅ |
|
|
248
|
+
| 0.1.x | 0.74.x | 18.x | ✅ 已验证(PECPortal) |
|
|
249
|
+
|
|
250
|
+
## 许可证
|
|
251
|
+
|
|
252
|
+
MIT
|
package/package.json
CHANGED
|
@@ -37,8 +37,12 @@ interface ChatContextValue<Message = ChatMessage> {
|
|
|
37
37
|
messages: MessageInfo<Message>[];
|
|
38
38
|
requesting: boolean;
|
|
39
39
|
suggestions: SuggestionItem[];
|
|
40
|
+
error: string | null;
|
|
40
41
|
onRequest: (input: unknown) => void;
|
|
41
42
|
abort: () => void;
|
|
43
|
+
resetChat: () => void;
|
|
44
|
+
loadSession: (messages: ChatMessage[]) => void;
|
|
45
|
+
clearError: () => void;
|
|
42
46
|
// useXConversations
|
|
43
47
|
sessions: SessionSummary[];
|
|
44
48
|
activeId: string;
|
|
@@ -54,7 +58,6 @@ interface ChatContextValue<Message = ChatMessage> {
|
|
|
54
58
|
|
|
55
59
|
const ChatContext = createContext<ChatContextValue | null>(null);
|
|
56
60
|
|
|
57
|
-
// 内存存储 fallback(无 storage 时使用)
|
|
58
61
|
const memoryStorage: ChatStorage = {
|
|
59
62
|
_data: new Map<string, string>(),
|
|
60
63
|
async getItem(key: string) {
|
package/src/hooks/useXChat.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 消费 ChatProvider,输出可渲染的 messages 列表
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {useCallback, useEffect, useRef} from 'react';
|
|
7
|
+
import {useCallback, useEffect, useRef, useSyncExternalStore} from 'react';
|
|
8
8
|
import {createChatStore} from '../stores/chatStore';
|
|
9
9
|
import type {AbstractChatProvider} from '../providers/AbstractChatProvider';
|
|
10
10
|
import type {ChatMessage, MessageStatus} from '../types/message';
|
|
@@ -36,8 +36,12 @@ export interface UseXChatReturn<
|
|
|
36
36
|
messages: MessageInfo<Message>[];
|
|
37
37
|
requesting: boolean;
|
|
38
38
|
suggestions: SuggestionItem[];
|
|
39
|
+
error: string | null;
|
|
39
40
|
onRequest: (input: Input) => void;
|
|
40
41
|
abort: () => void;
|
|
42
|
+
resetChat: () => void;
|
|
43
|
+
loadSession: (messages: ChatMessage[]) => void;
|
|
44
|
+
clearError: () => void;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
let _idCounter = 0;
|
|
@@ -55,27 +59,25 @@ export function useXChat<
|
|
|
55
59
|
const storeRef = useRef(createChatStore());
|
|
56
60
|
const store = storeRef.current;
|
|
57
61
|
|
|
58
|
-
// 订阅 store 变化
|
|
59
|
-
const state =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// 使用 useSyncExternalStore 订阅 zustand store 变化
|
|
63
|
+
const state = useSyncExternalStore(
|
|
64
|
+
store.subscribe,
|
|
65
|
+
store.getState,
|
|
66
|
+
store.getState,
|
|
67
|
+
);
|
|
64
68
|
|
|
65
69
|
const onRequest = useCallback(
|
|
66
70
|
(input: Input) => {
|
|
67
|
-
const s = getState();
|
|
68
|
-
if (s.isRequesting) return;
|
|
71
|
+
const s = store.getState();
|
|
72
|
+
if (s.isRequesting) return;
|
|
69
73
|
|
|
70
74
|
store.getState().setRequesting(true);
|
|
71
75
|
store.getState().setError(null);
|
|
72
76
|
store.getState().setSuggestions([]);
|
|
73
77
|
|
|
74
|
-
// 添加用户本地消息
|
|
75
78
|
const localMessage = provider.transformLocalMessage(input);
|
|
76
79
|
store.getState().addMessage(localMessage as unknown as ChatMessage);
|
|
77
80
|
|
|
78
|
-
// 发起请求
|
|
79
81
|
provider.sendMessage(input, {
|
|
80
82
|
onUpdate: (message) => {
|
|
81
83
|
store
|
|
@@ -101,9 +103,12 @@ export function useXChat<
|
|
|
101
103
|
.addMessage(fallbackMsg as unknown as ChatMessage);
|
|
102
104
|
}
|
|
103
105
|
},
|
|
106
|
+
onSuggestion: (items) => {
|
|
107
|
+
store.getState().setSuggestions(items);
|
|
108
|
+
},
|
|
104
109
|
});
|
|
105
110
|
},
|
|
106
|
-
[provider,
|
|
111
|
+
[provider, store, config]
|
|
107
112
|
);
|
|
108
113
|
|
|
109
114
|
const abort = useCallback(() => {
|
|
@@ -111,14 +116,27 @@ export function useXChat<
|
|
|
111
116
|
store.getState().setRequesting(false);
|
|
112
117
|
}, [provider, store]);
|
|
113
118
|
|
|
114
|
-
|
|
119
|
+
const resetChat = useCallback(() => {
|
|
120
|
+
store.getState().resetChat();
|
|
121
|
+
}, [store]);
|
|
122
|
+
|
|
123
|
+
const loadSession = useCallback(
|
|
124
|
+
(messages: ChatMessage[]) => {
|
|
125
|
+
store.getState().loadSession(messages);
|
|
126
|
+
},
|
|
127
|
+
[store]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const clearError = useCallback(() => {
|
|
131
|
+
store.getState().setError(null);
|
|
132
|
+
}, [store]);
|
|
133
|
+
|
|
115
134
|
useEffect(() => {
|
|
116
135
|
return () => {
|
|
117
136
|
provider.abort();
|
|
118
137
|
};
|
|
119
138
|
}, [provider]);
|
|
120
139
|
|
|
121
|
-
// 转换为 MessageInfo 格式
|
|
122
140
|
const messages: MessageInfo<Message>[] = state.messages.map((m) => ({
|
|
123
141
|
id: m.id,
|
|
124
142
|
message: m as unknown as Message,
|
|
@@ -130,7 +148,11 @@ export function useXChat<
|
|
|
130
148
|
messages,
|
|
131
149
|
requesting: state.isRequesting,
|
|
132
150
|
suggestions: state.suggestions,
|
|
151
|
+
error: state.error,
|
|
133
152
|
onRequest,
|
|
134
153
|
abort,
|
|
154
|
+
resetChat,
|
|
155
|
+
loadSession,
|
|
156
|
+
clearError,
|
|
135
157
|
};
|
|
136
158
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type {
|
|
|
20
20
|
SSEErrorEvent,
|
|
21
21
|
SSEDoneEvent,
|
|
22
22
|
SSEEnvelope,
|
|
23
|
+
SuggestionItem,
|
|
23
24
|
} from './types/sse';
|
|
24
25
|
|
|
25
26
|
export type {
|
|
@@ -49,7 +50,7 @@ export type {ChatStorage} from './stores/storage';
|
|
|
49
50
|
|
|
50
51
|
// Hooks
|
|
51
52
|
export {useXChat} from './hooks/useXChat';
|
|
52
|
-
export type {UseXChatConfig, MessageInfo} from './hooks/useXChat';
|
|
53
|
+
export type {UseXChatConfig, UseXChatReturn, MessageInfo} from './hooks/useXChat';
|
|
53
54
|
|
|
54
55
|
export {useXConversations} from './hooks/useXConversations';
|
|
55
56
|
export type {UseXConversationsOptions, SessionSummary} from './hooks/useXConversations';
|
package/src/tools/XRequest.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import {XStream} from './XStream';
|
|
8
8
|
import {SSEParser} from './SSEParser';
|
|
9
9
|
import type {SSEOutput} from '../types/provider';
|
|
10
|
+
import type {SSEEnvelope} from '../types/sse';
|
|
10
11
|
|
|
11
12
|
export interface XRequestConfig {
|
|
12
13
|
baseURL: string;
|
|
@@ -49,11 +50,12 @@ export class XRequest {
|
|
|
49
50
|
} = this.config;
|
|
50
51
|
|
|
51
52
|
// 注入 auth token
|
|
53
|
+
const reqHeaders = {...headers};
|
|
52
54
|
if (getToken) {
|
|
53
55
|
try {
|
|
54
56
|
const token = await getToken();
|
|
55
57
|
if (token) {
|
|
56
|
-
|
|
58
|
+
reqHeaders['Authorization'] = `Bearer ${token}`;
|
|
57
59
|
}
|
|
58
60
|
} catch {
|
|
59
61
|
// token 获取失败,继续无认证请求
|
|
@@ -65,14 +67,21 @@ export class XRequest {
|
|
|
65
67
|
this.stream = new XStream({
|
|
66
68
|
url,
|
|
67
69
|
method: 'POST',
|
|
68
|
-
headers,
|
|
70
|
+
headers: reqHeaders,
|
|
69
71
|
body: params,
|
|
70
72
|
timeout,
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
this.stream.connect({
|
|
74
76
|
onMessage: (output: SSEOutput) => {
|
|
75
|
-
|
|
77
|
+
// 通过 SSEParser 进行幂等去重和排序
|
|
78
|
+
const envelopes: SSEEnvelope[] = this.parser.process(output.data);
|
|
79
|
+
for (const envelope of envelopes) {
|
|
80
|
+
callbacks.onStream?.({
|
|
81
|
+
data: JSON.stringify(envelope),
|
|
82
|
+
event: envelope.type,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
76
85
|
},
|
|
77
86
|
onError: (error: Error) => {
|
|
78
87
|
callbacks.onError?.({
|
package/src/types/provider.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type {ChatMessage} from './message';
|
|
7
|
+
import type {SuggestionItem} from './sse';
|
|
7
8
|
|
|
8
9
|
export interface RequestParams<T = ChatMessage> {
|
|
9
10
|
message: T;
|
|
@@ -21,4 +22,5 @@ export interface ProviderCallbacks<Message = ChatMessage> {
|
|
|
21
22
|
onUpdate: (message: Message) => void;
|
|
22
23
|
onSuccess: (message: Message) => void;
|
|
23
24
|
onError: (error: Error) => void;
|
|
25
|
+
onSuggestion?: (items: SuggestionItem[]) => void;
|
|
24
26
|
}
|