@spatialwalk/avatarkit 1.0.0-beta.71 → 1.0.0-beta.73

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/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.0-beta.73] - 2026-01-22
9
+
10
+ ### ✨ New Features
11
+ - **Local Log Upload** - Automatic local log upload functionality
12
+
13
+ ## [1.0.0-beta.72] - 2026-01-17
14
+
15
+ ### 📚 Documentation
16
+ - **README Optimization** - Enhanced README with authentication section and improved API documentation
17
+
8
18
  ## [1.0.0-beta.71] - 2026-01-17
9
19
 
10
20
  ### 🔧 Improvements
package/README.md CHANGED
@@ -18,13 +18,34 @@ Real-time virtual avatar rendering SDK based on 3D Gaussian Splatting, supportin
18
18
  npm install @spatialwalk/avatarkit
19
19
  ```
20
20
 
21
- ## 🔧 Vite 配置(推荐)
21
+ ## 🚀 Demo Repository
22
22
 
23
- 如果你使用 Vite 作为构建工具,强烈推荐使用我们的 Vite 插件来自动处理 WASM 文件配置。插件会自动处理所有必要的配置,让你无需手动设置。
23
+ <div align="center">
24
24
 
25
- ### 使用插件
25
+ ### 📌 **Quick Start: Check Out Our Demo Repository**
26
26
 
27
- `vite.config.ts` 中添加插件:
27
+ We provide complete example code and best practices to help you quickly integrate the SDK.
28
+
29
+ **The demo repository includes:**
30
+ - ✅ Complete integration examples
31
+ - ✅ Usage examples for both SDK mode and Host mode
32
+ - ✅ Audio processing examples (PCM16, WAV, MP3, etc.)
33
+ - ✅ Vite configuration examples
34
+ - ✅ Best practices for common scenarios
35
+
36
+ **[👉 View Demo Repository](https://github.com/spatialwalk/avatarkit-demo)** | *If not yet created, please contact the team*
37
+
38
+ </div>
39
+
40
+ ---
41
+
42
+ ## 🔧 Vite Configuration (Recommended)
43
+
44
+ If you are using Vite as your build tool, we strongly recommend using our Vite plugin to automatically handle WASM file configuration. The plugin automatically handles all necessary configurations, so you don't need to set them up manually.
45
+
46
+ ### Using the Plugin
47
+
48
+ Add the plugin to `vite.config.ts`:
28
49
 
29
50
  ```typescript
30
51
  import { defineConfig } from 'vite'
@@ -32,27 +53,27 @@ import { avatarkitVitePlugin } from '@spatialwalk/avatarkit/vite'
32
53
 
33
54
  export default defineConfig({
34
55
  plugins: [
35
- avatarkitVitePlugin(), // 添加这一行即可
56
+ avatarkitVitePlugin(), // Just add this line
36
57
  ],
37
58
  })
38
59
  ```
39
60
 
40
- ### 插件功能
61
+ ### Plugin Features
41
62
 
42
- 插件会自动处理:
63
+ The plugin automatically handles:
43
64
 
44
- - ✅ **开发服务器**:自动设置 WASM 文件的正确 MIME 类型 (`application/wasm`)
45
- - ✅ **构建时**:自动复制 WASM 文件到 `dist/assets/` 目录
46
- - 智能检测:从 JS glue 文件中提取引用的 WASM 文件名(包括 hash
47
- - 自动匹配:确保复制的 WASM 文件与 JS glue 文件中的引用匹配
48
- - 支持 hash:正确处理带 hash WASM 文件(如 `avatar_core_wasm-{hash}.wasm`)
49
- - ✅ **WASM JS Glue**:自动复制 WASM JS glue 文件到 `dist/assets/` 目录
50
- - ✅ **Cloudflare Pages**:自动生成 `_headers` 文件,确保 WASM 文件使用正确的 MIME 类型
51
- - ✅ **Vite 配置**:自动配置 `optimizeDeps`、`assetsInclude`、`assetsInlineLimit` 等选项
65
+ - ✅ **Development Server**: Automatically sets the correct MIME type (`application/wasm`) for WASM files
66
+ - ✅ **Build Time**: Automatically copies WASM files to `dist/assets/` directory
67
+ - Smart Detection: Extracts referenced WASM file names (including hash) from JS glue files
68
+ - Auto Matching: Ensures copied WASM files match references in JS glue files
69
+ - Hash Support: Correctly handles hashed WASM files (e.g., `avatar_core_wasm-{hash}.wasm`)
70
+ - ✅ **WASM JS Glue**: Automatically copies WASM JS glue files to `dist/assets/` directory
71
+ - ✅ **Cloudflare Pages**: Automatically generates `_headers` file to ensure WASM files use the correct MIME type
72
+ - ✅ **Vite Configuration**: Automatically configures `optimizeDeps`, `assetsInclude`, `assetsInlineLimit`, and other options
52
73
 
53
- ### 手动配置(不使用插件)
74
+ ### Manual Configuration (Without Plugin)
54
75
 
55
- 如果你不使用 Vite 插件,需要手动配置以下内容:
76
+ If you don't use the Vite plugin, you need to manually configure the following:
56
77
 
57
78
  ```typescript
58
79
  // vite.config.ts
@@ -74,7 +95,7 @@ export default defineConfig({
74
95
  },
75
96
  },
76
97
  },
