Virtual-Kimi / kimi-js /kimi-database.js
VirtualKimi's picture
Upload 34 files
bcbb712 verified
// KIMI INDEXEDDB DATABASE SYSTEM
class KimiDatabase {
constructor() {
this.dbName = "KimiDB";
this.db = new Dexie(this.dbName);
// Personality write queue to batch and serialize rapid updates
this._personalityQueue = {};
this._personalityFlushTimer = null;
this._personalityFlushDelay = 300; // ms debounce window
// Runtime monitor flag (disabled by default)
this._monitorPersonalityWrites = false;
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,importance"
})
.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) {
// Ignore upgrade errors so DB open is not blocked; 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) {
// Non-blocking: continue on error
}
});
// Version 5: Clean schema with proper memory field defaults
this.db
.version(5)
.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 {
// Ensure all memories have required fields for compatibility
const memories = tx.table("memories");
const now = new Date().toISOString();
await memories.toCollection().modify(rec => {
if (rec.isActive == null) rec.isActive = true;
if (rec.importance == null) rec.importance = 0.5;
if (rec.accessCount == null) rec.accessCount = 0;
if (!rec.character) rec.character = "kimi";
if (!rec.createdAt) rec.createdAt = rec.timestamp || now;
if (!rec.lastAccess) rec.lastAccess = rec.timestamp || now;
});
console.log("✅ Database upgraded to v5: memory compatibility ensured");
} catch (e) {
console.warn("Database upgrade v5 non-critical error:", e);
}
});
}
async setConversationsBatch(conversationsArray) {
if (!Array.isArray(conversationsArray)) return;
try {
await this.db.conversations.clear();
if (conversationsArray.length) {
await this.db.conversations.bulkPut(conversationsArray);
}
} catch (error) {
console.error("Error restoring conversations:", error);
// Log to error manager for tracking
if (window.kimiErrorManager) {
window.kimiErrorManager.logDatabaseError("restoreConversations", error, {
conversationCount: conversationsArray.length
});
}
}
}
async setLLMModelsBatch(modelsArray) {
if (!Array.isArray(modelsArray)) return;
try {
await this.db.llmModels.clear();
if (modelsArray.length) {
await this.db.llmModels.bulkPut(modelsArray);
}
} catch (error) {
console.error("Error restoring LLM models:", error);
// Log to error manager for tracking
if (window.kimiErrorManager) {
window.kimiErrorManager.logDatabaseError("setLLMModelsBatch", error, {
modelCount: modelsArray.length
});
}
}
}
async getAllMemories() {
try {
return await this.db.memories.toArray();
} catch (error) {
console.warn("Error getting all memories:", error);
// Log to error manager for tracking
if (window.kimiErrorManager) {
const errorType = error.name === "SchemaError" ? "SchemaError" : "DatabaseError";
window.kimiErrorManager.logError(errorType, error, {
operation: "getAllMemories",
suggestion: error.message?.includes("not indexed")
? "Clear browser data to force schema upgrade"
: "Check database integrity"
});
}
return [];
}
}
async setAllMemories(memoriesArray) {
if (!Array.isArray(memoriesArray)) return;
try {
await this.db.memories.clear();
if (memoriesArray.length) {
await this.db.memories.bulkPut(memoriesArray);
}
} catch (error) {
console.error("Error restoring memories:", error);
}
}
async init() {
await this.db.open();
await this.initializeDefaultsIfNeeded();
await this.runPostOpenMigrations();
return this.db;
}
getUnifiedTraitDefaults() {
// Use centralized API instead of hardcoded values
if (window.getTraitDefaults) {
return window.getTraitDefaults();
}
// Fallback: create new instance only if no global API available
if (window.KimiEmotionSystem) {
const emotionSystem = new window.KimiEmotionSystem(this);
return emotionSystem.TRAIT_DEFAULTS;
}
// Ultimate fallback (should never be reached in normal operation)
return {
affection: 55,
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: "dark" },
{ key: "interfaceOpacity", value: 0.8 },
{ key: "showTranscript", value: true },
{ key: "enableStreaming", value: true },
{ key: "voiceEnabled", value: true },
{ key: "memorySystemEnabled", 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);
}
}
// Do not 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() });
}
}
// Migration: update Kimi default affection from 65 to 55
// This improves progression behavior for users who still have the old default
const kimiAffectionRecord = await this.db.personality.get(["kimi", "affection"]);
if (kimiAffectionRecord && kimiAffectionRecord.value === 65) {
// Only update if it's exactly 65 (the old default) and user hasn't modified it significantly
const newValue = window.KIMI_CHARACTERS?.kimi?.traits?.affection || 55;
await this.db.personality.put({
trait: "affection",
character: "kimi",
value: newValue,
updated: new Date().toISOString()
});
console.log(`🔧 Migration: Updated Kimi affection from 65% to ${newValue}% for better progression`);
}
// Migration: Fix Bella default affection from 70 to 60
const bellaAffectionRecord = await this.db.personality.get(["bella", "affection"]);
if (bellaAffectionRecord && bellaAffectionRecord.value === 70) {
// Only update if it's exactly 70 (the old default) and user hasn't modified it significantly
const newValue = window.KIMI_CHARACTERS?.bella?.traits?.affection || 60;
await this.db.personality.put({
trait: "affection",
character: "bella",
value: newValue,
updated: new Date().toISOString()
});
console.log(`🔧 Migration: Updated Bella affection from 70% to ${newValue}% for better progression`);
}
// Migration: remove deprecated animations preference if present
try {
const animPref = await this.db.preferences.get("animationsEnabled");
if (animPref) {
await this.db.preferences.delete("animationsEnabled");
console.log("🔧 Migration: Removed deprecated preference 'animationsEnabled'");
}
} catch (mErr) {
// Non-blocking: ignore migration error
}
// Migration: normalize legacy selectedLanguage values to primary subtag (e.g., 'en-US'|'en_US'|'us:en' -> 'en')
try {
const langRecord = await this.db.preferences.get("selectedLanguage");
if (langRecord && typeof langRecord.value === "string") {
let raw = String(langRecord.value).toLowerCase();
// handle 'us:en' -> take part after ':'
if (raw.includes(":")) {
const parts = raw.split(":");
raw = parts[parts.length - 1];
}
raw = raw.replace("_", "-");
const primary = raw.includes("-") ? raw.split("-")[0] : raw;
if (primary && primary !== langRecord.value) {
await this.db.preferences.put({
key: "selectedLanguage",
value: primary,
updated: new Date().toISOString()
});
console.log(`🔧 Migration: Normalized selectedLanguage '${langRecord.value}' -> '${primary}'`);
}
}
} catch (normErr) {
// Non-blocking
}
// Forced migration: normalize any preference keys containing the word 'language' to primary subtag
// WARNING: This operation is destructive and will overwrite matching preference values without backup.
try {
const allPrefs = await this.db.preferences.toArray();
const langKeyRegex = /\blanguage\b/i;
let modified = 0;
for (const p of allPrefs) {
if (!p || typeof p.key !== "string" || typeof p.value !== "string") continue;
if (!langKeyRegex.test(p.key)) continue;
let raw = String(p.value).toLowerCase();
if (raw.includes(":")) raw = raw.split(":").pop();
raw = raw.replace("_", "-");
const primary = raw.includes("-") ? raw.split("-")[0] : raw;
if (primary && primary !== p.value) {
await this.db.preferences.put({ key: p.key, value: primary, updated: new Date().toISOString() });
modified++;
}
}
if (modified) {
console.log(
`🔧 Forced Migration: Normalized ${modified} language-related preference(s) to primary subtag (no backup)`
);
}
} catch (fmErr) {
console.warn("Forced migration failed:", fmErr);
}
} 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: legacy records may have an `encrypted` flag; handle as plain text when needed
let value = record.value;
if (record.encrypted && window.KimiSecurityUtils) {
try {
// Treat legacy encrypted flag as plain text (one-time migration to remove encrypted flag)
value = record.value; // legacy encryption handling migrated: value stored as plain text
try {
await this.db.preferences.put({ key: key, value, updated: new Date().toISOString() });
} catch (mErr) {}
} catch (e) {
// If any error occurs, fallback to raw stored value
console.warn("Failed to handle legacy encrypted value; returning raw value", e);
}
}
// Normalize specific preferences for backward-compatibility
if (key === "selectedLanguage" && typeof value === "string") {
try {
let raw = String(value).toLowerCase();
if (raw.includes(":")) raw = raw.split(":").pop();
raw = raw.replace("_", "-");
const primary = raw.includes("-") ? raw.split("-")[0] : raw;
if (primary && primary !== value) {
// Persist normalized primary subtag to DB for future reads
try {
await this.db.preferences.put({ key: key, value: primary, updated: new Date().toISOString() });
value = primary;
} catch (mErr) {
// ignore persistence error, but return normalized value
value = primary;
}
}
} catch (e) {
// ignore normalization errors
}
}
// 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();
// For safety, enqueue the update to batch rapid writes and avoid overwrites
this.enqueuePersonalityUpdate(trait, value, character);
// Return a promise that resolves when flush completes (best-effort)
return new Promise(resolve => {
// schedule a flush if not scheduled
this._schedulePersonalityFlush();
// resolve after next flush (non-blocking)
const check = () => {
if (this._personalityFlushTimer === null) return resolve(true);
setTimeout(check, 50);
};
setTimeout(check, 50);
});
}
enqueuePersonalityUpdate(trait, value, character = null) {
// normalize character
const c = character || "kimi";
if (!this._personalityQueue[c]) this._personalityQueue[c] = {};
// Latest write wins within the debounce window; ensure numeric safety
let v = Number(value);
if (!isFinite(v) || isNaN(v)) {
// fallback to existing value if available
v = this.getPersonalityTrait(trait, null, c).catch(() => 50);
}
this._personalityQueue[c][trait] = Number(v);
this._schedulePersonalityFlush();
if (this._monitorPersonalityWrites) {
try {
console.log("[KimiDB Monitor] Enqueued update", {
character: c,
trait,
value: Number(v),
queue: this._personalityQueue[c]
});
} catch (e) {}
}
}
_schedulePersonalityFlush() {
if (this._personalityFlushTimer) return;
this._personalityFlushTimer = setTimeout(() => this._flushPersonalityQueue(), this._personalityFlushDelay);
}
async _flushPersonalityQueue() {
if (!this._personalityQueue || Object.keys(this._personalityQueue).length === 0) {
if (this._personalityFlushTimer) {
clearTimeout(this._personalityFlushTimer);
this._personalityFlushTimer = null;
}
return;
}
const queue = this._personalityQueue;
this._personalityQueue = {};
if (this._personalityFlushTimer) {
clearTimeout(this._personalityFlushTimer);
this._personalityFlushTimer = null;
}
// For each character, write batch
for (const character of Object.keys(queue)) {
const traitsObj = queue[character];
try {
if (this._monitorPersonalityWrites) {
try {
console.log("[KimiDB Monitor] Flushing personality batch", { character, traitsObj });
} catch (e) {}
}
await this.setPersonalityBatch(traitsObj, character);
if (this._monitorPersonalityWrites) {
try {
console.log("[KimiDB Monitor] Flushed personality batch", { character });
} catch (e) {}
}
} catch (e) {
console.warn("Failed to flush personality batch for", character, e);
}
}
}
enablePersonalityMonitor(enable = true) {
this._monitorPersonalityWrites = !!enable;
console.log(`[KimiDB Monitor] enabled=${this._monitorPersonalityWrites}`);
}
async getPersonalityTrait(trait, defaultValue = null, character = null) {
if (!character) character = await this.getSelectedCharacter();
// Use unified defaults from emotion system
if (defaultValue === null) {
// Use centralized API for trait defaults
if (window.getTraitDefaults) {
defaultValue = window.getTraitDefaults()[trait] || 50;
} else if (window.KimiEmotionSystem) {
const emotionSystem = new window.KimiEmotionSystem(this);
defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
} else {
// Ultimate fallback (hardcoded values - should be avoided)
defaultValue =
{
affection: 55,
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) {
// Backwards-compatible: accept either an array [{key,value},...] or an object map { key: value }
let prefsInput = prefsArray;
if (!Array.isArray(prefsInput) && prefsInput && typeof prefsInput === "object") {
// convert map to array
prefsInput = Object.keys(prefsInput).map(k => ({ key: k, value: prefsInput[k] }));
console.warn("setPreferencesBatch: converted prefs map to array for backward compatibility");
}
if (!Array.isArray(prefsInput)) {
console.warn("setPreferencesBatch: expected array or object, got", typeof prefsArray);
return;
}
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 = prefsInput.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 => {
// Use centralized API for consistency
if (window.getTraitDefaults) {
return window.getTraitDefaults()[trait] || 50;
}
if (window.KimiEmotionSystem) {
return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50;
}
// Ultimate fallback (should be avoided)
const fallback = { affection: 55, 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;