@yh-ui/hooks 0.1.10 → 0.1.12

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,190 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.qwenParser = exports.plainTextParser = exports.openaiParser = exports.ernieParser = 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 plainTextParser = raw => raw || null;
55
+ exports.plainTextParser = plainTextParser;
56
+ class TypewriterThrottle {
57
+ queue = [];
58
+ rafId = null;
59
+ onUpdate;
60
+ charsPerFrame;
61
+ constructor(onUpdate, charsPerFrame = 3) {
62
+ this.onUpdate = onUpdate;
63
+ this.charsPerFrame = charsPerFrame;
64
+ }
65
+ push(text) {
66
+ this.queue.push(...text.split(""));
67
+ if (this.rafId === null) {
68
+ this.schedule();
69
+ }
70
+ }
71
+ schedule() {
72
+ this.rafId = requestAnimationFrame(() => {
73
+ this.rafId = null;
74
+ if (this.queue.length === 0) return;
75
+ const batch = this.queue.splice(0, this.charsPerFrame).join("");
76
+ this.onUpdate(batch);
77
+ if (this.queue.length > 0) {
78
+ this.schedule();
79
+ }
80
+ });
81
+ }
82
+ flush() {
83
+ if (this.rafId !== null) {
84
+ cancelAnimationFrame(this.rafId);
85
+ this.rafId = null;
86
+ }
87
+ if (this.queue.length > 0) {
88
+ const remaining = this.queue.splice(0).join("");
89
+ this.onUpdate(remaining);
90
+ }
91
+ }
92
+ cancel() {
93
+ if (this.rafId !== null) {
94
+ cancelAnimationFrame(this.rafId);
95
+ this.rafId = null;
96
+ }
97
+ this.queue = [];
98
+ }
99
+ }
100
+ function useAiStream(options) {
101
+ const isStreaming = (0, _vue.ref)(false);
102
+ const currentContent = (0, _vue.ref)("");
103
+ let abortController = new AbortController();
104
+ let typewriter = null;
105
+ const parser = options.parser ?? plainTextParser;
106
+ const enableTypewriter = options.typewriter !== false;
107
+ const charsPerFrame = options.charsPerFrame ?? 3;
108
+ const stop = () => {
109
+ if (isStreaming.value) {
110
+ abortController.abort();
111
+ isStreaming.value = false;
112
+ typewriter?.flush();
113
+ }
114
+ };
115
+ const fetchStream = async (query, ...args) => {
116
+ isStreaming.value = true;
117
+ currentContent.value = "";
118
+ abortController = new AbortController();
119
+ if (enableTypewriter) {
120
+ typewriter = new TypewriterThrottle(chunk => {
121
+ currentContent.value += chunk;
122
+ options.onUpdate?.(chunk, currentContent.value);
123
+ }, charsPerFrame);
124
+ }
125
+ const pushText = text => {
126
+ if (!text) return;
127
+ if (enableTypewriter && typewriter) {
128
+ typewriter.push(text);
129
+ } else {
130
+ currentContent.value += text;
131
+ options.onUpdate?.(text, currentContent.value);
132
+ }
133
+ };
134
+ try {
135
+ const response = await options.request(query, ...args);
136
+ if (typeof response === "object" && response !== null && Symbol.asyncIterator in response) {
137
+ for await (const chunk of response) {
138
+ if (abortController.signal.aborted) break;
139
+ const parsed = parser(chunk);
140
+ if (parsed) pushText(parsed);
141
+ }
142
+ } else if (response instanceof Response && response.body) {
143
+ const reader = response.body.getReader();
144
+ const decoder = new TextDecoder("utf-8");
145
+ while (true) {
146
+ if (abortController.signal.aborted) {
147
+ reader.cancel();
148
+ break;
149
+ }
150
+ const {
151
+ done,
152
+ value
153
+ } = await reader.read();
154
+ if (done) break;
155
+ const chunkStr = decoder.decode(value, {
156
+ stream: true
157
+ });
158
+ const parsed = parser(chunkStr);
159
+ if (parsed) pushText(parsed);
160
+ }
161
+ }
162
+ if (!abortController.signal.aborted) {
163
+ if (enableTypewriter && typewriter) {
164
+ typewriter.flush();
165
+ }
166
+ isStreaming.value = false;
167
+ options.onFinish?.(currentContent.value);
168
+ }
169
+ } catch (e) {
170
+ if (e.name !== "AbortError") {
171
+ options.onError?.(e);
172
+ }
173
+ typewriter?.cancel();
174
+ isStreaming.value = false;
175
+ }
176
+ };
177
+ return {
178
+ isStreaming,
179
+ currentContent,
180
+ fetchStream,
181
+ stop,
182
+ // 暴露解析器供测试/自定义使用
183
+ parsers: {
184
+ openaiParser,
185
+ ernieParser,
186
+ qwenParser,
187
+ plainTextParser
188
+ }
189
+ };
190
+ }
@@ -0,0 +1,64 @@
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
+ * 纯文本流解析器(AsyncGenerator 输出的原始字符串)
19
+ */
20
+ export declare const plainTextParser: StreamChunkParser;
21
+ export interface AiStreamOptions {
22
+ /**
23
+ * 请求适配器,返回 AsyncGenerator 或 fetch Response
24
+ */
25
+ request: (query: string, ...args: unknown[]) => Promise<Response | AsyncGenerator<string, void, unknown>> | AsyncGenerator<string, void, unknown>;
26
+ /**
27
+ * 流式块解析器,用于适配不同厂商的格式
28
+ * @default plainTextParser(直接输出原始字符串)
29
+ */
30
+ parser?: StreamChunkParser;
31
+ /**
32
+ * 是否启用打字机平滑节流效果
33
+ * @default true
34
+ */
35
+ typewriter?: boolean;
36
+ /**
37
+ * 每帧渲染的字符数(打字机速度控制)
38
+ * @default 3
39
+ */
40
+ charsPerFrame?: number;
41
+ onUpdate?: (chunk: string, fullContent: string) => void;
42
+ onFinish?: (content: string) => void;
43
+ onError?: (err: Error) => void;
44
+ }
45
+ /**
46
+ * useAiStream - 多厂商兼容流式请求引擎
47
+ *
48
+ * 特性:
49
+ * - 支持 OpenAI / DeepSeek / 文心一言 / 通义千问 等主流格式(Adapter 模式)
50
+ * - 内置 rAF 打字机节流,保证平滑输出体验
51
+ * - 完整的 AbortController 取消支持
52
+ */
53
+ export declare function useAiStream(options: AiStreamOptions): {
54
+ isStreaming: import("vue").Ref<boolean, boolean>;
55
+ currentContent: import("vue").Ref<string, string>;
56
+ fetchStream: (query: string, ...args: unknown[]) => Promise<void>;
57
+ stop: () => void;
58
+ parsers: {
59
+ openaiParser: StreamChunkParser;
60
+ ernieParser: StreamChunkParser;
61
+ qwenParser: StreamChunkParser;
62
+ plainTextParser: StreamChunkParser;
63
+ };
64
+ };
@@ -0,0 +1,172 @@
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 plainTextParser = (raw) => raw || null;
48
+ class TypewriterThrottle {
49
+ queue = [];
50
+ rafId = null;
51
+ onUpdate;
52
+ charsPerFrame;
53
+ constructor(onUpdate, charsPerFrame = 3) {
54
+ this.onUpdate = onUpdate;
55
+ this.charsPerFrame = charsPerFrame;
56
+ }
57
+ push(text) {
58
+ this.queue.push(...text.split(""));
59
+ if (this.rafId === null) {
60
+ this.schedule();
61
+ }
62
+ }
63
+ schedule() {
64
+ this.rafId = requestAnimationFrame(() => {
65
+ this.rafId = null;
66
+ if (this.queue.length === 0) return;
67
+ const batch = this.queue.splice(0, this.charsPerFrame).join("");
68
+ this.onUpdate(batch);
69
+ if (this.queue.length > 0) {
70
+ this.schedule();
71
+ }
72
+ });
73
+ }
74
+ flush() {
75
+ if (this.rafId !== null) {
76
+ cancelAnimationFrame(this.rafId);
77
+ this.rafId = null;
78
+ }
79
+ if (this.queue.length > 0) {
80
+ const remaining = this.queue.splice(0).join("");
81
+ this.onUpdate(remaining);
82
+ }
83
+ }
84
+ cancel() {
85
+ if (this.rafId !== null) {
86
+ cancelAnimationFrame(this.rafId);
87
+ this.rafId = null;
88
+ }
89
+ this.queue = [];
90
+ }
91
+ }
92
+ export function useAiStream(options) {
93
+ const isStreaming = ref(false);
94
+ const currentContent = ref("");
95
+ let abortController = new AbortController();
96
+ let typewriter = null;
97
+ const parser = options.parser ?? plainTextParser;
98
+ const enableTypewriter = options.typewriter !== false;
99
+ const charsPerFrame = options.charsPerFrame ?? 3;
100
+ const stop = () => {
101
+ if (isStreaming.value) {
102
+ abortController.abort();
103
+ isStreaming.value = false;
104
+ typewriter?.flush();
105
+ }
106
+ };
107
+ const fetchStream = async (query, ...args) => {
108
+ isStreaming.value = true;
109
+ currentContent.value = "";
110
+ abortController = new AbortController();
111
+ if (enableTypewriter) {
112
+ typewriter = new TypewriterThrottle((chunk) => {
113
+ currentContent.value += chunk;
114
+ options.onUpdate?.(chunk, currentContent.value);
115
+ }, charsPerFrame);
116
+ }
117
+ const pushText = (text) => {
118
+ if (!text) return;
119
+ if (enableTypewriter && typewriter) {
120
+ typewriter.push(text);
121
+ } else {
122
+ currentContent.value += text;
123
+ options.onUpdate?.(text, currentContent.value);
124
+ }
125
+ };
126
+ try {
127
+ const response = await options.request(query, ...args);
128
+ if (typeof response === "object" && response !== null && Symbol.asyncIterator in response) {
129
+ for await (const chunk of response) {
130
+ if (abortController.signal.aborted) break;
131
+ const parsed = parser(chunk);
132
+ if (parsed) pushText(parsed);
133
+ }
134
+ } else if (response instanceof Response && response.body) {
135
+ const reader = response.body.getReader();
136
+ const decoder = new TextDecoder("utf-8");
137
+ while (true) {
138
+ if (abortController.signal.aborted) {
139
+ reader.cancel();
140
+ break;
141
+ }
142
+ const { done, value } = await reader.read();
143
+ if (done) break;
144
+ const chunkStr = decoder.decode(value, { stream: true });
145
+ const parsed = parser(chunkStr);
146
+ if (parsed) pushText(parsed);
147
+ }
148
+ }
149
+ if (!abortController.signal.aborted) {
150
+ if (enableTypewriter && typewriter) {
151
+ typewriter.flush();
152
+ }
153
+ isStreaming.value = false;
154
+ options.onFinish?.(currentContent.value);
155
+ }
156
+ } catch (e) {
157
+ if (e.name !== "AbortError") {
158
+ options.onError?.(e);
159
+ }
160
+ typewriter?.cancel();
161
+ isStreaming.value = false;
162
+ }
163
+ };
164
+ return {
165
+ isStreaming,
166
+ currentContent,
167
+ fetchStream,
168
+ stop,
169
+ // 暴露解析器供测试/自定义使用
170
+ parsers: { openaiParser, ernieParser, qwenParser, plainTextParser }
171
+ };
172
+ }
@@ -14,7 +14,7 @@ export interface FormContext {
14
14
  layout?: string;
15
15
  addField: (field: FormItemContext) => void;
16
16
  removeField: (field: FormItemContext) => void;
17
- themeOverrides?: Record<string, unknown>;
17
+ themeOverrides?: Record<string, string | undefined>;
18
18
  }
