From 8d046df04e9fc76e0abd7d4e0ec138f033827fd8 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Mon, 2 Mar 2026 16:59:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=B4=A7=E6=98=93?= =?UTF-8?q?=E9=80=9A=E5=9C=B0=E5=9D=80=E6=8F=90=E5=8F=96=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/data/constants/support.go | 25 +++++++ internal/pkg/util/string.go | 73 +++++++++++++++++++ internal/server/http.go | 4 +- internal/server/router/router.go | 5 +- internal/services/provider_set.go | 1 + internal/services/support.go | 108 +++++++++++++++++++++++++++++ 6 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 internal/data/constants/support.go create mode 100644 internal/services/support.go diff --git a/internal/data/constants/support.go b/internal/data/constants/support.go new file mode 100644 index 0000000..106ec8b --- /dev/null +++ b/internal/data/constants/support.go @@ -0,0 +1,25 @@ +package constants + +// Token +const ( + TokenAddressIngestHyt = "E632C7D3E60771B03264F2337CCFA014" // md5("address_ingest_hyt") +) + +// 系统提示词 +const ( + SystemPromptAddressIngestHyt = `# 你是一个地址信息结构化解析器。 +你的任务是从用户提供的非结构化文本中,准确抽取并区分以下字段: + +1. 收货人 recipient (真实姓名或带掩码姓名,如“张三”) +2. 联系电话 phone (中国大陆手机号,11位数字) +3. 收货地址 address + +解析规则: +- 电话号码只提取最可能的一个 +- 不要编造不存在的信息 + +输出示例: +{\"recipient\": \"张三\",\"phone\": \"13458968095\",\"address\": \"四川省成都市武侯区天府三街88号\"} + +输出格式必须为严格 JSON,不要输出任何解释性文字。` +) diff --git a/internal/pkg/util/string.go b/internal/pkg/util/string.go index 9dd4056..24a009e 100644 --- a/internal/pkg/util/string.go +++ b/internal/pkg/util/string.go @@ -2,6 +2,8 @@ package util import ( "encoding/json" + "fmt" + "regexp" "strconv" "strings" ) @@ -42,3 +44,74 @@ func Contains[T comparable](strings []T, str T) bool { } return false } + +// json LLM专用字符串修复 +func JSONRepair(input string) (string, error) { + s := strings.TrimSpace(input) + + s = trimToJSONObject(s) + s = normalizeQuotes(s) + s = removeTrailingCommas(s) + s = quoteObjectKeys(s) + s = balanceBrackets(s) + + // 最终校验 + var js any + if err := json.Unmarshal([]byte(s), &js); err != nil { + return "", fmt.Errorf("json repair failed: %w", err) + } + return s, nil +} + +// 裁剪前后垃圾文本 +func trimToJSONObject(s string) string { + start := strings.IndexAny(s, "{[") + end := strings.LastIndexAny(s, "}]") + if start == -1 || end == -1 || start >= end { + return s + } + return s[start : end+1] +} + +// 引号统一 +func normalizeQuotes(s string) string { + // 只替换“看起来像字符串的单引号” + re := regexp.MustCompile(`'([^']*)'`) + return re.ReplaceAllString(s, `"$1"`) +} + +// 删除尾随逗号 +func removeTrailingCommas(s string) string { + re := regexp.MustCompile(`,(\s*[}\]])`) + return re.ReplaceAllString(s, `$1`) +} + +// 给 object key 自动补双引号 +func quoteObjectKeys(s string) string { + re := regexp.MustCompile(`([{,]\s*)([a-zA-Z0-9_]+)\s*:`) + return re.ReplaceAllString(s, `$1"$2":`) +} + +// 括号补齐 +func balanceBrackets(s string) string { + var stack []rune + for _, r := range s { + switch r { + case '{', '[': + stack = append(stack, r) + case '}', ']': + if len(stack) > 0 { + stack = stack[:len(stack)-1] + } + } + } + for i := len(stack) - 1; i >= 0; i-- { + switch stack[i] { + case '{': + s += "}" + case '[': + s += "]" + } + } + return s +} diff --git a/internal/server/http.go b/internal/server/http.go index 53446c8..4935593 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -18,6 +18,7 @@ type HTTPServer struct { callback *services.CallbackService chatHis *services.HistoryService capabilityService *services.CapabilityService + supportService *services.SupportService } func NewHTTPServer( @@ -28,10 +29,11 @@ func NewHTTPServer( callback *services.CallbackService, chatHis *services.HistoryService, capabilityService *services.CapabilityService, + supportService *services.SupportService, ) *fiber.App { //构建 server app := initRoute() - router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService) + router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService, supportService) return app } diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 17fff12..da763fe 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -26,7 +26,7 @@ type RouterServer struct { // SetupRoutes 设置路由 func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService, gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService, - capabilityService *services.CapabilityService, + capabilityService *services.CapabilityService, supportService *services.SupportService, ) { app.Use(func(c *fiber.Ctx) error { // 设置 CORS 头 @@ -98,6 +98,9 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi // 能力 r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取 r.Post("/capability/product/ingest/:thread_id/confirm", capabilityService.ProductIngestConfirm) // 商品数据提取确认 + + // 外部系统支持 + r.Post("/support/address/ingest/hyt", supportService.AddressIngestHyt) // 货易通收获地址提取 } func routerSocket(app *fiber.App, chatService *services.ChatService) { diff --git a/internal/services/provider_set.go b/internal/services/provider_set.go index 55eed7a..809390f 100644 --- a/internal/services/provider_set.go +++ b/internal/services/provider_set.go @@ -15,4 +15,5 @@ var ProviderSetServices = wire.NewSet( NewHistoryService, NewCapabilityService, NewCronService, + NewSupportService, ) diff --git a/internal/services/support.go b/internal/services/support.go new file mode 100644 index 0000000..ac34626 --- /dev/null +++ b/internal/services/support.go @@ -0,0 +1,108 @@ +package services + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/data/constants" + errorcode "ai_scheduler/internal/data/error" + "ai_scheduler/internal/pkg/util" + "ai_scheduler/internal/pkg/utils_vllm" + "context" + "encoding/json" + "strings" + "time" + + "github.com/cloudwego/eino/schema" + "github.com/gofiber/fiber/v2" +) + +type SupportService struct { + cfg *config.Config +} + +func NewSupportService(cfg *config.Config) *SupportService { + return &SupportService{ + cfg: cfg, + } +} + +type AddressIngestHytReq struct { + Text string `json:"text"` // 待提取文本 +} + +type AddressIngestHytResp struct { + Recipient string `json:"recipient"` // 收货人 + Phone string `json:"phone"` // 联系电话 + Address string `json:"address"` // 收货地址 +} + +// AddressIngestHyt 货易通收获地址提取 +func (s *SupportService) AddressIngestHyt(c *fiber.Ctx) error { + ctx := context.Background() + + // 请求头校验 + if err := s.checkRequestHeader(c); err != nil { + return err + } + // 解析请求参数 body + req := AddressIngestHytReq{} + if err := c.BodyParser(&req); err != nil { + return errorcode.ParamErrf("invalid request body: %v", err) + } + // 必要参数校验 + if req.Text == "" { + return errorcode.ParamErrf("missing required fields") + } + + // 模型调用 + client, cleanup, err := utils_vllm.NewClient(s.cfg) + if err != nil { + return err + } + defer cleanup() + res, err := client.Chat(ctx, []*schema.Message{ + { + Role: "system", + Content: constants.SystemPromptAddressIngestHyt, + }, + { + Role: "user", + Content: req.Text, + }, + }) + if err != nil { + return err + } + + // 解析模型返回结果 + var addr AddressIngestHytResp + if err := json.Unmarshal([]byte(res.Content), &addr); err != nil { + // 修复json字符串 + res.Content, err = util.JSONRepair(res.Content) + if err != nil { + return errorcode.ParamErrf("invalid response body: %v", err) + } + if err := json.Unmarshal([]byte(res.Content), &addr); err != nil { + return errorcode.ParamErrf("invalid response body: %v", err) + } + } + + return c.JSON(addr) +} + +// checkRequestHeader 校验请求头 +func (s *SupportService) checkRequestHeader(c *fiber.Ctx) error { + // 读取头 + token := strings.TrimSpace(c.Get("X-Source-Key")) + ts := strings.TrimSpace(c.Get("X-Timestamp")) + + // 时间窗口校验 + if ts != "" && !util.IsInTimeWindow(ts, 5*time.Minute) { + return errorcode.AuthNotFound + } + // token校验 + if token == "" || token != constants.TokenAddressIngestHyt { + return errorcode.KeyNotFound + } + + return nil +}