Agents can now put images inside markdown/HTML artifacts instead of only
posting them as standalone artifacts.
- New Asset model: an image uploaded ahead of the artifact that embeds it.
- POST /api/assets uploads an image (image/* only, 50 MB cap) and returns
a stable URL the agent references normally, e.g. . No custom
syntax; existing markdown/fragment rendering handles it.
- GET /assets/{id} serves the image, owner-scoped (404 for other users).
- On artifact creation the content is scanned for asset URLs and matching
assets are attached to it, so the image dies with the artifact it belongs
to. Uploads never referenced are swept after a 24h grace period.
- Quota: assets count toward the owner's quota at upload time and can
trigger eviction; evicting an artifact frees its assets too, and usage
now sums artifacts plus assets.
- daybook_post.py gains --upload-image, which prints the URL to embed.
- Docs and SKILL.md cover the upload-then-reference workflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
from django.conf import settings
|
|
from django.db import models
|
|
|
|
|
|
class Artifact(models.Model):
|
|
HTML_FRAGMENT = "html_fragment"
|
|
HTML_FULL = "html_full"
|
|
MARKDOWN = "markdown"
|
|
DECK = "deck"
|
|
IMAGE = "image"
|
|
PDF = "pdf"
|
|
BINARY = "binary"
|
|
|
|
AGENT = "agent"
|
|
PERSON = "person"
|
|
SYSTEM = "system"
|
|
|
|
KIND_CHOICES = [
|
|
(HTML_FRAGMENT, "HTML fragment"),
|
|
(HTML_FULL, "Full HTML"),
|
|
(MARKDOWN, "Markdown"),
|
|
(DECK, "Deck"),
|
|
(IMAGE, "Image"),
|
|
(PDF, "PDF"),
|
|
(BINARY, "Binary"),
|
|
]
|
|
|
|
SOURCE_TYPE_CHOICES = [
|
|
(AGENT, "Agent"),
|
|
(PERSON, "Person"),
|
|
(SYSTEM, "System"),
|
|
]
|
|
|
|
id = models.CharField(max_length=8, primary_key=True)
|
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="artifacts")
|
|
source = models.CharField(max_length=80, db_index=True)
|
|
title = models.CharField(max_length=200)
|
|
kind = models.CharField(max_length=20, choices=KIND_CHOICES)
|
|
theme = models.CharField(max_length=40, blank=True)
|
|
day = models.DateField(db_index=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
path = models.CharField(max_length=500)
|
|
original_kind = models.CharField(max_length=20)
|
|
tags = models.JSONField(default=list, blank=True)
|
|
source_prompt = models.TextField(blank=True)
|
|
source_type = models.CharField(max_length=20, choices=SOURCE_TYPE_CHOICES, default=AGENT)
|
|
source_name = models.CharField(max_length=120, blank=True)
|
|
source_id = models.CharField(max_length=120, blank=True)
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
read_at = models.DateTimeField(null=True, blank=True)
|
|
is_kept = models.BooleanField(default=False)
|
|
size_bytes = models.BigIntegerField(default=0)
|
|
|
|
@property
|
|
def is_unread(self):
|
|
return self.read_at is None
|
|
|
|
class Meta:
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self):
|
|
return f"{self.title} ({self.source})"
|
|
|
|
|
|
class TagColor(models.Model):
|
|
name = models.CharField(max_length=40, primary_key=True)
|
|
color = models.CharField(max_length=20)
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.color})"
|
|
|
|
|
|
class UserProfile(models.Model):
|
|
DEFAULT_QUOTA_BYTES = 1024 * 1024 * 1024 # 1 GB
|
|
|
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
|
|
quota_bytes = models.BigIntegerField(default=DEFAULT_QUOTA_BYTES)
|
|
|
|
def __str__(self):
|
|
return f"{self.user} quota={self.quota_bytes}"
|
|
|
|
|
|
class Asset(models.Model):
|
|
"""An image uploaded for embedding inside an artifact's content.
|
|
|
|
Uploaded before the artifact exists, so `artifact` is null until the
|
|
artifact referencing its URL is created. Unattached assets are swept
|
|
after ASSET_ORPHAN_GRACE; attached ones die with their artifact.
|
|
"""
|
|
|
|
id = models.CharField(max_length=12, primary_key=True)
|
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="assets")
|
|
artifact = models.ForeignKey(
|
|
Artifact, on_delete=models.CASCADE, related_name="assets", null=True, blank=True
|
|
)
|
|
path = models.CharField(max_length=500)
|
|
content_type = models.CharField(max_length=100)
|
|
size_bytes = models.BigIntegerField(default=0)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self):
|
|
return f"{self.id} ({self.content_type})"
|