diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 3c5c501fd..0c4abe059 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -99,6 +99,7 @@ async def save(): user_canvas_id=req["id"], dsl=req["dsl"], title=UserCanvasVersionService.build_version_title(getattr(current_user, "nickname", current_user.id), req.get("title")), + release=req.get("release"), ) replica_ok = CanvasReplicaService.replace_for_set( canvas_id=req["id"], @@ -133,6 +134,25 @@ def get(canvas_id): ) except ValueError as e: return get_data_error_result(message=str(e)) + + # Get the last publication time (latest released version's update_time) + last_publish_time = None + versions = UserCanvasVersionService.list_by_canvas_id(canvas_id) + if versions: + released_versions = [v for v in versions if v.release] + if released_versions: + # Sort by update_time descending and get the latest + released_versions.sort(key=lambda x: x.update_time, reverse=True) + last_publish_time = released_versions[0].update_time + + # Add last_publish_time to response data + if isinstance(c, dict): + c["last_publish_time"] = last_publish_time + else: + # If c is a model object, convert to dict first + c = c.to_dict() + c["last_publish_time"] = last_publish_time + return get_json_result(data=c) diff --git a/api/db/db_models.py b/api/db/db_models.py index 6348a68a3..2e3824050 100644 --- a/api/db/db_models.py +++ b/api/db/db_models.py @@ -1075,6 +1075,7 @@ class UserCanvasVersion(DataBaseModel): title = CharField(max_length=255, null=True, help_text="Canvas title") description = TextField(null=True, help_text="Canvas description") + release = BooleanField(null=False, help_text="is released", default=False, index=True) dsl = JSONField(null=True, default={}) class Meta: @@ -1538,6 +1539,7 @@ def migrate_db(): alter_db_add_column(migrator, "dialog", "tenant_rerank_id", IntegerField(null=True, help_text="id in tenant_llm", index=True)) alter_db_add_column(migrator, "memory", "tenant_embd_id", IntegerField(null=True, help_text="id in tenant_llm", index=True)) alter_db_add_column(migrator, "memory", "tenant_llm_id", IntegerField(null=True, help_text="id in tenant_llm", index=True)) + alter_db_add_column(migrator, "user_canvas_version", "release", BooleanField(null=False, help_text="is released", default=False, index=True)) logging.disable(logging.NOTSET) # this is after re-enabling logging to allow logging changed user emails migrate_add_unique_email(migrator) diff --git a/api/db/services/canvas_service.py b/api/db/services/canvas_service.py index 838951a9a..f6aa41c4a 100644 --- a/api/db/services/canvas_service.py +++ b/api/db/services/canvas_service.py @@ -19,7 +19,7 @@ import time from uuid import uuid4 from agent.canvas import Canvas from api.db import CanvasCategory, TenantPermission -from api.db.db_models import DB, CanvasTemplate, User, UserCanvas, API4Conversation +from api.db.db_models import DB, CanvasTemplate, User, UserCanvas, API4Conversation, UserCanvasVersion from api.db.services.api_service import API4ConversationService from api.db.services.common_service import CommonService from common.misc_utils import get_uuid @@ -173,7 +173,23 @@ class UserCanvasService(CommonService): count = agents.count() if page_number and items_per_page: agents = agents.paginate(page_number, items_per_page) - return list(agents.dicts()), count + + agents_list = list(agents.dicts()) + + # Get latest release time for each canvas + if agents_list: + canvas_ids = [a['id'] for a in agents_list] + release_times = ( + UserCanvasVersion.select(UserCanvasVersion.user_canvas_id, fn.MAX(UserCanvasVersion.create_time).alias("release_time")) + .where((UserCanvasVersion.user_canvas_id.in_(canvas_ids)) & (UserCanvasVersion.release)) + .group_by(UserCanvasVersion.user_canvas_id) + ) + release_time_map = {r.user_canvas_id: r.release_time for r in release_times} + + for agent in agents_list: + agent['release_time'] = release_time_map.get(agent['id']) + + return agents_list, count @classmethod @DB.connection_context() diff --git a/api/db/services/user_canvas_version.py b/api/db/services/user_canvas_version.py index f8bd6dae0..d2861d576 100644 --- a/api/db/services/user_canvas_version.py +++ b/api/db/services/user_canvas_version.py @@ -45,7 +45,8 @@ class UserCanvasVersionService(CommonService): cls.model.create_date, cls.model.update_date, cls.model.user_canvas_id, - cls.model.update_time] + cls.model.update_time, + cls.model.release] ).where(cls.model.user_canvas_id == user_canvas_id) return user_canvas_version except DoesNotExist: @@ -74,14 +75,14 @@ class UserCanvasVersionService(CommonService): @DB.connection_context() def delete_all_versions(cls, user_canvas_id): try: - user_canvas_version = cls.model.select().where(cls.model.user_canvas_id == user_canvas_id).order_by( - cls.model.create_time.desc()) - if user_canvas_version.count() > 20: - delete_ids = [] - for i in range(20, user_canvas_version.count()): - delete_ids.append(user_canvas_version[i].id) + # Only get unpublished versions (False or None), keep all released versions + unpublished = cls.model.select().where(cls.model.user_canvas_id == user_canvas_id, (~cls.model.release) | (cls.model.release.is_null(True))).order_by(cls.model.create_time.desc()) + # Only delete old unpublished versions beyond the limit + if unpublished.count() > 20: + delete_ids = [v.id for v in unpublished[20:]] cls.delete_by_ids(delete_ids) + return True except DoesNotExist: return None @@ -90,12 +91,15 @@ class UserCanvasVersionService(CommonService): @classmethod @DB.connection_context() - def save_or_replace_latest(cls, user_canvas_id, dsl, title=None, description=None): + def save_or_replace_latest(cls, user_canvas_id, dsl, title=None, description=None, release=None): """ Persist a canvas snapshot into version history. If the latest version has the same DSL content, update that version in place instead of creating a new row. + + Exception: If the latest version is released (release=True) and current save is not, + create a new version to protect the released version. """ try: normalized_dsl = cls._normalize_dsl(dsl) @@ -107,11 +111,28 @@ class UserCanvasVersionService(CommonService): ) if latest and cls._normalize_dsl(latest.dsl) == normalized_dsl: + # Protect released version: if latest is released and current is not, + # create a new version instead of updating + if latest.release and not release: + insert_data = {"user_canvas_id": user_canvas_id, "dsl": normalized_dsl} + if title is not None: + insert_data["title"] = title + if description is not None: + insert_data["description"] = description + if release is not None: + insert_data["release"] = release + cls.insert(**insert_data) + cls.delete_all_versions(user_canvas_id) + return None, True + + # Normal case: update existing version update_data = {"dsl": normalized_dsl} if title is not None: update_data["title"] = title if description is not None: update_data["description"] = description + if release is not None: + update_data["release"] = release cls.update_by_id(latest.id, update_data) cls.delete_all_versions(user_canvas_id) return latest.id, False @@ -121,6 +142,8 @@ class UserCanvasVersionService(CommonService): insert_data["title"] = title if description is not None: insert_data["description"] = description + if release is not None: + insert_data["release"] = release cls.insert(**insert_data) cls.delete_all_versions(user_canvas_id) return None, True diff --git a/web/src/components/home-card.tsx b/web/src/components/home-card.tsx index 7320960b9..9e57a355f 100644 --- a/web/src/components/home-card.tsx +++ b/web/src/components/home-card.tsx @@ -2,6 +2,7 @@ import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { formatDate } from '@/utils/date'; import { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; interface IProps { data: { @@ -9,6 +10,7 @@ interface IProps { description?: string; avatar?: string; update_time?: string | number; + release_time?: number; }; onClick?: () => void; moreDropdown: React.ReactNode; @@ -16,6 +18,12 @@ interface IProps { icon?: React.ReactNode; testId?: string; } + +function Time({ time }: { time: string | number | undefined }) { + return ( +
{formatDate(time)}
+ ); +} export function HomeCard({ data, onClick, @@ -24,6 +32,8 @@ export function HomeCard({ icon, testId, }: IProps) { + const { t } = useTranslation(); + return (- {formatDate(data.update_time)} -
+ {data.release_time ? ( +