77
- // 开发服务器需要手动配置中间件设置 WASM MIME 类型
98
+ // Development server needs to manually configure middleware to set WASM MIME type
78
99
  configureServer(server) {
79
100
  server.middlewares.use((req, res, next) => {
80
101
  if (req.url?.endsWith('.wasm')) {
@@ -86,6 +107,53 @@ export default defineConfig({
86
107
  })
87
108
  ```
88
109
 
110
+ ## 🔐 Authentication
111
+
112
+ All environments require an **App ID** and **Session Token** for authentication.
113
+
114
+ ### App ID
115
+
116
+ The App ID is used to identify your application. You can obtain your App ID by:
117
+
118
+ 1. **For Testing**: Use the default test App ID provided in demo repositories (paired with test Session Token, only works with publicly available test avatars like Rohan, Dr.Kellan, Priya, Josh, etc.)
119
+ 2. **For Production**: Visit the [Developer Platform](https://dash.spatialreal.ai) to create your own App and avatars. You will receive your own App ID after creating an App.
120
+
121
+ ### Session Token
122
+
123
+ The Session Token is required for WebSocket authentication and must be obtained from your SDK provider.
124
+
125
+ **⚠️ Important Notes:**
126
+ - The Session Token must be valid and not expired
127
+ - In production applications, you **must** manually inject a valid Session Token obtained from your SDK provider
128
+ - The default Session Token provided in demo repositories is **only for demonstration purposes** and can only be used with test avatars
129
+ - If you want to create your own avatars and test them, please visit the [Developer Platform](https://dash.spatialreal.ai) to create your own App and generate Session Tokens
130
+
131
+ **How to Set Session Token:**
132
+
133
+ ```typescript
134
+ // Initialize SDK with App ID
135
+ await AvatarSDK.initialize('your-app-id', configuration)
136
+
137
+ // Set Session Token (can be called before or after initialization)
138
+ // If called before initialization, the token will be automatically set when you initialize the SDK
139
+ AvatarSDK.setSessionToken('your-session-token')
140
+
141
+ // Get current Session Token
142
+ const sessionToken = AvatarSDK.sessionToken
143
+ ```
144
+
145
+ **Token Management:**
146
+ - The Session Token can be set at any time using `AvatarSDK.setSessionToken(token)`
147
+ - If you set the token before initializing the SDK, it will be automatically applied during initialization
148
+ - If you set the token after initialization, it will be applied immediately
149
+ - Handle token refresh logic in your application as needed (e.g., when token expires)
150
+
151
+ **For Production Integration:**
152
+ - Obtain a valid Session Token from your SDK provider
153
+ - Store the token securely (never expose it in client-side code if possible)
154
+ - Implement token refresh logic to handle token expiration
155
+ - Use `AvatarSDK.setSessionToken(token)` to inject the token programmatically
156
+
89
157
  ## 🎯 Quick Start
90
158
 
91
159
  ### ⚠️ Important: Audio Context Initialization
@@ -126,8 +194,10 @@ const configuration: Configuration = {
126
194
 
127
195
  await AvatarSDK.initialize('your-app-id', configuration)
128
196
 
129
- // Set sessionToken (if needed, call separately)
130
- // AvatarSDK.setSessionToken('your-session-token')
197
+ // Set Session Token (required for authentication)
198
+ // You must obtain a valid Session Token from your SDK provider
199
+ // See Authentication section above for more details
200
+ AvatarSDK.setSessionToken('your-session-token')
131
201
 
132
202
  // 2. Load avatar
133
203
  const avatarManager = AvatarManager.shared
@@ -408,7 +478,9 @@ const appId = AvatarSDK.appId
408
478
  // Get configuration
409
479
  const config = AvatarSDK.configuration
410
480
 
411
- // Set sessionToken (if needed, call separately)
481
+ // Set Session Token (required for authentication)
482
+ // You must obtain a valid Session Token from your SDK provider
483
+ // See Authentication section for more details
412
484
  AvatarSDK.setSessionToken('your-session-token')
413
485
 
414
486
  // Set userId (optional, for telemetry)
@@ -437,7 +509,7 @@ const manager = AvatarManager.shared
437
509
 
438
510
  // Load avatar
439
511
  const avatar = await manager.load(
440
- characterId: string,
512
+ id: string,
441
513
  onProgress?: (progress: LoadProgressInfo) => void
442
514
  )
443
515
 
@@ -505,7 +577,7 @@ const newAvatar = await avatarManager.load('new-character-id')
505
577
  currentAvatarView = new AvatarView(newAvatar, container)
506
578
 
507
579
  // SDK mode: start connection (will throw error if not in SDK mode)
508
- await currentAvatarView.controller.start()
580
+ await currentAvatarView.controller.start()
509
581
  ```
510
582
 
511
583
  ### AvatarController
@@ -581,24 +653,30 @@ button.addEventListener('click', async () => {
581
653
 
582
654
  ```typescript
583
655
 
656
+ // Pause playback (from playing state)
657
+ avatarView.controller.pause()
658
+
659
+ // Resume playback (from paused state)
660
+ await avatarView.controller.resume()
661
+
584
662
  // Interrupt current playback (stops and clears data)
585
- avatarView.avatarController.interrupt()
663
+ avatarView.controller.interrupt()
586
664
 
587
665
  // Clear all data and resources
588
- avatarView.avatarController.clear()
666
+ avatarView.controller.clear()
589
667
 
590
668
  // Get current conversation ID (for Host mode)
591
- const conversationId = avatarView.avatarController.getCurrentConversationId()
669
+ const conversationId = avatarView.controller.getCurrentConversationId()
592
670
  // Returns: Current conversationId for the active audio session, or null if no active session
593
671
 
594
672
  // Volume control (affects only avatar audio player, not system volume)
595
- avatarView.avatarController.setVolume(0.5) // Set volume to 50% (0.0 to 1.0)
596
- const currentVolume = avatarView.avatarController.getVolume() // Get current volume (0.0 to 1.0)
673
+ avatarView.controller.setVolume(0.5) // Set volume to 50% (0.0 to 1.0)
674
+ const currentVolume = avatarView.controller.getVolume() // Get current volume (0.0 to 1.0)
597
675
 
598
676
  // Set event callbacks
599
- avatarView.avatarController.onConnectionState = (state: ConnectionState) => {} // SDK mode only
600
- avatarView.avatarController.onConversationState = (state: ConversationState) => {}
601
- avatarView.avatarController.onError = (error: Error) => {}
677
+ avatarView.controller.onConnectionState = (state: ConnectionState) => {} // SDK mode only
678
+ avatarView.controller.onConversationState = (state: ConversationState) => {}
679
+ avatarView.controller.onError = (error: Error) => {}
602
680
  ```
603
681
 
604
682
  #### Avatar Transform Methods
@@ -674,7 +752,7 @@ enum LogLevel {
674
752
  - Supported values: 8000, 16000, 22050, 24000, 32000, 44100, 48000
675
753
  - The configured sample rate will be used for both audio recording and playback
676
754
  - `characterApiBaseUrl`: Internal debug config, can be ignored
677
- - `sessionToken`: Set separately via `AvatarSDK.setSessionToken()`, not in Configuration
755
+ - `sessionToken`: **Required for authentication**. Set separately via `AvatarSDK.setSessionToken()`, not in Configuration. See [Authentication](#-authentication) section for details
678
756
 
679
757
  ```typescript
680
758
  enum Environment {
@@ -734,7 +812,7 @@ enum ConversationState {
734
812
  The SDK supports two rendering backends:
735
813
 
736
814
  - **WebGPU** - High-performance rendering for modern browsers
737
- - **WebGL** - Better compatibility traditional rendering
815
+ - **WebGL** - Better compatibility for traditional rendering
738
816
 
739
817
  The rendering system automatically selects the best backend, no manual configuration needed.
740
818
 
@@ -748,7 +826,7 @@ The SDK uses custom error types, providing more detailed error information:
748
826
  import { AvatarError } from '@spatialwalk/avatarkit'
749
827
 
750
828
  try {
751
- await avatarView.avatarController.start()
829
+ await avatarView.controller.start()
752
830
  } catch (error) {
753
831
  if (error instanceof AvatarError) {
754
832
  console.error('SDK Error:', error.message, error.code)
@@ -761,7 +839,7 @@ try {
761
839
  ### Error Callbacks
762
840
 
763
841
  ```typescript
764
- avatarView.avatarController.onError = (error: Error) => {
842
+ avatarView.controller.onError = (error: Error) => {
765
843
  console.error('AvatarController error:', error)
766
844
  // Handle error, such as reconnection, user notification, etc.
767
845
  }
@@ -777,14 +855,13 @@ avatarView.avatarController.onError = (error: Error) => {
777
855
  // Initialize
778
856
  const container = document.getElementById('avatar-container')
779
857
  const avatarView = new AvatarView(avatar, container)
780
- await avatarView.avatarController.start()
858
+ await avatarView.controller.start()
781
859
 
782
860
  // Use
783
- avatarView.avatarController.send(audioData, false)
861
+ avatarView.controller.send(audioData, false)
784
862
 
785
- // Cleanup
786
- avatarView.avatarController.close()
787
- avatarView.dispose() // Automatically cleans up all resources
863
+ // Cleanup - dispose() automatically cleans up all resources including WebSocket connections
864
+ avatarView.dispose()
788
865
  ```
789
866
 
790
867
  #### Host Mode Lifecycle
@@ -795,19 +872,21 @@ const container = document.getElementById('avatar-container')
795
872
  const avatarView = new AvatarView(avatar, container)
796
873
 
797
874
  // Use
798
- const conversationId = avatarView.avatarController.yieldAudioData(audioChunk, false)
799
- avatarView.avatarController.yieldFramesData(keyframesDataArray, conversationId) // keyframesDataArray: (Uint8Array | ArrayBuffer)[]
875
+ const conversationId = avatarView.controller.yieldAudioData(audioChunk, false)
876
+ avatarView.controller.yieldFramesData(keyframesDataArray, conversationId) // keyframesDataArray: (Uint8Array | ArrayBuffer)[]
800
877
 
801
- // Cleanup
802
- avatarView.avatarController.clear() // Clear all data and resources
803
- avatarView.dispose() // Automatically cleans up all resources
878
+ // Cleanup - dispose() automatically cleans up all resources including playback data
879
+ avatarView.dispose()
804
880
  ```
805
881
 
806
882
  **⚠️ Important Notes:**
807
- - When disposing AvatarView instances, must call `dispose()` to properly clean up resources
808
- - Not properly cleaning up may cause resource leaks and rendering errors
809
- - In SDK mode, call `close()` before `dispose()` to properly close WebSocket connections
810
- - In Host mode, call `clear()` before `dispose()` to clear all playback data
883
+ - `dispose()` automatically cleans up all resources, including:
884
+ - WebSocket connections (SDK mode)
885
+ - Playback data and animation resources (both modes)
886
+ - Render system and canvas elements
887
+ - All event listeners and callbacks
888
+ - Not properly calling `dispose()` may cause resource leaks and rendering errors
889
+ - If you need to manually close WebSocket connections or clear playback data before disposing, you can call `avatarView.controller.close()` (SDK mode) or `avatarView.controller.clear()` (both modes) first, but it's not required as `dispose()` handles this automatically
811
890
 
812
891
  ### Memory Optimization
813
892
 
@@ -1,7 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-U8QcNdma.js";
4
+ import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-AtEuBzqP.js";
5
5
  class StreamingAudioPlayer {
6
6
  // Mark if AudioContext is being resumed, avoid concurrent resume requests
7
7
  constructor(options) {
@@ -7350,74 +7350,249 @@ class AvatarError extends Error {
7350
7350
  this.name = "AvatarError";
7351
7351
  }
7352
7352
  }
7353
- {
7354
- setGlobalLogLevel(LogLevel$1.Warning);
7355
- }
7356
- const logger = useLogger("Web").withErrorProcessor((err) => {
7357
- captureErrorContext("error", err);
7358
- return err;
7359
- }).useGlobalConfig();
7360
- let originalLogger = null;
7361
- function setLogLevel(level) {
7362
- switch (level) {
7363
- case LogLevel.off:
7364
- if (!originalLogger) {
7365
- originalLogger = { ...logger };
7353
+ const _LogSink = class _LogSink {
7354
+ constructor() {
7355
+ __publicField(this, "db", null);
7356
+ __publicField(this, "dbName", "avatarkit_logs");
7357
+ __publicField(this, "storeName", "logs");
7358
+ __publicField(this, "version", 1);
7359
+ __publicField(this, "trimThreshold", 5e3);
7360
+ __publicField(this, "trimBufferSize", 1e3);
7361
+ __publicField(this, "isFlushing", false);
7362
+ }
7363
+ static getInstance() {
7364
+ if (!_LogSink.instance) {
7365
+ _LogSink.instance = new _LogSink();
7366
+ }
7367
+ return _LogSink.instance;
7368
+ }
7369
+ /**
7370
+ * Initialize IndexedDB
7371
+ */
7372
+ async initialize() {
7373
+ if (this.db) {
7374
+ return;
7375
+ }
7376
+ return new Promise((resolve2, reject) => {
7377
+ const request = indexedDB.open(this.dbName, this.version);
7378
+ request.onerror = () => {
7379
+ reject(new Error("Failed to open IndexedDB"));
7380
+ };
7381
+ request.onsuccess = () => {
7382
+ this.db = request.result;
7383
+ resolve2();
7384
+ };
7385
+ request.onupgradeneeded = (event) => {
7386
+ const db = event.target.result;
7387
+ if (!db.objectStoreNames.contains(this.storeName)) {
7388
+ const store = db.createObjectStore(this.storeName, { keyPath: "id", autoIncrement: true });
7389
+ store.createIndex("ts", "ts", { unique: false });
7390
+ }
7391
+ };
7392
+ });
7393
+ }
7394
+ /**
7395
+ * Append log entry to IndexedDB
7396
+ */
7397
+ async append(entry) {
7398
+ if (!this.db) {
7399
+ try {
7400
+ await this.initialize();
7401
+ } catch {
7402
+ return;
7366
7403
  }
7367
- Object.assign(logger, {
7368
- debug: () => {
7369
- },
7370
- verbose: () => {
7371
- },
7372
- log: () => {
7373
- },
7374
- warn: () => {
7375
- },
7376
- error: () => {
7377
- },
7378
- errorWithError: () => {
7404
+ }
7405
+ if (!this.db) {
7406
+ return;
7407
+ }
7408
+ try {
7409
+ const transaction = this.db.transaction([this.storeName], "readwrite");
7410
+ const store = transaction.objectStore(this.storeName);
7411
+ await new Promise((resolve2, reject) => {
7412
+ const request = store.add(entry);
7413
+ request.onsuccess = () => resolve2();
7414
+ request.onerror = () => reject(new Error("Failed to add log"));
7415
+ });
7416
+ this.getCount().then((count) => {
7417
+ if (count > this.trimThreshold) {
7418
+ this.trim().catch(() => {
7419
+ });
7379
7420
  }
7421
+ }).catch(() => {
7380
7422
  });
7381
- break;
7382
- case LogLevel.error:
7383
- if (originalLogger) {
7384
- Object.assign(logger, originalLogger);
7385
- originalLogger = null;
7423
+ } catch (error) {
7424
+ }
7425
+ }
7426
+ /**
7427
+ * Get total log count
7428
+ */
7429
+ async getCount() {
7430
+ if (!this.db) {
7431
+ return 0;
7432
+ }
7433
+ return new Promise((resolve2, reject) => {
7434
+ const transaction = this.db.transaction([this.storeName], "readonly");
7435
+ const store = transaction.objectStore(this.storeName);
7436
+ const request = store.count();
7437
+ request.onsuccess = () => {
7438
+ resolve2(request.result);
7439
+ };
7440
+ request.onerror = () => {
7441
+ reject(new Error("Failed to count logs"));
7442
+ };
7443
+ });
7444
+ }
7445
+ /**
7446
+ * Trim logs to keep only recent entries
7447
+ */
7448
+ async trim() {
7449
+ if (!this.db || this.isFlushing) {
7450
+ return;
7451
+ }
7452
+ try {
7453
+ const transaction = this.db.transaction([this.storeName], "readwrite");
7454
+ const store = transaction.objectStore(this.storeName);
7455
+ const getAllRequest = store.getAll();
7456
+ const entries = await new Promise((resolve2, reject) => {
7457
+ getAllRequest.onsuccess = () => {
7458
+ resolve2(getAllRequest.result);
7459
+ };
7460
+ getAllRequest.onerror = () => {
7461
+ reject(new Error("Failed to get logs"));
7462
+ };
7463
+ });
7464
+ const keepCount = this.trimThreshold - this.trimBufferSize;
7465
+ if (entries.length <= keepCount) {
7466
+ return;
7386
7467
  }
7387
- setGlobalLogLevel(LogLevel$1.Error);
7388
- break;
7389
- case LogLevel.warning:
7390
- if (originalLogger) {
7391
- Object.assign(logger, originalLogger);
7392
- originalLogger = null;
7468
+ entries.sort((a2, b2) => a2.ts - b2.ts);
7469
+ const toDelete = entries.slice(0, entries.length - keepCount);
7470
+ for (const entry of toDelete) {
7471
+ if (entry.id !== void 0) {
7472
+ await new Promise((resolve2, reject) => {
7473
+ const deleteRequest = store.delete(entry.id);
7474
+ deleteRequest.onsuccess = () => resolve2();
7475
+ deleteRequest.onerror = () => reject(new Error("Failed to delete log"));
7476
+ });
7477
+ }
7393
7478
  }
7394
- setGlobalLogLevel(LogLevel$1.Warning);
7395
- break;
7396
- case LogLevel.all:
7397
- if (originalLogger) {
7398
- Object.assign(logger, originalLogger);
7399
- originalLogger = null;
7479
+ } catch (error) {
7480
+ }
7481
+ }
7482
+ /**
7483
+ * Flush logs to server
7484
+ */
7485
+ async flush() {
7486
+ if (!this.db || this.isFlushing) {
7487
+ return;
7488
+ }
7489
+ this.isFlushing = true;
7490
+ try {
7491
+ const transaction = this.db.transaction([this.storeName], "readonly");
7492
+ const store = transaction.objectStore(this.storeName);
7493
+ const getAllRequest = store.getAll();
7494
+ const entries = await new Promise((resolve2, reject) => {
7495
+ getAllRequest.onsuccess = () => {
7496
+ resolve2(getAllRequest.result);
7497
+ };
7498
+ getAllRequest.onerror = () => {
7499
+ reject(new Error("Failed to get logs"));
7500
+ };
7501
+ });
7502
+ if (entries.length === 0) {
7503
+ this.isFlushing = false;
7504
+ return;
7400
7505
  }
7401
- setGlobalLogLevel(LogLevel$1.Debug);
7402
- break;
7403
- default:
7404
- if (originalLogger) {
7405
- Object.assign(logger, originalLogger);
7406
- originalLogger = null;
7506
+ const entriesWithPlatform = entries.map((entry) => ({
7507
+ ...entry,
7508
+ extra: {
7509
+ ...entry.extra,
7510
+ platform: "Web"
7511
+ }
7512
+ }));
7513
+ const requestBody = { logs: entriesWithPlatform };
7514
+ const jsonData = JSON.stringify(requestBody);
7515
+ const gzippedData = await this.gzip(jsonData);
7516
+ const response = await fetch("https://hogtool.spatialwalk.ai/batch-upload-log", {
7517
+ method: "POST",
7518
+ headers: {
7519
+ "Content-Type": "application/json",
7520
+ "Content-Encoding": "gzip"
7521
+ },
7522
+ body: gzippedData,
7523
+ signal: AbortSignal.timeout(15e3)
7524
+ // 15 second timeout
7525
+ });
7526
+ if (!response.ok) {
7527
+ throw new Error(`Failed to upload logs: ${response.status} ${response.statusText}`);
7407
7528
  }
7408
- setGlobalLogLevel(LogLevel$1.Warning);
7409
- break;
7529
+ await this.clearFirst(entries.length);
7530
+ } catch (error) {
7531
+ console.warn("[LogSink] Failed to flush logs:", error);
7532
+ throw error;
7533
+ } finally {
7534
+ this.isFlushing = false;
7535
+ }
7410
7536
  }
7411
- }
7412
- function captureErrorContext(level, error) {
7413
- const errorMessage = error instanceof Error ? error.message : String(error);
7414
- trackEvent("logger_error", level, {
7415
- error: errorMessage,
7416
- timestamp: Date.now(),
7417
- environment: "library",
7418
- user_agent: typeof navigator !== "undefined" ? navigator.userAgent : ""
7419
- });
7420
- }
7537
+ /**
7538
+ * Clear first N log entries
7539
+ */
7540
+ async clearFirst(count) {
7541
+ if (!this.db || count <= 0) {
7542
+ return;
7543
+ }
7544
+ try {
7545
+ const transaction = this.db.transaction([this.storeName], "readwrite");
7546
+ const store = transaction.objectStore(this.storeName);
7547
+ const getAllRequest = store.getAll();
7548
+ const entries = await new Promise((resolve2, reject) => {
7549
+ getAllRequest.onsuccess = () => {
7550
+ resolve2(getAllRequest.result);
7551
+ };
7552
+ getAllRequest.onerror = () => {
7553
+ reject(new Error("Failed to get logs"));
7554
+ };
7555
+ });
7556
+ entries.sort((a2, b2) => a2.ts - b2.ts);
7557
+ const toDelete = entries.slice(0, count);
7558
+ for (const entry of toDelete) {
7559
+ if (entry.id !== void 0) {
7560
+ await new Promise((resolve2, reject) => {
7561
+ const deleteRequest = store.delete(entry.id);
7562
+ deleteRequest.onsuccess = () => resolve2();
7563
+ deleteRequest.onerror = () => reject(new Error("Failed to delete log"));
7564
+ });
7565
+ }
7566
+ }
7567
+ } catch (error) {
7568
+ }
7569
+ }
7570
+ /**
7571
+ * Gzip string data
7572
+ */
7573
+ async gzip(data) {
7574
+ try {
7575
+ const encoder = new TextEncoder();
7576
+ const encoded = encoder.encode(data);
7577
+ const readableStream = new ReadableStream({
7578
+ start(controller) {
7579
+ controller.enqueue(encoded);
7580
+ controller.close();
7581
+ }
7582
+ });
7583
+ const compressedStream = readableStream.pipeThrough(new CompressionStream("gzip"));
7584
+ const response = new Response(compressedStream);
7585
+ const blob = await response.blob();
7586
+ return blob;
7587
+ } catch (error) {
7588
+ console.error("[LogSink] gzip() error:", error);
7589
+ throw error;
7590
+ }
7591
+ }
7592
+ };
7593
+ __publicField(_LogSink, "instance", null);
7594
+ let LogSink = _LogSink;
7595
+ const logSink = LogSink.getInstance();
7421
7596
  const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
7422
7597
  let random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
7423
7598
  let customRandom = (alphabet, defaultSize, getRandom) => {
@@ -7579,6 +7754,176 @@ class IdManager {
7579
7754
  }
7580
7755
  }
7581
7756
  const idManager = new IdManager();
7757
+ const ALGORITHM_TERMS = [
7758
+ "flame",
7759
+ "keyframe",
7760
+ "keyframes",
7761
+ "splat",
7762
+ "splats",
7763
+ "covariance",
7764
+ "gaussian",
7765
+ "cov6",
7766
+ "packeddata",
7767
+ "sortorder",
7768
+ "sortindex",
7769
+ "avatar_core",
7770
+ "emscripten",
7771
+ "wasm",
7772
+ "blendshape",
7773
+ "expressionparam",
7774
+ "rig"
7775
+ ];
7776
+ const ALGORITHM_REG = new RegExp(
7777
+ `\\b(${ALGORITHM_TERMS.join("|")})\\b`,
7778
+ "gi"
7779
+ );
7780
+ const CHINESE_REG = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/;
7781
+ const SENSITIVE_PATTERNS = [
7782
+ { reg: /\b(token|sessiontoken|accesstoken|apikey|apisecret|secret|password|auth)=([^\s,}\]'"]+)/gi, replacement: "$1=***" },
7783
+ { reg: /\b(conversationid|connectionid|clientid|sessionid)=([^\s,}\]'"]+)/gi, replacement: "$1=***" },
7784
+ { reg: /\b(appid|app_id)=([^\s,}\]'"]+)/gi, replacement: "$1=***" },
7785
+ { reg: /\b(userid|user_id)=([^\s,}\]'"]+)/gi, replacement: "$1=***" },
7786
+ { reg: /(https?:\/\/)[^\s,}\]'"]+/gi, replacement: "$1***" },
7787
+ { reg: /(wss?:\/\/)[^\s,}\]'"]+/gi, replacement: "$1***" }
7788
+ ];
7789
+ function hasChinese(msg) {
7790
+ return CHINESE_REG.test(msg);
7791
+ }
7792
+ function hasAlgorithmTerms(msg) {
7793
+ return ALGORITHM_REG.test(msg);
7794
+ }
7795
+ function redactSensitive(msg) {
7796
+ let out = msg;
7797
+ for (const { reg, replacement } of SENSITIVE_PATTERNS) {
7798
+ out = out.replace(reg, replacement);
7799
+ }
7800
+ return out;
7801
+ }
7802
+ function sanitizeForLocalLog(message) {
7803
+ if (hasChinese(message) || hasAlgorithmTerms(message)) {
7804
+ return { shouldPersist: false, sanitized: message };
7805
+ }
7806
+ return {
7807
+ shouldPersist: true,
7808
+ sanitized: redactSensitive(message)
7809
+ };
7810
+ }
7811
+ {
7812
+ setGlobalLogLevel(LogLevel$1.Warning);
7813
+ }
7814
+ const baseLogger = useLogger("Web").withErrorProcessor((err) => {
7815
+ captureErrorContext("error", err);
7816
+ return err;
7817
+ }).useGlobalConfig();
7818
+ logSink.initialize().catch(() => {
7819
+ });
7820
+ let currentLogLevel = LogLevel.off;
7821
+ async function writeToLocalLog(message, level, extra = {}) {
7822
+ try {
7823
+ const { shouldPersist, sanitized } = sanitizeForLocalLog(message);
7824
+ if (!shouldPersist) {
7825
+ return;
7826
+ }
7827
+ const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
7828
+ const version = AvatarSDK2.version;
7829
+ const appId = idManager.getAppId();
7830
+ const userId = idManager.getUserId();
7831
+ const logEntry = {
7832
+ level,
7833
+ ts: Date.now(),
7834
+ msg: sanitized,
7835
+ extra: {
7836
+ ...extra,
7837
+ _category: "Web",
7838
+ sdk_version: version || "",
7839
+ app_id: appId || "",
7840
+ user_id: userId || "",
7841
+ platform: "Web",
7842
+ framework: "",
7843
+ framework_version: ""
7844
+ }
7845
+ };
7846
+ await logSink.append(logEntry);
7847
+ } catch {
7848
+ }
7849
+ }
7850
+ function formatConsoleMessage(message) {
7851
+ return `[AvatarKit] ${message}`;
7852
+ }
7853
+ const logger = {
7854
+ debug: (message, ...args) => {
7855
+ writeToLocalLog(message, "info", {}).catch(() => {
7856
+ });
7857
+ if (currentLogLevel === LogLevel.all) {
7858
+ baseLogger.debug(formatConsoleMessage(message), ...args);
7859
+ }
7860
+ },
7861
+ verbose: (message, ...args) => {
7862
+ writeToLocalLog(message, "info", {}).catch(() => {
7863
+ });
7864
+ if (currentLogLevel === LogLevel.all) {
7865
+ baseLogger.verbose(formatConsoleMessage(message), ...args);
7866
+ }
7867
+ },
7868
+ log: (message, ...args) => {
7869
+ writeToLocalLog(message, "info", {}).catch(() => {
7870
+ });
7871
+ if (currentLogLevel === LogLevel.all) {
7872
+ baseLogger.log(formatConsoleMessage(message), ...args);
7873
+ }
7874
+ },
7875
+ warn: (message, ...args) => {
7876
+ writeToLocalLog(message, "warn", {}).catch(() => {
7877
+ });
7878
+ if (currentLogLevel === LogLevel.warning || currentLogLevel === LogLevel.all) {
7879
+ baseLogger.warn(formatConsoleMessage(message), ...args);
7880
+ }
7881
+ },
7882
+ error: (message, ..._args) => {
7883
+ writeToLocalLog(message, "error", {}).catch(() => {
7884
+ });
7885
+ if (currentLogLevel !== LogLevel.off) {
7886
+ baseLogger.error(formatConsoleMessage(message));
7887
+ }
7888
+ },
7889
+ errorWithError: (message, error) => {
7890
+ const errorMessage = error instanceof Error ? error.message : String(error);
7891
+ writeToLocalLog(`${message}: ${errorMessage}`, "error", {}).catch(() => {
7892
+ });
7893
+ if (currentLogLevel !== LogLevel.off) {
7894
+ baseLogger.errorWithError(formatConsoleMessage(message), error);
7895
+ }
7896
+ }
7897
+ };
7898
+ function setLogLevel(level) {
7899
+ currentLogLevel = level;
7900
+ switch (level) {
7901
+ case LogLevel.off:
7902
+ setGlobalLogLevel(LogLevel$1.Error);
7903
+ break;
7904
+ case LogLevel.error:
7905
+ setGlobalLogLevel(LogLevel$1.Error);
7906
+ break;
7907
+ case LogLevel.warning:
7908
+ setGlobalLogLevel(LogLevel$1.Warning);
7909
+ break;
7910
+ case LogLevel.all:
7911
+ setGlobalLogLevel(LogLevel$1.Debug);
7912
+ break;
7913
+ default:
7914
+ setGlobalLogLevel(LogLevel$1.Warning);
7915
+ break;
7916
+ }
7917
+ }
7918
+ function captureErrorContext(level, error) {
7919
+ const errorMessage = error instanceof Error ? error.message : String(error);
7920
+ trackEvent("logger_error", level, {
7921
+ error: errorMessage,
7922
+ timestamp: Date.now(),
7923
+ environment: "library",
7924
+ user_agent: typeof navigator !== "undefined" ? navigator.userAgent : ""
7925
+ });
7926
+ }
7582
7927
  let sdkVersion = "1.0.0";
7583
7928
  let currentEnvironment = null;
7584
7929
  let isInitialized = false;
@@ -7593,10 +7938,26 @@ function shouldFilterHostname() {
7593
7938
  const hostname = window.location.hostname;
7594
7939
  return FILTERED_HOSTNAMES.includes(hostname);
7595
7940
  }
7941
+ function getCommonFields() {
7942
+ const logContext = idManager.getLogContext();
7943
+ return {
7944
+ platform: "Web",
7945
+ sdk_version: sdkVersion,
7946
+ environment: currentEnvironment || "unknown",
7947
+ app_id: logContext.app_id || "",
7948
+ user_id: logContext.user_id || "",
7949
+ // 没有就传空字符串
7950
+ client_id: logContext.client_id,
7951
+ framework: "",
7952
+ // Web native 传空字符串
7953
+ framework_version: ""
7954
+ // Web native 传空字符串
7955
+ };
7956
+ }
7596
7957
  function initializePostHog(environment, version) {
7597
- if (shouldFilterHostname()) {
7598
- logger.log(`[PostHog] Tracking disabled due to filtered hostname: ${window.location.hostname}`);
7599
- return;
7958
+ const isHostnameFiltered = shouldFilterHostname();
7959
+ if (isHostnameFiltered) {
7960
+ logger.log(`[PostHog] Event tracking disabled due to filtered hostname: ${window.location.hostname}, but PostHog will still be initialized for feature flags`);
7600
7961
  }
7601
7962
  const { host, apiKey, disableCompression } = getPostHogConfig();
7602
7963
  if (isInitialized) {
@@ -7624,23 +7985,13 @@ function initializePostHog(environment, version) {
7624
7985
  logger.log(`[PostHog] Initialized successfully - environment: ${environment}, host: ${host}, instance: ${SDK_POSTHOG_INSTANCE_NAME}`);
7625
7986
  isInitialized = true;
7626
7987
  sdkPosthogInstance = posthogInstance;
7988
+ const userProperties = getCommonFields();
7627
7989
  if (logContext.user_id) {
7628
- posthogInstance.identify(logContext.user_id, {
7629
- app_id: logContext.app_id,
7630
- client_id: logContext.client_id,
7631
- sdk_version: sdkVersion,
7632
- platform: "Web",
7633
- environment
7634
- });
7990
+ posthogInstance.identify(logContext.user_id, userProperties);
7635
7991
  } else {
7636
- posthogInstance.register({
7637
- app_id: logContext.app_id,
7638
- client_id: logContext.client_id,
7639
- sdk_version: sdkVersion,
7640
- platform: "Web",
7641
- environment
7642
- });
7992
+ posthogInstance.register(userProperties);
7643
7993
  }
7994
+ posthogInstance.setPersonPropertiesForFlags(userProperties);
7644
7995
  flushEventQueue();
7645
7996
  }
7646
7997
  }, SDK_POSTHOG_INSTANCE_NAME);
@@ -7660,6 +8011,50 @@ function getSdkPosthogInstance() {
7660
8011
  return null;
7661
8012
  }
7662
8013
  }
8014
+ async function getLogsFeatureFlag() {
8015
+ let instance = getSdkPosthogInstance();
8016
+ if (!instance) {
8017
+ const maxWaitTime = 5e3;
8018
+ const checkInterval = 100;
8019
+ const startTime = Date.now();
8020
+ while (!instance && Date.now() - startTime < maxWaitTime) {
8021
+ await new Promise((resolve2) => setTimeout(resolve2, checkInterval));
8022
+ instance = getSdkPosthogInstance();
8023
+ }
8024
+ }
8025
+ if (!instance) {
8026
+ logger.log("[PostHog] PostHog not initialized after waiting, logs feature flag disabled (default: false)");
8027
+ return false;
8028
+ }
8029
+ try {
8030
+ const commonFields = getCommonFields();
8031
+ instance.setPersonPropertiesForFlags(commonFields);
8032
+ instance.reloadFeatureFlags();
8033
+ await new Promise((resolve2) => {
8034
+ instance.onFeatureFlags((_flags, _variants, { errorsLoading } = {}) => {
8035
+ if (errorsLoading) {
8036
+ logger.warn("[PostHog] Error loading feature flags");
8037
+ }
8038
+ resolve2();
8039
+ });
8040
+ });
8041
+ const flagValue = instance.isFeatureEnabled("logs_feature_flag");
8042
+ const isEnabled = flagValue === true;
8043
+ logger.log(`[PostHog] Logs feature flag: ${isEnabled ? "enabled" : "disabled"}`);
8044
+ return isEnabled;
8045
+ } catch (error) {
8046
+ logger.warn("[PostHog] Failed to get logs feature flag:", error instanceof Error ? error.message : String(error));
8047
+ return false;
8048
+ }
8049
+ }
8050
+ function updatePostHogPersonPropertiesForFlags() {
8051
+ const instance = getSdkPosthogInstance();
8052
+ if (!instance) {
8053
+ return;
8054
+ }
8055
+ const commonFields = getCommonFields();
8056
+ instance.setPersonPropertiesForFlags(commonFields);
8057
+ }
7663
8058
  function flushEventQueue() {
7664
8059
  if (!sdkPosthogInstance || eventQueue.length === 0) {
7665
8060
  return;
@@ -7669,15 +8064,11 @@ function flushEventQueue() {
7669
8064
  for (const { event, level, contents } of eventQueue) {
7670
8065
  try {
7671
8066
  const logContext = idManager.getLogContext();
8067
+ const commonFields = getCommonFields();
7672
8068
  const properties = {
7673
- service_module: "sdk",
7674
- platform: "Web",
7675
- sdk_version: sdkVersion,
8069
+ ...commonFields,
7676
8070
  level,
7677
- environment: currentEnvironment || "unknown",
7678
- app_id: logContext.app_id,
7679
- user_id: logContext.user_id,
7680
- client_id: logContext.client_id,
8071
+ service_module: "sdk",
7681
8072
  timestamp: Date.now(),
7682
8073
  ...contents
7683
8074
  };
@@ -7704,20 +8095,12 @@ function trackEvent(event, level = "info", contents = {}) {
7704
8095
  sdkPosthogInstance = instance;
7705
8096
  try {
7706
8097
  const logContext = idManager.getLogContext();
8098
+ const commonFields = getCommonFields();
7707
8099
  const properties = {
7708
- // Basic properties
7709
- service_module: "sdk",
7710
- platform: "Web",
7711
- sdk_version: sdkVersion,
8100
+ ...commonFields,
7712
8101
  level,
7713
- environment: currentEnvironment || "unknown",
7714
- // ID information
7715
- app_id: logContext.app_id,
7716
- user_id: logContext.user_id,
7717
- client_id: logContext.client_id,
7718
- // Timestamp
8102
+ service_module: "sdk",
7719
8103
  timestamp: Date.now(),
7720
- // Custom contents
7721
8104
  ...contents
7722
8105
  };
7723
8106
  if (logContext.connection_id) {
@@ -7860,7 +8243,7 @@ const _AnimationPlayer = class _AnimationPlayer {
7860
8243
  if (this.streamingPlayer) {
7861
8244
  return;
7862
8245
  }
7863
- const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-D8Q8WiEg.js");
8246
+ const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-C8IqtjfL.js");
7864
8247
  const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
7865
8248
  const audioFormat = AvatarSDK2.getAudioFormat();
7866
8249
  this.streamingPlayer = new StreamingAudioPlayer({
@@ -9331,6 +9714,10 @@ class AvatarSDK {
9331
9714
  });
9332
9715
  }
9333
9716
  logger.log(`[AvatarSDK] Successfully initialized`);
9717
+ setTimeout(() => {
9718
+ this.flushLogsIfNeeded().catch(() => {
9719
+ });
9720
+ }, 0);
9334
9721
  } catch (error) {
9335
9722
  const errorMessage = error instanceof Error ? error.message : String(error);
9336
9723
  logger.error("Failed to initialize AvatarSDK:", errorMessage);
@@ -9422,6 +9809,7 @@ class AvatarSDK {
9422
9809
  */
9423
9810
  static setUserId(userId) {
9424
9811
  idManager.setUserId(userId);
9812
+ updatePostHogPersonPropertiesForFlags();
9425
9813
  }
9426
9814
  // 只读属性
9427
9815
  static get isInitialized() {
@@ -9528,10 +9916,27 @@ class AvatarSDK {
9528
9916
  driveningressWsUrl
9529
9917
  };
9530
9918
  }
9919
+ /**
9920
+ * Flush logs if feature flag is enabled (private method)
9921
+ * First fetches latest feature flag from server, then decides whether to upload logs
9922
+ * @internal
9923
+ */
9924
+ static async flushLogsIfNeeded() {
9925
+ try {
9926
+ const isEnabled = await getLogsFeatureFlag();
9927
+ if (!isEnabled) {
9928
+ return;
9929
+ }
9930
+ await logSink.flush();
9931
+ } catch (error) {
9932
+ const errorMessage = error instanceof Error ? error.message : String(error);
9933
+ logger.warn("[AvatarSDK] Failed to flush logs:", errorMessage);
9934
+ }
9935
+ }
9531
9936
  }
9532
9937
  __publicField(AvatarSDK, "_isInitialized", false);
9533
9938
  __publicField(AvatarSDK, "_configuration", null);
9534
- __publicField(AvatarSDK, "_version", "1.0.0-beta.71");
9939
+ __publicField(AvatarSDK, "_version", "1.0.0-beta.73");
9535
9940
  __publicField(AvatarSDK, "_avatarCore", null);
9536
9941
  __publicField(AvatarSDK, "_dynamicSdkConfig", null);
9537
9942
  const AvatarSDK$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
@@ -11407,8 +11812,7 @@ class AvatarController {
11407
11812
  tap1Timestamp: 0,
11408
11813
  tap2Timestamp: 0,
11409
11814
  recvFirstFlameTimestamp: 0,
11410
- didRecvFirstFlame: false,
11411
- didReportLatency: false
11815
+ didRecvFirstFlame: false
11412
11816
  });
11413
11817
  __publicField(this, "audioBytesPerSecond", 16e3 * 2);
11414
11818
  this.avatar = avatar;
@@ -11820,10 +12224,6 @@ class AvatarController {
11820
12224
  if (this.playbackMode === DrivingServiceMode.host && !this.hostModeMetrics.didRecvFirstFlame) {
11821
12225
  this.hostModeMetrics.didRecvFirstFlame = true;
11822
12226
  this.hostModeMetrics.recvFirstFlameTimestamp = Date.now();
11823
- if (conversationId && !this.hostModeMetrics.didReportLatency) {
11824
- this.reportDrivingServiceLatency(conversationId);
11825
- this.hostModeMetrics.didReportLatency = true;
11826
- }
11827
12227
  }
11828
12228
  } else {
11829
12229
  this.currentKeyframes.push(...flameKeyframes);
@@ -11952,8 +12352,7 @@ class AvatarController {
11952
12352
  tap1Timestamp: 0,
11953
12353
  tap2Timestamp: 0,
11954
12354
  recvFirstFlameTimestamp: 0,
11955
- didRecvFirstFlame: false,
11956
- didReportLatency: false
12355
+ didRecvFirstFlame: false
11957
12356
  };
11958
12357
  }
11959
12358
  }
@@ -11976,8 +12375,7 @@ class AvatarController {
11976
12375
  tap1Timestamp: 0,
11977
12376
  tap2Timestamp: 0,
11978
12377
  recvFirstFlameTimestamp: 0,
11979
- didRecvFirstFlame: false,
11980
- didReportLatency: false
12378
+ didRecvFirstFlame: false
11981
12379
  };
11982
12380
  }
11983
12381
  }
@@ -12672,30 +13070,6 @@ class AvatarController {
12672
13070
  }
12673
13071
  return result2;
12674
13072
  }
12675
- /**
12676
- * Report driving_service_latency event (Host mode)
12677
- * Report once per session (when first frame is received)
12678
- * @internal
12679
- */
12680
- reportDrivingServiceLatency(conversationId) {
12681
- if (!conversationId || this.playbackMode !== DrivingServiceMode.host) {
12682
- return;
12683
- }
12684
- const metrics = this.hostModeMetrics;
12685
- logEvent("driving_service_latency", "info", {
12686
- req_id: conversationId,
12687
- dsm: "host",
12688
- // Host 模式
12689
- tap_0: metrics.startTimestamp || 0,
12690
- // 必须有非零值
12691
- tap_1: metrics.tap1Timestamp || 0,
12692
- // 缺省值为 0
12693
- tap_2: metrics.tap2Timestamp || 0,
12694
- // 缺省值为 0
12695
- tap_f: metrics.recvFirstFlameTimestamp || 0
12696
- // 必须有非零值
12697
- });
12698
- }
12699
13073
  }
12700
13074
  function errorToMessage(err) {
12701
13075
  if (err instanceof Error) {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b, c, m, f, d, j, g, C, i, D, E, k, h, L, R, n } from "./index-U8QcNdma.js";
1
+ import { b, c, m, f, d, j, g, C, i, D, E, k, h, L, R, n } from "./index-AtEuBzqP.js";
2
2
  export {
3
3
  b as Avatar,
4
4
  c as AvatarController,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Log sanitizer for local log persistence.
3
+ * Ensures: no sensitive content, no Chinese, no algorithm/internal terms (e.g. Flame, splat).
4
+ * @internal
5
+ */
6
+ export interface SanitizeResult {
7
+ shouldPersist: boolean;
8
+ sanitized: string;
9
+ }
10
+ /**
11
+ * Sanitize a log message for local persistence.
12
+ * - Do not persist if message contains Chinese or algorithm terms.
13
+ * - Otherwise persist, but redact sensitive content first.
14
+ */
15
+ export declare function sanitizeForLocalLog(message: string): SanitizeResult;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Log Sink - Local log storage and upload
3
+ * Stores logs in IndexedDB and provides upload functionality
4
+ * @internal
5
+ */
6
+ declare class LogSink {
7
+ private static instance;
8
+ private db;
9
+ private readonly dbName;
10
+ private readonly storeName;
11
+ private readonly version;
12
+ private readonly trimThreshold;
13
+ private readonly trimBufferSize;
14
+ private isFlushing;
15
+ private constructor();
16
+ static getInstance(): LogSink;
17
+ /**
18
+ * Initialize IndexedDB
19
+ */
20
+ initialize(): Promise<void>;
21
+ /**
22
+ * Append log entry to IndexedDB
23
+ */
24
+ append(entry: LogEntry): Promise<void>;
25
+ /**
26
+ * Get total log count
27
+ */
28
+ private getCount;
29
+ /**
30
+ * Trim logs to keep only recent entries
31
+ */
32
+ private trim;
33
+ /**
34
+ * Flush logs to server
35
+ */
36
+ flush(): Promise<void>;
37
+ /**
38
+ * Clear first N log entries
39
+ */
40
+ private clearFirst;
41
+ /**
42
+ * Gzip string data
43
+ */
44
+ private gzip;
45
+ }
46
+ export declare const logSink: LogSink;
47
+ export {};
@@ -1,26 +1,25 @@
1
- import { LogLevel as GuiiaiLogLevel } from '@guiiai/logg';
2
- export declare const logger: import('@guiiai/logg').Logg;
1
+ /**
2
+ * Logger Utility
3
+ * Environment-aware logger wrapper with local log persistence
4
+ * - Always writes to local log file (IndexedDB) regardless of logLevel
5
+ * - Console output is controlled by logLevel
6
+ * - Errors and warnings are always reported to PostHog (when enabled)
7
+ *
8
+ * Usage: Replace `console.log()` with `logger.log()`
9
+ */
10
+ export declare const logger: {
11
+ debug: (message: string, ...args: unknown[]) => void;
12
+ verbose: (message: string, ...args: unknown[]) => void;
13
+ log: (message: string, ...args: unknown[]) => void;
14
+ warn: (message: string, ...args: unknown[]) => void;
15
+ error: (message: string, ..._args: unknown[]) => void;
16
+ errorWithError: (message: string, error: Error | unknown) => void;
17
+ };
3
18
  export declare const loggerWithUnknown: {
4
19
  error: (message: string, error?: unknown) => void;
5
20
  warn: (message: string, error?: unknown) => void;
6
- useGlobalConfig: () => import('@guiiai/logg').Logg;
7
- child: (fields?: Record<string, any>) => import('@guiiai/logg').Logg;
8
- withContext: (context: string) => import('@guiiai/logg').Logg;
9
- withLogLevel: (logLevel: GuiiaiLogLevel) => import('@guiiai/logg').Logg;
10
- withLogLevelString: (logLevelString: import('@guiiai/logg').LogLevelString) => import('@guiiai/logg').Logg;
11
- withFormat: (format: import('@guiiai/logg').Format) => import('@guiiai/logg').Logg;
12
- withFields: (fields: Record<string, any>) => import('@guiiai/logg').Logg;
13
- withField: (key: string, value: any) => import('@guiiai/logg').Logg;
14
- withError: (err: Error | unknown) => import('@guiiai/logg').Logg;
15
- withCallStack: (errorLike: {
16
- message: string;
17
- stack?: string;
18
- }) => import('@guiiai/logg').Logg;
19
- debug: (message: any, ...optionalParams: [...any, string?]) => void;
20
- verbose: (message: any, ...optionalParams: [...any, string?]) => void;
21
- log: (message: any, ...optionalParams: any[]) => void;
22
- errorWithError: (message: any, err: Error | unknown, ...optionalParams: any[]) => void;
23
- withTimeFormat: (format: string) => import('@guiiai/logg').Logg;
24
- withTimeFormatter: (fn: (inputDate: Date) => string) => import('@guiiai/logg').Logg;
25
- withErrorProcessor: (fn: (err: Error | unknown) => Error | unknown) => import('@guiiai/logg').Logg;
21
+ debug: (message: string, ...args: unknown[]) => void;
22
+ verbose: (message: string, ...args: unknown[]) => void;
23
+ log: (message: string, ...args: unknown[]) => void;
24
+ errorWithError: (message: string, error: Error | unknown) => void;
26
25
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spatialwalk/avatarkit",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.71",
4
+ "version": "1.0.0-beta.73",
5
5
  "packageManager": "pnpm@10.18.2",
6
6
  "description": "AvatarKit SDK - 3D Gaussian Splatting Avatar Rendering SDK",
7
7
  "author": "AvatarKit Team",