feat: 创建 LobsterDiary 模型,支持数据库存储日记

- 创建 LobsterDiary 模型
  * 关联龙虾(ForeignKey)
  * 日期、标题、内容
  * 分类(成才之路/工作记忆/技术笔记)
  * 标签(JSONField)
  * Embedding 字段(预留 RAG 支持)
  * 数据库索引优化

- 数据库迁移
  * 添加 LobsterDiary 表
  * 添加索引:lobster+date, category+date, date

- 导入脚本
  * 创建 import_diaries 管理命令
  * 导入飞行侠的成才之路日记(3 篇)

- 更新 API
  * /api/lobsters/<id>/diary/dates/ - 从数据库查询
  * /api/lobsters/<id>/diary/<date>/ - 从数据库读取

- PostgreSQL 配置模板
  * settings_postgresql.py
  * 准备好 PostgreSQL 迁移

技术栈:SQLite(当前) → PostgreSQL(未来)
RAG 支持:预留 embedding 字段,未来可扩展

🗄️ 日记正式进入数据库时代!
This commit is contained in:
2026-04-03 17:38:18 +08:00
parent 689851e762
commit 24e4ca2c82
7 changed files with 330 additions and 28 deletions

View File

@@ -7,7 +7,7 @@ from datetime import datetime
import os import os
from pathlib import Path from pathlib import Path
import re import re
from lobsters.models import Lobster from lobsters.models import Lobster, LobsterDiary
@api_view(['GET']) @api_view(['GET'])
def lobster_list(request): def lobster_list(request):
@@ -121,38 +121,41 @@ def lobster_memory_detail(request, lobster_id, date):
@api_view(['GET']) @api_view(['GET'])
def lobster_diary_dates(request, lobster_id): def lobster_diary_dates(request, lobster_id):
"""获取龙虾有日记(成才之路)的日期列表""" """获取龙虾有日记(成才之路)的日期列表 - 从数据库读取"""
# 日记文件目录 try:
diary_dir = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路') lobster = Lobster.objects.get(id=lobster_id)
except Lobster.DoesNotExist:
return Response({'error': '龙虾不存在'}, status=404)
# 获取所有日记文件 # 从数据库查询日记日期
dates = [] diaries = LobsterDiary.objects.filter(
if diary_dir.exists(): lobster=lobster,
for file in diary_dir.glob('*.md'): category='chengcai'
# 提取日期 (YYYY-MM-DD.md 或 YYYY-MM-DD-*.md) ).values_list('date', flat=True).distinct()
match = re.match(r'(\d{4}-\d{2}-\d{2})(?:-.*)?\.md', file.name)
if match:
dates.append(match.group(1))
dates = list(set(dates)) # 去重 dates = [str(date) for date in sorted(diaries, reverse=True)]
dates.sort(reverse=True)
return Response({'dates': dates}) return Response({'dates': dates})
@api_view(['GET']) @api_view(['GET'])
def lobster_diary_detail(request, lobster_id, date): def lobster_diary_detail(request, lobster_id, date):
"""获取指定日期的日记内容(成才之路)""" """获取指定日期的日记内容(成才之路) - 从数据库读取"""
# 优先查找故事版,其次技术版,再其次普通版 try:
diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}-故事版.md') lobster = Lobster.objects.get(id=lobster_id)
if not diary_file.exists(): except Lobster.DoesNotExist:
diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}-技术版.md') return Response({'error': '龙虾不存在'}, status=404)
if not diary_file.exists():
diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}.md')
if not diary_file.exists(): # 从数据库查询日记
try:
diary = LobsterDiary.objects.get(
lobster=lobster,
date=date,
category='chengcai'
)
return Response({
'date': str(diary.date),
'content': diary.content,
'title': diary.title,
'tags': diary.tags,
})
except LobsterDiary.DoesNotExist:
return Response({'error': '该日期没有日记'}, status=404) return Response({'error': '该日期没有日记'}, status=404)
content = diary_file.read_text(encoding='utf-8')
return Response({
'date': date,
'content': content
})

