package hikrcs import ( "bytes" "crypto/hmac" "crypto/md5" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "sort" "strings" "time" "github.com/google/uuid" ) /* 海康 RCS-2000(调度系统)HTTP API 客户端。 签名规则(《AGV海康.md》数字签名授权): 1. 请求头需带:AUTHORIZATION / HOST / X-LR-APPKEY / X-LR-REQUEST-ID / X-LR-VERSION (存在其他以 X-LR- 开头的头时也一并按 key 排序参与拼接) 2. 待签名字符串 = "METHOD PATH HTTP/1.1\n" + 各 HEADER 行按 key 升序逐行 (KEY: VALUE \n) 3. 签名 = md5( HMAC-SHA256( AppSecret, 待签字符串 ) ) 小写 hex 正式对接前若厂商样例算法细节略有出入,只需调整 buildSignature() 一处。 */ type Client struct { BaseURL string // 如 http://192.168.1.210:8099 AppKey string AppSecret string Version string // X-LR-VERSION,如 v1.0 HTTP *http.Client } func New(baseURL, appKey, appSecret, version string) *Client { return &Client{ BaseURL: strings.TrimRight(baseURL, "/"), AppKey: appKey, AppSecret: appSecret, Version: version, HTTP: &http.Client{Timeout: 15 * time.Second}, } } // SubmitTaskBody 提交任务请求体 type SubmitTaskBody struct { TaskCode string `json:"taskCode"` // 任务号(唯一) PrjCode string `json:"prjCode"` // 项目编码(多项目调度需要) Pri int `json:"pri"` // 优先级 0~14,默认 4 Task any `json:"task"` // 任务类型定义(子任务/模板ID等),暂放由调用方传 } func (c *Client) sign(method, path string, headers map[string]string) string { keys := make([]string, 0, len(headers)) for k := range headers { keys = append(keys, k) } sort.Strings(keys) var b strings.Builder b.WriteString(strings.ToUpper(method)) b.WriteString(" ") b.WriteString(path) b.WriteString(" HTTP/1.1\n") for _, k := range keys { b.WriteString(k) b.WriteString(": ") b.WriteString(headers[k]) b.WriteString("\n") } content := []byte(b.String()) mac := hmac.New(sha256.New, []byte(c.AppSecret)) mac.Write(content) sum := mac.Sum(nil) digest := md5.Sum(sum) return hex.EncodeToString(digest[:]) } func (c *Client) do(method, path string, body any) (json.RawMessage, error) { u := c.BaseURL + path host := "" if p, err := url.Parse(c.BaseURL); err == nil { host = p.Host } requestID := strings.ToUpper(uuid.NewString()) headers := map[string]string{ "AUTHORIZATION": "Bearer " + c.AppKey, "HOST": host, "X-LR-APPKEY": c.AppKey, "X-LR-REQUEST-ID": requestID, "X-LR-VERSION": c.Version, } signature := c.sign(method, path, headers) var rd io.Reader if body != nil { buf, err := json.Marshal(body) if err != nil { return nil, err } rd = bytes.NewReader(buf) } req, err := http.NewRequest(method, u, rd) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") for k, v := range headers { http.CanonicalHeaderKey(k) req.Header.Set(canonical(k), v) } req.Header.Set("X-LR-SIGNATURE", signature) resp, err := c.HTTP.Do(req) if err != nil { return nil, fmt.Errorf("RCS 不可达(%s): %w", path, err) } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) var out struct { Code string `json:"code"` Message string `json:"message"` ErrorMessage string `json:"errorMessage"` Data json.RawMessage `json:"data"` } _ = json.Unmarshal(raw, &out) if resp.StatusCode >= 400 || (out.Code != "" && out.Code != "0") { msg := out.Message if msg == "" { msg = out.ErrorMessage } return out.Data, fmt.Errorf("RCS 返回错误(%s): code=%s %s", path, out.Code, truncateStr(msg, 200)) } return out.Data, nil } // canonical 将全大写签名头名转成常规首字母大写形式 func canonical(s string) string { s = strings.ToLower(s) parts := strings.Split(s, "-") for i, p := range parts { if p != "" { parts[i] = strings.ToUpper(p[:1]) + p[1:] } } return strings.Join(parts, "-") } func truncateStr(s string, n int) string { if len(s) > n { return s[:n] } return s } // Submit 提交搬运任务 func (c *Client) Submit(taskCode, prjCode string, pri int, task any) (json.RawMessage, error) { body := map[string]any{"taskCode": taskCode, "pri": pri, "task": task} if prjCode != "" { body["prjCode"] = prjCode } return c.do(http.MethodPost, "/rcs/rtas/api/robot/controller/task/submit", body) } // QueryTask 查询任务状态 func (c *Client) QueryTask(taskCode string) (json.RawMessage, error) { path := fmt.Sprintf("/rcs/rtas/api/robot/controller/task/query?taskCode=%s", url.QueryEscape(taskCode)) return c.do(http.MethodGet, path, nil) }