@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 +10 -0
- package/README.md +128 -49
- package/dist/{StreamingAudioPlayer-D8Q8WiEg.js → StreamingAudioPlayer-C8IqtjfL.js} +1 -1
- package/dist/{index-U8QcNdma.js → index-AtEuBzqP.js} +505 -131
- package/dist/index.js +1 -1
- package/dist/utils/log-sanitizer.d.ts +15 -0
- package/dist/utils/log-sink.d.ts +47 -0
- package/dist/utils/logger.d.ts +21 -22
- package/package.json +1 -1
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
|
-
##
|
|
21
|
+
## 🚀 Demo Repository
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
<div align="center">
|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### 📌 **Quick Start: Check Out Our Demo Repository**
|
|
26
26
|
|
|
27
|
-
|
|
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
|
-
- ✅
|
|
45
|
-
- ✅
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
- ✅ **WASM JS Glue
|
|
50
|
-
- ✅ **Cloudflare Pages
|
|
51
|
-
- ✅ **Vite
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
130
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
663
|
+
avatarView.controller.interrupt()
|
|
586
664
|
|
|
587
665
|
// Clear all data and resources
|
|
588
|
-
avatarView.
|
|
666
|
+
avatarView.controller.clear()
|
|
589
667
|
|
|
590
668
|
// Get current conversation ID (for Host mode)
|
|
591
|
-
const conversationId = avatarView.
|
|
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.
|
|
596
|
-
const currentVolume = avatarView.
|
|
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.
|
|
600
|
-
avatarView.
|
|
601
|
-
avatarView.
|
|
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.
|
|
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.
|
|
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.
|
|
858
|
+
await avatarView.controller.start()
|
|
781
859
|
|
|
782
860
|
// Use
|
|
783
|
-
avatarView.
|
|
861
|
+
avatarView.controller.send(audioData, false)
|
|
784
862
|
|
|
785
|
-
// Cleanup
|
|
786
|
-
avatarView.
|
|
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.
|
|
799
|
-
avatarView.
|
|
875
|
+
const conversationId = avatarView.controller.yieldAudioData(audioChunk, false)
|
|
876
|
+
avatarView.controller.yieldFramesData(keyframesDataArray, conversationId) // keyframesDataArray: (Uint8Array | ArrayBuffer)[]
|
|
800
877
|
|
|
801
|
-
// Cleanup
|
|
802
|
-
avatarView.
|
|
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
|
-
-
|
|
808
|
-
-
|
|
809
|
-
-
|
|
810
|
-
-
|
|
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-
|
|
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
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
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
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
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
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
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
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
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
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
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
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
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
|
-
|
|
7409
|
-
|
|
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
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
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
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
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
|
-
|
|
7674
|
-
platform: "Web",
|
|
7675
|
-
sdk_version: sdkVersion,
|
|
8069
|
+
...commonFields,
|
|
7676
8070
|
level,
|
|
7677
|
-
|
|
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
|
-
|
|
7709
|
-
service_module: "sdk",
|
|
7710
|
-
platform: "Web",
|
|
7711
|
-
sdk_version: sdkVersion,
|
|
8100
|
+
...commonFields,
|
|
7712
8101
|
level,
|
|
7713
|
-
|
|
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-
|
|
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.
|
|
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
|
@@ -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 {};
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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