View File

@@ -0,0 +1,33 @@
"""
PostgreSQL 数据库配置模板
使用方法:
1. 安装 PostgreSQL
2. 创建数据库和用户
3. 复制此文件为 settings_postgresql.py
4. 修改密码
5. 修改 backend/settings.py 中的 DATABASES 配置
"""
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'lobster_db',
'USER': 'lobster_user',
'PASSWORD': 'lobster2026', # 请修改为你的密码
'HOST': 'localhost',
'PORT': '5432',
'CONN_MAX_AGE': 600, # 连接持久化
'OPTIONS': {
'connect_timeout': 10,
},
}
}
# PostgreSQL 特定优化
# 生产环境建议添加
# CACHES = {
# 'default': {
# 'BACKEND': 'django.core.cache.backends.locmem.LocMem',
# }
# }

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python
"""
导入现有日记文件到数据库
使用方法:
python manage.py import_diaries
"""
from django.core.management.base import BaseCommand
from pathlib import Path
from datetime import datetime
from lobsters.models import Lobster, LobsterDiary
class Command(BaseCommand):
help = '导入现有日记文件到数据库'
def handle(self, *args, **kwargs):
self.stdout.write('🚀 开始导入日记...')
# 获取飞行侠
try:
feixingxia = Lobster.objects.get(name='飞行侠')
self.stdout.write(f'✅ 找到龙虾:{feixingxia}')
except Lobster.DoesNotExist:
self.stdout.write(self.style.ERROR('❌ 未找到飞行侠'))
return
# 日记目录
diary_dir = Path('/home/node/.openclaw/workspace/flying-hero/memory/成才之路')
if not diary_dir.exists():
self.stdout.write(self.style.ERROR(f'❌ 日记目录不存在:{diary_dir}'))
return
self.stdout.write(f'📂 扫描目录:{diary_dir}')
# 导入日记
imported_count = 0
for file in diary_dir.glob('*.md'):
# 提取日期
filename = file.name
if filename.endswith('-故事版.md'):
date_str = filename.replace('-故事版.md', '')
category = 'chengcai'
title = f'成才之路 · 故事版 · {date_str}'
elif filename.endswith('-技术版.md'):
date_str = filename.replace('-技术版.md', '')
category = 'chengcai'
title = f'成才之路 · 技术版 · {date_str}'
else:
date_str = filename.replace('.md', '')
category = 'chengcai'
title = f'成才之路 · {date_str}'
try:
date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
self.stdout.write(self.style.WARNING(f'⚠️ 跳过无效日期文件:{filename}'))
continue
# 读取内容
content = file.read_text(encoding='utf-8')
# 创建或更新日记
diary, created = LobsterDiary.objects.update_or_create(
lobster=feixingxia,
date=date,
category=category,
defaults={
'title': title,
'content': content,
'tags': ['成才之路', '成长日记'],
}
)
if created:
self.stdout.write(self.style.SUCCESS(f'✅ 导入:{title}'))
else:
self.stdout.write(self.style.WARNING(f'🔄 更新:{title}'))
imported_count += 1
self.stdout.write(self.style.SUCCESS(f'\n🎉 导入完成!共导入 {imported_count} 篇日记'))

View File

