From dc39d28894de1e1e1ee126f1d47314679f9e0f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 10 Jun 2026 19:03:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/my_work/agents/function_tool.go | 83 +++++++++++++ backend/my_work/agents/ops_ai_assistant.go | 124 ++++++++++++++++++++ backend/my_work/routers/apiAIChat.go | 128 +++------------------ backend/my_work/routers/apiCalendar.go | 54 +++++++++ 4 files changed, 276 insertions(+), 113 deletions(-) create mode 100644 backend/my_work/agents/function_tool.go create mode 100644 backend/my_work/agents/ops_ai_assistant.go diff --git a/backend/my_work/agents/function_tool.go b/backend/my_work/agents/function_tool.go new file mode 100644 index 0000000..3463f27 --- /dev/null +++ b/backend/my_work/agents/function_tool.go @@ -0,0 +1,83 @@ +package agents + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" +) + +type FunctionToolSchema struct { + Name string + Description string + Parameters map[string]interface{} +} + +type FunctionToolRuntime struct { + UserID uint +} + +func FunctionToolSchemas(configs []ToolConfig) []FunctionToolSchema { + tools := make([]FunctionToolSchema, 0) + for _, config := range configs { + if !config.Enabled { + continue + } + switch strings.ToLower(strings.TrimSpace(config.Name)) { + case "time": + tools = append(tools, timeFunctionToolSchema()) + case "ops_ai_assistant_schedule_query", "ops_ai_assistant": + tools = append(tools, opsAIAssistantScheduleQuerySchema()) + } + } + return tools +} + +func ExecuteFunctionTool(ctx context.Context, runtime FunctionToolRuntime, name string, rawArgs []byte) ([]byte, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + switch strings.ToLower(strings.TrimSpace(name)) { + case "time": + var args TimeRangeArgs + if len(rawArgs) > 0 { + if err := json.Unmarshal(rawArgs, &args); err != nil { + return nil, err + } + } + result, err := ResolveTimeRange(args, time.Now()) + if err != nil { + return nil, err + } + return json.Marshal(result) + case "ops_ai_assistant_schedule_query", "ops_ai_assistant": + return executeOpsAIAssistantScheduleQuery(ctx, runtime, rawArgs) + default: + return nil, fmt.Errorf("unknown tool: %s", name) + } +} + +func timeFunctionToolSchema() FunctionToolSchema { + return FunctionToolSchema{ + Name: "time", + Description: "解析当前时间、相对日期和日期范围。遇到本月、今天、本周等相对日期时先调用本工具获得明确日期。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "range": map[string]interface{}{ + "type": "string", + "enum": []string{"today", "yesterday", "tomorrow", "this_week", "last_week", "next_week", "this_month", "last_month", "next_month", "this_year", "custom"}, + "description": "要解析的日期范围。", + }, + "timezone": map[string]interface{}{"type": "string", "description": "可选时区,例如 Asia/Shanghai。"}, + "start_date": map[string]interface{}{"type": "string", "description": "custom 范围开始日期,格式 YYYY-MM-DD。"}, + "end_date": map[string]interface{}{"type": "string", "description": "custom 范围结束日期,格式 YYYY-MM-DD。"}, + }, + "required": []string{"range"}, + }, + } +} diff --git a/backend/my_work/agents/ops_ai_assistant.go b/backend/my_work/agents/ops_ai_assistant.go new file mode 100644 index 0000000..f1ced84 --- /dev/null +++ b/backend/my_work/agents/ops_ai_assistant.go @@ -0,0 +1,124 @@ +package agents + +import ( + "context" + "encoding/json" + "fmt" + "time" +) + +type ScheduleQueryArgs struct { + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + CalendarID uint `json:"calendar_id,omitempty"` + Limit int `json:"limit,omitempty"` +} + +type ScheduleCalendar struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +type ScheduleEvent struct { + ID uint `json:"id"` + CalendarID uint `json:"calendar_id"` + UserID uint `json:"user_id"` + Title string `json:"title"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + ScheduleType string `json:"schedule_type"` + IsPublic bool `json:"is_public"` + Remark string `json:"remark,omitempty"` + CanEdit bool `json:"canEdit"` +} + +type ScheduleQuery struct { + StartDate string + EndDate string + CalendarID uint + UserID uint + Limit int +} + +type ScheduleProvider interface { + QuerySchedules(ctx context.Context, query ScheduleQuery) ([]ScheduleEvent, error) +} + +var registeredScheduleProvider ScheduleProvider = nil + +func RegisterScheduleProvider(provider ScheduleProvider) { + registeredScheduleProvider = provider +} + +func opsAIAssistantScheduleQuerySchema() FunctionToolSchema { + return FunctionToolSchema{ + Name: "ops_ai_assistant_schedule_query", + Description: "按明确日期范围查询当前用户可见的 OPS 日历/日程。相对日期需先调用 time 获取 start_date/end_date。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "start_date": map[string]interface{}{"type": "string", "description": "开始日期,格式 YYYY-MM-DD。"}, + "end_date": map[string]interface{}{"type": "string", "description": "结束日期,格式 YYYY-MM-DD。"}, + "calendar_id": map[string]interface{}{"type": "integer", "description": "可选日历 ID;不传则查询全部可见日程。"}, + "limit": map[string]interface{}{"type": "integer", "description": "可选返回上限,默认 100,最大 200。"}, + }, + "required": []string{"start_date", "end_date"}, + }, + // ScheduleQueryResult is the return type packed as JSON + } +} + +func executeOpsAIAssistantScheduleQuery(ctx context.Context, runtime FunctionToolRuntime, rawArgs []byte) ([]byte, error) { + var args ScheduleQueryArgs + if err := json.Unmarshal(rawArgs, &args); err != nil { + return nil, err + } + startDate, err := time.Parse("2006-01-02", args.StartDate) + if err != nil { + return nil, fmt.Errorf("invalid start_date: %w", err) + } + endDate, err := time.Parse("2006-01-02", args.EndDate) + if err != nil { + return nil, fmt.Errorf("invalid end_date: %w", err) + } + if endDate.Before(startDate) { + return nil, fmt.Errorf("end_date must be after start_date") + } + limit := args.Limit + if limit <= 0 { + limit = 100 + } + if limit > 200 { + limit = 200 + } + + if registeredScheduleProvider == nil { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "日程查询服务未注册", + }) + } + + events, err := registeredScheduleProvider.QuerySchedules(ctx, ScheduleQuery{ + StartDate: args.StartDate, + EndDate: args.EndDate, + CalendarID: args.CalendarID, + UserID: runtime.UserID, + Limit: limit, + }) + if err != nil { + return nil, err + } + if events == nil { + events = []ScheduleEvent{} + } + + return json.Marshal(map[string]interface{}{ + "ok": true, + "start_date": args.StartDate, + "end_date": args.EndDate, + "count": len(events), + "limit": limit, + "events": events, + }) +} diff --git a/backend/my_work/routers/apiAIChat.go b/backend/my_work/routers/apiAIChat.go index 4c0b93d..8832e20 100644 --- a/backend/my_work/routers/apiAIChat.go +++ b/backend/my_work/routers/apiAIChat.go @@ -888,53 +888,17 @@ func buildToolConfigs(configs []models.ConfigsAIChatTool_) []agents.ToolConfig { } func buildFunctionTools(configs []agents.ToolConfig) []openaiTool { - tools := make([]openaiTool, 0) - for _, config := range configs { - if !config.Enabled { - continue - } - switch strings.ToLower(strings.TrimSpace(config.Name)) { - case "time": - tools = append(tools, openaiTool{ - Type: "function", - Function: openaiFunctionDefinition{ - Name: "time", - Description: "解析当前时间、相对日期和日期范围。遇到本月、今天、本周等相对日期时先调用本工具获得明确日期。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "range": map[string]interface{}{ - "type": "string", - "enum": []string{"today", "yesterday", "tomorrow", "this_week", "last_week", "next_week", "this_month", "last_month", "next_month", "this_year", "custom"}, - "description": "要解析的日期范围。", - }, - "timezone": map[string]interface{}{"type": "string", "description": "可选时区,例如 Asia/Shanghai。"}, - "start_date": map[string]interface{}{"type": "string", "description": "custom 范围开始日期,格式 YYYY-MM-DD。"}, - "end_date": map[string]interface{}{"type": "string", "description": "custom 范围结束日期,格式 YYYY-MM-DD。"}, - }, - "required": []string{"range"}, - }, - }, - }) - case "ops_ai_assistant_schedule_query", "ops_ai_assistant": - tools = append(tools, openaiTool{ - Type: "function", - Function: openaiFunctionDefinition{ - Name: "ops_ai_assistant_schedule_query", - Description: "按明确日期范围查询当前用户可见的 OPS 日历/日程。相对日期需先调用 time 获取 start_date/end_date。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "start_date": map[string]interface{}{"type": "string", "description": "开始日期,格式 YYYY-MM-DD。"}, - "end_date": map[string]interface{}{"type": "string", "description": "结束日期,格式 YYYY-MM-DD。"}, - "calendar_id": map[string]interface{}{"type": "integer", "description": "可选日历 ID;不传则查询全部可见日程。"}, - "limit": map[string]interface{}{"type": "integer", "description": "可选返回上限,默认 100,最大 200。"}, - }, - "required": []string{"start_date", "end_date"}, - }, - }, - }) - } + schemas := agents.FunctionToolSchemas(configs) + tools := make([]openaiTool, 0, len(schemas)) + for _, schema := range schemas { + tools = append(tools, openaiTool{ + Type: "function", + Function: openaiFunctionDefinition{ + Name: schema.Name, + Description: schema.Description, + Parameters: schema.Parameters, + }, + }) } return tools } @@ -1015,74 +979,12 @@ func runOpenAIToolLoop(ctx context.Context, profile models.ConfigsAIChatOpenAI_, return messages, toolExecuted, fmt.Errorf("工具调用超过最大轮数") } -type scheduleQueryArgs struct { - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - CalendarID uint `json:"calendar_id,omitempty"` - Limit int `json:"limit,omitempty"` -} - func executeAIFunctionTool(ctx context.Context, name string, rawArgs []byte, currentUser *TabUser) ([]byte, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - switch name { - case "time": - var args agents.TimeRangeArgs - if len(rawArgs) > 0 { - if err := json.Unmarshal(rawArgs, &args); err != nil { - return nil, err - } - } - result, err := agents.ResolveTimeRange(args, time.Now()) - if err != nil { - return nil, err - } - return json.Marshal(result) - case "ops_ai_assistant_schedule_query", "ops_ai_assistant": - var args scheduleQueryArgs - if err := json.Unmarshal(rawArgs, &args); err != nil { - return nil, err - } - startDate, err := time.Parse("2006-01-02", args.StartDate) - if err != nil { - return nil, fmt.Errorf("invalid start_date: %w", err) - } - endDate, err := time.Parse("2006-01-02", args.EndDate) - if err != nil { - return nil, fmt.Errorf("invalid end_date: %w", err) - } - limit := args.Limit - if limit <= 0 { - limit = 100 - } - if limit > 200 { - limit = 200 - } - events, err := QueryCalendarSchedulesForAI(CalendarScheduleQuery{ - CalendarID: args.CalendarID, - StartDate: startDate, - EndDate: endDate, - User: currentUser, - Limit: limit, - }) - if err != nil { - return nil, err - } - return json.Marshal(map[string]interface{}{ - "ok": true, - "start_date": args.StartDate, - "end_date": args.EndDate, - "count": len(events), - "limit": limit, - "events": events, - }) - default: - return nil, fmt.Errorf("unknown tool: %s", name) + runtime := agents.FunctionToolRuntime{} + if currentUser != nil { + runtime.UserID = currentUser.ID } + return agents.ExecuteFunctionTool(ctx, runtime, name, rawArgs) } func selectOpenAIProfile(cfg models.ConfigsAIChat_, name string) (models.ConfigsAIChatOpenAI_, bool) { diff --git a/backend/my_work/routers/apiCalendar.go b/backend/my_work/routers/apiCalendar.go index 766562f..781cf23 100644 --- a/backend/my_work/routers/apiCalendar.go +++ b/backend/my_work/routers/apiCalendar.go @@ -1,7 +1,9 @@ package routers import ( + "context" "encoding/json" + "ops/agents" "ops/models" "slices" "time" @@ -141,6 +143,57 @@ type CalendarScheduleEvent struct { CanEdit bool `json:"canEdit"` } +type calendarScheduleProvider struct{} + +func (calendarScheduleProvider) QuerySchedules(ctx context.Context, query agents.ScheduleQuery) ([]agents.ScheduleEvent, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + startDate, err := time.Parse("2006-01-02", query.StartDate) + if err != nil { + return nil, err + } + endDate, err := time.Parse("2006-01-02", query.EndDate) + if err != nil { + return nil, err + } + + var user *TabUser + if query.UserID > 0 { + user = &TabUser{ID: query.UserID} + } + events, err := QueryCalendarSchedulesForAI(CalendarScheduleQuery{ + CalendarID: query.CalendarID, + StartDate: startDate, + EndDate: endDate, + User: user, + Limit: query.Limit, + }) + if err != nil { + return nil, err + } + + 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, + CanEdit: event.CanEdit, + }) + } + return result, nil +} + func QueryCalendarSchedulesForAI(query CalendarScheduleQuery) ([]CalendarScheduleEvent, error) { startDate := dateOnly(query.StartDate) endDate := dateOnly(query.EndDate) @@ -261,6 +314,7 @@ func ApiCalendarInit() { }) CalendarUpdateAdminsCash() + agents.RegisterScheduleProvider(calendarScheduleProvider{}) } func ApiCalendar(r *gin.RouterGroup) {