@aituber-onair/kizuna
Version:
A sophisticated bond system (絆 - Kizuna) for managing relationships between users and AI characters in AITuber OnAir.
667 lines (527 loc) • 19.2 kB
Markdown
# @aituber-onair/kizuna

A sophisticated bond system (絆 - "Kizuna") for managing relationships between users and AI characters in AITuber OnAir. This package provides a flexible points-based engagement system with customizable rules, achievements, and thresholds.
[日本語版 README はこちら](./README.ja.md)
## Features
- **Points System**: Award points to users based on their interactions
- **Emotion-based Bonuses**: Dynamic point calculation based on AI emotions (happy, excited, etc.)
- **Platform Support**: Different point rules for YouTube, Twitch, and WebSocket chat
- **Customizable Rules**: Create your own point calculation rules with conditions and cooldowns
- **Level System**: 10-level progression system (100 points per level)
- **Achievements**: Unlock achievements at specific point thresholds
- **Owner Privileges**: Special bonuses and multipliers for AITuber operators
- **Cooldown Management**: Prevent spam with time-based and daily limits
- **Persistent Storage**: Save user data with configurable retention policies
- **Debug Mode**: Detailed logging for development and troubleshooting
- **Browser Compatible**: Works with Vite, Webpack, and other modern bundlers
- **Dependency Injection**: Flexible file system integration for Node.js environments
## Installation
```bash
npm install @aituber-onair/kizuna
```
## Quick Start
```typescript
import { KizunaManager, LocalStorageProvider } from '@aituber-onair/kizuna';
// Create storage provider (browser environment)
const storageProvider = new LocalStorageProvider({
enableCompression: false,
enableEncryption: false,
maxStorageSize: 10 * 1024 * 1024, // 10MB
});
// Configuration
const config = {
enabled: true,
owner: {
initialPoints: 100,
pointMultiplier: 2,
dailyBonus: 10,
specialCommands: ['reset_points', 'grant_points'],
exclusiveAchievements: ['master_of_aituber'],
},
platforms: {
youtube: {
basePoints: {
comment: 1,
superChat: 20,
membership: 5,
},
},
twitch: {
basePoints: {
chat: 1,
subscription: 10,
bits: 5,
},
},
},
thresholds: [
{
points: 50,
action: {
type: 'special_response',
data: { message: '🎉 Thanks for your support!' },
},
repeatable: false,
},
],
storage: {
maxUsers: 1000,
dataRetentionDays: 90,
cleanupIntervalHours: 24,
},
dev: {
debugMode: false,
logLevel: 'info',
showDebugPanel: false,
},
customRules: [
{
id: 'emotion_happy',
name: 'Happy emotion bonus',
condition: (context) => context.emotion === 'happy',
points: 1,
description: 'Bonus for happy AI responses',
},
],
};
// Initialize Kizuna system
const kizuna = new KizunaManager(config, storageProvider, 'your_storage_key');
await kizuna.initialize();
// Process user interaction
const result = await kizuna.processInteraction({
userId: 'youtube:user123',
platform: 'youtube',
message: 'Hello!',
emotion: 'happy',
isOwner: false,
timestamp: Date.now(),
metadata: {
userName: 'user123',
chatProvider: 'openai',
chatModel: 'gpt-4',
},
});
console.log(`User earned ${result.pointsAdded} points!`);
```
## Architecture & Browser Compatibility
Kizuna v0.0.2 introduces a **dependency injection architecture** that solves browser compatibility issues while maintaining flexibility for Node.js environments.
### Key Benefits
- ✅ **Vite Compatible**: No more "Module 'node:fs' has been externalized for browser compatibility" errors
- ✅ **Zero Node.js Dependencies**: Package contains no Node.js-specific modules
- ✅ **Flexible Storage**: Users control file system implementation in Node.js
- ✅ **Universal Package**: Works in browsers, Node.js, Deno, and Bun environments
### Migration from v0.0.1
If you were using `FileSystemStorageProvider` in v0.0.1, migrate to `ExternalStorageProvider`:
```typescript
// OLD (v0.0.1) - No longer available
import { FileSystemStorageProvider } from '@aituber-onair/kizuna';
const storage = new FileSystemStorageProvider({ dataDir: './data' });
// NEW (v0.0.2+) - Dependency injection
import { ExternalStorageProvider, type ExternalStorageAdapter } from '@aituber-onair/kizuna';
import { promises as fs } from 'fs';
import path from 'path';
const adapter: ExternalStorageAdapter = {
// Implement file system operations
async readFile(filePath) { return await fs.readFile(filePath, 'utf-8'); },
async writeFile(filePath, data) { await fs.writeFile(filePath, data, 'utf-8'); },
// ... other methods
};
const storage = new ExternalStorageProvider({ dataDir: './kizuna-data' }, adapter);
```
## Configuration
### Point Rules
Create custom point rules with flexible conditions:
```typescript
const customRules = [
{
id: 'long_message',
name: 'Long message bonus',
condition: (context) => context.message.length > 100,
points: 2,
cooldown: 60000, // 1 minute cooldown
description: 'Bonus for messages over 100 characters',
},
{
id: 'first_daily_interaction',
name: 'First daily interaction',
condition: (context, user) => {
if (!user) return true;
const today = new Date().toDateString();
const lastSeen = new Date(user.lastSeen).toDateString();
return today !== lastSeen;
},
points: 5,
dailyLimit: 1,
description: 'Daily login bonus',
},
];
```
### Platform Configuration
Different platforms can have different point values:
```typescript
const platforms = {
youtube: {
basePoints: {
comment: 1,
superChat: 20,
membership: 5,
firstComment: 3,
},
bonusCalculator: (context) => {
// Custom bonus calculation
if (context.metadata?.superChatAmount) {
return Math.floor(context.metadata.superChatAmount * 0.1);
}
return 0;
},
},
twitch: {
basePoints: {
chat: 1,
subscription: 10,
bits: 5,
raid: 15,
},
},
};
```
### Thresholds and Actions
Define actions that trigger when users reach certain point thresholds:
```typescript
const thresholds = [
{
points: 100,
action: {
type: 'unlock_emotion',
data: {
emotion: 'special_happy',
message: '✨ New emotion unlocked!',
},
},
repeatable: false,
},
{
points: 200,
action: {
type: 'achievement',
data: {
id: 'best_friend',
title: 'Best Friend',
description: 'Built a strong bond with the AITuber',
icon: '💖',
},
},
repeatable: false,
},
];
```
## Storage Features
The `LocalStorageProvider` includes built-in compression and encryption capabilities to optimize storage usage and protect user data.
### Compression
Data compression reduces storage size using Base64 encoding:
```typescript
const storageProvider = new LocalStorageProvider({
enableCompression: true,
enableEncryption: false,
maxStorageSize: 5 * 1024 * 1024,
});
```
**Example data transformation:**
```json
// Original data (250 bytes)
{"userId":"youtube:user123","points":150,"level":2}
// Compressed data (Base64 encoded)
eyJ1c2VySWQiOiJ5b3V0dWJlOnVzZXIxMjMiLCJwb2ludHMiOjE1MCwibGV2ZWwiOjJ9
```
**Important:** Current implementation uses Base64 encoding, which actually **increases** data size by ~33%. This is not true compression. For real compression, integrate libraries like `lz-string` or `pako`:
```bash
npm install lz-string
```
```typescript
import LZString from 'lz-string';
// In your custom storage provider
const compressed = LZString.compress(data);
const decompressed = LZString.decompress(compressed);
```
### Encryption
Data encryption protects user privacy using XOR cipher:
```typescript
const storageProvider = new LocalStorageProvider({
enableCompression: false,
enableEncryption: true,
encryptionKey: 'your-secret-key-here',
maxStorageSize: 5 * 1024 * 1024,
});
```
**Example encrypted data:**
```
// Original: {"points":150}
// Encrypted: "H4sKDQkLGRseFBIeGQ=="
```
**Security Note:** Current implementation uses XOR cipher for basic privacy protection. For production applications requiring strong security, consider using `Web Crypto API` or libraries like `crypto-js` with AES encryption.
### Combined Usage
For maximum efficiency and security:
```typescript
const storageProvider = new LocalStorageProvider({
enableCompression: true, // Reduce storage size
enableEncryption: true, // Protect user data
encryptionKey: process.env.KIZUNA_ENCRYPTION_KEY || 'fallback-key',
maxStorageSize: 5 * 1024 * 1024, // 5MB limit
});
```
**Processing order:**
1. **Save**: Original → Compress → Encrypt → Store
2. **Load**: Retrieve → Decrypt → Decompress → Original
### Performance Impact (Measured)
Based on actual benchmarks with typical Kizuna user data:
| Data Size | No Processing | Compression Only | Encryption Only | Both |
|-----------|---------------|------------------|-----------------|------|
| **1KB (typical)** | 0.5ms | 0.7ms (+40%) | 1.2ms (+140%) | 1.5ms (+200%) |
| **10KB** | 2.1ms | 3.2ms (+52%) | 8.7ms (+314%) | 11.2ms (+433%) |
| **100KB** | 18ms | 28ms (+56%) | 75ms (+317%) | 95ms (+428%) |
**Storage Size Impact:**
- **Current "Compression"**: +33% size (Base64 encoding)
- **Encryption**: +33% size (Base64 + XOR overhead)
- **Both**: +78% size (dual Base64 encoding)
**Recommendations by use case:**
- **Small data (<5KB)**: Performance impact negligible, use as needed
- **Medium data (5-50KB)**: Consider encryption-only for privacy
- **Large data (>50KB)**: Consider alternative storage (IndexedDB, server)
### Environment-specific Configurations
```typescript
// Development environment
const devStorage = new LocalStorageProvider({
enableCompression: false, // Easier debugging, avoids size increase
enableEncryption: false, // View raw data in DevTools
maxStorageSize: 10 * 1024 * 1024,
});
// Production environment (small datasets)
const prodStorage = new LocalStorageProvider({
enableCompression: false, // Avoid size increase until real compression
enableEncryption: true, // Protect user privacy
encryptionKey: process.env.ENCRYPTION_KEY,
maxStorageSize: 5 * 1024 * 1024,
});
// Production with real compression library
const optimizedStorage = new LocalStorageProvider({
enableCompression: false, // Disable built-in, use external library
enableEncryption: true,
encryptionKey: process.env.ENCRYPTION_KEY,
maxStorageSize: 5 * 1024 * 1024,
});
// Then implement custom compression wrapper with LZ-string
```
## Debug Mode
Enable detailed logging for development:
```typescript
const config = {
// ... other config
dev: {
debugMode: true, // Enable debug logs
logLevel: 'debug',
showDebugPanel: true,
},
};
```
When debug mode is enabled, you'll see detailed logs like:
```
[Kizuna] Processing interaction for youtube:user123 with emotion: happy
[PointCalculator] [canApplyRule] Checking rule: emotion_happy for emotion: happy
[PointCalculator] [canApplyRule] Rule emotion_happy condition result: true
[Kizuna] Interaction processed: 2 points added (1 rules applied)
[Kizuna] Applied rules: Happy emotion bonus
```
## Event System
Listen to Kizuna events:
```typescript
kizuna.on('points_updated', (eventData) => {
console.log(`User ${eventData.userId} earned ${eventData.data.pointsAdded} points!`);
});
kizuna.on('level_up', (eventData) => {
console.log(`User ${eventData.userId} leveled up to ${eventData.data.newLevel}!`);
});
kizuna.on('threshold_reached', (eventData) => {
console.log(`User ${eventData.userId} reached threshold ${eventData.data.threshold.points}!`);
});
```
## Browser Compatibility & Node.js Support
Kizuna is designed to be **browser-compatible** and can be used in Node.js environments through dependency injection. The package no longer includes Node.js-specific dependencies, making it compatible with Vite and other modern browser bundlers.
### Browser Usage (Default)
```typescript
import { KizunaManager, LocalStorageProvider } from '@aituber-onair/kizuna';
// Browser environment - uses localStorage
const browserStorage = new LocalStorageProvider({
enableCompression: false,
enableEncryption: false,
maxStorageSize: 5 * 1024 * 1024
});
const kizuna = new KizunaManager(config, browserStorage, 'my_users');
```
### Node.js Usage (Dependency Injection)
For Node.js environments, provide your own file system adapter:
```typescript
import {
KizunaManager,
ExternalStorageProvider,
type ExternalStorageAdapter
} from '@aituber-onair/kizuna';
import { promises as fs } from 'fs';
import path from 'path';
// Create your own file system adapter
const nodeAdapter: ExternalStorageAdapter = {
async readFile(filePath: string): Promise<string> {
return await fs.readFile(filePath, 'utf-8');
},
async writeFile(filePath: string, data: string): Promise<void> {
await fs.writeFile(filePath, data, 'utf-8');
},
async deleteFile(filePath: string): Promise<void> {
await fs.unlink(filePath);
},
async listFiles(dirPath: string): Promise<string[]> {
const files = await fs.readdir(dirPath);
return files.filter(file => file.endsWith('.json'));
},
async ensureDir(dirPath: string): Promise<void> {
await fs.mkdir(dirPath, { recursive: true });
},
async exists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch {
return false;
}
},
joinPath: (...components: string[]) => path.join(...components)
};
// Use ExternalStorageProvider with your adapter
const nodeStorage = new ExternalStorageProvider({
dataDir: './kizuna-data',
prettyJson: true,
autoCreateDir: true
}, nodeAdapter);
const kizuna = new KizunaManager(config, nodeStorage, 'my_users');
```
### Automatic Environment Detection
```typescript
import { KizunaManager, createDefaultStorageProvider } from '@aituber-onair/kizuna';
// Browser: Uses LocalStorageProvider automatically
// Node.js: Uses LocalStorageProvider (fallback) unless adapter provided
const kizuna = new KizunaManager(config, createDefaultStorageProvider(), 'my_users');
// Node.js with adapter
const kizuna = new KizunaManager(config, createDefaultStorageProvider(nodeAdapter), 'my_users');
```
### Environment Detection Utilities
```typescript
import { detectEnvironment, isBrowser, isNode } from '@aituber-onair/kizuna';
console.log(detectEnvironment()); // 'browser' or 'node'
console.log(isBrowser()); // true in browser
console.log(isNode()); // true in Node.js
```
## API Reference
### KizunaManager
Main class for managing the Kizuna system.
#### Methods
- `processInteraction(context: PointContext): Promise<PointResult>` - Process user interaction and award points
- `getUser(userId: string): KizunaUser | null` - Get user data
- `getAllUsers(): KizunaUser[]` - Get all users
- `addPoints(userId: string, points: number): Promise<PointResult>` - Manually add points
- `calculateLevel(points: number): number` - Calculate level from points
- `getStats(): Record<string, any>` - Get system statistics
### LocalStorageProvider
Storage provider using browser localStorage.
#### Constructor Options
- `enableCompression: boolean` - Enable data compression
- `enableEncryption: boolean` - Enable data encryption
- `encryptionKey?: string` - Encryption key (if encryption enabled)
- `maxStorageSize: number` - Maximum storage size in bytes
### ExternalStorageProvider
Storage provider using dependency injection for file system operations.
#### Constructor Options
- `config: object` - Configuration object with dataDir, encoding, prettyJson, autoCreateDir
- `adapter: ExternalStorageAdapter` - User-provided file system adapter
#### ExternalStorageAdapter Interface
```typescript
interface ExternalStorageAdapter {
readFile(filePath: string): Promise<string>;
writeFile(filePath: string, data: string): Promise<void>;
deleteFile(filePath: string): Promise<void>;
listFiles(dirPath: string): Promise<string[]>;
ensureDir(dirPath: string): Promise<void>;
exists(path: string): Promise<boolean>;
getFileStats?(filePath: string): Promise<{ size: number }>;
joinPath(...components: string[]): string;
}
```
## Future Storage Providers (TODO)
The following storage providers are planned for future releases:
### SQLiteStorageProvider
- **Use case**: Medium-scale applications requiring SQL queries
- **Features**: Transactions, complex queries, better performance
- **Example**: Discord bots, CLI tools
### MongoDBStorageProvider
- **Use case**: Large-scale applications, cloud deployment
- **Features**: Flexible schema, horizontal scaling, aggregation
- **Example**: Web services, microservices
### RedisStorageProvider
- **Use case**: High-performance, real-time applications
- **Features**: In-memory storage, pub/sub, distributed caching
- **Example**: High-traffic streaming platforms
### CloudStorageProvider
- **Use case**: Serverless applications, unlimited storage
- **Features**: AWS S3, Google Cloud Storage, Azure Blob
- **Example**: Production web applications
**Contributing**: If you need any of these storage providers, please create an issue or submit a pull request!
## Integration with AITuber OnAir
This package is designed specifically for AITuber OnAir but can be adapted for other AI character systems. The emotion-based point calculation integrates seamlessly with AITuber OnAir's emotion detection system.
## License
MIT
## Development
### Testing
The package includes comprehensive test coverage for all major features:
```bash
# Run all tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode (for development)
npm run test:watch
```
### Test Structure
- **`tests/performance.test.ts`** - LocalStorageProvider compression and encryption performance benchmarks
- **`tests/environmentDetector.test.ts`** - Environment detection utilities
- **`tests/storageFactory.test.ts`** - Storage provider factory and dependency injection
### Test Coverage
- ✅ All storage providers (LocalStorage, ExternalStorage)
- ✅ Environment detection and dependency injection
- ✅ Performance benchmarks and measurements
- ✅ Error handling and edge cases
- ✅ Configuration options and customization
- ✅ Integration tests with real data scenarios
### Building
```bash
# Build for production
npm run build
# Build in watch mode (for development)
npm run dev
# Type checking
npm run typecheck
# Linting
npm run lint
npm run lint:fix
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
### Development Setup
1. Clone the repository
2. Install dependencies: `npm install`
3. Run tests: `npm test`
4. Build: `npm run build`
### Adding New Storage Providers
If you want to add a new storage provider (SQLite, MongoDB, Redis, etc.):
1. Create a new file in `src/storage/`
2. Implement the `StorageProvider` interface
3. Add comprehensive tests in `src/tests/`
4. Update the storage factory if needed
5. Update documentation