@yh-ui/hooks 0.1.17 → 0.1.21

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,128 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.IndexedDBAdapter = exports.APIClient = void 0;
7
+ class IndexedDBAdapter {
8
+ dbName;
9
+ storeName;
10
+ db = null;
11
+ constructor(dbName = "yh-ui-db", storeName = "yh-ui-store") {
12
+ this.dbName = dbName;
13
+ this.storeName = storeName;
14
+ }
15
+ async getDB() {
16
+ if (this.db) return this.db;
17
+ return new Promise((resolve, reject) => {
18
+ const request = indexedDB.open(this.dbName, 1);
19
+ request.onerror = () => reject(request.error);
20
+ request.onsuccess = () => {
21
+ this.db = request.result;
22
+ resolve(request.result);
23
+ };
24
+ request.onupgradeneeded = event => {
25
+ const db = event.target.result;
26
+ if (!db.objectStoreNames.contains(this.storeName)) {
27
+ db.createObjectStore(this.storeName, {
28
+ keyPath: "key"
29
+ });
30
+ }
31
+ };
32
+ });
33
+ }
34
+ async get(key) {
35
+ const db = await this.getDB();
36
+ return new Promise((resolve, reject) => {
37
+ const transaction = db.transaction(this.storeName, "readonly");
38
+ const store = transaction.objectStore(this.storeName);
39
+ const request = store.get(key);
40
+ request.onerror = () => reject(request.error);
41
+ request.onsuccess = () => resolve(request.result?.value ?? null);
42
+ });
43
+ }
44
+ async set(key, value) {
45
+ const db = await this.getDB();
46
+ return new Promise((resolve, reject) => {
47
+ const transaction = db.transaction(this.storeName, "readwrite");
48
+ const store = transaction.objectStore(this.storeName);
49
+ const request = store.put({
50
+ key,
51
+ value
52
+ });
53
+ request.onerror = () => reject(request.error);
54
+ request.onsuccess = () => resolve();
55
+ });
56
+ }
57
+ async remove(key) {
58
+ const db = await this.getDB();
59
+ return new Promise((resolve, reject) => {
60
+ const transaction = db.transaction(this.storeName, "readwrite");
61
+ const store = transaction.objectStore(this.storeName);
62
+ const request = store.delete(key);
63
+ request.onerror = () => reject(request.error);
64
+ request.onsuccess = () => resolve();
65
+ });
66
+ }
67
+ async clear() {
68
+ const db = await this.getDB();
69
+ return new Promise((resolve, reject) => {
70
+ const transaction = db.transaction(this.storeName, "readwrite");
71
+ const store = transaction.objectStore(this.storeName);
72
+ const request = store.clear();
73
+ request.onerror = () => reject(request.error);
74
+ request.onsuccess = () => resolve();
75
+ });
76
+ }
77
+ }
78
+ exports.IndexedDBAdapter = IndexedDBAdapter;
79
+ class APIClient {
80
+ baseURL;
81
+ headers;
82
+ constructor(baseURL, headers = {}) {
83
+ this.baseURL = baseURL.replace(/\/$/, "");
84
+ this.headers = headers;
85
+ }
86
+ async get(key) {
87
+ const response = await fetch(`${this.baseURL}/storage/${key}`, {
88
+ headers: this.headers
89
+ });
90
+ if (!response.ok) {
91
+ if (response.status === 404) return null;
92
+ throw new Error(`HTTP ${response.status}`);
93
+ }
94
+ return response.json();
95
+ }
96
+ async set(key, value) {
97
+ const response = await fetch(`${this.baseURL}/storage/${key}`, {
98
+ method: "PUT",
99
+ headers: {
100
+ ...this.headers,
101
+ "Content-Type": "application/json"
102
+ },
103
+ body: JSON.stringify(value)
104
+ });
105
+ if (!response.ok) {
106
+ throw new Error(`HTTP ${response.status}`);
107
+ }
108
+ }
109
+ async remove(key) {
110
+ const response = await fetch(`${this.baseURL}/storage/${key}`, {
111
+ method: "DELETE",
112
+ headers: this.headers
113
+ });
114
+ if (!response.ok && response.status !== 404) {
115
+ throw new Error(`HTTP ${response.status}`);
116
+ }
117
+ }
118
+ async clear() {
119
+ const response = await fetch(`${this.baseURL}/storage`, {
120
+ method: "DELETE",
121
+ headers: this.headers
122
+ });
123
+ if (!response.ok && response.status !== 404) {
124
+ throw new Error(`HTTP ${response.status}`);
125
+ }
126
+ }
127
+ }
128
+ exports.APIClient = APIClient;
@@ -0,0 +1,26 @@
1
+ export interface StorageAdapter {
2
+ get<T>(key: string): Promise<T | null>;
3
+ set<T>(key: string, value: T): Promise<void>;
4
+ remove(key: string): Promise<void>;
5
+ clear(): Promise<void>;
6
+ }
7
+ export declare class IndexedDBAdapter implements StorageAdapter {
8
+ private dbName;
9
+ private storeName;
10
+ private db;
11
+ constructor(dbName?: string, storeName?: string);
12
+ private getDB;
13
+ get<T>(key: string): Promise<T | null>;
14
+ set<T>(key: string, value: T): Promise<void>;
15
+ remove(key: string): Promise<void>;
16
+ clear(): Promise<void>;
17
+ }
18
+ export declare class APIClient implements StorageAdapter {
19
+ private baseURL;
20
+ private headers;
21
+ constructor(baseURL: string, headers?: Record<string, string>);
22
+ get<T>(key: string): Promise<T | null>;
23
+ set<T>(key: string, value: T): Promise<void>;
24
+ remove(key: string): Promise<void>;
25
+ clear(): Promise<void>;
26
+ }
@@ -0,0 +1,112 @@
1
+ export class IndexedDBAdapter {
2
+ dbName;
3
+ storeName;
4
+ db = null;
5
+ constructor(dbName = "yh-ui-db", storeName = "yh-ui-store") {
6
+ this.dbName = dbName;
7
+ this.storeName = storeName;
8
+ }
9
+ async getDB() {
10
+ if (this.db) return this.db;
11
+ return new Promise((resolve, reject) => {
12
+ const request = indexedDB.open(this.dbName, 1);
13
+ request.onerror = () => reject(request.error);
14
+ request.onsuccess = () => {
15
+ this.db = request.result;
16
+ resolve(request.result);
17
+ };
18
+ request.onupgradeneeded = (event) => {
19
+ const db = event.target.result;
20
+ if (!db.objectStoreNames.contains(this.storeName)) {
21
+ db.createObjectStore(this.storeName, { keyPath: "key" });
22
+ }
23
+ };
24
+ });
25
+ }
26
+ async get(key) {
27
+ const db = await this.getDB();
28
+ return new Promise((resolve, reject) => {
29
+ const transaction = db.transaction(this.storeName, "readonly");
30
+ const store = transaction.objectStore(this.storeName);
31
+ const request = store.get(key);
32
+ request.onerror = () => reject(request.error);
33
+ request.onsuccess = () => resolve(request.result?.value ?? null);
34
+ });
35
+ }
36
+ async set(key, value) {
37
+ const db = await this.getDB();
38
+ return new Promise((resolve, reject) => {
39
+ const transaction = db.transaction(this.storeName, "readwrite");
40
+ const store = transaction.objectStore(this.storeName);
41
+ const request = store.put({ key, value });
42
+ request.onerror = () => reject(request.error);
43
+ request.onsuccess = () => resolve();
44
+ });
45
+ }
46
+ async remove(key) {
47
+ const db = await this.getDB();
48
+ return new Promise((resolve, reject) => {
49
+ const transaction = db.transaction(this.storeName, "readwrite");
50
+ const store = transaction.objectStore(this.storeName);
51
+ const request = store.delete(key);
52
+ request.onerror = () => reject(request.error);
53
+ request.onsuccess = () => resolve();
54
+ });
55
+ }
56
+ async clear() {
57
+ const db = await this.getDB();
58
+ return new Promise((resolve, reject) => {
59
+ const transaction = db.transaction(this.storeName, "readwrite");
60
+ const store = transaction.objectStore(this.storeName);
61
+ const request = store.clear();
62
+ request.onerror = () => reject(request.error);
63
+ request.onsuccess = () => resolve();
64
+ });
65
+ }
66
+ }
67
+ export class APIClient {
68
+ baseURL;
69
+ headers;
70
+ constructor(baseURL, headers = {}) {
71
+ this.baseURL = baseURL.replace(/\/$/, "");
72
+ this.headers = headers;
73
+ }
74
+ async get(key) {
75
+ const response = await fetch(`${this.baseURL}/storage/${key}`, {
76
+ headers: this.headers
77
+ });
78
+ if (!response.ok) {
79
+ if (response.status === 404) return null;
80
+ throw new Error(`HTTP ${response.status}`);
81
+ }
82
+ return response.json();
83
+ }
84
+ async set(key, value) {
85
+ const response = await fetch(`${this.baseURL}/storage/${key}`, {
86
+ method: "PUT",
87
+ headers: { ...this.headers, "Content-Type": "application/json" },
88
+ body: JSON.stringify(value)
89
+ });
90
+ if (!response.ok) {
91
+ throw new Error(`HTTP ${response.status}`);
92
+ }
93
+ }
94
+ async remove(key) {
95
+ const response = await fetch(`${this.baseURL}/storage/${key}`, {
96
+ method: "DELETE",
97
+ headers: this.headers
98
+ });
99
+ if (!response.ok && response.status !== 404) {
100
+ throw new Error(`HTTP ${response.status}`);
101
+ }
102
+ }
103
+ async clear() {
104
+ const response = await fetch(`${this.baseURL}/storage`, {
105
+ method: "DELETE",
106
+ headers: this.headers
107
+ });
108
+ if (!response.ok && response.status !== 404) {
109
+ throw new Error(`HTTP ${response.status}`);
110
+ }
111
+ }
112
+ }
@@ -57,4 +57,15 @@ Object.keys(_useAiVoice).forEach(function (key) {
57
57
  return _useAiVoice[key];
58
58
  }
59
59
  });
