Add mood sensing and auto collections to memory cloud

This commit is contained in:
OpenClaw
2026-04-24 22:40:46 +02:00
parent a13d08251d
commit e4195fdde2
3 changed files with 244 additions and 27 deletions
+91 -7
View File
@@ -100,6 +100,44 @@ const SMART_TAG_RULES = [
{ tag: 'family', patterns: [/kids/i, /family/i, /home/i] },
]
const TAG_ALIASES = {
login: 'auth',
oauth: 'auth',
permissions: 'auth',
permission: 'auth',
visual: 'design',
theme: 'design',
ui: 'design',
archive: 'memory',
memories: 'memory',
deploy: 'infra',
deployment: 'infra',
cloudflare: 'infra',
service: 'infra',
openclaw: 'agents',
agentboard: 'agents',
codex: 'agents',
child: 'kidsstories',
stories: 'kidsstories',
home: 'family',
}
const MOOD_PROFILES = [
{ mood: 'focused', score: 0, patterns: [/fix/i, /ship/i, /build/i, /implement/i, /polish/i, /deploy/i, /migrate/i] },
{ mood: 'playful', score: 0, patterns: [/fun/i, /mischief/i, /awesome/i, /play/i, /spark/i] },
{ mood: 'reflective', score: 0, patterns: [/remember/i, /reflect/i, /journal/i, /learn/i, /insight/i] },
{ mood: 'tense', score: 0, patterns: [/blocked/i, /worry/i, /broken/i, /error/i, /issue/i, /frustrat/i] },
{ mood: 'warm', score: 0, patterns: [/family/i, /kids/i, /home/i, /story/i, /love/i] },
]
const COLLECTION_RULES = [
{ id: 'infra-week', label: 'Infra week', tags: ['infra', 'appwrite', 'auth'] },
{ id: 'design-streak', label: 'Design streak', tags: ['design', 'memory'] },
{ id: 'family-notes', label: 'Family notes', tags: ['family', 'kidsstories'] },
{ id: 'agent-ops', label: 'Agent ops', tags: ['agents', 'appwrite', 'dashboard'] },
{ id: 'product-loop', label: 'Product loop', tags: ['product', 'design', 'appwrite'] },
]
const THEME_PROFILES = {
nord: { name: 'Nord', accent: '#8fbcbb', accentSoft: 'rgba(143,188,187,0.18)', accentStrong: '#a3d5d3', glow: 'rgba(143,188,187,0.28)' },
plum: { name: 'Plum', accent: '#b48ead', accentSoft: 'rgba(180,142,173,0.18)', accentStrong: '#d1a9ca', glow: 'rgba(180,142,173,0.28)' },
@@ -111,12 +149,21 @@ function isProbablyNoiseTag(value) {
return /^\d{4}-\d{2}-\d{2}$/.test(value) || /^\d+$/.test(value)
}
function normalizeTag(value) {
const cleaned = String(value || '').toLowerCase().replace(/[^a-z0-9+-]+/g, '').trim()
return TAG_ALIASES[cleaned] || cleaned
}
function generateTags(content, filename) {
const tags = []
const scores = new Map()
const addScore = (tag, amount) => scores.set(tag, (scores.get(tag) || 0) + amount)
const addScore = (tag, amount) => {
const normalized = normalizeTag(tag)
if (!normalized) return
scores.set(normalized, (scores.get(normalized) || 0) + amount)
}
const pushTag = (value) => {
const cleaned = String(value || '').toLowerCase().replace(/[^a-z0-9+-]+/g, '').trim()
const cleaned = normalizeTag(value)
if (!cleaned || cleaned.length < 3 || TAG_STOPWORDS.has(cleaned) || tags.includes(cleaned) || isProbablyNoiseTag(cleaned)) return
tags.push(cleaned)
}
@@ -170,6 +217,35 @@ function buildThemeProfile(content = '', tags = []) {
return THEME_PROFILES.nord
}
function detectMood(content = '', tags = []) {
const haystack = `${content} ${(tags || []).join(' ')}`.toLowerCase()
let best = { mood: 'steady', score: 0 }
for (const profile of MOOD_PROFILES) {
let score = 0
for (const pattern of profile.patterns) {
const matches = haystack.match(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)) || []
score += matches.length
}
if (score > best.score) best = { mood: profile.mood, score }
}
return best.mood
}
function buildCollections(files = []) {
return COLLECTION_RULES.map((rule) => {
const matchedFiles = files.filter((file) => rule.tags.some((tag) => (file.tags || []).includes(tag)))
if (!matchedFiles.length) return null
return {
id: rule.id,
label: rule.label,
count: matchedFiles.length,
tags: rule.tags,
latest: matchedFiles[0]?.dateLabel,
filenames: matchedFiles.slice(0, 6).map((file) => file.filename),
}
}).filter(Boolean)
}
function formatDateLabel(filename) {
const match = filename.match(/^(\d{4})-(\d{2})-(\d{2})\.md$/)
if (!match) return filename
@@ -285,6 +361,7 @@ async function readMemoryDocument(filename) {
function buildFileMeta(doc) {
const content = doc.content || ''
const tags = generateTags(content, doc.filename)
const mtimeMs = doc.$updatedAt ? new Date(doc.$updatedAt).getTime() : Date.now()
return {
filename: doc.filename,
@@ -293,7 +370,8 @@ function buildFileMeta(doc) {
wordCount: wordCount(content),
entryCount: getEntryCount(content),
dateLabel: formatDateLabel(doc.filename),
tags: generateTags(content, doc.filename),
tags,
mood: detectMood(content, tags),
}
}
@@ -339,8 +417,10 @@ app.get('/api/meta', async (req, res) => {
}, { dailyFileCount: 0, totalDailyEntries: 0, totalDailyWords: 0 })
const theme = buildThemeProfile(mainDoc?.content || '', mainMemory?.tags || [])
const collections = buildCollections(dailyFiles)
const mood = detectMood(mainDoc?.content || '', mainMemory?.tags || [])
res.json({ mainMemory, dailyFiles, summary, theme })
res.json({ mainMemory: mainMemory ? { ...mainMemory, mood } : null, dailyFiles, summary, theme, collections })
} catch (error) {
res.status(500).json({ error: error.message })
}
@@ -370,7 +450,8 @@ app.get('/api/memories/:filename', async (req, res) => {
await ensureSync()
const doc = await readMemoryDocument(filename)
if (!doc) return res.status(404).json({ error: 'File not found' })
return res.json({ content: doc.content || '', tags: generateTags(doc.content || '', filename) })
const tags = generateTags(doc.content || '', filename)
return res.json({ content: doc.content || '', tags, mood: detectMood(doc.content || '', tags) })
} catch (error) {
return res.status(500).json({ error: error.message })
}
@@ -381,7 +462,8 @@ app.get('/api/main-memory', async (req, res) => {
await ensureSync()
const doc = await readMemoryDocument('MEMORY.md')
if (!doc) return res.status(404).json({ error: 'MEMORY.md not found' })
res.json({ content: doc.content || '', tags: generateTags(doc.content || '', 'MEMORY.md') })
const tags = generateTags(doc.content || '', 'MEMORY.md')
res.json({ content: doc.content || '', tags, mood: detectMood(doc.content || '', tags) })
} catch (error) {
res.status(500).json({ error: error.message })
}
@@ -455,12 +537,14 @@ app.get('/api/search', async (req, res) => {
const windowEnd = Math.min(content.length, firstMatchIdx + 120)
const snippet = content.slice(windowStart, windowEnd).replace(/\n/g, ' ')
const tags = generateTags(content, doc.filename)
results.push({
filename: doc.filename,
dateLabel: formatDateLabel(doc.filename),
entryCount: getEntryCount(content),
wordCount: wordCount(content),
tags: generateTags(content, doc.filename),
tags,
mood: detectMood(content, tags),
matchCount: (lower.match(new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length,
snippet: `${windowStart > 0 ? '…' : ''}${snippet}${windowEnd < content.length ? '…' : ''}`,
matchingLines,