diff --git a/code/backend/api/views.py b/code/backend/api/views.py index 4296489..b81517b 100644 --- a/code/backend/api/views.py +++ b/code/backend/api/views.py @@ -7,7 +7,7 @@ from datetime import datetime import os from pathlib import Path import re -from lobsters.models import Lobster +from lobsters.models import Lobster, LobsterDiary @api_view(['GET']) def lobster_list(request): @@ -121,38 +121,41 @@ def lobster_memory_detail(request, lobster_id, date): @api_view(['GET']) def lobster_diary_dates(request, lobster_id): - """获取龙虾有日记(成才之路)的日期列表""" - # 日记文件目录 - diary_dir = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路') + """获取龙虾有日记(成才之路)的日期列表 - 从数据库读取""" + try: + lobster = Lobster.objects.get(id=lobster_id) + except Lobster.DoesNotExist: + return Response({'error': '龙虾不存在'}, status=404) - # 获取所有日记文件 - dates = [] - if diary_dir.exists(): - for file in diary_dir.glob('*.md'): - # 提取日期 (YYYY-MM-DD.md 或 YYYY-MM-DD-*.md) - match = re.match(r'(\d{4}-\d{2}-\d{2})(?:-.*)?\.md', file.name) - if match: - dates.append(match.group(1)) + # 从数据库查询日记日期 + diaries = LobsterDiary.objects.filter( + lobster=lobster, + category='chengcai' + ).values_list('date', flat=True).distinct() - dates = list(set(dates)) # 去重 - dates.sort(reverse=True) + dates = [str(date) for date in sorted(diaries, reverse=True)] return Response({'dates': dates}) @api_view(['GET']) def lobster_diary_detail(request, lobster_id, date): - """获取指定日期的日记内容(成才之路)""" - # 优先查找故事版,其次技术版,再其次普通版 - diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}-故事版.md') - if not diary_file.exists(): - diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}-技术版.md') - if not diary_file.exists(): - diary_file = Path(f'/home/node/.openclaw/workspace/flying-hero/memory/成才之路/{date}.md') + """获取指定日期的日记内容(成才之路) - 从数据库读取""" + try: + lobster = Lobster.objects.get(id=lobster_id) + except Lobster.DoesNotExist: + return Response({'error': '龙虾不存在'}, status=404) - 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) - - content = diary_file.read_text(encoding='utf-8') - return Response({ - 'date': date, - 'content': content - }) diff --git a/code/backend/backend/settings_postgresql.py b/code/backend/backend/settings_postgresql.py new file mode 100644 index 0000000..f6a6859 --- /dev/null +++ b/code/backend/backend/settings_postgresql.py @@ -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', +# } +# } diff --git a/code/backend/lobsters/management/__init__.py b/code/backend/lobsters/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/backend/lobsters/management/commands/__init__.py b/code/backend/lobsters/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/backend/lobsters/management/commands/import_diaries.py b/code/backend/lobsters/management/commands/import_diaries.py new file mode 100644 index 0000000..10dbb4a --- /dev/null +++ b/code/backend/lobsters/management/commands/import_diaries.py @@ -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} 篇日记')) diff --git a/code/backend/lobsters/migrations/0002_lobster_workspace_lobsterdiary_and_more.py b/code/backend/lobsters/migrations/0002_lobster_workspace_lobsterdiary_and_more.py new file mode 100644 index 0000000..bd5aa26 --- /dev/null +++ b/code/backend/lobsters/migrations/0002_lobster_workspace_lobsterdiary_and_more.py @@ -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"), + ), + ] diff --git a/code/backend/lobsters/models.py b/code/backend/lobsters/models.py index 2ad8ad6..55ed296 100644 --- a/code/backend/lobsters/models.py +++ b/code/backend/lobsters/models.py @@ -1,4 +1,6 @@ from django.db import models +from django.utils import timezone + class Lobster(models.Model): """龙虾模型""" @@ -9,6 +11,7 @@ class Lobster(models.Model): container = models.CharField(max_length=100, 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') + workspace = models.CharField(max_length=100, blank=True, default='', verbose_name='工作区') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') @@ -19,3 +22,73 @@ class Lobster(models.Model): def __str__(self): 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] + '...'