Spaces:
Running
Running
// KIMI INDEXEDDB DATABASE SYSTEM | |
class KimiDatabase { | |
constructor() { | |
this.dbName = "KimiDB"; | |
this.db = new Dexie(this.dbName); | |
this.db | |
.version(3) | |
.stores({ | |
conversations: "++id,timestamp,favorability,character", | |
preferences: "key", | |
settings: "category", | |
personality: "[character+trait],character", | |
llmModels: "id", | |
memories: "++id,[character+category],character,timestamp,isActive" | |
}) | |
.upgrade(async tx => { | |
try { | |
const preferences = tx.table("preferences"); | |
const settings = tx.table("settings"); | |
const conversations = tx.table("conversations"); | |
const llmModels = tx.table("llmModels"); | |
await preferences.toCollection().modify(rec => { | |
if (Object.prototype.hasOwnProperty.call(rec, "encrypted")) { | |
delete rec.encrypted; | |
} | |
}); | |
const llmSetting = await settings.get("llm"); | |
if (!llmSetting) { | |
await settings.put({ | |
category: "llm", | |
settings: { | |
temperature: 0.9, | |
maxTokens: 400, | |
top_p: 0.9, | |
frequency_penalty: 0.9, | |
presence_penalty: 0.8 | |
}, | |
updated: new Date().toISOString() | |
}); | |
} | |
await conversations.toCollection().modify(rec => { | |
if (!rec.character) rec.character = "kimi"; | |
}); | |
const modelsCount = await llmModels.count(); | |
if (modelsCount === 0) { | |
await llmModels.put({ | |
id: "mistralai/mistral-small-3.2-24b-instruct", | |
name: "Mistral Small 3.2", | |
provider: "openrouter", | |
apiKey: "", | |
config: { temperature: 0.9, maxTokens: 400 }, | |
added: new Date().toISOString(), | |
lastUsed: null | |
}); | |
} | |
} catch (e) { | |
// Swallow upgrade errors to avoid blocking DB open; post-open migrations will attempt fixes | |
} | |
}); | |
// Version 4: extend memories metadata (importance, accessCount, lastAccess, createdAt) | |
this.db | |
.version(4) | |
.stores({ | |
conversations: "++id,timestamp,favorability,character", | |
preferences: "key", | |
settings: "category", | |
personality: "[character+trait],character", | |
llmModels: "id", | |
memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount" | |
}) | |
.upgrade(async tx => { | |
try { | |
const memories = tx.table("memories"); | |
const now = new Date().toISOString(); | |
await memories.toCollection().modify(rec => { | |
if (rec.importance == null) rec.importance = rec.type === "explicit_request" ? 0.9 : 0.5; | |
if (rec.accessCount == null) rec.accessCount = 0; | |
if (!rec.createdAt) rec.createdAt = rec.timestamp || now; | |
if (!rec.lastAccess) rec.lastAccess = rec.timestamp || now; | |
}); | |
} catch (e) { | |
// Silent; non-blocking | |
} | |
}); | |
} | |
async init() { | |
await this.db.open(); | |
await this.initializeDefaultsIfNeeded(); | |
await this.runPostOpenMigrations(); | |
return this.db; | |
} | |
getUnifiedTraitDefaults() { | |
if (window.KimiEmotionSystem) { | |
const emotionSystem = new window.KimiEmotionSystem(this); | |
return emotionSystem.TRAIT_DEFAULTS; | |
} | |
return { | |
affection: 65, | |
playfulness: 55, | |
intelligence: 70, | |
empathy: 75, | |
humor: 60, | |
romance: 50 | |
}; | |
} | |
getDefaultPreferences() { | |
return [ | |
{ key: "selectedLanguage", value: "en" }, | |
{ key: "selectedVoice", value: "auto" }, | |
{ key: "voiceRate", value: 1.1 }, | |
{ key: "voicePitch", value: 1.1 }, | |
{ key: "voiceVolume", value: 0.8 }, | |
{ key: "selectedCharacter", value: "kimi" }, | |
{ key: "colorTheme", value: "purple" }, | |
{ key: "interfaceOpacity", value: 0.8 }, | |
{ key: "animationsEnabled", value: true }, | |
{ key: "showTranscript", value: true }, | |
{ key: "llmProvider", value: "openrouter" }, | |
{ key: "llmBaseUrl", value: "https://openrouter.ai/api/v1/chat/completions" }, | |
{ key: "llmModelId", value: "mistralai/mistral-small-3.2-24b-instruct" }, | |
{ key: "providerApiKey", value: "" } | |
]; | |
} | |
getDefaultSettings() { | |
return [ | |
{ | |
category: "llm", | |
settings: { | |
temperature: 0.9, | |
maxTokens: 400, | |
top_p: 0.9, | |
frequency_penalty: 0.9, | |
presence_penalty: 0.8 | |
} | |
} | |
]; | |
} | |
getCharacterTraitDefaults() { | |
if (!window.KIMI_CHARACTERS) return {}; | |
const characterDefaults = {}; | |
Object.keys(window.KIMI_CHARACTERS).forEach(characterKey => { | |
const character = window.KIMI_CHARACTERS[characterKey]; | |
if (character && character.traits) { | |
characterDefaults[characterKey] = character.traits; | |
} | |
}); | |
return characterDefaults; | |
} | |
getDefaultLLMModels() { | |
return [ | |
{ | |
id: "mistralai/mistral-small-3.2-24b-instruct", | |
name: "Mistral Small 3.2", | |
provider: "openrouter", | |
apiKey: "", | |
config: { temperature: 0.9, maxTokens: 400 }, | |
added: new Date().toISOString(), | |
lastUsed: null | |
} | |
]; | |
} | |
async initializeDefaultsIfNeeded() { | |
const defaults = this.getUnifiedTraitDefaults(); | |
const defaultPreferences = this.getDefaultPreferences(); | |
const defaultSettings = this.getDefaultSettings(); | |
const personalityDefaults = this.getCharacterTraitDefaults(); | |
const defaultLLMModels = this.getDefaultLLMModels(); | |
const prefCount = await this.db.preferences.count(); | |
if (prefCount === 0) { | |
for (const pref of defaultPreferences) { | |
await this.db.preferences.put({ ...pref, updated: new Date().toISOString() }); | |
} | |
const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} }); | |
for (const character of characters) { | |
const prompt = window.KIMI_CHARACTERS[character]?.defaultPrompt || ""; | |
await this.db.preferences.put({ | |
key: `systemPrompt_${character}`, | |
value: prompt, | |
updated: new Date().toISOString() | |
}); | |
} | |
} | |
const setCount = await this.db.settings.count(); | |
if (setCount === 0) { | |
for (const setting of defaultSettings) { | |
await this.db.settings.put({ ...setting, updated: new Date().toISOString() }); | |
} | |
} | |
const persCount = await this.db.personality.count(); | |
if (persCount === 0) { | |
const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} }); | |
for (const character of characters) { | |
// Use real character-specific traits, not generic defaults | |
const characterTraits = personalityDefaults[character] || {}; | |
const traitsToInitialize = [ | |
{ trait: "affection", value: characterTraits.affection || defaults.affection }, | |
{ trait: "playfulness", value: characterTraits.playfulness || defaults.playfulness }, | |
{ trait: "intelligence", value: characterTraits.intelligence || defaults.intelligence }, | |
{ trait: "empathy", value: characterTraits.empathy || defaults.empathy }, | |
{ trait: "humor", value: characterTraits.humor || defaults.humor }, | |
{ trait: "romance", value: characterTraits.romance || defaults.romance } | |
]; | |
for (const trait of traitsToInitialize) { | |
await this.db.personality.put({ ...trait, character, updated: new Date().toISOString() }); | |
} | |
} | |
} | |
const llmCount = await this.db.llmModels.count(); | |
if (llmCount === 0) { | |
for (const model of defaultLLMModels) { | |
await this.db.llmModels.put(model); | |
} | |
} | |
// Fix: never recreate default conversations | |
const convCount = await this.db.conversations.count(); | |
if (convCount === 0) { | |
} | |
} | |
async runPostOpenMigrations() { | |
try { | |
const defaultPreferences = this.getDefaultPreferences(); | |
for (const pref of defaultPreferences) { | |
const existing = await this.db.preferences.get(pref.key); | |
if (!existing) { | |
await this.db.preferences.put({ | |
key: pref.key, | |
value: pref.value, | |
updated: new Date().toISOString() | |
}); | |
} | |
} | |
const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} }); | |
for (const character of characters) { | |
const promptKey = `systemPrompt_${character}`; | |
const hasPrompt = await this.db.preferences.get(promptKey); | |
if (!hasPrompt) { | |
const prompt = window.KIMI_CHARACTERS[character]?.defaultPrompt || ""; | |
await this.db.preferences.put({ key: promptKey, value: prompt, updated: new Date().toISOString() }); | |
} | |
} | |
const defaultSettings = this.getDefaultSettings(); | |
for (const setting of defaultSettings) { | |
const existing = await this.db.settings.get(setting.category); | |
if (!existing) { | |
await this.db.settings.put({ ...setting, updated: new Date().toISOString() }); | |
} else { | |
const merged = { ...setting.settings, ...existing.settings }; | |
await this.db.settings.put({ | |
category: setting.category, | |
settings: merged, | |
updated: new Date().toISOString() | |
}); | |
} | |
} | |
const defaults = this.getUnifiedTraitDefaults(); | |
const personalityDefaults = this.getCharacterTraitDefaults(); | |
for (const character of Object.keys(window.KIMI_CHARACTERS || { kimi: {} })) { | |
const characterTraits = personalityDefaults[character] || {}; | |
const traits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"]; | |
for (const trait of traits) { | |
const key = [character, trait]; | |
const found = await this.db.personality.get(key); | |
if (!found) { | |
const value = Number(characterTraits[trait] ?? defaults[trait] ?? 50); | |
const v = isFinite(value) ? Math.max(0, Math.min(100, value)) : 50; | |
await this.db.personality.put({ trait, character, value: v, updated: new Date().toISOString() }); | |
} | |
} | |
} | |
const llmCount = await this.db.llmModels.count(); | |
if (llmCount === 0) { | |
for (const model of this.getDefaultLLMModels()) { | |
await this.db.llmModels.put(model); | |
} | |
} | |
const allConvs = await this.db.conversations.toArray(); | |
const toPatch = allConvs.filter(c => !c.character); | |
if (toPatch.length) { | |
for (const c of toPatch) { | |
c.character = "kimi"; | |
await this.db.conversations.put(c); | |
} | |
} | |
const allPrefs = await this.db.preferences.toArray(); | |
const legacy = allPrefs.filter(p => Object.prototype.hasOwnProperty.call(p, "encrypted")); | |
if (legacy.length) { | |
for (const p of legacy) { | |
const { key, value } = p; | |
await this.db.preferences.put({ key, value, updated: new Date().toISOString() }); | |
} | |
} | |
} catch {} | |
} | |
async saveConversation(userText, kimiResponse, favorability, timestamp = new Date(), character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
const conversation = { | |
user: userText, | |
kimi: kimiResponse, | |
favorability: favorability, | |
timestamp: timestamp.toISOString(), | |
date: timestamp.toDateString(), | |
character: character | |
}; | |
return this.db.conversations.add(conversation); | |
} | |
async getRecentConversations(limit = 10, character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
// Dexie limitation: orderBy() cannot follow a where() chain. | |
// Use compound index path by querying all then sorting, or use a custom index strategy. | |
// Here we query filtered by character, then sort in JS and take the last N. | |
return this.db.conversations | |
.where("character") | |
.equals(character) | |
.toArray() | |
.then(arr => { | |
arr.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); | |
return arr.slice(-limit); | |
}); | |
} | |
async getAllConversations(character = null) { | |
try { | |
if (!character) character = await this.getSelectedCharacter(); | |
return await this.db.conversations.where("character").equals(character).toArray(); | |
} catch (error) { | |
console.warn("Error getting all conversations:", error); | |
return []; | |
} | |
} | |
async setPreference(key, value) { | |
if (key === "providerApiKey") { | |
const isValid = window.KIMI_VALIDATORS?.validateApiKey(value) || window.KimiSecurityUtils?.validateApiKey(value); | |
if (!isValid && value.length > 0) { | |
throw new Error("Invalid API key format"); | |
} | |
// Store keys in plain text (no encryption) per request | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") { | |
window.KimiCacheManager.set(`pref_${key}`, value, 60000); | |
} | |
return this.db.preferences.put({ | |
key: key, | |
value: value, | |
// do not set encrypted flag anymore | |
updated: new Date().toISOString() | |
}); | |
} | |
// Centralized numeric validation using KIMI_CONFIG ranges (only if key matches known numeric preference) | |
const numericMap = { | |
voiceRate: "VOICE_RATE", | |
voicePitch: "VOICE_PITCH", | |
voiceVolume: "VOICE_VOLUME", | |
interfaceOpacity: "INTERFACE_OPACITY", | |
llmTemperature: "LLM_TEMPERATURE", | |
llmMaxTokens: "LLM_MAX_TOKENS", | |
llmTopP: "LLM_TOP_P", | |
llmFrequencyPenalty: "LLM_FREQUENCY_PENALTY", | |
llmPresencePenalty: "LLM_PRESENCE_PENALTY" | |
}; | |
if (numericMap[key] && window.KIMI_CONFIG && typeof window.KIMI_CONFIG.validate === "function") { | |
const validation = window.KIMI_CONFIG.validate(value, numericMap[key]); | |
if (validation.valid) { | |
value = validation.value; | |
} | |
} | |
// Update cache for regular preferences | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") { | |
window.KimiCacheManager.set(`pref_${key}`, value, 60000); | |
} | |
const result = await this.db.preferences.put({ | |
key: key, | |
value: value, | |
updated: new Date().toISOString() | |
}); | |
if (window.dispatchEvent) { | |
try { | |
window.dispatchEvent(new CustomEvent("preferenceUpdated", { detail: { key, value } })); | |
} catch {} | |
} | |
return result; | |
} | |
async getPreference(key, defaultValue = null) { | |
// Try cache first (use a singleton cache instance) | |
const cacheKey = `pref_${key}`; | |
const cache = | |
window.KimiCacheManager && typeof window.KimiCacheManager.get === "function" ? window.KimiCacheManager : null; | |
if (cache && typeof cache.get === "function") { | |
const cached = cache.get(cacheKey); | |
if (cached !== null) { | |
return cached; | |
} | |
} | |
try { | |
const record = await this.db.preferences.get(key); | |
if (!record) { | |
const cache = | |
window.KimiCacheManager && typeof window.KimiCacheManager.set === "function" ? window.KimiCacheManager : null; | |
if (cache && typeof cache.set === "function") { | |
cache.set(cacheKey, defaultValue, 60000); // Cache for 1 minute | |
} | |
return defaultValue; | |
} | |
// Backward compatibility: decrypt legacy encrypted values | |
let value = record.value; | |
if (record.encrypted && window.KimiSecurityUtils) { | |
try { | |
value = record.value; // decrypt removed – stored as plain text | |
// One-time migration: store back as plain text without encrypted flag | |
try { | |
await this.db.preferences.put({ key: key, value, updated: new Date().toISOString() }); | |
} catch (mErr) {} | |
} catch (e) { | |
// If decryption fails, fallback to raw value | |
console.warn("Failed to decrypt legacy API key; returning raw value", e); | |
} | |
} | |
// Cache the result | |
const cache = | |
window.KimiCacheManager && typeof window.KimiCacheManager.set === "function" ? window.KimiCacheManager : null; | |
if (cache && typeof cache.set === "function") { | |
cache.set(cacheKey, value, 60000); // Cache for 1 minute | |
} | |
return value; | |
} catch (error) { | |
console.warn(`Error getting preference ${key}:`, error); | |
return defaultValue; | |
} | |
} | |
async getAllPreferences() { | |
try { | |
const all = await this.db.preferences.toArray(); | |
const prefs = {}; | |
all.forEach(item => { | |
prefs[item.key] = item.value; | |
}); | |
return prefs; | |
} catch (error) { | |
console.warn("Error getting all preferences:", error); | |
return {}; | |
} | |
} | |
async setSetting(category, settings) { | |
return this.db.settings.put({ | |
category: category, | |
settings: settings, | |
updated: new Date().toISOString() | |
}); | |
} | |
async getSetting(category, defaultSettings = {}) { | |
const result = await this.db.settings.get(category); | |
return result ? result.settings : defaultSettings; | |
} | |
async setPersonalityTrait(trait, value, character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
// Invalidate cache | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") { | |
window.KimiCacheManager.delete(`trait_${character}_${trait}`); | |
window.KimiCacheManager.delete(`all_traits_${character}`); | |
} | |
return this.db.personality.put({ | |
trait: trait, | |
character: character, | |
value: value, | |
updated: new Date().toISOString() | |
}); | |
} | |
async getPersonalityTrait(trait, defaultValue = null, character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
// Use unified defaults from emotion system | |
if (defaultValue === null) { | |
if (window.KimiEmotionSystem) { | |
const emotionSystem = new window.KimiEmotionSystem(this); | |
defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50; | |
} else { | |
// Fallback defaults (must match KimiEmotionSystem.TRAIT_DEFAULTS exactly) | |
if (window.getTraitDefaults) { | |
defaultValue = window.getTraitDefaults()[trait] || 50; | |
} else { | |
defaultValue = | |
{ | |
affection: 65, | |
playfulness: 55, | |
intelligence: 70, | |
empathy: 75, | |
humor: 60, | |
romance: 50 | |
}[trait] || 50; | |
} | |
} | |
} | |
// Try cache first | |
const cacheKey = `trait_${character}_${trait}`; | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") { | |
const cached = window.KimiCacheManager.get(cacheKey); | |
if (cached !== null) { | |
return cached; | |
} | |
} | |
const found = await this.db.personality.get([character, trait]); | |
const value = found ? found.value : defaultValue; | |
// Cache the result | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") { | |
window.KimiCacheManager.set(cacheKey, value, 120000); // Cache for 2 minutes | |
} | |
return value; | |
} | |
async getAllPersonalityTraits(character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
// Try cache first | |
const cacheKey = `all_traits_${character}`; | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") { | |
const cached = window.KimiCacheManager.get(cacheKey); | |
if (cached !== null) { | |
// Correction : valider les valeurs du cache | |
const safeTraits = {}; | |
for (const [trait, value] of Object.entries(cached)) { | |
let v = Number(value); | |
if (!isFinite(v) || isNaN(v)) v = 50; | |
v = Math.max(0, Math.min(100, v)); | |
safeTraits[trait] = v; | |
} | |
return safeTraits; | |
} | |
} | |
const all = await this.db.personality.where("character").equals(character).toArray(); | |
const traits = {}; | |
all.forEach(item => { | |
let v = Number(item.value); | |
if (!isFinite(v) || isNaN(v)) v = 50; | |
v = Math.max(0, Math.min(100, v)); | |
traits[item.trait] = v; | |
}); | |
// If no traits stored yet for this character, seed from character defaults (one-time) | |
if (Object.keys(traits).length === 0 && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character]) { | |
const seed = window.KIMI_CHARACTERS[character].traits || {}; | |
const safeSeed = {}; | |
for (const [k, v] of Object.entries(seed)) { | |
const num = typeof v === "number" && isFinite(v) ? Math.max(0, Math.min(100, v)) : 50; | |
safeSeed[k] = num; | |
try { | |
await this.setPersonalityTrait(k, num, character); | |
} catch {} | |
} | |
return safeSeed; | |
} | |
// Cache the result | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") { | |
window.KimiCacheManager.set(cacheKey, traits, 120000); // Cache for 2 minutes | |
} | |
return traits; | |
} | |
async savePersonality(personalityObj, character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
// Invalidate caches for all affected traits and the aggregate cache for this character | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") { | |
try { | |
Object.keys(personalityObj).forEach(trait => { | |
window.KimiCacheManager.delete(`trait_${character}_${trait}`); | |
}); | |
window.KimiCacheManager.delete(`all_traits_${character}`); | |
} catch (e) {} | |
} | |
const entries = Object.entries(personalityObj).map(([trait, value]) => | |
this.db.personality.put({ | |
trait: trait, | |
character: character, | |
value: value, | |
updated: new Date().toISOString() | |
}) | |
); | |
return Promise.all(entries); | |
} | |
async getPersonality(character = null) { | |
return this.getAllPersonalityTraits(character); | |
} | |
async saveLLMModel(id, name, provider, apiKey, config) { | |
return this.db.llmModels.put({ | |
id: id, | |
name: name, | |
provider: provider, | |
apiKey: apiKey, | |
config: config, | |
added: new Date().toISOString(), | |
lastUsed: null | |
}); | |
} | |
async getLLMModel(id) { | |
return this.db.llmModels.get(id); | |
} | |
async getAllLLMModels() { | |
try { | |
return await this.db.llmModels.toArray(); | |
} catch (error) { | |
console.warn("Error getting all LLM models:", error); | |
return []; | |
} | |
} | |
async deleteLLMModel(id) { | |
return this.db.llmModels.delete(id); | |
} | |
async cleanOldConversations(days = null, character = null) { | |
// If days not provided, fallback to full clean (legacy behavior) | |
if (days === null) { | |
if (character) { | |
const all = await this.db.conversations.where("character").equals(character).toArray(); | |
const ids = all.map(item => item.id); | |
return this.db.conversations.bulkDelete(ids); | |
} else { | |
return this.db.conversations.clear(); | |
} | |
} | |
const threshold = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); | |
if (character) { | |
const toDelete = await this.db.conversations | |
.where("character") | |
.equals(character) | |
.and(c => c.timestamp < threshold) | |
.toArray(); | |
const ids = toDelete.map(item => item.id); | |
return this.db.conversations.bulkDelete(ids); | |
} else { | |
const toDelete = await this.db.conversations.where("timestamp").below(threshold).toArray(); | |
const ids = toDelete.map(item => item.id); | |
return this.db.conversations.bulkDelete(ids); | |
} | |
} | |
async getStorageStats() { | |
try { | |
const conversations = await this.getAllConversations(); | |
const preferences = await this.getAllPreferences(); | |
const models = await this.getAllLLMModels(); | |
return { | |
conversations: conversations ? conversations.length : 0, | |
preferences: preferences ? Object.keys(preferences).length : 0, | |
models: models ? models.length : 0, | |
totalSize: JSON.stringify({ | |
conversations: conversations || [], | |
preferences: preferences || {}, | |
models: models || [] | |
}).length | |
}; | |
} catch (error) { | |
console.error("Error getting storage stats:", error); | |
return { | |
conversations: 0, | |
preferences: 0, | |
models: 0, | |
totalSize: 0 | |
}; | |
} | |
} | |
async deleteSingleMessage(conversationId, sender) { | |
const conv = await this.db.conversations.get(conversationId); | |
if (!conv) return; | |
if (sender === "user") { | |
conv.user = ""; | |
} else if (sender === "kimi") { | |
conv.kimi = ""; | |
} | |
if ((conv.user === undefined || conv.user === "") && (conv.kimi === undefined || conv.kimi === "")) { | |
await this.db.conversations.delete(conversationId); | |
} else { | |
await this.db.conversations.put(conv); | |
} | |
} | |
async setPreferencesBatch(prefsArray) { | |
const numericMap = { | |
voiceRate: "VOICE_RATE", | |
voicePitch: "VOICE_PITCH", | |
voiceVolume: "VOICE_VOLUME", | |
interfaceOpacity: "INTERFACE_OPACITY", | |
llmTemperature: "LLM_TEMPERATURE", | |
llmMaxTokens: "LLM_MAX_TOKENS", | |
llmTopP: "LLM_TOP_P", | |
llmFrequencyPenalty: "LLM_FREQUENCY_PENALTY", | |
llmPresencePenalty: "LLM_PRESENCE_PENALTY" | |
}; | |
const batch = prefsArray.map(({ key, value }) => { | |
if (numericMap[key] && window.KIMI_CONFIG && typeof window.KIMI_CONFIG.validate === "function") { | |
const validation = window.KIMI_CONFIG.validate(value, numericMap[key]); | |
if (validation.valid) value = validation.value; | |
} | |
return { key, value, updated: new Date().toISOString() }; | |
}); | |
return this.db.preferences.bulkPut(batch); | |
} | |
async setPersonalityBatch(traitsObj, character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
// Invalidate caches for all affected traits and the aggregate cache for this character | |
if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") { | |
try { | |
Object.keys(traitsObj).forEach(trait => { | |
window.KimiCacheManager.delete(`trait_${character}_${trait}`); | |
}); | |
window.KimiCacheManager.delete(`all_traits_${character}`); | |
} catch (e) {} | |
} | |
// Validation stricte : empêcher NaN ou valeurs non numériques | |
const getDefault = trait => { | |
if (window.KimiEmotionSystem) { | |
return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50; | |
} | |
const fallback = { affection: 65, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 }; | |
return fallback[trait] || 50; | |
}; | |
const batch = Object.entries(traitsObj).map(([trait, value]) => { | |
let v = Number(value); | |
if (!isFinite(v) || isNaN(v)) v = getDefault(trait); | |
v = Math.max(0, Math.min(100, v)); | |
return { | |
trait, | |
character, | |
value: v, | |
updated: new Date().toISOString() | |
}; | |
}); | |
return this.db.personality.bulkPut(batch); | |
} | |
async setSettingsBatch(settingsArray) { | |
const batch = settingsArray.map(({ category, settings }) => ({ | |
category, | |
settings, | |
updated: new Date().toISOString() | |
})); | |
return this.db.settings.bulkPut(batch); | |
} | |
async getPreferencesBatch(keys) { | |
const results = await this.db.preferences.where("key").anyOf(keys).toArray(); | |
const out = {}; | |
for (const item of results) { | |
let val = item.value; | |
if (item.encrypted && window.KimiSecurityUtils) { | |
try { | |
val = item.value; // decrypt removed – stored as plain text | |
// Migrate back as plain | |
try { | |
await this.db.preferences.put({ key: item.key, value: val, updated: new Date().toISOString() }); | |
} catch (mErr) {} | |
} catch (e) { | |
console.warn("Failed to decrypt legacy pref in batch:", item.key, e); | |
} | |
} | |
out[item.key] = val; | |
} | |
return out; | |
} | |
async getPersonalityTraitsBatch(traits, character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
const results = await this.db.personality.where("character").equals(character).toArray(); | |
const out = {}; | |
traits.forEach(trait => { | |
const found = results.find(item => item.trait === trait); | |
out[trait] = found ? found.value : 50; | |
}); | |
return out; | |
} | |
async getSelectedCharacter() { | |
try { | |
return await this.getPreference("selectedCharacter", "kimi"); | |
} catch (error) { | |
console.warn("Error getting selected character:", error); | |
return "kimi"; | |
} | |
} | |
async setSelectedCharacter(character) { | |
try { | |
await this.setPreference("selectedCharacter", character); | |
} catch (error) { | |
console.error("Error setting selected character:", error); | |
} | |
} | |
async getSystemPromptForCharacter(character = null) { | |
if (!character) character = await this.getSelectedCharacter(); | |
try { | |
const prompt = await this.getPreference(`systemPrompt_${character}`, null); | |
if (prompt) return prompt; | |
if (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character] && window.KIMI_CHARACTERS[character].defaultPrompt) { | |
return window.KIMI_CHARACTERS[character].defaultPrompt; | |
} | |
return window.DEFAULT_SYSTEM_PROMPT || ""; | |
} catch (error) { | |
console.warn("Error getting system prompt for character:", error); | |
return window.DEFAULT_SYSTEM_PROMPT || ""; | |
} | |
} | |
async setSystemPromptForCharacter(character, prompt) { | |
if (!character) character = await this.getSelectedCharacter(); | |
try { | |
await this.setPreference(`systemPrompt_${character}`, prompt); | |
} catch (error) { | |
console.error("Error setting system prompt for character:", error); | |
} | |
} | |
} | |
export default KimiDatabase; | |
// Export for usage | |
window.KimiDatabase = KimiDatabase; | |