From 774020fe75e031acd093c38beaca2e5df55943f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Thu, 11 Jun 2026 15:15:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=97=A5=E7=A8=8B=E7=9A=84ai?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/my_work/agents/function_tool.go | 16 + backend/my_work/agents/ops_ai_assistant.go | 336 +++++++++++++++++++++ backend/my_work/models/configs.go | 4 + backend/my_work/routers/apiAIChat.go | 2 +- backend/my_work/routers/apiAIChatConfig.go | 10 +- backend/my_work/routers/apiCalendar.go | 209 ++++++++++++- 6 files changed, 563 insertions(+), 14 deletions(-) diff --git a/backend/my_work/agents/function_tool.go b/backend/my_work/agents/function_tool.go index 098574a..0931f5c 100644 --- a/backend/my_work/agents/function_tool.go +++ b/backend/my_work/agents/function_tool.go @@ -33,6 +33,14 @@ func FunctionToolSchemas(configs []ToolConfig) []FunctionToolSchema { tools = append(tools, timeFunctionToolSchema()) case "ops_ai_assistant_schedule_query", "ops_ai_assistant": tools = append(tools, opsAIAssistantScheduleQuerySchema()) + case "ops_ai_assistant_calendar_list": + tools = append(tools, opsAIAssistantCalendarListSchema()) + case "ops_ai_assistant_schedule_create": + tools = append(tools, opsAIAssistantScheduleCreateSchema()) + case "ops_ai_assistant_schedule_update": + tools = append(tools, opsAIAssistantScheduleUpdateSchema()) + case "ops_ai_assistant_schedule_delete": + tools = append(tools, opsAIAssistantScheduleDeleteSchema()) case "ops_ai_assistant_current_user": tools = append(tools, opsAIAssistantCurrentUserSchema()) case "ops_ai_assistant_purchase_query": @@ -64,6 +72,14 @@ func ExecuteFunctionTool(ctx context.Context, runtime FunctionToolRuntime, name return json.Marshal(result) case "ops_ai_assistant_schedule_query", "ops_ai_assistant": return executeOpsAIAssistantScheduleQuery(ctx, runtime, rawArgs) + case "ops_ai_assistant_calendar_list": + return executeOpsAIAssistantCalendarList(ctx, runtime, rawArgs) + case "ops_ai_assistant_schedule_create": + return executeOpsAIAssistantScheduleCreate(ctx, runtime, rawArgs) + case "ops_ai_assistant_schedule_update": + return executeOpsAIAssistantScheduleUpdate(ctx, runtime, rawArgs) + case "ops_ai_assistant_schedule_delete": + return executeOpsAIAssistantScheduleDelete(ctx, runtime, rawArgs) case "ops_ai_assistant_current_user": return executeOpsAIAssistantCurrentUser(ctx, runtime, rawArgs) case "ops_ai_assistant_purchase_query": diff --git a/backend/my_work/agents/ops_ai_assistant.go b/backend/my_work/agents/ops_ai_assistant.go index 349dd1f..4b3a5d3 100644 --- a/backend/my_work/agents/ops_ai_assistant.go +++ b/backend/my_work/agents/ops_ai_assistant.go @@ -114,6 +114,35 @@ type ScheduleCalendar struct { Name string `json:"name"` } +type CalendarListOutput struct { + ID uint `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Color string `json:"color,omitempty"` + IsPublic bool `json:"is_public"` + CanEdit bool `json:"canEdit"` +} + +type CreateScheduleInput struct { + CalendarID uint + Title string + StartDate string + EndDate string + ScheduleType string + IsPublic bool + Remark string +} + +type UpdateScheduleInput struct { + EventID uint + Title string + StartDate string + EndDate string + ScheduleType string + IsPublic bool + Remark string +} + type ScheduleEvent struct { ID uint `json:"id"` CalendarID uint `json:"calendar_id"` @@ -138,6 +167,10 @@ type ScheduleQuery struct { type ScheduleProvider interface { QuerySchedules(ctx context.Context, query ScheduleQuery) ([]ScheduleEvent, error) + ListCalendars(ctx context.Context, userID uint) ([]CalendarListOutput, error) + CreateScheduleEvent(ctx context.Context, userID uint, input CreateScheduleInput) (*ScheduleEvent, error) + UpdateScheduleEvent(ctx context.Context, userID uint, input UpdateScheduleInput) (*ScheduleEvent, error) + DeleteScheduleEvent(ctx context.Context, userID uint, eventID uint) error } type PurchaseProvider interface { @@ -388,3 +421,306 @@ func executeOpsAIAssistantScheduleQuery(ctx context.Context, runtime FunctionToo "events": events, }) } + +type CalendarListArgs struct { + Action string `json:"action,omitempty"` +} + +type ScheduleEventCreateArgs struct { + Action string `json:"action,omitempty"` + CalendarID uint `json:"calendar_id"` + Title string `json:"title"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + ScheduleType string `json:"schedule_type,omitempty"` + IsPublic bool `json:"is_public,omitempty"` + Remark string `json:"remark,omitempty"` +} + +type ScheduleEventUpdateArgs struct { + Action string `json:"action,omitempty"` + EventID uint `json:"event_id"` + Title string `json:"title"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + ScheduleType string `json:"schedule_type,omitempty"` + IsPublic bool `json:"is_public,omitempty"` + Remark string `json:"remark,omitempty"` +} + +type ScheduleEventDeleteArgs struct { + Action string `json:"action,omitempty"` + EventID uint `json:"event_id"` +} + +func opsAIAssistantCalendarListSchema() FunctionToolSchema { + return FunctionToolSchema{ + Name: "ops_ai_assistant_calendar_list", + Description: "只读工具:查询当前用户可见的 OPS 日历列表。创建/修改日程前如用户未提供 calendar_id,应先调用本工具让用户选择日历。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "action": map[string]interface{}{ + "type": "string", + "enum": []string{"list"}, + "description": "固定为 list。", + }, + }, + "required": []string{"action"}, + }, + } +} + +func opsAIAssistantScheduleCreateSchema() FunctionToolSchema { + return FunctionToolSchema{ + Name: "ops_ai_assistant_schedule_create", + Description: "写入工具:在指定 OPS 日历中创建日程。仅当用户明确要求新增/创建/添加日程且已提供日历 ID、标题和明确日期范围时调用;相对日期需先调用 time 获取 start_date/end_date。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": scheduleEventWriteProperties(), + "required": []string{"action", "calendar_id", "title", "start_date", "end_date"}, + }, + } +} + +func opsAIAssistantScheduleUpdateSchema() FunctionToolSchema { + props := scheduleEventWriteProperties() + props["event_id"] = map[string]interface{}{"type": "integer", "description": "要修改的日程事件 ID。"} + delete(props, "calendar_id") + props["action"] = map[string]interface{}{ + "type": "string", + "enum": []string{"update"}, + "description": "固定为 update。", + } + return FunctionToolSchema{ + Name: "ops_ai_assistant_schedule_update", + Description: "写入工具:修改指定 OPS 日程事件。仅当用户明确要求修改/更新已有日程且已提供事件 ID、标题和明确日期范围时调用;相对日期需先调用 time 获取 start_date/end_date。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": props, + "required": []string{"action", "event_id", "title", "start_date", "end_date"}, + }, + } +} + +func opsAIAssistantScheduleDeleteSchema() FunctionToolSchema { + return FunctionToolSchema{ + Name: "ops_ai_assistant_schedule_delete", + Description: "写入工具:删除指定 OPS 日程事件。仅当用户明确要求删除/移除日程且已提供事件 ID 时调用。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "action": map[string]interface{}{ + "type": "string", + "enum": []string{"delete"}, + "description": "固定为 delete。", + }, + "event_id": map[string]interface{}{"type": "integer", "description": "要删除的日程事件 ID。"}, + }, + "required": []string{"action", "event_id"}, + }, + } +} + +func scheduleEventWriteProperties() map[string]interface{} { + return map[string]interface{}{ + "action": map[string]interface{}{ + "type": "string", + "enum": []string{"create"}, + "description": "固定为 create。", + }, + "calendar_id": map[string]interface{}{"type": "integer", "description": "目标日历 ID。"}, + "title": map[string]interface{}{"type": "string", "description": "日程标题。"}, + "start_date": map[string]interface{}{"type": "string", "description": "开始日期,格式 YYYY-MM-DD。"}, + "end_date": map[string]interface{}{"type": "string", "description": "结束日期,格式 YYYY-MM-DD。"}, + "schedule_type": map[string]interface{}{"type": "string", "enum": []string{"", "work", "duty", "exam", "standby", "personal_holiday", "public_holiday"}, "description": "日程类型,默认 work。"}, + "is_public": map[string]interface{}{"type": "boolean", "description": "是否公开,默认 false。"}, + "remark": map[string]interface{}{"type": "string", "description": "备注。"}, + } +} + +func executeOpsAIAssistantCalendarList(ctx context.Context, runtime FunctionToolRuntime, rawArgs []byte) ([]byte, error) { + var args CalendarListArgs + if len(rawArgs) > 0 { + if err := json.Unmarshal(rawArgs, &args); err != nil { + return nil, err + } + } + if args.Action != "" && args.Action != "list" { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "ops_ai_assistant_calendar_list 仅允许 list 查询操作", + }) + } + if runtime.UserID <= 0 { + return json.Marshal(map[string]interface{}{ + "ok": true, + "loggedIn": false, + "message": "需要登录才能查询日历列表。", + }) + } + if registeredScheduleProvider == nil { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "日历服务未注册", + }) + } + calendars, err := registeredScheduleProvider.ListCalendars(ctx, runtime.UserID) + if err != nil { + return nil, err + } + if calendars == nil { + calendars = []CalendarListOutput{} + } + return json.Marshal(map[string]interface{}{ + "ok": true, + "loggedIn": true, + "count": len(calendars), + "calendars": calendars, + }) +} + +func executeOpsAIAssistantScheduleCreate(ctx context.Context, runtime FunctionToolRuntime, rawArgs []byte) ([]byte, error) { + var args ScheduleEventCreateArgs + if err := json.Unmarshal(rawArgs, &args); err != nil { + return nil, err + } + if args.Action != "" && args.Action != "create" { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "ops_ai_assistant_schedule_create 仅允许 create 操作", + }) + } + if err := validateScheduleWrite(runtime.UserID, args.Title, args.StartDate, args.EndDate); err != nil { + return nil, err + } + if args.CalendarID <= 0 { + return nil, fmt.Errorf("calendar_id is required") + } + if registeredScheduleProvider == nil { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "日历服务未注册", + }) + } + event, err := registeredScheduleProvider.CreateScheduleEvent(ctx, runtime.UserID, CreateScheduleInput{ + CalendarID: args.CalendarID, + Title: args.Title, + StartDate: args.StartDate, + EndDate: args.EndDate, + ScheduleType: args.ScheduleType, + IsPublic: args.IsPublic, + Remark: args.Remark, + }) + if err != nil { + return nil, err + } + return json.Marshal(map[string]interface{}{ + "ok": true, + "loggedIn": true, + "event": event, + "message": "日程已创建。", + }) +} + +func executeOpsAIAssistantScheduleUpdate(ctx context.Context, runtime FunctionToolRuntime, rawArgs []byte) ([]byte, error) { + var args ScheduleEventUpdateArgs + if err := json.Unmarshal(rawArgs, &args); err != nil { + return nil, err + } + if args.Action != "" && args.Action != "update" { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "ops_ai_assistant_schedule_update 仅允许 update 操作", + }) + } + if args.EventID <= 0 { + return nil, fmt.Errorf("event_id is required") + } + if err := validateScheduleWrite(runtime.UserID, args.Title, args.StartDate, args.EndDate); err != nil { + return nil, err + } + if registeredScheduleProvider == nil { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "日历服务未注册", + }) + } + event, err := registeredScheduleProvider.UpdateScheduleEvent(ctx, runtime.UserID, UpdateScheduleInput{ + EventID: args.EventID, + Title: args.Title, + StartDate: args.StartDate, + EndDate: args.EndDate, + ScheduleType: args.ScheduleType, + IsPublic: args.IsPublic, + Remark: args.Remark, + }) + if err != nil { + return nil, err + } + return json.Marshal(map[string]interface{}{ + "ok": true, + "loggedIn": true, + "event": event, + "message": "日程已更新。", + }) +} + +func executeOpsAIAssistantScheduleDelete(ctx context.Context, runtime FunctionToolRuntime, rawArgs []byte) ([]byte, error) { + var args ScheduleEventDeleteArgs + if err := json.Unmarshal(rawArgs, &args); err != nil { + return nil, err + } + if args.Action != "" && args.Action != "delete" { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "ops_ai_assistant_schedule_delete 仅允许 delete 操作", + }) + } + if runtime.UserID <= 0 { + return json.Marshal(map[string]interface{}{ + "ok": true, + "loggedIn": false, + "message": "需要登录才能删除日程。", + }) + } + if args.EventID <= 0 { + return nil, fmt.Errorf("event_id is required") + } + if registeredScheduleProvider == nil { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "日历服务未注册", + }) + } + if err := registeredScheduleProvider.DeleteScheduleEvent(ctx, runtime.UserID, args.EventID); err != nil { + return nil, err + } + return json.Marshal(map[string]interface{}{ + "ok": true, + "loggedIn": true, + "event_id": args.EventID, + "message": "日程已删除。", + }) +} + +func validateScheduleWrite(userID uint, title string, startDate string, endDate string) error { + if userID <= 0 { + return fmt.Errorf("需要登录才能操作日程") + } + if title == "" { + return fmt.Errorf("title is required") + } + start, err := time.Parse("2006-01-02", startDate) + if err != nil { + return fmt.Errorf("invalid start_date: %w", err) + } + end, err := time.Parse("2006-01-02", endDate) + if err != nil { + return fmt.Errorf("invalid end_date: %w", err) + } + if end.Before(start) { + return fmt.Errorf("end_date must be after start_date") + } + return nil +} diff --git a/backend/my_work/models/configs.go b/backend/my_work/models/configs.go index b5788f0..ec050d8 100644 --- a/backend/my_work/models/configs.go +++ b/backend/my_work/models/configs.go @@ -108,7 +108,11 @@ func ConfigAllInit() error { {Name: "time", Enabled: true, Description: "解析当前时间、相对日期和日期范围。"}, {Name: "ops_ai_assistant_current_user", Enabled: true, Description: "返回当前登录用户信息;未登录时提示需要登录才能获取信息。"}, {Name: "ops_ai_assistant_purchase_query", Enabled: true, Description: "查询采购订单列表、详情和状态数量统计。"}, + {Name: "ops_ai_assistant_calendar_list", Enabled: true, Description: "查询当前用户可见的 OPS 日历列表。"}, {Name: "ops_ai_assistant_schedule_query", Enabled: true, Description: "按日期范围查询当前用户可见的 OPS 日历/日程。"}, + {Name: "ops_ai_assistant_schedule_create", Enabled: true, Description: "创建 OPS 日历日程。"}, + {Name: "ops_ai_assistant_schedule_update", Enabled: true, Description: "修改 OPS 日历日程。"}, + {Name: "ops_ai_assistant_schedule_delete", Enabled: true, Description: "删除 OPS 日历日程。"}, }, }, } diff --git a/backend/my_work/routers/apiAIChat.go b/backend/my_work/routers/apiAIChat.go index 0935cba..e4cfb47 100644 --- a/backend/my_work/routers/apiAIChat.go +++ b/backend/my_work/routers/apiAIChat.go @@ -319,7 +319,7 @@ func handleChat(ctx *gin.Context) { toolNames = append(toolNames, tool.Function.Name) } emitTrace("function_tools", "prepare", "success", "已启用 Function Calling 工具", map[string]interface{}{"tools": toolNames}) - openaiMsgs = append([]openaiMessage{{Role: "system", Content: "可用工具使用规则:当用户询问“我是谁”“当前登录用户是谁”“我的用户信息”等当前身份问题时,调用 ops_ai_assistant_current_user;工具返回 loggedIn=true 时按工具结果回答当前用户信息,返回 loggedIn=false 时说明不知道并提示需要登录才能获取信息。当用户询问采购订单列表、采购订单详情、采购状态或数量统计时,调用 ops_ai_assistant_purchase_query;该工具只允许查询,禁止新增、修改、删除采购数据。当用户询问本月、今天、本周、下周等相对日期的日程时,先调用 time 获取明确 start_date/end_date,再调用 ops_ai_assistant_schedule_query 查询日程。不要臆造工具结果中不存在的信息。"}}, openaiMsgs...) + openaiMsgs = append([]openaiMessage{{Role: "system", Content: "可用工具使用规则:当用户询问“我是谁”“当前登录用户是谁”“我的用户信息”等当前身份问题时,调用 ops_ai_assistant_current_user;工具返回 loggedIn=true 时按工具结果回答当前用户信息,返回 loggedIn=false 时说明不知道并提示需要登录才能获取信息。当用户询问采购订单列表、采购订单详情、采购状态或数量统计时,调用 ops_ai_assistant_purchase_query;该工具只允许查询,禁止新增、修改、删除采购数据。当用户询问本月、今天、本周、下周等相对日期的日程时,先调用 time 获取明确 start_date/end_date,再调用 ops_ai_assistant_schedule_query 查询日程。用户询问有哪些日历或创建日程但未提供 calendar_id 时,调用 ops_ai_assistant_calendar_list 获取可用日历;用户明确要求新增/修改/删除日程时,分别调用 ops_ai_assistant_schedule_create、ops_ai_assistant_schedule_update、ops_ai_assistant_schedule_delete,写入工具必须基于用户明确指令和明确日期,不要自行猜测日历或事件 ID。不要臆造工具结果中不存在的信息。"}}, openaiMsgs...) var toolExecuted bool openaiMsgs, toolExecuted, err = runOpenAIToolLoop(ctx.Request.Context(), profile, openaiMsgs, functionTools, currentUser, tracker, emitTrace) if err != nil { diff --git a/backend/my_work/routers/apiAIChatConfig.go b/backend/my_work/routers/apiAIChatConfig.go index b96c0a4..a6ebddb 100644 --- a/backend/my_work/routers/apiAIChatConfig.go +++ b/backend/my_work/routers/apiAIChatConfig.go @@ -111,7 +111,11 @@ func ensureBuiltinAIChatTools() error { {Name: "time", Enabled: true, Description: "解析当前时间、相对日期和日期范围。", SortOrder: 0}, {Name: "ops_ai_assistant_current_user", Enabled: true, Description: "返回当前登录用户信息;未登录时提示需要登录才能获取信息。", SortOrder: 5}, {Name: "ops_ai_assistant_purchase_query", Enabled: true, Description: "查询采购订单列表、详情和状态数量统计。", SortOrder: 8}, - {Name: "ops_ai_assistant_schedule_query", Enabled: true, Description: "按日期范围查询当前用户可见的 OPS 日历/日程。", SortOrder: 10}, + {Name: "ops_ai_assistant_calendar_list", Enabled: true, Description: "查询当前用户可见的 OPS 日历列表。", SortOrder: 10}, + {Name: "ops_ai_assistant_schedule_query", Enabled: true, Description: "按日期范围查询当前用户可见的 OPS 日历/日程。", SortOrder: 12}, + {Name: "ops_ai_assistant_schedule_create", Enabled: true, Description: "创建 OPS 日历日程。", SortOrder: 14}, + {Name: "ops_ai_assistant_schedule_update", Enabled: true, Description: "修改 OPS 日历日程。", SortOrder: 16}, + {Name: "ops_ai_assistant_schedule_delete", Enabled: true, Description: "删除 OPS 日历日程。", SortOrder: 18}, } for _, builtin := range builtins { var existing TabAIChatTool @@ -208,7 +212,11 @@ func seedAIChatConfigFromYAMLIfEmpty() error { {Name: "time", Enabled: true, Description: "解析当前时间、相对日期和日期范围。"}, {Name: "ops_ai_assistant_current_user", Enabled: true, Description: "返回当前登录用户信息;未登录时提示需要登录才能获取信息。"}, {Name: "ops_ai_assistant_purchase_query", Enabled: true, Description: "查询采购订单列表、详情和状态数量统计。"}, + {Name: "ops_ai_assistant_calendar_list", Enabled: true, Description: "查询当前用户可见的 OPS 日历列表。"}, {Name: "ops_ai_assistant_schedule_query", Enabled: true, Description: "按日期范围查询当前用户可见的 OPS 日历/日程。"}, + {Name: "ops_ai_assistant_schedule_create", Enabled: true, Description: "创建 OPS 日历日程。"}, + {Name: "ops_ai_assistant_schedule_update", Enabled: true, Description: "修改 OPS 日历日程。"}, + {Name: "ops_ai_assistant_schedule_delete", Enabled: true, Description: "删除 OPS 日历日程。"}, } } for i, tool := range tools { diff --git a/backend/my_work/routers/apiCalendar.go b/backend/my_work/routers/apiCalendar.go index 3db0805..3d9a601 100644 --- a/backend/my_work/routers/apiCalendar.go +++ b/backend/my_work/routers/apiCalendar.go @@ -179,23 +179,208 @@ func (calendarScheduleProvider) QuerySchedules(ctx context.Context, query agents result := make([]agents.ScheduleEvent, 0, len(events)) for _, event := range events { - result = append(result, agents.ScheduleEvent{ - ID: event.ID, - CalendarID: event.CalendarID, - UserID: event.UserID, - Title: event.Title, - StartDate: event.StartDate, - EndDate: event.EndDate, - ScheduleType: event.ScheduleType, - IsPublic: event.IsPublic, - Remark: event.Remark, - AccessNote: event.AccessNote, - CanEdit: event.CanEdit, + result = append(result, calendarScheduleEventToAgent(event)) + } + return result, nil +} + +func (calendarScheduleProvider) ListCalendars(ctx context.Context, userID uint) ([]agents.CalendarListOutput, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + var calendars []TabCalendar + if err := models.DB.Where("deleted_at IS NULL AND (is_public = ? OR user_id = ?)", true, userID).Order("created_at DESC").Find(&calendars).Error; err != nil { + return nil, err + } + + result := make([]agents.CalendarListOutput, 0, len(calendars)) + for _, cal := range calendars { + result = append(result, agents.CalendarListOutput{ + ID: cal.ID, + Name: cal.Name, + Description: cal.Description, + Color: cal.Color, + IsPublic: cal.IsPublic, + CanEdit: canModifyCalendar(userID, cal.UserID), }) } return result, nil } +func (calendarScheduleProvider) CreateScheduleEvent(ctx context.Context, userID uint, input agents.CreateScheduleInput) (*agents.ScheduleEvent, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + calendar := TabCalendar{} + if err := models.DB.Where("id = ? AND deleted_at IS NULL", input.CalendarID).First(&calendar).Error; err != nil { + return nil, err + } + if !calendar.IsPublic && !canModifyCalendar(userID, calendar.UserID) { + return nil, gorm.ErrRecordNotFound + } + + startDate, err := time.Parse("2006-01-02", input.StartDate) + if err != nil { + return nil, err + } + endDate, err := time.Parse("2006-01-02", input.EndDate) + if err != nil { + return nil, err + } + if input.ScheduleType == "" { + input.ScheduleType = "work" + } + + event := TabCalendarEvent{ + CalendarID: input.CalendarID, + UserID: userID, + Title: input.Title, + StartDate: &startDate, + EndDate: &endDate, + ScheduleType: input.ScheduleType, + IsPublic: input.IsPublic, + Remark: input.Remark, + } + if err := models.DB.Create(&event).Error; err != nil { + return nil, err + } + newContent, _ := json.Marshal(event) + models.DB.Create(&TabCalendarLog{CalendarID: event.CalendarID, EventID: event.ID, UserID: userID, ActionType: "create_event", NewContent: string(newContent), Remark: "AI 助手创建日程"}) + + return calendarEventToAgent(event, userID), nil +} + +func (calendarScheduleProvider) UpdateScheduleEvent(ctx context.Context, userID uint, input agents.UpdateScheduleInput) (*agents.ScheduleEvent, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + oldEvent := TabCalendarEvent{} + if err := models.DB.Where("id = ?", input.EventID).First(&oldEvent).Error; err != nil { + return nil, err + } + if !canModifyCalendarEvent(userID, oldEvent) { + return nil, gorm.ErrRecordNotFound + } + + startDate, err := time.Parse("2006-01-02", input.StartDate) + if err != nil { + return nil, err + } + endDate, err := time.Parse("2006-01-02", input.EndDate) + if err != nil { + return nil, err + } + if input.ScheduleType == "" { + input.ScheduleType = "work" + } + + updates := map[string]interface{}{ + "title": input.Title, + "start_date": &startDate, + "end_date": &endDate, + "schedule_type": input.ScheduleType, + "is_public": input.IsPublic, + "remark": input.Remark, + } + if err := models.DB.Model(&oldEvent).Updates(updates).Error; err != nil { + return nil, err + } + oldContent, _ := json.Marshal(oldEvent) + newContent, _ := json.Marshal(updates) + models.DB.Create(&TabCalendarLog{CalendarID: oldEvent.CalendarID, EventID: oldEvent.ID, UserID: userID, ActionType: "update_event", OldContent: string(oldContent), NewContent: string(newContent), Remark: "AI 助手更新日程"}) + + var event TabCalendarEvent + if err := models.DB.Where("id = ?", input.EventID).First(&event).Error; err != nil { + return nil, err + } + return calendarEventToAgent(event, userID), nil +} + +func (calendarScheduleProvider) DeleteScheduleEvent(ctx context.Context, userID uint, eventID uint) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + event := TabCalendarEvent{} + if err := models.DB.Where("id = ?", eventID).First(&event).Error; err != nil { + return err + } + if !canModifyCalendarEvent(userID, event) { + return gorm.ErrRecordNotFound + } + if err := models.DB.Delete(&event).Error; err != nil { + return err + } + oldContent, _ := json.Marshal(event) + models.DB.Create(&TabCalendarLog{CalendarID: event.CalendarID, EventID: event.ID, UserID: userID, ActionType: "delete_event", OldContent: string(oldContent), Remark: "AI 助手删除日程"}) + return nil +} + +func calendarScheduleEventToAgent(event CalendarScheduleEvent) agents.ScheduleEvent { + return agents.ScheduleEvent{ + ID: event.ID, + CalendarID: event.CalendarID, + UserID: event.UserID, + Title: event.Title, + StartDate: event.StartDate, + EndDate: event.EndDate, + ScheduleType: event.ScheduleType, + IsPublic: event.IsPublic, + Remark: event.Remark, + AccessNote: event.AccessNote, + CanEdit: event.CanEdit, + } +} + +func calendarEventToAgent(event TabCalendarEvent, userID uint) *agents.ScheduleEvent { + accessNote := "" + canEdit := false + if userID > 0 { + var calendar TabCalendar + if models.DB.Where("id = ?", event.CalendarID).First(&calendar).Error == nil { + canEdit = canModifyCalendar(userID, event.UserID) || calendar.UserID == userID + } + if !event.IsPublic { + accessNote = "非公开日程,仅因当前用户具备相关权限可见" + } + } + return &agents.ScheduleEvent{ + ID: event.ID, + CalendarID: event.CalendarID, + UserID: event.UserID, + Title: event.Title, + StartDate: formatDatePtr(event.StartDate), + EndDate: formatDatePtr(event.EndDate), + ScheduleType: event.ScheduleType, + IsPublic: event.IsPublic, + Remark: event.Remark, + AccessNote: accessNote, + CanEdit: canEdit, + } +} + +func canModifyCalendarEvent(userID uint, event TabCalendarEvent) bool { + if userID == event.UserID { + return true + } + var calendar TabCalendar + if models.DB.Where("id = ?", event.CalendarID).First(&calendar).Error == nil { + return canModifyCalendar(userID, calendar.UserID) + } + return false +} + func QueryCalendarSchedulesForAI(query CalendarScheduleQuery) ([]CalendarScheduleEvent, error) { startDate := dateOnly(query.StartDate) endDate := dateOnly(query.EndDate)