diff --git a/src/openclaw/migrations/0001_initial.py b/src/openclaw/migrations/0001_initial.py new file mode 100644 index 0000000..6b27b22 --- /dev/null +++ b/src/openclaw/migrations/0001_initial.py @@ -0,0 +1,102 @@ +# Generated by Django 5.2.12 on 2026-04-05 06:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Session', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_id', models.CharField(max_length=64)), + ('agent_name', models.CharField(max_length=128)), + ('source_node', models.CharField(max_length=64)), + ('session_version', models.IntegerField(default=0)), + ('model_provider', models.CharField(blank=True, default='', max_length=64)), + ('model_id', models.CharField(blank=True, default='', max_length=128)), + ('thinking_level', models.CharField(blank=True, default='', max_length=64)), + ('start_time', models.DateTimeField(blank=True, null=True)), + ('end_time', models.DateTimeField(blank=True, null=True)), + ('cwd', models.CharField(blank=True, default='', max_length=512)), + ('total_tokens', models.IntegerField(default=0)), + ('total_cost', models.FloatField(default=0.0)), + ('message_count', models.IntegerField(default=0)), + ('tool_call_count', models.IntegerField(default=0)), + ('error_count', models.IntegerField(default=0)), + ('raw_file_path', models.TextField(blank=True, default='')), + ('pushed_at', models.DateTimeField(blank=True, null=True)), + ('status', models.CharField(default='active', max_length=16)), + ('metadata', models.JSONField(blank=True, default=dict)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'db_table': 'sessions', + 'ordering': ['-start_time'], + 'unique_together': {('session_id', 'agent_name')}, + }, + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message_id', models.CharField(max_length=128)), + ('parent_id', models.CharField(blank=True, default='', max_length=128)), + ('seq', models.IntegerField(default=0)), + ('role', models.CharField(max_length=32)), + ('content_text', models.TextField(blank=True, default='')), + ('raw_content', models.JSONField(blank=True, default=list)), + ('raw_message', models.JSONField(blank=True, default=dict)), + ('timestamp', models.DateTimeField()), + ('model', models.CharField(blank=True, default='', max_length=128)), + ('provider', models.CharField(blank=True, default='', max_length=64)), + ('stop_reason', models.CharField(blank=True, default='', max_length=64)), + ('tokens_input', models.IntegerField(default=0)), + ('tokens_output', models.IntegerField(default=0)), + ('tokens_cache_read', models.IntegerField(default=0)), + ('tokens_cache_write', models.IntegerField(default=0)), + ('tokens_total', models.IntegerField(default=0)), + ('cost_total', models.FloatField(default=0.0)), + ('tool_call_id', models.CharField(blank=True, default='', max_length=128)), + ('tool_name', models.CharField(blank=True, default='', max_length=128)), + ('is_error', models.BooleanField(default=False)), + ('exit_code', models.IntegerField(blank=True, null=True)), + ('duration_ms', models.IntegerField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='openclaw.session')), + ], + options={ + 'db_table': 'messages', + 'ordering': ['seq'], + }, + ), + migrations.CreateModel( + name='ToolCall', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tool_call_id', models.CharField(max_length=128)), + ('tool_name', models.CharField(max_length=128)), + ('arguments', models.JSONField(blank=True, default=dict)), + ('result_text', models.TextField(blank=True, default='')), + ('is_error', models.BooleanField(default=False)), + ('exit_code', models.IntegerField(blank=True, null=True)), + ('duration_ms', models.IntegerField(blank=True, null=True)), + ('seq', models.IntegerField(default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tool_calls', to='openclaw.message')), + ('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tool_calls', to='openclaw.session')), + ], + options={ + 'db_table': 'tool_calls', + 'ordering': ['seq'], + }, + ), + ] diff --git a/src/openclaw/migrations/__init__.py b/src/openclaw/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/openclaw/models.py b/src/openclaw/models.py new file mode 100644 index 0000000..5e7c394 --- /dev/null +++ b/src/openclaw/models.py @@ -0,0 +1,99 @@ +from django.db import models + + +class Session(models.Model): + session_id = models.CharField(max_length=64) + agent_name = models.CharField(max_length=128) + source_node = models.CharField(max_length=64) + session_version = models.IntegerField(default=0) + model_provider = models.CharField(max_length=64, blank=True, default="") + model_id = models.CharField(max_length=128, blank=True, default="") + thinking_level = models.CharField(max_length=64, blank=True, default="") + start_time = models.DateTimeField(null=True, blank=True) + end_time = models.DateTimeField(null=True, blank=True) + cwd = models.CharField(max_length=512, blank=True, default="") + total_tokens = models.IntegerField(default=0) + total_cost = models.FloatField(default=0.0) + message_count = models.IntegerField(default=0) + tool_call_count = models.IntegerField(default=0) + error_count = models.IntegerField(default=0) + raw_file_path = models.TextField(blank=True, default="") + pushed_at = models.DateTimeField(null=True, blank=True) + status = models.CharField(max_length=16, default="active") + metadata = models.JSONField(default=dict, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "sessions" + unique_together = ("session_id", "agent_name") + ordering = ["-start_time"] + + def __str__(self): + return f"Session({self.session_id} {self.agent_name})" + + +class Message(models.Model): + session = models.ForeignKey( + Session, on_delete=models.CASCADE, related_name="messages" + ) + message_id = models.CharField(max_length=128) + parent_id = models.CharField(max_length=128, blank=True, default="") + seq = models.IntegerField(default=0) + role = models.CharField(max_length=32) + content_text = models.TextField(blank=True, default="") + raw_content = models.JSONField(default=list, blank=True) + raw_message = models.JSONField(default=dict, blank=True) + timestamp = models.DateTimeField() + # assistant 专用 + model = models.CharField(max_length=128, blank=True, default="") + provider = models.CharField(max_length=64, blank=True, default="") + stop_reason = models.CharField(max_length=64, blank=True, default="") + tokens_input = models.IntegerField(default=0) + tokens_output = models.IntegerField(default=0) + tokens_cache_read = models.IntegerField(default=0) + tokens_cache_write = models.IntegerField(default=0) + tokens_total = models.IntegerField(default=0) + cost_total = models.FloatField(default=0.0) + # toolResult 专用 + tool_call_id = models.CharField(max_length=128, blank=True, default="") + tool_name = models.CharField(max_length=128, blank=True, default="") + is_error = models.BooleanField(default=False) + exit_code = models.IntegerField(null=True, blank=True) + duration_ms = models.IntegerField(null=True, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "messages" + ordering = ["seq"] + + def __str__(self): + return f"Message({self.message_id} {self.role})" + + +class ToolCall(models.Model): + session = models.ForeignKey( + Session, on_delete=models.CASCADE, related_name="tool_calls" + ) + message = models.ForeignKey( + Message, on_delete=models.CASCADE, related_name="tool_calls" + ) + tool_call_id = models.CharField(max_length=128) + tool_name = models.CharField(max_length=128) + arguments = models.JSONField(default=dict, blank=True) + result_text = models.TextField(blank=True, default="") + is_error = models.BooleanField(default=False) + exit_code = models.IntegerField(null=True, blank=True) + duration_ms = models.IntegerField(null=True, blank=True) + seq = models.IntegerField(default=0) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "tool_calls" + ordering = ["seq"] + + def __str__(self): + return f"ToolCall({self.tool_name} {self.tool_call_id})" diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..cf8ea3f --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,57 @@ +from datetime import datetime, timezone + +import pytest +from openclaw.models import Session, Message, ToolCall + +@pytest.mark.django_db +class TestModelFields: + def test_session_creation(self): + s = Session.objects.create( + session_id="a" * 36, + agent_name="xingyao", + source_node="macmini", + status="active", + ) + assert s.session_id == "a" * 36 + assert s.total_tokens == 0 + assert s.message_count == 0 + + def test_message_creation(self): + s = Session.objects.create( + session_id="b" * 36, + agent_name="test", + source_node="ubuntu1", + status="active", + ) + msg = Message.objects.create( + session=s, + message_id="msg-001", + parent_id="root", + role="assistant", + timestamp=datetime(2026, 4, 5, 10, 0, tzinfo=timezone.utc), + ) + assert msg.role == "assistant" + assert msg.tokens_total == 0 + + def test_toolcall_creation(self): + s = Session.objects.create( + session_id="c" * 36, + agent_name="test", + source_node="ubuntu2", + status="active", + ) + msg = Message.objects.create( + session=s, + message_id="msg-002", + parent_id="root", + role="assistant", + timestamp=datetime(2026, 4, 5, 10, 0, tzinfo=timezone.utc), + ) + tc = ToolCall.objects.create( + session=s, + message=msg, + tool_call_id="call_0", + tool_name="exec", + ) + assert tc.tool_name == "exec" + assert tc.is_error is False