60
+ });
61
+ var _useAiPersistence = require("./use-ai-persistence.cjs");
62
+ Object.keys(_useAiPersistence).forEach(function (key) {
63
+ if (key === "default" || key === "__esModule") return;
64
+ if (key in exports && exports[key] === _useAiPersistence[key]) return;
65
+ Object.defineProperty(exports, key, {
66
+ enumerable: true,
67
+ get: function () {
68
+ return _useAiPersistence[key];
69
+ }
70
+ });
60
71
  });
@@ -3,3 +3,4 @@ export * from './use-ai-stream';
3
3
  export * from './use-ai-conversations';
4
4
  export * from './use-ai-request';
5
5
  export * from './use-ai-voice';
6
+ export * from './use-ai-persistence';
@@ -3,3 +3,4 @@ export * from "./use-ai-stream.mjs";
3
3
  export * from "./use-ai-conversations.mjs";
4
4
  export * from "./use-ai-request.mjs";
5
5
  export * from "./use-ai-voice.mjs";
6
+ export * from "./use-ai-persistence.mjs";
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.localStorageAdapter = exports.IndexedDBAdapter = void 0;
6
+ exports.localStorageAdapter = exports.AiIndexedDBAdapter = void 0;
7
7
  exports.useAiConversations = useAiConversations;