@@ -0,0 +1,109 @@
# Generated by Django 4.2 on 2026-04-03 09:37
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("lobsters", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="lobster",
name="workspace",
field=models.CharField(
blank=True, default="", max_length=100, verbose_name="工作区"
),
),
migrations.CreateModel(
name="LobsterDiary",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateField(verbose_name="日期")),
("title", models.CharField(max_length=200, verbose_name="标题")),
("content", models.TextField(verbose_name="内容")),
(
"category",
models.CharField(
choices=[
("chengcai", "成才之路"),
("memory", "工作记忆"),
("tech", "技术笔记"),
("other", "其他"),
],
default="other",
max_length=50,
verbose_name="分类",
),
),
(
"tags",
models.JSONField(blank=True, default=list, verbose_name="标签"),
),
(
"embedding",
models.TextField(
blank=True, null=True, verbose_name="文本向量 (JSON 格式)"
),
),
(
"embedding_model",
models.CharField(
blank=True,
default="",
max_length=50,
verbose_name="Embedding 模型版本",
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
),
(
"lobster",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="diaries",
to="lobsters.lobster",
verbose_name="龙虾",
),
),
],
options={
"verbose_name": "龙虾日记",
"verbose_name_plural": "龙虾日记",
"ordering": ["-date", "-created_at"],
},
),
migrations.AddIndex(
model_name="lobsterdiary",
index=models.Index(
fields=["lobster", "date"], name="lobsters_lo_lobster_5895f8_idx"
),
),
migrations.AddIndex(
model_name="lobsterdiary",
index=models.Index(
fields=["category", "date"], name="lobsters_lo_categor_177677_idx"
),
),
migrations.AddIndex(
model_name="lobsterdiary",
index=models.Index(fields=["date"], name="lobsters_lo_date_d11f64_idx"),
),
]

View File

@@ -1,4 +1,6 @@
from django.db import models from django.db import models
from django.utils import timezone
class Lobster(models.Model): class Lobster(models.Model):
"""龙虾模型""" """龙虾模型"""
@@ -9,6 +11,7 @@ class Lobster(models.Model):
container = models.CharField(max_length=100, verbose_name='容器') container = models.CharField(max_length=100, verbose_name='容器')
app_name = models.CharField(max_length=100, blank=True, default='', verbose_name='应用名称') app_name = models.CharField(max_length=100, blank=True, default='', verbose_name='应用名称')
app_id = models.CharField(max_length=50, blank=True, default='', verbose_name='应用 ID') app_id = models.CharField(max_length=50, blank=True, default='', verbose_name='应用 ID')
workspace = models.CharField(max_length=100, blank=True, default='', verbose_name='工作区')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
@@ -19,3 +22,73 @@ class Lobster(models.Model):
def __str__(self): def __str__(self):
return f'{self.emoji} {self.name}' return f'{self.emoji} {self.name}'
class LobsterDiary(models.Model):
"""龙虾日记模型(支持 RAG"""
# 分类选择
CATEGORY_CHOICES = [
('chengcai', '成才之路'),
('memory', '工作记忆'),
('tech', '技术笔记'),
('other', '其他'),
]
# 关联龙虾
lobster = models.ForeignKey(
Lobster,
on_delete=models.CASCADE,
related_name='diaries',
verbose_name='龙虾'
)
# 基本信息
date = models.DateField(verbose_name='日期')
title = models.CharField(max_length=200, verbose_name='标题')
content = models.TextField(verbose_name='内容')
# 分类和标签RAG 检索的关键元数据)
category = models.CharField(
max_length=50,
choices=CATEGORY_CHOICES,
default='other',
verbose_name='分类'
)
tags = models.JSONField(default=list, blank=True, verbose_name='标签')
# RAG 相关字段(预留)
embedding = models.TextField(
blank=True,
null=True,
verbose_name='文本向量 (JSON 格式)'
)
embedding_model = models.CharField(
max_length=50,
blank=True,
default='',
verbose_name='Embedding 模型版本'
)
# 时间戳
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
verbose_name = '龙虾日记'
verbose_name_plural = '龙虾日记'
ordering = ['-date', '-created_at']
indexes = [
models.Index(fields=['lobster', 'date']),
models.Index(fields=['category', 'date']),
models.Index(fields=['date']),
]
def __str__(self):
return f'{self.lobster.emoji} {self.lobster.name} - {self.date} - {self.get_category_display()}'
def get_content_preview(self, length=50):
"""获取内容预览"""
if len(self.content) <= length:
return self.content
return self.content[:length] + '...'