@unif/react-native-chat-sdk 0.1.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.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * AbstractChatProvider — Provider 抽象基类
3
+ *
4
+ * 3 个抽象方法:transformParams / transformLocalMessage / transformMessage
5
+ */
6
+
7
+ import type {XRequest, XRequestCallbacks} from '../tools/XRequest';
8
+ import type {ChatMessage} from '../types/message';
9
+ import type {RequestParams, SSEOutput, ProviderCallbacks} from '../types/provider';
10
+
11
+ export abstract class AbstractChatProvider<
12
+ Message = ChatMessage,
13
+ Input = RequestParams<Message>,
14
+ Output = SSEOutput,
15
+ > {
16
+ protected request: XRequest;
17
+
18
+ constructor(config: { request: XRequest }) {
19
+ this.request = config.request;
20
+ }
21
+
22
+ /**
23
+ * 转换请求参数
24
+ * 将 useXChat.onRequest() 传入的参数转换为 XRequest 需要的格式
25
+ */
26
+ abstract transformParams(input: Input): Record<string, unknown>;
27
+
28
+ /**
29
+ * 转换本地消息
30
+ * 将请求参数转换为用户发送的本地 ChatMessage(用于消息列表渲染)
31
+ */
32
+ abstract transformLocalMessage(input: Input): Message;
33
+
34
+ /**
35
+ * 转换响应消息
36
+ * 将 XRequest 返回的流数据转换为 ChatMessage(用于 assistant 消息渲染)
37
+ */
38
+ abstract transformMessage(output: Output, current?: Message): Message;
39
+
40
+ /**
41
+ * 发起请求(内部调用 XRequest)
42
+ */
43
+ sendMessage(input: Input, callbacks: ProviderCallbacks<Message>): void {
44
+ const params = this.transformParams(input);
45
+ let currentMessage: Message | undefined;
46
+
47
+ const requestCallbacks: XRequestCallbacks = {
48
+ onStream: (chunk) => {
49
+ currentMessage = this.transformMessage(
50
+ chunk as unknown as Output,
51
+ currentMessage
52
+ );
53
+ callbacks.onUpdate(currentMessage);
54
+ },
55
+ onSuccess: () => {
56
+ if (currentMessage) {
57
+ callbacks.onSuccess(currentMessage);
58
+ }
59
+ },
60
+ onError: (error) => {
61
+ callbacks.onError(new Error(error.message));
62
+ },
63
+ };
64
+
65
+ this.request.create(params, requestCallbacks);
66
+ }
67
+
68
+ /**
69
+ * 中止请求
70
+ */
71
+ abort(): void {
72
+ this.request.abort();
73
+ }
74
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * DeepSeekChatProvider — DeepSeek Provider
3
+ *
4
+ * 继承 OpenAIChatProvider(DeepSeek API 兼容 OpenAI 格式)
5
+ * 额外处理 reasoning_content 字段(DeepSeek 的思考过程)→ 映射到 <think> 标签
6
+ */
7
+
8
+ import {OpenAIChatProvider} from './OpenAIChatProvider';
9
+ import type {ChatMessage} from '../types/message';
10
+ import type {SSEOutput} from '../types/provider';
11
+
12
+ export interface DeepSeekChatProviderConfig {
13
+ apiKey?: string;
14
+ model?: string;
15
+ getToken?: () => Promise<string | null>;
16
+ baseURL?: string;
17
+ }
18
+
19
+ export class DeepSeekChatProvider extends OpenAIChatProvider {
20
+ constructor(config: DeepSeekChatProviderConfig) {
21
+ super({
22
+ baseURL: config.baseURL ?? 'https://api.deepseek.com',
23
+ apiKey: config.apiKey,
24
+ model: config.model ?? 'deepseek-chat',
25
+ getToken: config.getToken,
26
+ });
27
+ }
28
+
29
+ /**
30
+ * 覆写 transformMessage,处理 DeepSeek reasoning_content
31
+ */
32
+ override transformMessage(
33
+ output: SSEOutput,
34
+ current?: ChatMessage
35
+ ): ChatMessage {
36
+ const raw = output.data;
37
+
38
+ if (raw.trim() === '[DONE]') {
39
+ return {
40
+ ...(current ?? {
41
+ id: '',
42
+ text: '',
43
+ role: 'assistant' as const,
44
+ createdAt: new Date(),
45
+ messageType: 'text' as const,
46
+ status: 'success' as const,
47
+ turnId: '',
48
+ }),
49
+ status: 'success',
50
+ };
51
+ }
52
+
53
+ try {
54
+ const parsed = JSON.parse(raw);
55
+ const delta = parsed.choices?.[0]?.delta;
56
+ const content = delta?.content ?? '';
57
+ const reasoningContent = delta?.reasoning_content ?? '';
58
+ const finishReason = parsed.choices?.[0]?.finish_reason;
59
+
60
+ const prevText = current?.text ?? '';
61
+ let newText = prevText;
62
+
63
+ // 将 reasoning_content 映射为 <think> 标签包裹
64
+ if (reasoningContent) {
65
+ // 如果还没有开始 think 标签,添加开始标签
66
+ if (!prevText.includes('<think>')) {
67
+ newText += '<think>';
68
+ }
69
+ newText += reasoningContent;
70
+ }
71
+
72
+ if (content) {
73
+ // 如果有未关闭的 think 标签,先关闭
74
+ if (
75
+ newText.includes('<think>') &&
76
+ !newText.includes('</think>')
77
+ ) {
78
+ newText += '</think>';
79
+ }
80
+ newText += content;
81
+ }
82
+
83
+ // 完成时确保 think 标签闭合
84
+ if (
85
+ finishReason &&
86
+ newText.includes('<think>') &&
87
+ !newText.includes('</think>')
88
+ ) {
89
+ newText += '</think>';
90
+ }
91
+
92
+ return {
93
+ id: current?.id ?? `msg-${Date.now()}`,
94
+ text: newText,
95
+ role: 'assistant',
96
+ createdAt: current?.createdAt ?? new Date(),
97
+ messageType: 'text',
98
+ status: finishReason ? 'success' : 'updating',
99
+ turnId: current?.turnId ?? `turn-${Date.now()}`,
100
+ };
101
+ } catch {
102
+ return current ?? super.transformMessage(output, current);
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * OpenAIChatProvider — OpenAI 兼容 Provider
3
+ *
4
+ * 兼容所有 OpenAI 标准 API(OpenAI、Azure、Moonshot、通义千问兼容模式等)
5
+ */
6
+
7
+ import {AbstractChatProvider} from './AbstractChatProvider';
8
+ import {XRequest} from '../tools/XRequest';
9
+ import type {ChatMessage, MessageStatus} from '../types/message';
10
+ import type {RequestParams, SSEOutput} from '../types/provider';
11
+
12
+ export interface OpenAIChatProviderConfig {
13
+ baseURL: string;
14
+ apiKey?: string;
15
+ model?: string;
16
+ getToken?: () => Promise<string | null>;
17
+ endpoint?: string;
18
+ }
19
+
20
+ let _idCounter = 0;
21
+ const genId = () => `msg-${Date.now()}-${++_idCounter}`;
22
+
23
+ export class OpenAIChatProvider extends AbstractChatProvider<
24
+ ChatMessage,
25
+ RequestParams<ChatMessage>,
26
+ SSEOutput
27
+ > {
28
+ private model: string;
29
+
30
+ constructor(config: OpenAIChatProviderConfig) {
31
+ const headers: Record<string, string> = {};
32
+ if (config.apiKey) {
33
+ headers['Authorization'] = `Bearer ${config.apiKey}`;
34
+ }
35
+
36
+ const request = new XRequest({
37
+ baseURL: config.baseURL,
38
+ endpoint: config.endpoint ?? '/v1/chat/completions',
39
+ headers,
40
+ getToken: config.getToken,
41
+ });
42
+
43
+ super({ request });
44
+ this.model = config.model ?? 'gpt-4o';
45
+ }
46
+
47
+ /**
48
+ * 将消息转为 OpenAI messages 格式: [{role, content}]
49
+ */
50
+ transformParams(input: RequestParams<ChatMessage>): Record<string, unknown> {
51
+ const messages = (input.messages ?? []).map((m) => ({
52
+ role: m.role,
53
+ content: m.text,
54
+ }));
55
+
56
+ messages.push({
57
+ role: input.message.role,
58
+ content: input.message.text,
59
+ });
60
+
61
+ return {
62
+ model: input.model ?? this.model,
63
+ messages,
64
+ stream: true,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * 创建本地用户消息
70
+ */
71
+ transformLocalMessage(input: RequestParams<ChatMessage>): ChatMessage {
72
+ return {
73
+ id: genId(),
74
+ text: input.message.text,
75
+ role: 'user',
76
+ createdAt: new Date(),
77
+ messageType: 'text',
78
+ status: 'local' as MessageStatus,
79
+ turnId: input.extra?.turnId as string ?? genId(),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * 将 SSE delta 转为 ChatMessage(累积 content)
85
+ */
86
+ transformMessage(output: SSEOutput, current?: ChatMessage): ChatMessage {
87
+ const raw = output.data;
88
+
89
+ // OpenAI 结束标记
90
+ if (raw.trim() === '[DONE]') {
91
+ return {
92
+ ...(current ?? this.createEmptyAssistant()),
93
+ status: 'success' as MessageStatus,
94
+ };
95
+ }
96
+
97
+ try {
98
+ const parsed = JSON.parse(raw);
99
+ const delta = parsed.choices?.[0]?.delta;
100
+ const content = delta?.content ?? '';
101
+ const finishReason = parsed.choices?.[0]?.finish_reason;
102
+
103
+ const prevText = current?.text ?? '';
104
+
105
+ return {
106
+ id: current?.id ?? genId(),
107
+ text: prevText + content,
108
+ role: 'assistant',
109
+ createdAt: current?.createdAt ?? new Date(),
110
+ messageType: 'text',
111
+ status: finishReason ? 'success' : 'updating',
112
+ turnId: current?.turnId ?? genId(),
113
+ };
114
+ } catch {
115
+ return current ?? this.createEmptyAssistant();
116
+ }
117
+ }
118
+
119
+ private createEmptyAssistant(): ChatMessage {
120
+ return {
121
+ id: genId(),
122
+ text: '',
123
+ role: 'assistant',
124
+ createdAt: new Date(),
125
+ messageType: 'text',
126
+ status: 'updating',
127
+ turnId: genId(),
128
+ };
129
+ }
130
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Chat 消息状态管理(Zustand)
3
+ * 内部 store,通过 useXChat hook 封装后对外暴露
4
+ */
5
+
6
+ import {create} from 'zustand';
7
+ import type {ChatMessage} from '../types/message';
8
+ import type {SuggestionItem} from '../types/sse';
9
+
10
+ interface ChatState {
11
+ messages: ChatMessage[];
12
+ streamingText: string;
13
+ suggestions: SuggestionItem[];
14
+ isRequesting: boolean;
15
+ error: string | null;
16
+
17
+ addMessage: (message: ChatMessage) => void;
18
+ updateMessage: (id: string, updater: (msg: ChatMessage) => ChatMessage) => void;
19
+ upsertMessage: (message: ChatMessage) => void;
20
+ setSuggestions: (items: SuggestionItem[]) => void;
21
+ setRequesting: (v: boolean) => void;
22
+ setError: (error: string | null) => void;
23
+ resetChat: () => void;
24
+ loadSession: (messages: ChatMessage[]) => void;
25
+ }
26
+
27
+ export const createChatStore = () =>
28
+ create<ChatState>((set, get) => ({
29
+ messages: [],
30
+ streamingText: '',
31
+ suggestions: [],
32
+ isRequesting: false,
33
+ error: null,
34
+
35
+ addMessage: (message) =>
36
+ set((state) => ({
37
+ messages: [message, ...state.messages],
38
+ })),
39
+
40
+ updateMessage: (id, updater) =>
41
+ set((state) => ({
42
+ messages: state.messages.map((m) => (m.id === id ? updater(m) : m)),
43
+ })),
44
+
45
+ upsertMessage: (message) => {
46
+ const state = get();
47
+ const existingIdx = state.messages.findIndex((m) => m.id === message.id);
48
+
49
+ if (existingIdx >= 0) {
50
+ const newMessages = [...state.messages];
51
+ newMessages[existingIdx] = message;
52
+ set({ messages: newMessages });
53
+ } else {
54
+ set({ messages: [message, ...state.messages] });
55
+ }
56
+ },
57
+
58
+ setSuggestions: (items) => set({ suggestions: items }),
59
+ setRequesting: (v) => set({ isRequesting: v }),
60
+ setError: (error) => set({ error }),
61
+
62
+ resetChat: () =>
63
+ set({
64
+ messages: [],
65
+ streamingText: '',
66
+ suggestions: [],
67
+ isRequesting: false,
68
+ error: null,
69
+ }),
70
+
71
+ loadSession: (messages) =>
72
+ set({
73
+ messages,
74
+ streamingText: '',
75
+ suggestions: [],
76
+ isRequesting: false,
77
+ error: null,
78
+ }),
79
+ }));
@@ -0,0 +1,117 @@
1
+ /**
2
+ * 历史会话持久化
3
+ * 使用 ChatStorage 抽象,不直接依赖 AsyncStorage
4
+ */
5
+
6
+ import type {ChatMessage} from '../types/message';
7
+ import type {ChatStorage} from './storage';
8
+
9
+ const MAX_SESSIONS = 50;
10
+ const MESSAGE_KEY_PREFIX = 'chat-session-';
11
+
12
+ export interface SessionSummary {
13
+ id: string;
14
+ title: string;
15
+ lastMessage: string;
16
+ timestamp: number;
17
+ messageCount: number;
18
+ }
19
+
20
+ export interface HistoryStore {
21
+ sessions: SessionSummary[];
22
+ archiveSession: (
23
+ id: string,
24
+ title: string,
25
+ messages: ChatMessage[]
26
+ ) => Promise<void>;
27
+ loadSessionMessages: (id: string) => Promise<ChatMessage[]>;
28
+ deleteSession: (id: string) => Promise<void>;
29
+ clearAll: () => Promise<void>;
30
+ }
31
+
32
+ export function createHistoryStore(storage: ChatStorage): HistoryStore {
33
+ let sessions: SessionSummary[] = [];
34
+ let initialized = false;
35
+
36
+ const loadIndex = async () => {
37
+ if (initialized) return;
38
+ try {
39
+ const raw = await storage.getItem('chat-history-index');
40
+ if (raw) {
41
+ sessions = JSON.parse(raw);
42
+ }
43
+ } catch {
44
+ sessions = [];
45
+ }
46
+ initialized = true;
47
+ };
48
+
49
+ const saveIndex = async () => {
50
+ await storage.setItem('chat-history-index', JSON.stringify(sessions));
51
+ };
52
+
53
+ return {
54
+ get sessions() {
55
+ return sessions;
56
+ },
57
+
58
+ archiveSession: async (id, title, messages) => {
59
+ await loadIndex();
60
+ if (messages.length === 0) return;
61
+
62
+ await storage.setItem(
63
+ `${MESSAGE_KEY_PREFIX}${id}`,
64
+ JSON.stringify(messages)
65
+ );
66
+
67
+ const lastMsg = messages[0];
68
+ const summary: SessionSummary = {
69
+ id,
70
+ title,
71
+ lastMessage: lastMsg?.text?.slice(0, 50) ?? '',
72
+ timestamp: Date.now(),
73
+ messageCount: messages.length,
74
+ };
75
+
76
+ // 去重(如果已存在则更新)
77
+ sessions = sessions.filter((s) => s.id !== id);
78
+ sessions = [summary, ...sessions];
79
+
80
+ // FIFO 淘汰
81
+ if (sessions.length > MAX_SESSIONS) {
82
+ const removed = sessions.splice(MAX_SESSIONS);
83
+ await Promise.all(
84
+ removed.map((s) =>
85
+ storage.removeItem(`${MESSAGE_KEY_PREFIX}${s.id}`)
86
+ )
87
+ );
88
+ }
89
+
90
+ await saveIndex();
91
+ },
92
+
93
+ loadSessionMessages: async (id) => {
94
+ const raw = await storage.getItem(`${MESSAGE_KEY_PREFIX}${id}`);
95
+ if (!raw) return [];
96
+ return JSON.parse(raw) as ChatMessage[];
97
+ },
98
+
99
+ deleteSession: async (id) => {
100
+ await loadIndex();
101
+ await storage.removeItem(`${MESSAGE_KEY_PREFIX}${id}`);
102
+ sessions = sessions.filter((s) => s.id !== id);
103
+ await saveIndex();
104
+ },
105
+
106
+ clearAll: async () => {
107
+ await loadIndex();
108
+ await Promise.all(
109
+ sessions.map((s) =>
110
+ storage.removeItem(`${MESSAGE_KEY_PREFIX}${s.id}`)
111
+ )
112
+ );
113
+ sessions = [];
114
+ await saveIndex();
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 模型选择状态(Zustand)
3
+ * 内部 store,通过 useXModel hook 封装后对外暴露
4
+ */
5
+
6
+ import {create} from 'zustand';
7
+
8
+ interface ModelState {
9
+ selectedModel: string;
10
+ setSelectedModel: (id: string) => void;
11
+ }
12
+
13
+ export const createModelStore = (defaultModel: string) =>
14
+ create<ModelState>((set) => ({
15
+ selectedModel: defaultModel,
16
+ setSelectedModel: (id) => set({ selectedModel: id }),
17
+ }));
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 会话状态管理(Zustand)
3
+ * 内部 store,通过 useXConversations hook 封装后对外暴露
4
+ */
5
+
6
+ import {create} from 'zustand';
7
+
8
+ interface SessionState {
9
+ sessionId: string;
10
+ sessionTitle: string;
11
+ setSessionId: (id: string) => void;
12
+ setSessionTitle: (title: string) => void;
13
+ }
14
+
15
+ export const createSessionStore = (generateId: () => string) =>
16
+ create<SessionState>((set) => ({
17
+ sessionId: generateId(),
18
+ sessionTitle: '',
19
+ setSessionId: (id) => set({ sessionId: id }),
20
+ setSessionTitle: (title) => set({ sessionTitle: title }),
21
+ }));
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ChatStorage — 存储抽象接口
3
+ * 不绑定 AsyncStorage,支持注入任意存储实现
4
+ */
5
+
6
+ export interface ChatStorage {
7
+ getItem: (key: string) => Promise<string | null>;
8
+ setItem: (key: string, value: string) => Promise<void>;
9
+ removeItem: (key: string) => Promise<void>;
10
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * SSE 解析器
3
+ * - event_id 幂等去重
4
+ * - seq 严格排序(乱序缓冲,顺序到达后 flush)
5
+ * - 每轮对话调用 reset() 重置
6
+ */
7
+
8
+ import type {SSEEnvelope} from '../types/sse';
9
+
10
+ export class SSEParser {
11
+ private processedIds = new Set<string>();
12
+ private buffer = new Map<number, SSEEnvelope>();
13
+ private nextSeq = 1;
14
+
15
+ reset(): void {
16
+ this.processedIds.clear();
17
+ this.buffer.clear();
18
+ this.nextSeq = 1;
19
+ }
20
+
21
+ /**
22
+ * 解析原始 SSE data 字符串,返回按 seq 排序的事件数组
23
+ * 已处理的 event_id 会被自动跳过(幂等)
24
+ */
25
+ process(raw: string): SSEEnvelope[] {
26
+ let envelope: SSEEnvelope;
27
+ try {
28
+ envelope = JSON.parse(raw);
29
+ } catch {
30
+ return [];
31
+ }
32
+
33
+ // 幂等去重
34
+ if (this.processedIds.has(envelope.event_id)) {
35
+ return [];
36
+ }
37
+ this.processedIds.add(envelope.event_id);
38
+
39
+ // 缓冲排序
40
+ this.buffer.set(envelope.seq, envelope);
41
+
42
+ // flush 连续的事件
43
+ const result: SSEEnvelope[] = [];
44
+ while (this.buffer.has(this.nextSeq)) {
45
+ result.push(this.buffer.get(this.nextSeq)!);
46
+ this.buffer.delete(this.nextSeq);
47
+ this.nextSeq++;
48
+ }
49
+
50
+ return result;
51
+ }
52
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * XRequest — 面向 LLM 的 HTTP 请求工具
3
+ *
4
+ * 组合 XStream + SSEParser 实现完整 SSE 请求链路
5
+ */
6
+
7
+ import {XStream} from './XStream';
8
+ import {SSEParser} from './SSEParser';
9
+ import type {SSEOutput} from '../types/provider';
10
+
11
+ export interface XRequestConfig {
12
+ baseURL: string;
13
+ endpoint?: string;
14
+ model?: string;
15
+ headers?: Record<string, string>;
16
+ timeout?: number;
17
+ getToken?: () => Promise<string | null>;
18
+ }
19
+
20
+ export interface XRequestCallbacks {
21
+ onStream?: (chunk: SSEOutput) => void;
22
+ onSuccess?: (message: string) => void;
23
+ onError?: (error: { type: string; message: string }) => void;
24
+ onAbort?: () => void;
25
+ }
26
+
27
+ export class XRequest {
28
+ private config: XRequestConfig;
29
+ private stream: XStream | null = null;
30
+ private parser = new SSEParser();
31
+
32
+ constructor(config: XRequestConfig) {
33
+ this.config = config;
34
+ }
35
+
36
+ async create(
37
+ params: Record<string, unknown>,
38
+ callbacks: XRequestCallbacks
39
+ ): Promise<void> {
40
+ this.abort();
41
+ this.parser.reset();
42
+
43
+ const {
44
+ baseURL,
45
+ endpoint = '/v1/chat/completions',
46
+ headers = {},
47
+ timeout,
48
+ getToken,
49
+ } = this.config;
50
+
51
+ // 注入 auth token
52
+ if (getToken) {
53
+ try {
54
+ const token = await getToken();
55
+ if (token) {
56
+ headers['Authorization'] = `Bearer ${token}`;
57
+ }
58
+ } catch {
59
+ // token 获取失败,继续无认证请求
60
+ }
61
+ }
62
+
63
+ const url = `${baseURL}${endpoint}`;
64
+
65
+ this.stream = new XStream({
66
+ url,
67
+ method: 'POST',
68
+ headers,
69
+ body: params,
70
+ timeout,
71
+ });
72
+
73
+ this.stream.connect({
74
+ onMessage: (output: SSEOutput) => {
75
+ callbacks.onStream?.(output);
76
+ },
77
+ onError: (error: Error) => {
78
+ callbacks.onError?.({
79
+ type: 'stream_error',
80
+ message: error.message,
81
+ });
82
+ },
83
+ onComplete: () => {
84
+ callbacks.onSuccess?.('');
85
+ },
86
+ });
87
+ }
88
+
89
+ abort(): void {
90
+ if (this.stream) {
91
+ this.stream.abort();
92
+ this.stream = null;
93
+ }
94
+ }
95
+
96
+ dispose(): void {
97
+ this.abort();
98
+ }
99
+ }