8
8
  var _vue = require("vue");
9
9
  const localStorageAdapter = exports.localStorageAdapter = {
@@ -25,7 +25,7 @@ const localStorageAdapter = exports.localStorageAdapter = {
25
25
  } catch {}
26
26
  }
27
27
  };
28
- class IndexedDBAdapter {
28
+ class AiIndexedDBAdapter {
29
29
  db = null;
30
30
  dbName;
31
31
  storeName = "ai_conversations";
@@ -80,7 +80,7 @@ class IndexedDBAdapter {
80
80
  });
81
81
  }
82
82
  }
83
- exports.IndexedDBAdapter = IndexedDBAdapter;
83
+ exports.AiIndexedDBAdapter = AiIndexedDBAdapter;
84
84
  function getGroupLabel(updatedAt) {
85
85
  const now = Date.now();
86
86
  const diff = now - updatedAt;
@@ -96,13 +96,16 @@ function useAiConversations(options = {}) {
96
96
  idGenerator = () => Math.random().toString(36).substring(2, 9),
97
97
  storage = "localStorage",
98
98
  storageKey = "yh-ui-ai-conversations",
99
- pageSize = 20
99
+ pageSize = 20,
100
+ remoteSync,
101
+ autoSync = false,
102
+ syncInterval = 3e4
100
103
  } = options;
101
104
  let adapter = null;
102
105
  if (storage === "localStorage") {
103
106
  adapter = localStorageAdapter;
104
107
  } else if (storage === "indexedDB") {
105
- adapter = new IndexedDBAdapter();
108
+ adapter = new AiIndexedDBAdapter();
106
109
  } else if (storage && typeof storage === "object") {
107
110
  adapter = storage;
108
111
  }
@@ -225,6 +228,61 @@ function useAiConversations(options = {}) {
225
228
  await adapter.removeItem(storageKey);
226
229
  }
227
230
  };
