@yh-ui/hooks 0.1.10 → 0.1.15

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,29 @@
1
+ import { ref } from "vue";
2
+ export function useAiRequest(options) {
3
+ const loading = ref(false);
4
+ const data = ref("");
5
+ const error = ref(null);
6
+ const send = async (query, ...args) => {
7
+ loading.value = true;
8
+ error.value = null;
9
+ try {
10
+ const result = await options.request(query, ...args);
11
+ data.value = result;
12
+ options.onSuccess?.(result);
13
+ return result;
14
+ } catch (e) {
15
+ const err = e instanceof Error ? e : new Error(String(e));
16
+ error.value = err;
17
+ options.onError?.(err);
18
+ throw err;
19
+ } finally {
20
+ loading.value = false;
21
+ }
22
+ };
23
+ return {
24
+ loading,
25
+ data,
26
+ error,
27
+ send
28
+ };
29
+ }
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.qwenParser = exports.plainTextParser = exports.openaiParser = exports.geminiParser = exports.ernieParser = exports.claudeParser = void 0;
7
+ exports.useAiStream = useAiStream;
8
+ var _vue = require("vue");
9
+ const openaiParser = raw => {
10
+ const lines = raw.split("\n");
11
+ let text = "";
12
+ for (const line of lines) {
13
+ if (!line.startsWith("data: ")) continue;
14
+ const data = line.slice(6).trim();
15
+ if (data === "[DONE]") break;
16
+ try {
17
+ const json = JSON.parse(data);
18
+ const delta = json?.choices?.[0]?.delta?.content;
19
+ if (delta) text += delta;
20
+ } catch {}
21
+ }
22
+ return text || null;
23
+ };
24
+ exports.openaiParser = openaiParser;
25
+ const ernieParser = raw => {
26
+ const lines = raw.split("\n");
27
+ let text = "";
28
+ for (const line of lines) {
29
+ if (!line.startsWith("data: ")) continue;
30
+ const data = line.slice(6).trim();
31
+ try {
32
+ const json = JSON.parse(data);
33
+ if (json?.result) text += json.result;
34
+ } catch {}
35
+ }
36
+ return text || null;
37
+ };
38
+ exports.ernieParser = ernieParser;
39
+ const qwenParser = raw => {
40
+ const lines = raw.split("\n");
41
+ let text = "";
42
+ for (const line of lines) {
43
+ if (!line.startsWith("data: ")) continue;
44
+ const data = line.slice(6).trim();
45
+ try {
46
+ const json = JSON.parse(data);
47
+ const t = json?.output?.text;
48
+ if (t) text += t;
49
+ } catch {}
50
+ }
51
+ return text || null;
52
+ };
53
+ exports.qwenParser = qwenParser;
54
+ const claudeParser = raw => {
55
+ const lines = raw.split("\n");
56
+ let text = "";
57
+ for (const line of lines) {
58
+ if (!line.startsWith("data: ")) continue;
59
+ const data = line.slice(6).trim();
60
+ try {
61
+ const json = JSON.parse(data);
62
+ if (json?.type === "content_block_delta" && json?.delta?.text) {
63
+ text += json.delta.text;
64
+ }
65
+ } catch {}
66
+ }
67
+ return text || null;
68
+ };
69
+ exports.claudeParser = claudeParser;
70
+ const geminiParser = raw => {
71
+ const lines = raw.split("\n");
72
+ let text = "";
73
+ for (const line of lines) {
74
+ const content = line.startsWith("data: ") ? line.slice(6).trim() : line.trim();
75
+ if (!content) continue;
76
+ try {
77
+ const json = JSON.parse(content);
78
+ const part = json?.candidates?.[0]?.content?.parts?.[0]?.text;
79
+ if (part) text += part;
80
+ } catch {}
81
+ }
82
+ return text || null;
83
+ };
84
+ exports.geminiParser = geminiParser;
85
+ const plainTextParser = raw => raw || null;
86
+ exports.plainTextParser = plainTextParser;
87
+ class TypewriterThrottle {
88
+ queue = [];
89
+ rafId = null;
90
+ onUpdate;
91
+ charsPerFrame;
92
+ constructor(onUpdate, charsPerFrame = 3) {
93
+ this.onUpdate = onUpdate;
94
+ this.charsPerFrame = charsPerFrame;
95
+ }
96
+ push(text) {
97
+ this.queue.push(...text.split(""));
98
+ if (this.rafId === null) {
99
+ this.schedule();
100
+ }
101
+ }
102
+ schedule() {
103
+ this.rafId = requestAnimationFrame(() => {
104
+ this.rafId = null;
105
+ if (this.queue.length === 0) return;
106
+ const batch = this.queue.splice(0, this.charsPerFrame).join("");
107
+ this.onUpdate(batch);
108
+ if (this.queue.length > 0) {
109
+ this.schedule();
110
+ }
111
+ });
112
+ }
113
+ flush() {
114
+ if (this.rafId !== null) {
115
+ cancelAnimationFrame(this.rafId);
116
+ this.rafId = null;
117
+ }
118
+ if (this.queue.length > 0) {
119
+ const remaining = this.queue.splice(0).join("");
120
+ this.onUpdate(remaining);
121
+ }
122
+ }
123
+ cancel() {
124
+ if (this.rafId !== null) {
125
+ cancelAnimationFrame(this.rafId);
126
+ this.rafId = null;
127
+ }
128
+ this.queue = [];
129
+ }
130
+ }
131
+ function useAiStream(options) {
132
+ const isStreaming = (0, _vue.ref)(false);
133
+ const currentContent = (0, _vue.ref)("");
134
+ let abortController = new AbortController();
135
+ let typewriter = null;
136
+ const parser = options.parser ?? plainTextParser;
137
+ const enableTypewriter = options.typewriter !== false;
138
+ const charsPerFrame = options.charsPerFrame ?? 3;
139
+ const stop = () => {
140
+ if (isStreaming.value) {
141
+ abortController.abort();
142
+ isStreaming.value = false;
143
+ typewriter?.flush();
144
+ }
145
+ };
146
+ const fetchStream = async (query, ...args) => {
147
+ isStreaming.value = true;
148
+ currentContent.value = "";
149
+ abortController = new AbortController();
150
+ if (enableTypewriter) {
151
+ typewriter = new TypewriterThrottle(chunk => {
152
+ currentContent.value += chunk;
153
+ options.onUpdate?.(chunk, currentContent.value);
154
+ }, charsPerFrame);
155
+ }
156
+ const pushText = text => {
157
+ if (!text) return;
158
+ if (enableTypewriter && typewriter) {
159
+ typewriter.push(text);
160
+ } else {
161
+ currentContent.value += text;
162
+ options.onUpdate?.(text, currentContent.value);
163
+ }
164
+ };
165
+ try {
166
+ const response = await options.request(query, ...args);
167
+ if (typeof response === "object" && response !== null && Symbol.asyncIterator in response) {
168
+ for await (const chunk of response) {
169
+ if (abortController.signal.aborted) break;
170
+ const parsed = parser(chunk);
171
+ if (parsed) pushText(parsed);
172
+ }
173
+ } else if (response instanceof Response && response.body) {
174
+ const reader = response.body.getReader();
175
+ const decoder = new TextDecoder("utf-8");
176
+ while (true) {
177
+ if (abortController.signal.aborted) {
178
+ reader.cancel();
179
+ break;
180
+ }
181
+ const {
182
+ done,
183
+ value
184
+ } = await reader.read();
185
+ if (done) break;
186
+ const chunkStr = decoder.decode(value, {
187
+ stream: true
188
+ });
189
+ const parsed = parser(chunkStr);
190
+ if (parsed) pushText(parsed);
191
+ }
192
+ }
193
+ if (!abortController.signal.aborted) {
194
+ if (enableTypewriter && typewriter) {
195
+ typewriter.flush();
196
+ }
197
+ isStreaming.value = false;
198
+ options.onFinish?.(currentContent.value);
199
+ }
200
+ } catch (e) {
201
+ if (e.name !== "AbortError") {
202
+ options.onError?.(e);
203
+ }
204
+ typewriter?.cancel();
205
+ isStreaming.value = false;
206
+ }
207
+ };
208
+ return {
209
+ isStreaming,
210
+ currentContent,
211
+ fetchStream,
212
+ stop,
213
+ // 暴露解析器供测试/自定义使用
214
+ parsers: {
215
+ openaiParser,
216
+ ernieParser,
217
+ qwenParser,
218
+ claudeParser,
219
+ geminiParser,
220
+ plainTextParser
221
+ }
222
+ };
223
+ }
@@ -0,0 +1,76 @@
1
+ export type StreamChunkParser = (raw: string) => string | null;
2
+ /**
3
+ * OpenAI / DeepSeek 格式解析器
4
+ * data: {"choices":[{"delta":{"content":"hello"}}]}
5
+ */
6
+ export declare const openaiParser: StreamChunkParser;
7
+ /**
8
+ * 文心一言 / ERNIE 格式解析器
9
+ * data: {"result":"hello","is_end":false}
10
+ */
11
+ export declare const ernieParser: StreamChunkParser;
12
+ /**
13
+ * 通义千问 / Qwen 格式解析器
14
+ * data: {"output":{"text":"hello"},"finish_reason":null}
15
+ */
16
+ export declare const qwenParser: StreamChunkParser;
17
+ /**
18
+ * Anthropic / Claude 格式解析器
19
+ * data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"hello"}}
20
+ */
21
+ export declare const claudeParser: StreamChunkParser;
22
+ /**
23
+ * Google / Gemini 格式解析器
24
+ * data: {"candidates":[{"content":{"parts":[{"text":"hello"}]}}]}
25
+ */
26
+ export declare const geminiParser: StreamChunkParser;
27
+ /**
28
+ * 纯文本流解析器(AsyncGenerator 输出的原始字符串)
29
+ */
30
+ export declare const plainTextParser: StreamChunkParser;
31
+ export interface AiStreamOptions {
32
+ /**
33
+ * 请求适配器,返回 AsyncGenerator 或 fetch Response
34
+ */
35
+ request: (query: string, ...args: unknown[]) => Promise<Response | AsyncGenerator<string, void, unknown>> | AsyncGenerator<string, void, unknown>;
36
+ /**
37
+ * 流式块解析器,用于适配不同厂商的格式
38
+ * @default plainTextParser(直接输出原始字符串)
39
+ */
40
+ parser?: StreamChunkParser;
41
+ /**
42
+ * 是否启用打字机平滑节流效果
43
+ * @default true
44
+ */
45
+ typewriter?: boolean;
46
+ /**
47
+ * 每帧渲染的字符数(打字机速度控制)
48
+ * @default 3
49
+ */
50
+ charsPerFrame?: number;
51
+ onUpdate?: (chunk: string, fullContent: string) => void;
52
+ onFinish?: (content: string) => void;
53
+ onError?: (err: Error) => void;
54
+ }
55
+ /**
56
+ * useAiStream - 多厂商兼容流式请求引擎
57
+ *
58
+ * 特性:
59
+ * - 支持 OpenAI / DeepSeek / 文心一言 / 通义千问 等主流格式(Adapter 模式)
60
+ * - 内置 rAF 打字机节流,保证平滑输出体验
61
+ * - 完整的 AbortController 取消支持
62
+ */
63
+ export declare function useAiStream(options: AiStreamOptions): {
64
+ isStreaming: import("vue").Ref<boolean, boolean>;
65
+ currentContent: import("vue").Ref<string, string>;
66
+ fetchStream: (query: string, ...args: unknown[]) => Promise<void>;
67
+ stop: () => void;
68
+ parsers: {
69
+ openaiParser: StreamChunkParser;
70
+ ernieParser: StreamChunkParser;
71
+ qwenParser: StreamChunkParser;
72
+ claudeParser: StreamChunkParser;
73
+ geminiParser: StreamChunkParser;
74
+ plainTextParser: StreamChunkParser;
75
+ };
76
+ };
@@ -0,0 +1,210 @@
1
+ import { ref } from "vue";
2
+ export const openaiParser = (raw) => {
3
+ const lines = raw.split("\n");
4
+ let text = "";
5
+ for (const line of lines) {
6
+ if (!line.startsWith("data: ")) continue;
7
+ const data = line.slice(6).trim();
8
+ if (data === "[DONE]") break;
9
+ try {
10
+ const json = JSON.parse(data);
11
+ const delta = json?.choices?.[0]?.delta?.content;
12
+ if (delta) text += delta;
13
+ } catch {
14
+ }
15
+ }
16
+ return text || null;
17
+ };
18
+ export const ernieParser = (raw) => {
19
+ const lines = raw.split("\n");
20
+ let text = "";
21
+ for (const line of lines) {
22
+ if (!line.startsWith("data: ")) continue;
23
+ const data = line.slice(6).trim();
24
+ try {
25
+ const json = JSON.parse(data);
26
+ if (json?.result) text += json.result;
27
+ } catch {
28
+ }
29
+ }
30
+ return text || null;
31
+ };
32
+ export const qwenParser = (raw) => {
33
+ const lines = raw.split("\n");
34
+ let text = "";
35
+ for (const line of lines) {
36
+ if (!line.startsWith("data: ")) continue;
37
+ const data = line.slice(6).trim();
38
+ try {
39
+ const json = JSON.parse(data);
40
+ const t = json?.output?.text;
41
+ if (t) text += t;
42
+ } catch {
43
+ }
44
+ }
45
+ return text || null;
46
+ };
47
+ export const claudeParser = (raw) => {
48
+ const lines = raw.split("\n");
49
+ let text = "";
50
+ for (const line of lines) {
51
+ if (!line.startsWith("data: ")) continue;
52
+ const data = line.slice(6).trim();
53
+ try {
54
+ const json = JSON.parse(data);
55
+ if (json?.type === "content_block_delta" && json?.delta?.text) {
56
+ text += json.delta.text;
57
+ }
58
+ } catch {
59
+ }
60
+ }
61
+ return text || null;
62
+ };
63
+ export const geminiParser = (raw) => {
64
+ const lines = raw.split("\n");
65
+ let text = "";
66
+ for (const line of lines) {
67
+ const content = line.startsWith("data: ") ? line.slice(6).trim() : line.trim();
68
+ if (!content) continue;
69
+ try {
70
+ const json = JSON.parse(content);
71
+ const part = json?.candidates?.[0]?.content?.parts?.[0]?.text;
72
+ if (part) text += part;
73
+ } catch {
74
+ }
75
+ }
76
+ return text || null;
77
+ };
78
+ export const plainTextParser = (raw) => raw || null;
79
+ class TypewriterThrottle {
80
+ queue = [];
81
+ rafId = null;
82
+ onUpdate;
83
+ charsPerFrame;
84
+ constructor(onUpdate, charsPerFrame = 3) {
85
+ this.onUpdate = onUpdate;
86
+ this.charsPerFrame = charsPerFrame;
87
+ }
88
+ push(text) {
89
+ this.queue.push(...text.split(""));
90
+ if (this.rafId === null) {
91
+ this.schedule();
92
+ }
93
+ }
94
+ schedule() {
95
+ this.rafId = requestAnimationFrame(() => {
96
+ this.rafId = null;
97
+ if (this.queue.length === 0) return;
98
+ const batch = this.queue.splice(0, this.charsPerFrame).join("");
99
+ this.onUpdate(batch);
100
+ if (this.queue.length > 0) {
101
+ this.schedule();
102
+ }
103
+ });
104
+ }
105
+ flush() {
106
+ if (this.rafId !== null) {
107
+ cancelAnimationFrame(this.rafId);
108
+ this.rafId = null;
109
+ }
110
+ if (this.queue.length > 0) {
111
+ const remaining = this.queue.splice(0).join("");
112
+ this.onUpdate(remaining);
113
+ }
114
+ }
115
+ cancel() {
116
+ if (this.rafId !== null) {
117
+ cancelAnimationFrame(this.rafId);
118
+ this.rafId = null;
119
+ }
120
+ this.queue = [];
121
+ }
122
+ }
123
+ export function useAiStream(options) {
124
+ const isStreaming = ref(false);
125
+ const currentContent = ref("");
126
+ let abortController = new AbortController();
127
+ let typewriter = null;
128
+ const parser = options.parser ?? plainTextParser;
129
+ const enableTypewriter = options.typewriter !== false;
130
+ const charsPerFrame = options.charsPerFrame ?? 3;
131
+ const stop = () => {
132
+ if (isStreaming.value) {
133
+ abortController.abort();
134
+ isStreaming.value = false;
135
+ typewriter?.flush();
136
+ }
137
+ };
138
+ const fetchStream = async (query, ...args) => {
139
+ isStreaming.value = true;
140
+ currentContent.value = "";
141
+ abortController = new AbortController();
142
+ if (enableTypewriter) {
143
+ typewriter = new TypewriterThrottle((chunk) => {
144
+ currentContent.value += chunk;
145
+ options.onUpdate?.(chunk, currentContent.value);
146
+ }, charsPerFrame);
147
+ }
148
+ const pushText = (text) => {
149
+ if (!text) return;
150
+ if (enableTypewriter && typewriter) {
151
+ typewriter.push(text);
152
+ } else {
153
+ currentContent.value += text;
154
+ options.onUpdate?.(text, currentContent.value);
155
+ }
156
+ };
157
+ try {
158
+ const response = await options.request(query, ...args);
159
+ if (typeof response === "object" && response !== null && Symbol.asyncIterator in response) {
160
+ for await (const chunk of response) {
161
+ if (abortController.signal.aborted) break;
162
+ const parsed = parser(chunk);
163
+ if (parsed) pushText(parsed);
164
+ }
165
+ } else if (response instanceof Response && response.body) {
166
+ const reader = response.body.getReader();
167
+ const decoder = new TextDecoder("utf-8");
168
+ while (true) {
169
+ if (abortController.signal.aborted) {
170
+ reader.cancel();
171
+ break;
172
+ }
173
+ const { done, value } = await reader.read();
174
+ if (done) break;
175
+ const chunkStr = decoder.decode(value, { stream: true });
176
+ const parsed = parser(chunkStr);
177
+ if (parsed) pushText(parsed);
178
+ }
179
+ }
180
+ if (!abortController.signal.aborted) {
181
+ if (enableTypewriter && typewriter) {
182
+ typewriter.flush();
183
+ }
184
+ isStreaming.value = false;
185
+ options.onFinish?.(currentContent.value);
186
+ }
187
+ } catch (e) {
188
+ if (e.name !== "AbortError") {
189
+ options.onError?.(e);
190
+ }
191
+ typewriter?.cancel();
192
+ isStreaming.value = false;
193
+ }
194
+ };
195
+ return {
196
+ isStreaming,
197
+ currentContent,
198
+ fetchStream,
199
+ stop,
200
+ // 暴露解析器供测试/自定义使用
201
+ parsers: {
202
+ openaiParser,
203
+ ernieParser,
204
+ qwenParser,
205
+ claudeParser,
206
+ geminiParser,
207
+ plainTextParser
208
+ }
209
+ };
210
+ }