2026-07-18 21:58:15 +08:00
package projects
import (
"errors"
2026-07-19 21:46:43 +08:00
"fmt"
2026-07-18 21:58:15 +08:00
"strings"
2026-07-19 21:46:43 +08:00
"time"
2026-07-18 21:58:15 +08:00
2026-07-21 00:46:09 +08:00
"gorm.io/gorm"
2026-07-18 21:58:15 +08:00
"senlinai-agent/backend/internal/models"
)
type Service struct {
}
type Dashboard struct {
ProjectID uint ` json:"project_id" `
PendingInboxCount int64 ` json:"pending_inbox_count" `
OpenTaskCount int64 ` json:"open_task_count" `
RecentNoteCount int64 ` json:"recent_note_count" `
RecentSessionCount int64 ` json:"recent_session_count" `
}
2026-07-19 21:46:43 +08:00
type WorkbenchProject struct {
ID uint ` json:"id" `
Name string ` json:"name" `
2026-07-21 00:46:09 +08:00
Identifier string ` json:"identifier" `
Icon string ` json:"icon" `
Background string ` json:"background" `
2026-07-19 21:46:43 +08:00
Description string ` json:"description" `
Initials string ` json:"initials" `
UnreadCount int64 ` json:"unreadCount" `
}
type WorkbenchChannel struct {
ID string ` json:"id" `
ProjectID uint ` json:"projectID" `
Type string ` json:"type" `
Title string ` json:"title" `
Icon string ` json:"icon" `
Count * int64 ` json:"count,omitempty" `
URL string ` json:"url,omitempty" `
SortOrder int ` json:"sortOrder" `
}
type InboxMessage struct {
ID string ` json:"id" `
Source string ` json:"source" `
Title string ` json:"title" `
Summary string ` json:"summary" `
Status string ` json:"status" `
Tag string ` json:"tag" `
Time string ` json:"time" `
}
type WorkTask struct {
2026-07-21 01:00:27 +08:00
ID string ` json:"id" `
Title string ` json:"title" `
Summary string ` json:"summary" `
Completed bool ` json:"completed" `
Owner string ` json:"owner" `
Due string ` json:"due" `
CreatedAt string ` json:"createdAt" `
CompletedAt string ` json:"completedAt" `
Duration string ` json:"duration" `
Tag string ` json:"tag" `
2026-07-19 21:46:43 +08:00
}
type AISessionItem struct {
ID string ` json:"id" `
Title string ` json:"title" `
Summary string ` json:"summary" `
UpdatedAt string ` json:"updatedAt" `
References [ ] string ` json:"references" `
}
type NoteSourceItem struct {
ID string ` json:"id" `
Kind string ` json:"kind" `
Title string ` json:"title" `
UpdatedAt string ` json:"updatedAt" `
Tag string ` json:"tag" `
Source string ` json:"source" `
}
type CronPlan struct {
ID string ` json:"id" `
Title string ` json:"title" `
Schedule string ` json:"schedule" `
NextRun string ` json:"nextRun" `
Enabled bool ` json:"enabled" `
LastResult string ` json:"lastResult" `
Owner string ` json:"owner" `
}
type ProjectWorkspace struct {
Project WorkbenchProject ` json:"project" `
Channels [ ] WorkbenchChannel ` json:"channels" `
Tags [ ] string ` json:"tags" `
RecentSessions [ ] AISessionItem ` json:"recentSessions" `
Inbox [ ] InboxMessage ` json:"inbox" `
Tasks [ ] WorkTask ` json:"tasks" `
AISessions [ ] AISessionItem ` json:"aiSessions" `
NotesSources [ ] NoteSourceItem ` json:"notesSources" `
CronPlans [ ] CronPlan ` json:"cronPlans" `
}
2026-07-21 00:46:09 +08:00
type CreateProjectInput struct {
Name string
Identifier string
Icon string
Background string
Description string
}
2026-07-20 21:45:36 +08:00
type CreateTaskInput struct {
Title string
Description string
Status string
DueAt * time . Time
2026-07-21 00:46:09 +08:00
Tag string
}
type UpdateTaskInput struct {
Title string
Description string
Status string
Completed bool
NextProjectID uint
Tag string
2026-07-20 21:45:36 +08:00
}
type CreateFileSourceInput struct {
Title string
FilePath string
}
type CreateCronPlanInput struct {
Title string
Schedule string
Enabled bool
NextRunAt * time . Time
}
2026-07-18 21:58:15 +08:00
func NewService ( ) * Service {
return & Service { }
}
func ( s * Service ) CreateProject ( ownerID uint , name string , description string ) ( * models . SenlinAgentProject , error ) {
2026-07-21 00:46:09 +08:00
return s . CreateProjectWithInput ( ownerID , CreateProjectInput { Name : name , Description : description } )
}
func ( s * Service ) CreateProjectWithInput ( ownerID uint , input CreateProjectInput ) ( * models . SenlinAgentProject , error ) {
name := strings . TrimSpace ( input . Name )
2026-07-18 21:58:15 +08:00
if name == "" {
return nil , errors . New ( "project name is required" )
}
2026-07-21 00:46:09 +08:00
identifier := strings . TrimSpace ( input . Identifier )
if identifier == "" {
identifier = projectInitials ( name )
}
project := & models . SenlinAgentProject {
OwnerID : ownerID ,
Name : name ,
Identifier : identifier ,
Icon : strings . TrimSpace ( input . Icon ) ,
Background : strings . TrimSpace ( input . Background ) ,
Description : strings . TrimSpace ( input . Description ) ,
}
2026-07-18 21:58:15 +08:00
return project , models . DBService . Create ( project ) . Error
}
func ( s * Service ) ListProjects ( ownerID uint ) ( [ ] models . SenlinAgentProject , error ) {
var projects [ ] models . SenlinAgentProject
err := models . DBService . Where ( "owner_id = ?" , ownerID ) . Order ( "updated_at desc" ) . Find ( & projects ) . Error
return projects , err
}
2026-07-20 21:45:36 +08:00
func ( s * Service ) CreateTask ( ownerID uint , projectID uint , input CreateTaskInput ) ( * models . SenlinAgentTask , error ) {
if err := ensureProjectOwner ( ownerID , projectID ) ; err != nil {
return nil , err
}
title := strings . TrimSpace ( input . Title )
if title == "" {
return nil , errors . New ( "task title is required" )
}
status := strings . TrimSpace ( input . Status )
if status == "" {
status = "open"
}
2026-07-21 00:46:09 +08:00
tagID , err := s . findOrCreateTagID ( projectID , input . Tag )
if err != nil {
return nil , err
}
2026-07-20 21:45:36 +08:00
task := & models . SenlinAgentTask {
ProjectID : projectID ,
CreatedBy : ownerID ,
2026-07-21 00:46:09 +08:00
TagID : tagID ,
2026-07-20 21:45:36 +08:00
Title : title ,
Description : strings . TrimSpace ( input . Description ) ,
Status : status ,
DueAt : input . DueAt ,
}
return task , models . DBService . Create ( task ) . Error
}
2026-07-21 00:46:09 +08:00
func ( s * Service ) UpdateTask ( ownerID uint , projectID uint , taskID uint , input UpdateTaskInput ) ( * models . SenlinAgentTask , error ) {
if err := ensureProjectOwner ( ownerID , projectID ) ; err != nil {
return nil , err
}
var task models . SenlinAgentTask
if err := models . DBService . Where ( "id = ? AND project_id = ?" , taskID , projectID ) . First ( & task ) . Error ; err != nil {
return nil , err
}
nextProjectID := input . NextProjectID
if nextProjectID == 0 {
nextProjectID = task . ProjectID
}
if err := ensureProjectOwner ( ownerID , nextProjectID ) ; err != nil {
return nil , err
}
title := strings . TrimSpace ( input . Title )
if title == "" {
return nil , errors . New ( "task title is required" )
}
tagID , err := s . findOrCreateTagID ( nextProjectID , input . Tag )
if err != nil {
return nil , err
}
projectIdentity , err := projectIdentity ( nextProjectID )
if err != nil {
return nil , err
}
tagIdentity , err := optionalTagIdentity ( tagID )
if err != nil {
return nil , err
}
status := strings . TrimSpace ( input . Status )
if status == "" {
if input . Completed {
status = "done"
} else {
status = "open"
}
}
task . ProjectID = nextProjectID
task . ProjectIdentity = projectIdentity
task . Title = title
task . Description = strings . TrimSpace ( input . Description )
task . Status = status
task . TagID = tagID
task . TagIdentity = tagIdentity
if err := models . DBService . Save ( & task ) . Error ; err != nil {
return nil , err
}
return & task , nil
}
2026-07-20 21:45:36 +08:00
func ( s * Service ) CreateFileSource ( ownerID uint , projectID uint , input CreateFileSourceInput ) ( * models . SenlinAgentSource , error ) {
if err := ensureProjectOwner ( ownerID , projectID ) ; err != nil {
return nil , err
}
title := strings . TrimSpace ( input . Title )
if title == "" {
return nil , errors . New ( "source title is required" )
}
filePath := strings . TrimSpace ( input . FilePath )
if filePath == "" {
return nil , errors . New ( "source file path is required" )
}
source := & models . SenlinAgentSource {
ProjectID : projectID ,
CreatedBy : ownerID ,
Kind : "file" ,
Title : title ,
FilePath : filePath ,
}
return source , models . DBService . Create ( source ) . Error
}
func ( s * Service ) CreateCronPlan ( ownerID uint , projectID uint , input CreateCronPlanInput ) ( * models . SenlinAgentCronPlan , error ) {
if err := ensureProjectOwner ( ownerID , projectID ) ; err != nil {
return nil , err
}
title := strings . TrimSpace ( input . Title )
if title == "" {
return nil , errors . New ( "cron plan title is required" )
}
schedule := strings . TrimSpace ( input . Schedule )
if schedule == "" {
return nil , errors . New ( "cron schedule is required" )
}
plan := & models . SenlinAgentCronPlan {
ProjectID : projectID ,
CreatedBy : ownerID ,
Title : title ,
Schedule : schedule ,
Enabled : input . Enabled ,
NextRunAt : input . NextRunAt ,
LastResult : "Not run yet" ,
}
return plan , models . DBService . Create ( plan ) . Error
}
2026-07-21 00:46:09 +08:00
func ( s * Service ) CreateProjectTag ( ownerID uint , projectID uint , name string ) ( * models . SenlinAgentTag , error ) {
if err := ensureProjectOwner ( ownerID , projectID ) ; err != nil {
return nil , err
}
return s . CreateTag ( projectID , name )
}
2026-07-18 21:58:15 +08:00
func ( s * Service ) CreateTag ( projectID uint , name string ) ( * models . SenlinAgentTag , error ) {
name = strings . TrimSpace ( name )
if name == "" {
return nil , errors . New ( "tag name is required" )
}
2026-07-21 00:46:09 +08:00
var existing models . SenlinAgentTag
err := models . DBService . Where ( "project_id = ? AND name = ?" , projectID , name ) . First ( & existing ) . Error
if err == nil {
return & existing , nil
}
if err != gorm . ErrRecordNotFound {
return nil , err
}
2026-07-18 21:58:15 +08:00
tag := & models . SenlinAgentTag { ProjectID : projectID , Name : name }
return tag , models . DBService . Create ( tag ) . Error
}
2026-07-21 00:46:09 +08:00
func ( s * Service ) ListProjectTags ( ownerID uint , projectID uint ) ( [ ] models . SenlinAgentTag , error ) {
if err := ensureProjectOwner ( ownerID , projectID ) ; err != nil {
return nil , err
}
return s . ListTags ( projectID )
}
2026-07-18 21:58:15 +08:00
func ( s * Service ) ListTags ( projectID uint ) ( [ ] models . SenlinAgentTag , error ) {
var tags [ ] models . SenlinAgentTag
err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "name asc" ) . Find ( & tags ) . Error
return tags , err
}
2026-07-21 00:46:09 +08:00
func ( s * Service ) findOrCreateTagID ( projectID uint , name string ) ( * uint , error ) {
name = strings . TrimSpace ( name )
if name == "" {
return nil , nil
}
tag , err := s . CreateTag ( projectID , name )
if err != nil {
return nil , err
}
return & tag . ID , nil
}
2026-07-18 21:58:15 +08:00
func ( s * Service ) Dashboard ( ownerID uint , projectID uint ) ( Dashboard , error ) {
2026-07-20 21:45:36 +08:00
if err := ensureProjectOwner ( ownerID , projectID ) ; err != nil {
2026-07-18 21:58:15 +08:00
return Dashboard { } , err
}
dashboard := Dashboard { ProjectID : projectID }
if err := models . DBService . Model ( & models . SenlinAgentInboxItem { } ) . Where ( "project_id = ? AND status = ?" , projectID , "open" ) . Count ( & dashboard . PendingInboxCount ) . Error ; err != nil {
return dashboard , err
}
if err := models . DBService . Model ( & models . SenlinAgentTask { } ) . Where ( "project_id = ? AND status <> ?" , projectID , "done" ) . Count ( & dashboard . OpenTaskCount ) . Error ; err != nil {
return dashboard , err
}
if err := models . DBService . Model ( & models . SenlinAgentNote { } ) . Where ( "project_id = ?" , projectID ) . Count ( & dashboard . RecentNoteCount ) . Error ; err != nil {
return dashboard , err
}
if err := models . DBService . Model ( & models . SenlinAgentAISession { } ) . Where ( "project_id = ?" , projectID ) . Count ( & dashboard . RecentSessionCount ) . Error ; err != nil {
return dashboard , err
}
return dashboard , nil
}
2026-07-19 21:46:43 +08:00
func ( s * Service ) Workspace ( ownerID uint , projectID uint ) ( ProjectWorkspace , error ) {
var project models . SenlinAgentProject
if err := models . DBService . Where ( "id = ? AND owner_id = ?" , projectID , ownerID ) . First ( & project ) . Error ; err != nil {
return ProjectWorkspace { } , err
}
counts , err := s . workspaceCounts ( projectID )
if err != nil {
return ProjectWorkspace { } , err
}
tags , err := s . workspaceTags ( projectID )
if err != nil {
return ProjectWorkspace { } , err
}
channels , err := s . workspaceChannels ( projectID , counts )
if err != nil {
return ProjectWorkspace { } , err
}
inboxItems , err := s . workspaceInbox ( projectID )
if err != nil {
return ProjectWorkspace { } , err
}
tasks , err := s . workspaceTasks ( projectID )
if err != nil {
return ProjectWorkspace { } , err
}
aiSessions , err := s . workspaceAISessions ( projectID )
if err != nil {
return ProjectWorkspace { } , err
}
notesSources , err := s . workspaceNotesSources ( projectID )
if err != nil {
return ProjectWorkspace { } , err
}
cronPlans , err := s . workspaceCronPlans ( projectID )
if err != nil {
return ProjectWorkspace { } , err
}
return ProjectWorkspace {
Project : WorkbenchProject {
ID : project . ID ,
Name : project . Name ,
2026-07-21 00:46:09 +08:00
Identifier : project . Identifier ,
Icon : project . Icon ,
Background : project . Background ,
2026-07-19 21:46:43 +08:00
Description : project . Description ,
2026-07-21 00:46:09 +08:00
Initials : defaultString ( project . Identifier , projectInitials ( project . Name ) ) ,
2026-07-19 21:46:43 +08:00
UnreadCount : counts . inbox ,
} ,
Channels : channels ,
Tags : tags ,
RecentSessions : aiSessions ,
Inbox : inboxItems ,
Tasks : tasks ,
AISessions : aiSessions ,
NotesSources : notesSources ,
CronPlans : cronPlans ,
} , nil
}
2026-07-20 21:45:36 +08:00
func ensureProjectOwner ( ownerID uint , projectID uint ) error {
var count int64
if err := models . DBService . Model ( & models . SenlinAgentProject { } ) . Where ( "id = ? AND owner_id = ?" , projectID , ownerID ) . Count ( & count ) . Error ; err != nil {
return err
}
if count == 0 {
return errors . New ( "project not found" )
}
return nil
}
2026-07-19 21:46:43 +08:00
type workspaceCounts struct {
inbox int64
tasks int64
aiSessions int64
notesSources int64
cronPlans int64
}
func ( s * Service ) workspaceCounts ( projectID uint ) ( workspaceCounts , error ) {
var counts workspaceCounts
if err := models . DBService . Model ( & models . SenlinAgentInboxItem { } ) . Where ( "project_id = ? AND status = ?" , projectID , "open" ) . Count ( & counts . inbox ) . Error ; err != nil {
return counts , err
}
if err := models . DBService . Model ( & models . SenlinAgentTask { } ) . Where ( "project_id = ? AND status <> ?" , projectID , "done" ) . Count ( & counts . tasks ) . Error ; err != nil {
return counts , err
}
if err := models . DBService . Model ( & models . SenlinAgentAISession { } ) . Where ( "project_id = ?" , projectID ) . Count ( & counts . aiSessions ) . Error ; err != nil {
return counts , err
}
var noteCount int64
if err := models . DBService . Model ( & models . SenlinAgentNote { } ) . Where ( "project_id = ?" , projectID ) . Count ( & noteCount ) . Error ; err != nil {
return counts , err
}
var sourceCount int64
if err := models . DBService . Model ( & models . SenlinAgentSource { } ) . Where ( "project_id = ?" , projectID ) . Count ( & sourceCount ) . Error ; err != nil {
return counts , err
}
counts . notesSources = noteCount + sourceCount
if err := models . DBService . Model ( & models . SenlinAgentCronPlan { } ) . Where ( "project_id = ?" , projectID ) . Count ( & counts . cronPlans ) . Error ; err != nil {
return counts , err
}
return counts , nil
}
func ( s * Service ) workspaceTags ( projectID uint ) ( [ ] string , error ) {
var tags [ ] models . SenlinAgentTag
if err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "name asc" ) . Find ( & tags ) . Error ; err != nil {
return nil , err
}
names := [ ] string { "all" }
for _ , tag := range tags {
names = append ( names , tag . Name )
}
return names , nil
}
func ( s * Service ) workspaceChannels ( projectID uint , counts workspaceCounts ) ( [ ] WorkbenchChannel , error ) {
total := counts . inbox + counts . tasks + counts . aiSessions + counts . notesSources + counts . cronPlans
channels := [ ] WorkbenchChannel {
{ ID : fmt . Sprintf ( "%d-overview" , projectID ) , ProjectID : projectID , Type : "overview" , Title : "Overview" , Icon : "home" , Count : & total , SortOrder : 1 } ,
{ ID : fmt . Sprintf ( "%d-inbox" , projectID ) , ProjectID : projectID , Type : "inbox" , Title : "Message Flow" , Icon : "mail" , Count : & counts . inbox , SortOrder : 2 } ,
{ ID : fmt . Sprintf ( "%d-tasks" , projectID ) , ProjectID : projectID , Type : "tasks" , Title : "Work Plan" , Icon : "list" , Count : & counts . tasks , SortOrder : 3 } ,
{ ID : fmt . Sprintf ( "%d-ai" , projectID ) , ProjectID : projectID , Type : "ai_sessions" , Title : "AI Sessions" , Icon : "sparkles" , Count : & counts . aiSessions , SortOrder : 4 } ,
{ ID : fmt . Sprintf ( "%d-notes" , projectID ) , ProjectID : projectID , Type : "notes_sources" , Title : "Notes & Sources" , Icon : "file" , Count : & counts . notesSources , SortOrder : 5 } ,
{ ID : fmt . Sprintf ( "%d-cron" , projectID ) , ProjectID : projectID , Type : "cron" , Title : "Cron Plans" , Icon : "clock" , Count : & counts . cronPlans , SortOrder : 6 } ,
}
var customChannels [ ] models . SenlinAgentProjectChannel
if err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "sort_order asc, id asc" ) . Find ( & customChannels ) . Error ; err != nil {
return nil , err
}
for _ , channel := range customChannels {
channels = append ( channels , WorkbenchChannel {
ID : fmt . Sprintf ( "%d-custom-%d" , projectID , channel . ID ) ,
ProjectID : projectID ,
Type : "custom_link" ,
Title : channel . Title ,
Icon : channel . Icon ,
URL : channel . URL ,
SortOrder : channel . SortOrder ,
} )
}
return channels , nil
}
func ( s * Service ) workspaceInbox ( projectID uint ) ( [ ] InboxMessage , error ) {
var items [ ] models . SenlinAgentInboxItem
if err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "updated_at desc, id desc" ) . Limit ( 50 ) . Find ( & items ) . Error ; err != nil {
return nil , err
}
messages := make ( [ ] InboxMessage , 0 , len ( items ) )
for _ , item := range items {
messages = append ( messages , InboxMessage {
ID : fmt . Sprint ( item . ID ) ,
Source : item . SourceType ,
Title : item . Title ,
Summary : item . Body ,
Status : item . Status ,
Tag : "" ,
Time : displayTime ( item . UpdatedAt ) ,
} )
}
return messages , nil
}
func ( s * Service ) workspaceTasks ( projectID uint ) ( [ ] WorkTask , error ) {
var tasks [ ] models . SenlinAgentTask
if err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "CASE WHEN status = 'done' THEN 1 ELSE 0 END asc" ) . Order ( "sort_order asc, updated_at desc, id desc" ) . Limit ( 50 ) . Find ( & tasks ) . Error ; err != nil {
return nil , err
}
2026-07-21 00:46:09 +08:00
tagNames , err := taskTagNames ( tasks )
if err != nil {
return nil , err
}
2026-07-19 21:46:43 +08:00
items := make ( [ ] WorkTask , 0 , len ( tasks ) )
for _ , task := range tasks {
owner , err := userDisplayName ( task . AssigneeID , task . CreatedBy )
if err != nil {
return nil , err
}
2026-07-21 01:00:27 +08:00
completed := task . Status == "done"
completedAt := ""
duration := ""
if completed {
completedAt = displayTime ( task . UpdatedAt )
duration = displayDuration ( task . CreatedAt , task . UpdatedAt )
}
2026-07-19 21:46:43 +08:00
items = append ( items , WorkTask {
2026-07-21 01:00:27 +08:00
ID : fmt . Sprint ( task . ID ) ,
Title : task . Title ,
Summary : task . Description ,
Completed : completed ,
Owner : owner ,
Due : displayOptionalTime ( task . DueAt ) ,
CreatedAt : displayTime ( task . CreatedAt ) ,
CompletedAt : completedAt ,
Duration : duration ,
Tag : tagNames [ task . ID ] ,
2026-07-19 21:46:43 +08:00
} )
}
return items , nil
}
2026-07-21 00:46:09 +08:00
func taskTagNames ( tasks [ ] models . SenlinAgentTask ) ( map [ uint ] string , error ) {
names := make ( map [ uint ] string )
tagIDs := make ( [ ] uint , 0 )
for _ , task := range tasks {
if task . TagID != nil {
tagIDs = append ( tagIDs , * task . TagID )
}
}
if len ( tagIDs ) == 0 {
return names , nil
}
var tags [ ] models . SenlinAgentTag
if err := models . DBService . Where ( "id IN ?" , tagIDs ) . Find ( & tags ) . Error ; err != nil {
return nil , err
}
tagByID := make ( map [ uint ] string , len ( tags ) )
for _ , tag := range tags {
tagByID [ tag . ID ] = tag . Name
}
for _ , task := range tasks {
if task . TagID != nil {
names [ task . ID ] = tagByID [ * task . TagID ]
}
}
return names , nil
}
func projectIdentity ( projectID uint ) ( string , error ) {
var project models . SenlinAgentProject
if err := models . DBService . Select ( "identity" ) . First ( & project , projectID ) . Error ; err != nil {
return "" , err
}
return project . Identity , nil
}
func optionalTagIdentity ( tagID * uint ) ( * string , error ) {
if tagID == nil {
return nil , nil
}
var tag models . SenlinAgentTag
if err := models . DBService . Select ( "identity" ) . First ( & tag , * tagID ) . Error ; err != nil {
return nil , err
}
return & tag . Identity , nil
}
2026-07-19 21:46:43 +08:00
func ( s * Service ) workspaceAISessions ( projectID uint ) ( [ ] AISessionItem , error ) {
var sessions [ ] models . SenlinAgentAISession
if err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "updated_at desc, id desc" ) . Limit ( 50 ) . Find ( & sessions ) . Error ; err != nil {
return nil , err
}
items := make ( [ ] AISessionItem , 0 , len ( sessions ) )
for _ , session := range sessions {
items = append ( items , AISessionItem {
ID : fmt . Sprint ( session . ID ) ,
Title : session . Title ,
Summary : session . Context ,
UpdatedAt : displayTime ( session . UpdatedAt ) ,
References : [ ] string { } ,
} )
}
return items , nil
}
func ( s * Service ) workspaceNotesSources ( projectID uint ) ( [ ] NoteSourceItem , error ) {
var notes [ ] models . SenlinAgentNote
if err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "updated_at desc, id desc" ) . Limit ( 50 ) . Find ( & notes ) . Error ; err != nil {
return nil , err
}
items := make ( [ ] NoteSourceItem , 0 , len ( notes ) )
for _ , note := range notes {
items = append ( items , NoteSourceItem {
ID : fmt . Sprintf ( "note-%d" , note . ID ) ,
Kind : "note" ,
Title : note . Title ,
UpdatedAt : displayTime ( note . UpdatedAt ) ,
Tag : "" ,
Source : "Markdown" ,
} )
}
var sources [ ] models . SenlinAgentSource
if err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "updated_at desc, id desc" ) . Limit ( 50 ) . Find ( & sources ) . Error ; err != nil {
return nil , err
}
for _ , source := range sources {
items = append ( items , NoteSourceItem {
ID : fmt . Sprintf ( "%s-%d" , source . Kind , source . ID ) ,
Kind : source . Kind ,
Title : source . Title ,
UpdatedAt : displayTime ( source . UpdatedAt ) ,
Tag : "" ,
Source : sourceDisplayName ( source ) ,
} )
}
return items , nil
}
func ( s * Service ) workspaceCronPlans ( projectID uint ) ( [ ] CronPlan , error ) {
var plans [ ] models . SenlinAgentCronPlan
if err := models . DBService . Where ( "project_id = ?" , projectID ) . Order ( "enabled desc, updated_at desc, id desc" ) . Limit ( 50 ) . Find ( & plans ) . Error ; err != nil {
return nil , err
}
items := make ( [ ] CronPlan , 0 , len ( plans ) )
for _ , plan := range plans {
owner , err := userDisplayName ( nil , plan . CreatedBy )
if err != nil {
return nil , err
}
items = append ( items , CronPlan {
ID : fmt . Sprint ( plan . ID ) ,
Title : plan . Title ,
Schedule : plan . Schedule ,
NextRun : displayOptionalTime ( plan . NextRunAt ) ,
Enabled : plan . Enabled ,
LastResult : defaultString ( plan . LastResult , "Not run yet" ) ,
Owner : owner ,
} )
}
return items , nil
}
func projectInitials ( name string ) string {
words := strings . Fields ( name )
if len ( words ) == 0 {
return "P"
}
for i := len ( words ) - 1 ; i >= 0 ; i -- {
word := strings . Trim ( words [ i ] , "#_-" )
if isShortCode ( word ) {
return strings . ToUpper ( word )
}
}
if len ( words ) == 1 {
runes := [ ] rune ( words [ 0 ] )
if len ( runes ) == 1 {
return strings . ToUpper ( string ( runes [ 0 ] ) )
}
return strings . ToUpper ( string ( runes [ : 2 ] ) )
}
return strings . ToUpper ( string ( [ ] rune ( words [ 0 ] ) [ 0 ] ) + string ( [ ] rune ( words [ len ( words ) - 1 ] ) [ 0 ] ) )
}
func isShortCode ( value string ) bool {
runes := [ ] rune ( value )
if len ( runes ) == 0 || len ( runes ) > 3 {
return false
}
hasDigit := false
for _ , r := range runes {
if r >= '0' && r <= '9' {
hasDigit = true
continue
}
if ( r >= 'a' && r <= 'z' ) || ( r >= 'A' && r <= 'Z' ) {
continue
}
return false
}
return hasDigit
}
func displayTime ( value time . Time ) string {
if value . IsZero ( ) {
return ""
}
return value . UTC ( ) . Format ( "2006-01-02 15:04" )
}
func displayOptionalTime ( value * time . Time ) string {
if value == nil {
return ""
}
return displayTime ( * value )
}
2026-07-21 01:00:27 +08:00
func displayDuration ( start time . Time , end time . Time ) string {
if start . IsZero ( ) || end . IsZero ( ) || end . Before ( start ) {
return ""
}
duration := end . Sub ( start )
days := int ( duration . Hours ( ) ) / 24
if days > 0 {
return fmt . Sprintf ( "%d 天" , days )
}
hours := int ( duration . Hours ( ) )
if hours > 0 {
return fmt . Sprintf ( "%d 小时" , hours )
}
minutes := int ( duration . Minutes ( ) )
if minutes > 0 {
return fmt . Sprintf ( "%d 分钟" , minutes )
}
return "1 分钟内"
}
2026-07-19 21:46:43 +08:00
func sourceDisplayName ( source models . SenlinAgentSource ) string {
switch source . Kind {
case "file" :
return "Attachment"
case "link" :
return "URL"
default :
return strings . Title ( source . Kind )
}
}
func userDisplayName ( assigneeID * uint , createdBy uint ) ( string , error ) {
userID := createdBy
if assigneeID != nil && * assigneeID != 0 {
userID = * assigneeID
}
var user models . SenlinAgentUser
if err := models . DBService . Select ( "display_name" ) . Where ( "id = ?" , userID ) . First ( & user ) . Error ; err != nil {
return fmt . Sprintf ( "User %d" , userID ) , nil
}
return user . DisplayName , nil
}
func defaultString ( value string , fallback string ) string {
if strings . TrimSpace ( value ) == "" {
return fallback
}
return value
}