231
+ const isSyncing = (0, _vue.ref)(false);
232
+ const lastSyncTime = (0, _vue.ref)(0);
233
+ const syncError = (0, _vue.ref)(null);
234
+ const syncToRemote = async () => {
235
+ if (!remoteSync || isSyncing.value) return;
236
+ isSyncing.value = true;
237
+ syncError.value = null;
238
+ try {
239
+ await remoteSync.batchUpdate(conversations.value);
240
+ lastSyncTime.value = Date.now();
241
+ } catch (err) {
242
+ syncError.value = err instanceof Error ? err : new Error(String(err));
243
+ } finally {
244
+ isSyncing.value = false;
245
+ }
246
+ };
247
+ const fetchFromRemote = async () => {
248
+ if (!remoteSync || isSyncing.value) return;
249
+ isSyncing.value = true;
250
+ syncError.value = null;
251
+ try {
252
+ const remoteConversations = await remoteSync.fetchConversations();
253
+ for (const rc of remoteConversations) {
254
+ if (!conversations.value.find(c => c.id === rc.id)) {
255
+ conversations.value.push(rc);
256
+ }
257
+ }
258
+ conversations.value.sort((a, b) => {
259
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
260
+ return b.updatedAt - a.updatedAt;
261
+ });
262
+ lastSyncTime.value = Date.now();
263
+ await persist();
264
+ } catch (err) {
265
+ syncError.value = err instanceof Error ? err : new Error(String(err));
266
+ } finally {
267
+ isSyncing.value = false;
268
+ }
269
+ };
270
+ let syncTimer = null;
271
+ const startSync = () => {
272
+ if (!remoteSync || syncTimer) return;
273
+ if (syncInterval > 0) {
274
+ syncTimer = setInterval(syncToRemote, syncInterval);
275
+ }
276
+ };
277
+ const stopSync = () => {
278
+ if (syncTimer) {
279
+ clearInterval(syncTimer);
280
+ syncTimer = null;
281
+ }
282
+ };
283
+ if (remoteSync && autoSync) {
284
+ startSync();
285
+ }
228
286
  return {
229
287
  /** 完整会话列表 */
230
288
  conversations,
@@ -249,6 +307,15 @@ function useAiConversations(options = {}) {
249
307
  /** 置顶/取消置顶 */
250
308
  pinConversation,
251
309
  /** 清空全部 */
252
- clear
310
+ clear,
311
+ /** 远程同步状态 */
312
+ isSyncing,
313
+ lastSyncTime,
314
+ syncError,
315
+ /** 远程同步方法 */
316
+ syncToRemote,
317
+ fetchFromRemote,
318
+ startSync,
319
+ stopSync
253
320
  };
254
321
  }
