Files
shop-crm-agent/internal/client/client.go
T
Shiny 3466e19d13 feat: 初始化shop-crm-agent项目,实现基础CLI与CRM操作能力
本提交初始化完整的shop-crm-agent项目:
1. 创建基础项目结构与配置文件(.gitignore、go.mod/go.sum)
2. 添加项目许可协议与文档(README.md、AGENTS.md、SKILL.md)
3. 实现核心CLI入口与命令框架
4. 封装Protobuf API客户端与完整的命令实现
5. 添加单元测试覆盖核心功能与边界情况
6. 提供构建脚本与版本管理能力
2026-08-01 01:36:37 +08:00

167 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package client
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
api "code.zhecent.com/open/shop-crm-agent/internal/api"
"google.golang.org/protobuf/proto"
)
const (
apiPath = "/api/shop/crm-agent/v1"
maxResponseSize = 16 << 20
)
type Client struct {
baseURL string
token string
httpClient *http.Client
}
func New(baseURL, token string, httpClient *http.Client) (*Client, error) {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return nil, errors.New("LIGHTCORE_API_BASE_URL 必须是有效的 http/https URL")
}
if strings.TrimSpace(token) == "" {
return nil, errors.New("LIGHTCORE_SHOP_CRM_AGENT_TOKEN 不能为空")
}
if httpClient == nil {
httpClient = &http.Client{Timeout: 30 * time.Second}
}
return &Client{baseURL: baseURL, token: strings.TrimSpace(token), httpClient: httpClient}, nil
}
type responseWithHeader interface {
proto.Message
GetHeader() *api.ResponseHeader
}
func doRequest[Req proto.Message, Resp responseWithHeader](ctx context.Context, client *Client, path string, request Req, response Resp) (Resp, error) {
data, err := proto.Marshal(request)
if err != nil {
return response, fmt.Errorf("编码 Protobuf 请求失败: %w", err)
}
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL+apiPath+path, bytes.NewReader(data))
if err != nil {
return response, err
}
httpRequest.Header.Set("Content-Type", "application/x-protobuf")
httpRequest.Header.Set("Accept", "application/x-protobuf")
httpRequest.Header.Set("Authorization", "Bearer "+client.token)
httpResponse, err := client.httpClient.Do(httpRequest)
if err != nil {
return response, err
}
defer httpResponse.Body.Close()
body, err := io.ReadAll(io.LimitReader(httpResponse.Body, maxResponseSize+1))
if err != nil {
return response, err
}
if len(body) > maxResponseSize {
return response, errors.New("CRM Agent API 响应超过16 MiB限制")
}
if err := proto.Unmarshal(body, response); err != nil {
return response, fmt.Errorf("解析 Protobuf 响应失败(HTTP %d: %w", httpResponse.StatusCode, err)
}
header := response.GetHeader()
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
if header != nil && header.Message != "" {
return response, fmt.Errorf("CRM Agent API HTTP %d: %s", httpResponse.StatusCode, header.Message)
}
return response, fmt.Errorf("CRM Agent API HTTP %d", httpResponse.StatusCode)
}
if header == nil {
return response, errors.New("CRM Agent API 响应缺少 ResponseHeader")
}
if header.Code != 200 {
return response, fmt.Errorf("CRM Agent API 错误 %d: %s", header.Code, header.Message)
}
return response, nil
}
func (c *Client) CustomerList(ctx context.Context, req *api.CustomerListRequest) (*api.CustomerListResponse, error) {
return doRequest(ctx, c, "/customers/list", req, &api.CustomerListResponse{})
}
func (c *Client) ProjectList(ctx context.Context, req *api.ProjectListRequest) (*api.ProjectListResponse, error) {
return doRequest(ctx, c, "/projects/list", req, &api.ProjectListResponse{})
}
func (c *Client) ProjectGet(ctx context.Context, req *api.ProjectGetRequest) (*api.ProjectResponse, error) {
return doRequest(ctx, c, "/projects/get", req, &api.ProjectResponse{})
}
func (c *Client) ProjectCreate(ctx context.Context, req *api.ProjectCreateRequest) (*api.ProjectResponse, error) {
return doRequest(ctx, c, "/projects/create", req, &api.ProjectResponse{})
}
func (c *Client) ProjectUpdate(ctx context.Context, req *api.ProjectUpdateRequest) (*api.ProjectResponse, error) {
return doRequest(ctx, c, "/projects/update", req, &api.ProjectResponse{})
}
func (c *Client) ProjectStatus(ctx context.Context, req *api.ProjectStatusRequest) (*api.ProjectResponse, error) {
return doRequest(ctx, c, "/projects/status", req, &api.ProjectResponse{})
}
func (c *Client) ProjectDeletePreview(ctx context.Context, req *api.ProjectDeletePreviewRequest) (*api.DeletePreviewResponse, error) {
return doRequest(ctx, c, "/projects/delete/preview", req, &api.DeletePreviewResponse{})
}
func (c *Client) ProjectDelete(ctx context.Context, req *api.ProjectDeleteRequest) (*api.DeletePreviewResponse, error) {
return doRequest(ctx, c, "/projects/delete", req, &api.DeletePreviewResponse{})
}
func (c *Client) QuotationList(ctx context.Context, req *api.QuotationListRequest) (*api.QuotationListResponse, error) {
return doRequest(ctx, c, "/quotations/list", req, &api.QuotationListResponse{})
}
func (c *Client) QuotationGet(ctx context.Context, req *api.QuotationGetRequest) (*api.QuotationResponse, error) {
return doRequest(ctx, c, "/quotations/get", req, &api.QuotationResponse{})
}
func (c *Client) QuotationCreate(ctx context.Context, req *api.QuotationCreateRequest) (*api.QuotationResponse, error) {
return doRequest(ctx, c, "/quotations/create", req, &api.QuotationResponse{})
}
func (c *Client) QuotationCopy(ctx context.Context, req *api.QuotationCopyRequest) (*api.QuotationResponse, error) {
return doRequest(ctx, c, "/quotations/copy", req, &api.QuotationResponse{})
}
func (c *Client) QuotationTitle(ctx context.Context, req *api.QuotationTitleRequest) (*api.QuotationResponse, error) {
return doRequest(ctx, c, "/quotations/title", req, &api.QuotationResponse{})
}
func (c *Client) QuotationStatus(ctx context.Context, req *api.QuotationStatusRequest) (*api.QuotationResponse, error) {
return doRequest(ctx, c, "/quotations/status", req, &api.QuotationResponse{})
}
func (c *Client) QuotationLock(ctx context.Context, req *api.QuotationLockRequest) (*api.QuotationResponse, error) {
return doRequest(ctx, c, "/quotations/lock", req, &api.QuotationResponse{})
}
func (c *Client) QuotationUnlock(ctx context.Context, req *api.QuotationLockRequest) (*api.QuotationResponse, error) {
return doRequest(ctx, c, "/quotations/unlock", req, &api.QuotationResponse{})
}
func (c *Client) QuotationDeletePreview(ctx context.Context, req *api.QuotationDeletePreviewRequest) (*api.DeletePreviewResponse, error) {
return doRequest(ctx, c, "/quotations/delete/preview", req, &api.DeletePreviewResponse{})
}
func (c *Client) QuotationDelete(ctx context.Context, req *api.QuotationDeleteRequest) (*api.DeletePreviewResponse, error) {
return doRequest(ctx, c, "/quotations/delete", req, &api.DeletePreviewResponse{})
}
func (c *Client) CatalogSearch(ctx context.Context, req *api.CatalogSearchRequest) (*api.CatalogSearchResponse, error) {
return doRequest(ctx, c, "/catalog/search", req, &api.CatalogSearchResponse{})
}
func (c *Client) EditBegin(ctx context.Context, req *api.QuotationEditBeginRequest) (*api.EditSessionResponse, error) {
return doRequest(ctx, c, "/quotation/edit/begin", req, &api.EditSessionResponse{})
}
func (c *Client) EditCommand(ctx context.Context, req *api.QuotationEditCommandRequest) (*api.QuotationEditCommandResponse, error) {
return doRequest(ctx, c, "/quotation/edit/command", req, &api.QuotationEditCommandResponse{})
}
func (c *Client) EditPreview(ctx context.Context, req *api.QuotationEditPreviewRequest) (*api.QuotationEditPreviewResponse, error) {
return doRequest(ctx, c, "/quotation/edit/preview", req, &api.QuotationEditPreviewResponse{})
}
func (c *Client) EditCommit(ctx context.Context, req *api.QuotationEditCommitRequest) (*api.QuotationResponse, error) {
return doRequest(ctx, c, "/quotation/edit/commit", req, &api.QuotationResponse{})
}
func (c *Client) EditDiscard(ctx context.Context, req *api.QuotationEditDiscardRequest) (*api.EditSessionResponse, error) {
return doRequest(ctx, c, "/quotation/edit/discard", req, &api.EditSessionResponse{})
}