19
19
  export interface FormItemContext {
20
20
  prop: string;
@@ -6,20 +6,18 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.updateDayjsMonths = exports.setDayjsLocaleSync = exports.setDayjsLocale = exports.getDayjsLocale = void 0;
7
7
  var _dayjs = _interopRequireWildcard(require("dayjs"));
8
8
  require("dayjs/locale/en");
9
- require("dayjs/locale/zh-cn");
10
- require("dayjs/locale/zh-tw");
11
- require("dayjs/locale/ja");
12
- require("dayjs/locale/ko");
13
9
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
14
10
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
15
11
  const dayjs = _dayjs.default || _dayjs;
16
- const loadedLocales = /* @__PURE__ */new Set(["en", "zh-cn", "zh-tw", "ja", "ko"]);
12
+ const dayjsLocales = import.meta.glob(["../../../../node_modules/dayjs/locale/*.js", "!../../../../node_modules/dayjs/locale/en.js"], {
13
+ eager: false
14
+ });
15
+ const loadedLocales = /* @__PURE__ */new Set(["en"]);
17
16
  const localeMapping = {
18
17
  "zh-cn": "zh-cn",
19
18
  "zh-tw": "zh-tw",
20
19
  "zh-hk": "zh-hk",
21
20
  "zh-mo": "zh-tw",
22
- // 澳门使用繁体
23
21
  en: "en",
24
22
  ja: "ja",
25
23
  ko: "ko",
@@ -94,12 +92,21 @@ const setDayjsLocale = async localeCode => {
94
92
  dayjs.locale(dayjsLocale);
95
93
  return;
96
94
  }
97
- try {
98
- await Promise.resolve(`../../../../node_modules/dayjs/locale/${dayjsLocale}.js`).then(s => require(s));
99
- loadedLocales.add(dayjsLocale);
100
- dayjs.locale(dayjsLocale);
101
- } catch {
102
- console.warn(`[yh-ui] Failed to load dayjs locale: ${dayjsLocale}, falling back to 'en'`);
95
+ if (dayjsLocale === "en") {
96
+ dayjs.locale("en");
97
+ return;
98
+ }
99
+ const path = `../../../../node_modules/dayjs/locale/${dayjsLocale}.js`;
100
+ const loader = dayjsLocales[path];
101
+ if (loader) {
102
+ try {
103
+ await loader();
104
+ loadedLocales.add(dayjsLocale);
105
+ dayjs.locale(dayjsLocale);
106
+ } catch {
107
+ dayjs.locale("en");
108
+ }
109
+ } else {
103
110
  dayjs.locale("en");
104
111
  }
105
112
  };
@@ -1,25 +1,18 @@
1
1
  import 'dayjs/locale/en';
2
- import 'dayjs/locale/zh-cn';
3
- import 'dayjs/locale/zh-tw';
4
- import 'dayjs/locale/ja';
5
- import 'dayjs/locale/ko';
6
2
  /**
7
3
  * 获取 dayjs locale code
8
4
  */
9
5
  export declare const getDayjsLocale: (localeCode: string) => string;
10
6
  /**
11
7
  * 动态加载并设置 dayjs locale
12
- * 使用动态导入来按需加载,避免打包所有语言
13
8
  */
14
9
  export declare const setDayjsLocale: (localeCode: string) => Promise<void>;
15
10
  /**
16
- * 同步设置 dayjs locale(不推荐,会阻塞)
17
- * 用于需要立即同步的场景
11
+ * 同步设置 dayjs locale(立即生效,异步加载后会更新)
18
12
  */
19
13
  export declare const setDayjsLocaleSync: (localeCode: string) => void;
20
14
  /**
21
15
  * 使用自定义月份名称更新 dayjs locale
22
- * 这样可以确保 dayjs 使用我们语言包中定义的月份名称
23
16
  */
24
17
  export declare const updateDayjsMonths: (localeCode: string, months: {
25
18
  jan: string;
@@ -1,17 +1,16 @@
1
1
  import * as _dayjs from "dayjs";
2
2
  const dayjs = _dayjs.default || _dayjs;
3
3
  import "dayjs/locale/en";
4
- import "dayjs/locale/zh-cn";
5
- import "dayjs/locale/zh-tw";
6
- import "dayjs/locale/ja";
7
- import "dayjs/locale/ko";
8
- const loadedLocales = /* @__PURE__ */ new Set(["en", "zh-cn", "zh-tw", "ja", "ko"]);
4
+ const dayjsLocales = import.meta.glob(
5
+ ["../../../../node_modules/dayjs/locale/*.js", "!../../../../node_modules/dayjs/locale/en.js"],
6
+ { eager: false }
7
+ );
8
+ const loadedLocales = /* @__PURE__ */ new Set(["en"]);
9
9
  const localeMapping = {
10
10
  "zh-cn": "zh-cn",
11
11
  "zh-tw": "zh-tw",
12
12
  "zh-hk": "zh-hk",
13
13
  "zh-mo": "zh-tw",
14
- // 澳门使用繁体
15
14
  en: "en",
16
15
  ja: "ja",
17
16
  ko: "ko",
@@ -85,12 +84,21 @@ export const setDayjsLocale = async (localeCode) => {
85
84
  dayjs.locale(dayjsLocale);
86
85
  return;
87
86
  }
88
- try {
89
- await import(`../../../../node_modules/dayjs/locale/${dayjsLocale}.js`);
90
- loadedLocales.add(dayjsLocale);
91
- dayjs.locale(dayjsLocale);
92
- } catch {
93
- console.warn(`[yh-ui] Failed to load dayjs locale: ${dayjsLocale}, falling back to 'en'`);
87
+ if (dayjsLocale === "en") {
88
+ dayjs.locale("en");
89
+ return;
90
+ }
91
+ const path = `../../../../node_modules/dayjs/locale/${dayjsLocale}.js`;
92
+ const loader = dayjsLocales[path];
93
+ if (loader) {
94
+ try {
95
+ await loader();
96
+ loadedLocales.add(dayjsLocale);
97
+ dayjs.locale(dayjsLocale);
98
+ } catch {
99
+ dayjs.locale("en");
100
+ }
101
+ } else {
94
102
  dayjs.locale("en");
95
103
  }
96
104
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yh-ui/hooks",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "YH-UI composition hooks",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -23,8 +23,9 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@yh-ui/utils": "0.1.10",
27
- "@yh-ui/locale": "0.1.10"
26
+ "dayjs": "^1.11.19",
27
+ "@yh-ui/locale": "0.1.12",
28
+ "@yh-ui/utils": "0.1.12"
28
29
  },
29
30
  "devDependencies": {
30
31
  "vue": "^3.5.27",