@@ -18,7 +18,7 @@ export interface AiConversation {
18
18
  */
19
19
  meta?: Record<string, unknown>;
20
20
  }
21
- export interface StorageAdapter {
21
+ export interface AiStorageAdapter {
22
22
  getItem(key: string): string | null | Promise<string | null>;
23
23
  setItem(key: string, value: string): void | Promise<void>;
24
24
  removeItem(key: string): void | Promise<void>;
@@ -26,11 +26,11 @@ export interface StorageAdapter {
26
26
  /**
27
27
  * localStorage 适配器(默认)
28
28
  */
29
- export declare const localStorageAdapter: StorageAdapter;
29
+ export declare const localStorageAdapter: AiStorageAdapter;
30
30
  /**
31
31
  * IndexedDB 适配器(适合大量数据持久化)
32
32
  */
33
- export declare class IndexedDBAdapter implements StorageAdapter {
33
+ export declare class AiIndexedDBAdapter implements AiStorageAdapter {
34
34
  private db;
35
35
  private dbName;
36
36
  private storeName;
@@ -45,6 +45,18 @@ export type ConversationGroup = {
45
45
  label: string;
46
46
  items: AiConversation[];
47
47
  };
48
+ export interface RemoteSyncAdapter {
49
+ /** 获取所有会话 */
50
+ fetchConversations(): Promise<AiConversation[]>;
51
+ /** 创建会话 */
52
+ createConversation(conversation: AiConversation): Promise<AiConversation>;
53
+ /** 更新会话 */
54
+ updateConversation(id: string, updates: Partial<AiConversation>): Promise<AiConversation>;
55
+ /** 删除会话 */
56
+ deleteConversation(id: string): Promise<void>;
57
+ /** 批量更新(用于同步) */
58
+ batchUpdate(conversations: AiConversation[]): Promise<AiConversation[]>;
59
+ }
48
60
  export interface UseAiConversationsOptions {
49
61
  /** 初始化数据(或从后端的直出) */
50
62
  initialConversations?: AiConversation[];
@@ -57,7 +69,7 @@ export interface UseAiConversationsOptions {
57
69
  * - 'indexedDB': 使用 IndexedDB
58
70
  * - StorageAdapter: 自定义适配器
59
71
  */
60
- storage?: false | 'localStorage' | 'indexedDB' | StorageAdapter;
72
+ storage?: false | 'localStorage' | 'indexedDB' | AiStorageAdapter;
61
73
  /** 持久化 key 前缀 */
62
74
  storageKey?: string;
63
75
  /**
@@ -65,6 +77,15 @@ export interface UseAiConversationsOptions {
65
77
  * @default 20
66
78
  */
67
79
  pageSize?: number;
80
+ /**
81
+ * 远程同步适配器(用于与后端同步)
82
+ * 设置后会自动进行本地与远程的数据同步
83
+ */
84
+ remoteSync?: RemoteSyncAdapter;
85
+ /** 是否自动同步(仅在设置了 remoteSync 时有效) */
86
+ autoSync?: boolean;
87
+ /** 同步间隔(毫秒),0 表示实时同步 */
88
+ syncInterval?: number;
68
89
  }
69
90
  /**
70
91
  * useAiConversations - 会话历史管理 Hook
@@ -145,4 +166,13 @@ export declare function useAiConversations(options?: UseAiConversationsOptions):
145
166
  pinConversation: (id: string, pinned?: boolean) => Promise<void>;
146
167
  /** 清空全部 */
147
168
  clear: () => Promise<void>;
169
+ /** 远程同步状态 */
170
+ isSyncing: import("vue").Ref<boolean, boolean>;
171
+ lastSyncTime: import("vue").Ref<number, number>;
172
+ syncError: import("vue").Ref<Error | null, Error | null>;
173
+ /** 远程同步方法 */
174
+ syncToRemote: () => Promise<void>;
175
+ fetchFromRemote: () => Promise<void>;
176
+ startSync: () => void;
177
+ stopSync: () => void;
148
178
  };
@@ -20,7 +20,7 @@ export const localStorageAdapter = {
20
20
  }
21
21
  }
22
22
  };
23
- export class IndexedDBAdapter {
23
+ export class AiIndexedDBAdapter {
24
24
  db = null;
25
25
  dbName;
26
26
  storeName = "ai_conversations";
@@ -90,13 +90,16 @@ export function useAiConversations(options = {}) {
90
90
  idGenerator = () => Math.random().toString(36).substring(2, 9),
91
91
  storage = "localStorage",
92
92
  storageKey = "yh-ui-ai-conversations",
93
- pageSize = 20
93
+ pageSize = 20,
94
+ remoteSync,
95
+ autoSync = false,
96
+ syncInterval = 3e4
94
97
  } = options;
95
98
  let adapter = null;
96
99
  if (storage === "localStorage") {
97
100
  adapter = localStorageAdapter;
98
101
  } else if (storage === "indexedDB") {
99
- adapter = new IndexedDBAdapter();
102
+ adapter = new AiIndexedDBAdapter();
100
103
  } else if (storage && typeof storage === "object") {
101
104
  adapter = storage;
102
105
  }
@@ -212,6 +215,61 @@ export function useAiConversations(options = {}) {
212
215
  await adapter.removeItem(storageKey);
213
216
  }
214
217
  };
218
+ const isSyncing = ref(false);
219
+ const lastSyncTime = ref(0);
220
+ const syncError = ref(null);
221
+ const syncToRemote = async () => {
222
+ if (!remoteSync || isSyncing.value) return;
223
+ isSyncing.value = true;
224
+ syncError.value = null;
225
+ try {
226
+ await remoteSync.batchUpdate(conversations.value);
227
+ lastSyncTime.value = Date.now();
228
+ } catch (err) {
229
+ syncError.value = err instanceof Error ? err : new Error(String(err));
230
+ } finally {
231
+ isSyncing.value = false;
232
+ }
233
+ };
234
+ const fetchFromRemote = async () => {
235
+ if (!remoteSync || isSyncing.value) return;
236
+ isSyncing.value = true;
237
+ syncError.value = null;
238
+ try {
239
+ const remoteConversations = await remoteSync.fetchConversations();
240
+ for (const rc of remoteConversations) {
241
+ if (!conversations.value.find((c) => c.id === rc.id)) {
242
+ conversations.value.push(rc);
243
+ }
244
+ }
245
+ conversations.value.sort((a, b) => {
246
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
247
+ return b.updatedAt - a.updatedAt;
248
+ });
249
+ lastSyncTime.value = Date.now();
250
+ await persist();
251
+ } catch (err) {
252
+ syncError.value = err instanceof Error ? err : new Error(String(err));
253
+ } finally {
254
+ isSyncing.value = false;
255
+ }
256
+ };
257
+ let syncTimer = null;
258
+ const startSync = () => {
259
+ if (!remoteSync || syncTimer) return;
260
+ if (syncInterval > 0) {
261
+ syncTimer = setInterval(syncToRemote, syncInterval);
262
+ }
263
+ };
264
+ const stopSync = () => {
265
+ if (syncTimer) {
266
+ clearInterval(syncTimer);
267
+ syncTimer = null;
268
+ }
269
+ };
270
+ if (remoteSync && autoSync) {
271
+ startSync();
272
+ }
215
273
  return {
216
274
  /** 完整会话列表 */
217
275
  conversations,
@@ -236,6 +294,15 @@ export function useAiConversations(options = {}) {
236
294
  /** 置顶/取消置顶 */
237
295
  pinConversation,
238
296
  /** 清空全部 */
239
- clear
297
+ clear,
298
+ /** 远程同步状态 */
299
+ isSyncing,
300
+ lastSyncTime,
301
+ syncError,
302
+ /** 远程同步方法 */
303
+ syncToRemote,
304
+ fetchFromRemote,
305
+ startSync,
306
+ stopSync
240
307
  };
241
308
  }