From 3437e462900f1eb0ad2a8733ced6138b8de9cf0d 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 20:11:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=AE=A2=E5=8D=95=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/my_work/agents/function_tool.go | 4 + backend/my_work/agents/ops_ai_assistant.go | 184 ++++++++++++++ backend/my_work/models/configs.go | 1 + backend/my_work/routers/apiAIChat.go | 2 +- backend/my_work/routers/apiAIChatConfig.go | 2 + backend/my_work/routers/apiPurchase.go | 266 +++++++++++++++++++++ 6 files changed, 458 insertions(+), 1 deletion(-) diff --git a/backend/my_work/agents/function_tool.go b/backend/my_work/agents/function_tool.go index 85dd02b..098574a 100644 --- a/backend/my_work/agents/function_tool.go +++ b/backend/my_work/agents/function_tool.go @@ -35,6 +35,8 @@ func FunctionToolSchemas(configs []ToolConfig) []FunctionToolSchema { tools = append(tools, opsAIAssistantScheduleQuerySchema()) case "ops_ai_assistant_current_user": tools = append(tools, opsAIAssistantCurrentUserSchema()) + case "ops_ai_assistant_purchase_query": + tools = append(tools, opsAIAssistantPurchaseQuerySchema()) } } return tools @@ -64,6 +66,8 @@ func ExecuteFunctionTool(ctx context.Context, runtime FunctionToolRuntime, name return executeOpsAIAssistantScheduleQuery(ctx, runtime, rawArgs) case "ops_ai_assistant_current_user": return executeOpsAIAssistantCurrentUser(ctx, runtime, rawArgs) + case "ops_ai_assistant_purchase_query": + return executeOpsAIAssistantPurchaseQuery(ctx, runtime, rawArgs) default: return nil, fmt.Errorf("unknown tool: %s", name) } diff --git a/backend/my_work/agents/ops_ai_assistant.go b/backend/my_work/agents/ops_ai_assistant.go index 5d8ccbc..349dd1f 100644 --- a/backend/my_work/agents/ops_ai_assistant.go +++ b/backend/my_work/agents/ops_ai_assistant.go @@ -31,6 +31,84 @@ type CurrentUserInfo struct { Language string `json:"language,omitempty"` } +type PurchaseQueryArgs struct { + Action string `json:"action"` + OrderID uint `json:"order_id,omitempty"` + Search string `json:"search,omitempty"` + Status string `json:"status,omitempty"` + StartDate string `json:"start_date,omitempty"` + EndDate string `json:"end_date,omitempty"` + Page int `json:"page,omitempty"` + Limit int `json:"limit,omitempty"` +} + +type PurchaseQuery struct { + Action string + OrderID uint + Search string + Status string + StartDate string + EndDate string + Page int + Limit int + UserID uint +} + +type PurchaseCost struct { + ID uint `json:"id"` + OrderID uint `json:"order_id"` + UserID uint `json:"user_id"` + Price int `json:"price"` + Quantity int `json:"quantity"` + CurrencyType int `json:"currency_type"` + CurrencyName string `json:"currency_name"` + CostType int `json:"cost_type"` + CostTypeName string `json:"cost_type_name"` +} + +type PurchaseCommit struct { + ID uint `json:"id"` + OrderID uint `json:"order_id"` + UserID uint `json:"user_id"` + Action string `json:"action"` + Status string `json:"status,omitempty"` + OldStatus string `json:"old_status,omitempty"` + Comment string `json:"comment,omitempty"` + CreatedAt string `json:"created_at,omitempty"` +} + +type PurchaseOrder struct { + ID uint `json:"id"` + UserID uint `json:"user_id"` + Title string `json:"title"` + Remark string `json:"remark,omitempty"` + Link string `json:"link,omitempty"` + DetailURL string `json:"detail_url"` + Styles string `json:"styles,omitempty"` + OrderStatus string `json:"order_status"` + OrderStatusName string `json:"order_status_name"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + CanModify bool `json:"can_modify,omitempty"` + Costs []PurchaseCost `json:"costs,omitempty"` + Commits []PurchaseCommit `json:"commits,omitempty"` +} + +type PurchaseQueryResult struct { + Ok bool `json:"ok"` + Action string `json:"action"` + LoggedIn bool `json:"loggedIn"` + Count int `json:"count,omitempty"` + Total int64 `json:"total,omitempty"` + Page int `json:"page,omitempty"` + Limit int `json:"limit,omitempty"` + Orders []PurchaseOrder `json:"orders,omitempty"` + Order *PurchaseOrder `json:"order,omitempty"` + Counts map[string]int64 `json:"counts,omitempty"` + Filters map[string]interface{} `json:"filters,omitempty"` + Message string `json:"message,omitempty"` +} + type ScheduleCalendar struct { ID uint `json:"id"` Name string `json:"name"` @@ -62,12 +140,21 @@ type ScheduleProvider interface { QuerySchedules(ctx context.Context, query ScheduleQuery) ([]ScheduleEvent, error) } +type PurchaseProvider interface { + QueryPurchases(ctx context.Context, query PurchaseQuery) (*PurchaseQueryResult, error) +} + var registeredScheduleProvider ScheduleProvider = nil +var registeredPurchaseProvider PurchaseProvider = nil func RegisterScheduleProvider(provider ScheduleProvider) { registeredScheduleProvider = provider } +func RegisterPurchaseProvider(provider PurchaseProvider) { + registeredPurchaseProvider = provider +} + func opsAIAssistantScheduleQuerySchema() FunctionToolSchema { return FunctionToolSchema{ Name: "ops_ai_assistant_schedule_query", @@ -109,6 +196,31 @@ func opsAIAssistantCurrentUserSchema() FunctionToolSchema { } } +func opsAIAssistantPurchaseQuerySchema() FunctionToolSchema { + return FunctionToolSchema{ + Name: "ops_ai_assistant_purchase_query", + Description: "只读工具:查询采购模块订单。用户询问采购订单、采购状态、待处理/已下单/已到达/已收件/丢件/退件数量或列表、指定采购订单详情时调用。禁止新增、修改、删除订单或状态。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "action": map[string]interface{}{ + "type": "string", + "enum": []string{"list", "get", "count"}, + "description": "list 查询订单列表;get 查询单个订单详情;count 统计各状态数量。", + }, + "order_id": map[string]interface{}{"type": "integer", "description": "action=get 时的采购订单 ID。"}, + "search": map[string]interface{}{"type": "string", "description": "按订单 ID、标题或备注搜索。"}, + "status": map[string]interface{}{"type": "string", "enum": []string{"", "pending", "ordered", "arrived", "received", "lost", "returned"}, "description": "订单状态过滤:pending 待处理,ordered 已下单,arrived 已到达,received 已收件,lost 丢件,returned 退件。"}, + "start_date": map[string]interface{}{"type": "string", "description": "可选创建日期开始,格式 YYYY-MM-DD。"}, + "end_date": map[string]interface{}{"type": "string", "description": "可选创建日期结束,格式 YYYY-MM-DD。"}, + "page": map[string]interface{}{"type": "integer", "description": "分页页码,默认 1。"}, + "limit": map[string]interface{}{"type": "integer", "description": "返回上限,默认 20,最大 100。"}, + }, + "required": []string{"action"}, + }, + } +} + func executeOpsAIAssistantCurrentUser(ctx context.Context, runtime FunctionToolRuntime, rawArgs []byte) ([]byte, error) { var args CurrentUserArgs if len(rawArgs) > 0 { @@ -144,6 +256,78 @@ func executeOpsAIAssistantCurrentUser(ctx context.Context, runtime FunctionToolR }) } +func executeOpsAIAssistantPurchaseQuery(ctx context.Context, runtime FunctionToolRuntime, rawArgs []byte) ([]byte, error) { + var args PurchaseQueryArgs + if err := json.Unmarshal(rawArgs, &args); err != nil { + return nil, err + } + if args.Action != "list" && args.Action != "get" && args.Action != "count" { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "ops_ai_assistant_purchase_query 是只读工具,仅允许 list/get/count 查询操作", + }) + } + if runtime.UserID <= 0 { + return json.Marshal(map[string]interface{}{ + "ok": true, + "action": args.Action, + "loggedIn": false, + "message": "需要登录才能查询采购模块信息。", + }) + } + if args.Action == "get" && args.OrderID <= 0 { + return nil, fmt.Errorf("order_id is required when action=get") + } + if args.StartDate != "" { + if _, err := time.Parse("2006-01-02", args.StartDate); err != nil { + return nil, fmt.Errorf("invalid start_date: %w", err) + } + } + if args.EndDate != "" { + if _, err := time.Parse("2006-01-02", args.EndDate); err != nil { + return nil, fmt.Errorf("invalid end_date: %w", err) + } + } + if args.StartDate != "" && args.EndDate != "" { + startDate, _ := time.Parse("2006-01-02", args.StartDate) + endDate, _ := time.Parse("2006-01-02", args.EndDate) + if endDate.Before(startDate) { + return nil, fmt.Errorf("end_date must be after start_date") + } + } + if args.Page <= 0 { + args.Page = 1 + } + if args.Limit <= 0 { + args.Limit = 20 + } + if args.Limit > 100 { + args.Limit = 100 + } + if registeredPurchaseProvider == nil { + return json.Marshal(map[string]interface{}{ + "ok": false, + "error": "采购查询服务未注册", + }) + } + + result, err := registeredPurchaseProvider.QueryPurchases(ctx, PurchaseQuery{ + Action: args.Action, + OrderID: args.OrderID, + Search: args.Search, + Status: args.Status, + StartDate: args.StartDate, + EndDate: args.EndDate, + Page: args.Page, + Limit: args.Limit, + UserID: runtime.UserID, + }) + if err != nil { + return nil, err + } + return json.Marshal(result) +} + func executeOpsAIAssistantScheduleQuery(ctx context.Context, runtime FunctionToolRuntime, rawArgs []byte) ([]byte, error) { var args ScheduleQueryArgs if err := json.Unmarshal(rawArgs, &args); err != nil { diff --git a/backend/my_work/models/configs.go b/backend/my_work/models/configs.go index a038856..b5788f0 100644 --- a/backend/my_work/models/configs.go +++ b/backend/my_work/models/configs.go @@ -107,6 +107,7 @@ func ConfigAllInit() error { Tools: []ConfigsAIChatTool_{ {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_schedule_query", Enabled: true, Description: "按日期范围查询当前用户可见的 OPS 日历/日程。"}, }, }, diff --git a/backend/my_work/routers/apiAIChat.go b/backend/my_work/routers/apiAIChat.go index 369f5bb..0935cba 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 时说明不知道并提示需要登录才能获取信息。当用户询问本月、今天、本周、下周等相对日期的日程时,先调用 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 查询日程。不要臆造工具结果中不存在的信息。"}}, 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 c215a9a..b96c0a4 100644 --- a/backend/my_work/routers/apiAIChatConfig.go +++ b/backend/my_work/routers/apiAIChatConfig.go @@ -110,6 +110,7 @@ func ensureBuiltinAIChatTools() error { builtins := []TabAIChatTool{ {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}, } for _, builtin := range builtins { @@ -206,6 +207,7 @@ func seedAIChatConfigFromYAMLIfEmpty() error { tools = []models.ConfigsAIChatTool_{ {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_schedule_query", Enabled: true, Description: "按日期范围查询当前用户可见的 OPS 日历/日程。"}, } } diff --git a/backend/my_work/routers/apiPurchase.go b/backend/my_work/routers/apiPurchase.go index 73d497c..0c01a0a 100644 --- a/backend/my_work/routers/apiPurchase.go +++ b/backend/my_work/routers/apiPurchase.go @@ -1,8 +1,10 @@ package routers import ( + "context" "encoding/json" parsefmt "fmt" + "ops/agents" "ops/models" "slices" "strings" @@ -119,6 +121,269 @@ type TabPurchaseLog struct { CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:操作时间"` } +type purchaseProvider struct{} + +func (purchaseProvider) QueryPurchases(ctx context.Context, query agents.PurchaseQuery) (*agents.PurchaseQueryResult, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + result := &agents.PurchaseQueryResult{ + Ok: true, + Action: query.Action, + LoggedIn: query.UserID > 0, + } + if !result.LoggedIn { + result.Message = "需要登录才能查询采购模块信息。" + return result, nil + } + + switch query.Action { + case "count": + counts, err := queryPurchaseCounts(query) + if err != nil { + return nil, err + } + result.Counts = counts + return result, nil + case "get": + order, err := queryPurchaseOrderDetail(query.OrderID, query.UserID) + if err != nil { + return nil, err + } + result.Order = order + if order != nil { + result.Count = 1 + } + return result, nil + default: + orders, total, err := queryPurchaseOrderList(query) + if err != nil { + return nil, err + } + result.Orders = orders + result.Count = len(orders) + result.Total = total + result.Page = query.Page + result.Limit = query.Limit + result.Filters = map[string]interface{}{ + "search": query.Search, + "status": query.Status, + "start_date": query.StartDate, + "end_date": query.EndDate, + } + return result, nil + } +} + +func applyPurchaseQueryFilters(db *gorm.DB, query agents.PurchaseQuery) (*gorm.DB, error) { + if query.Search != "" { + var id uint + if _, err := parsefmt.Sscanf(query.Search, "%d", &id); err == nil && id > 0 { + db = db.Where("id = ?", id) + } else { + db = db.Where("title LIKE ? OR remark LIKE ?", "%"+query.Search+"%", "%"+query.Search+"%") + } + } + if query.Status != "" { + db = db.Where("order_status = ?", query.Status) + } + if query.StartDate != "" { + startDate, err := time.Parse("2006-01-02", query.StartDate) + if err != nil { + return nil, err + } + db = db.Where("created_at >= ?", startDate) + } + if query.EndDate != "" { + endDate, err := time.Parse("2006-01-02", query.EndDate) + if err != nil { + return nil, err + } + db = db.Where("created_at < ?", endDate.AddDate(0, 0, 1)) + } + return db, nil +} + +func queryPurchaseOrderList(query agents.PurchaseQuery) ([]agents.PurchaseOrder, int64, error) { + db, err := applyPurchaseQueryFilters(models.DB.Model(&TabPurchaseOrder{}), query) + if err != nil { + return nil, 0, err + } + var total int64 + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + var rows []TabPurchaseOrder + if err := db.Order("updated_at DESC, id DESC").Offset(query.Limit * (query.Page - 1)).Limit(query.Limit).Find(&rows).Error; err != nil { + return nil, 0, err + } + + orders := make([]agents.PurchaseOrder, 0, len(rows)) + for _, row := range rows { + order, err := queryPurchaseOrderDetail(row.ID, query.UserID) + if err != nil { + return nil, 0, err + } + if order != nil { + orders = append(orders, *order) + } + } + return orders, total, nil +} + +func queryPurchaseOrderDetail(orderID uint, userID uint) (*agents.PurchaseOrder, error) { + var row TabPurchaseOrder + if err := models.DB.Where("id = ?", orderID).First(&row).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + order := buildPurchaseOrder(row, userID) + + var costs []TabPurchaseCosts + if err := models.DB.Where("order_id = ?", orderID).Order("id asc").Find(&costs).Error; err != nil { + return nil, err + } + order.Costs = make([]agents.PurchaseCost, 0, len(costs)) + for _, cost := range costs { + order.Costs = append(order.Costs, agents.PurchaseCost{ + ID: cost.ID, + OrderID: cost.OrderID, + UserID: cost.UserID, + Price: cost.Price, + Quantity: cost.Quantity, + CurrencyType: cost.CurrencyType, + CurrencyName: purchaseCurrencyName(cost.CurrencyType), + CostType: cost.CostType, + CostTypeName: purchaseCostTypeName(cost.CostType), + }) + } + + var commits []TabPurchaseCommit + if err := models.DB.Where("order_id = ?", orderID).Order("created_at DESC").Find(&commits).Error; err != nil { + return nil, err + } + order.Commits = make([]agents.PurchaseCommit, 0, len(commits)) + for _, commit := range commits { + order.Commits = append(order.Commits, agents.PurchaseCommit{ + ID: commit.ID, + OrderID: commit.OrderID, + UserID: commit.UserID, + Action: commit.Action, + Status: commit.Status, + OldStatus: commit.OldStatus, + Comment: commit.Comment, + CreatedAt: formatTimePtr(commit.CreatedAt), + }) + } + return &order, nil +} + +func queryPurchaseCounts(query agents.PurchaseQuery) (map[string]int64, error) { + counts := map[string]int64{} + statuses := []string{"pending", "ordered", "arrived", "received", "lost", "returned"} + var total int64 + base, err := applyPurchaseQueryFilters(models.DB.Model(&TabPurchaseOrder{}), agents.PurchaseQuery{Search: query.Search, StartDate: query.StartDate, EndDate: query.EndDate}) + if err != nil { + return nil, err + } + if err := base.Count(&total).Error; err != nil { + return nil, err + } + counts["total"] = total + for _, status := range statuses { + statusQuery := agents.PurchaseQuery{Search: query.Search, Status: status, StartDate: query.StartDate, EndDate: query.EndDate} + db, err := applyPurchaseQueryFilters(models.DB.Model(&TabPurchaseOrder{}), statusQuery) + if err != nil { + return nil, err + } + var count int64 + if err := db.Count(&count).Error; err != nil { + return nil, err + } + counts[status] = count + } + return counts, nil +} + +func buildPurchaseOrder(row TabPurchaseOrder, currentUserID uint) agents.PurchaseOrder { + return agents.PurchaseOrder{ + ID: row.ID, + UserID: row.UserID, + Title: row.Title, + Remark: row.Remark, + Link: row.Link, + DetailURL: purchaseOrderDetailURL(row.ID), + Styles: row.Styles, + OrderStatus: row.OrderStatus, + OrderStatusName: purchaseStatusName(row.OrderStatus), + CreatedAt: formatTimePtr(row.CreatedAt), + UpdatedAt: formatTimePtr(row.UpdatedAt), + CanModify: canModifyPurchase(currentUserID, row.UserID), + } +} + +func purchaseOrderDetailURL(orderID uint) string { + return parsefmt.Sprintf("/purchase/showorder/%d", orderID) +} + +func formatTimePtr(t *time.Time) string { + if t == nil { + return "" + } + return t.Format("2006-01-02 15:04:05") +} + +func purchaseStatusName(status string) string { + switch status { + case "pending": + return "待处理" + case "ordered": + return "已下单" + case "arrived": + return "已到达" + case "received": + return "已收件" + case "lost": + return "丢件" + case "returned": + return "退件" + default: + return status + } +} + +func purchaseCurrencyName(currencyType int) string { + switch currencyType { + case 1: + return "CNY" + case 2: + return "MOP" + case 3: + return "HKD" + case 4: + return "USD" + default: + return "未知" + } +} + +func purchaseCostTypeName(costType int) string { + switch costType { + case 1: + return "单价" + case 2: + return "运费" + default: + return "未知" + } +} + func ApiPurchaseInit() { models.DB.AutoMigrate(&TabPurchaseOrder{}) @@ -136,6 +401,7 @@ func ApiPurchaseInit() { models.DB.Create(&purchaseUserGroup) } + agents.RegisterPurchaseProvider(purchaseProvider{}) } func ApiPurchase(r *gin.RouterGroup) {