Repository: alibaba/higress
Pull request #1921: feat: add xfyun emb to ai-cache
Author: login: Beatrueman
Created: 2025-03-18T07:57:13Z | Merged: 2025-03-20T03:05:37Z

### Ⅰ. Describe what this PR did

AI 缓存插件对接 讯飞星火

### Ⅱ. Does this pull request fix one issue?

Fix [#1446](https://github.com/alibaba/higress/issues/1446)

### Ⅲ. Why don't you add test cases (unit test/integration test)?

### Ⅳ. Describe how to verify it

**1.环境介绍**

ollama本地部署llama3.2，并且使用ngrok内网穿透

ai-proxy使用ollama作为provider

**2.编译插件**

`ai-proxy`

```
cd ./higress/plugins/wasm-go/extensions/ai-proxy

tinygo build -o ai.wasm -scheduler=none -target=wasi -gc=custom -tags="custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100" ./
```

`ai-cache`

```
cd ./higress/plugins/wasm-go/extensions/ai-cache

tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags="custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100" ./
```

**3.Docker部署higress**

 `docker-compose.yaml`

```
services:
  envoy:
    image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.0.2
    entrypoint: /usr/local/bin/envoy
    command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
    networks:
      - wasmtest
    ports:
      - "10002:10002"
    volumes:
      - ./envoy.yaml:/etc/envoy/envoy.yaml
      - ./main.wasm:/etc/envoy/main.wasm
      - ./ai.wasm:/etc/envoy/ai.wasm

networks:
  wasmtest: {}

```

`envoy.yaml`

```
admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901
static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address:
          protocol: TCP
          address: 0.0.0.0
          port_value: 10002
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                scheme_header_transformation:
                  scheme_to_overwrite: https
                stat_prefix: ingress_http
                # Output envoy logs to stdout
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                # Modify as required
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: [ "*" ]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: ollama
                            timeout: 30s
                http_filters:
                  - name: wasmtest
                    typed_config:
                      "@type": type.googleapis.com/udpa.type.v1.TypedStruct
                      type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                      value:
                        config:
                          name: wasmtest
                          vm_config:
                            runtime: envoy.wasm.runtime.v8
                            code:
                              local:
                                filename: /etc/envoy/ai.wasm
                          configuration:
                            "@type": "type.googleapis.com/google.protobuf.StringValue"
                            value: |
                              {
                                "provider": {
                                  "type": "ollama",
                                  "apiTokens": [
                                    "sk-"
                                  ],
                                "ollamaServerHost": "0a3f-113-250-211-184.ngrok-free.app",
                                "ollamaServerPort": 443,
                                "ollamaModel": ""
                                }
                              }

                  - name: cache
                    typed_config:
                      "@type": type.googleapis.com/udpa.type.v1.TypedStruct
                      type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                      value:
                        config:
                          name: cache
                          vm_config:
                            runtime: envoy.wasm.runtime.v8
                            code:
                              local:
                                filename: /etc/envoy/main.wasm
                          configuration:
                            "@type": "type.googleapis.com/google.protobuf.StringValue"
                            value: |
                              {
                                "embedding": {
                                  "type": "xfyun",
                                  "serviceName": "xfyun.dns",
                                  "apiKey": "your APIkey",
                                  "APPID": "your APPID",
                                  "APISecret": "your APISecret"
                                },
                                "vector": {
                                  "type": "dashvector",
                                  "serviceName": "dashvector.dns",
                                  "collectionID": "test1",
                                  "serviceHost": "your host",
                                  "apiKey": "your key",
                                  "threshold": 0.4
                                },
                                "cache": {
                                  "serviceName": "",
                                  "type": ""
                                }
                              }
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
    - name: ollama
      connect_timeout: 30s
      type: LOGICAL_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: ollama
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 05da-113-250-208-95.ngrok-free.app
                      port_value: 443
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
          "sni": "0a3f-113-250-211-184.ngrok-free.app"

    - name: outbound|443||xfyun.dns
      connect_timeout: 30s
      type: LOGICAL_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: outbound|443||xfyun.dns
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: emb-cn-huabei-1.xf-yun.com
                      port_value: 443
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
          "sni": "emb-cn-huabei-1.xf-yun.com"

    - name: outbound|443||dashvector.dns
      connect_timeout: 30s
      type: LOGICAL_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: outbound|443||dashvector.dns
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: vrs-*aliyuncs.com
                      port_value: 443
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
          "sni": "vrs-*aliyuncs.com"

```

**4. 测试**

```
curl "http://localhost:10002/ai/v1/chat/completions"  -H "Content-Type: application/json" -d '{
  "model": "llama3.2",
  "messages": [
    {
      "role": "user",
      "content": "你好"
    }
  ]
}'
```

回显

![image-20250318153118718](https://gitee.com/beatrueman/images/raw/master/img/202503181531825.png)

容器日志

![img_v3_02kg_b9f464ed-7e98-44b5-88fd-cd21d7bdab3g](https://gitee.com/beatrueman/images/raw/master/img/202503181531910.png)

**说明**

讯飞星火鉴权说明API文档

[http协议通用鉴权URL生成说明 | 讯飞开放平台文档中心](https://www.xfyun.cn/doc/spark/http_url_authentication.html#_1-鉴权说明)

我在鉴权时通过后缀拼接url进行鉴权，不知道是否合规。
![image-20250318153416212](https://gitee.com/beatrueman/images/raw/master/img/202503181534247.png)

整体对`xfyun emb`的请求与响应解析参考了官方[Python demo](https://openres.xfyun.cn/xfyundoc/2024-03-26/78dc60db-b67d-4fb7-97a9-710fa5e226b0/1711443170449/Embedding.zip)


Diff:
diff --git a/plugins/wasm-go/extensions/ai-cache/README.md b/plugins/wasm-go/extensions/ai-cache/README.md
index 70f3e1b9d4..c70241b638 100644
--- a/plugins/wasm-go/extensions/ai-cache/README.md
+++ b/plugins/wasm-go/extensions/ai-cache/README.md
@@ -100,14 +100,58 @@ LLM 结果缓存插件，默认配置方式可以直接用于 openai 协议的
 | responseTemplate | string | optional | `{"id":"ai-cache.hit","choices":[{"index":0,"message":{"role":"assistant","content":%s},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` | 返回 HTTP 响应的模版，用 %s 标记需要被 cache value 替换的部分 |
 | streamResponseTemplate | string | optional | `data:{"id":"ai-cache.hit","choices":[{"index":0,"delta":{"role":"assistant","content":%s},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}\n\ndata:[DONE]\n\n` | 返回流式 HTTP 响应的模版，用 %s 标记需要被 cache value 替换的部分 |
 
-# 向量数据库提供商特有配置
-## Chroma
+## 文本向量化提供商特有配置
+
+### OpenAI
+
+OpenAI 所对应的 `embedding.type` 为 `openai`。它并无特有的配置字段。需要提前创建 [API Key](https://platform.openai.com/settings/organization/api-keys)，并将其填入`embedding.apiKey`，一个 API Key 的示例为` sk-xxxxxxx`。
+
+### Ollama
+
+Ollama 所对应的 `embedding.type` 为 `ollama`。它并无特有的配置字段。
+
+### 讯飞星火
+
+讯飞星火 所对应的 `embedding.type` 为 `xfyun`。它需要提前创建[应用](https://console.xfyun.cn/services/emb)，获取`APPID`  、`APISecret`和`APIKey`，并将`APIKey`填入`embedding.apiKey`中。
+
+它特有的配置字段如下：
+
+| 名称                  | 数据类型 | 填写要求 | 默认值 | 描述                 | 填写值           |
+| --------------------- | -------- | -------- | ------ | -------------------- | ---------------- |
+| `embedding.appId`     | string   | 必填     | -      | 应用 ID              | 获取的 APPID     |
+| `embedding.apiSecret` | string   | 必填     | -      | 调用 API 所需 Secret | 获取的 APISecret |
+
+### Textln
+
+Textln 所对应的 `embedding.type` 为 `textln`。它需要提前获取[`app-id` 和`secret-code`](https://www.textin.com/document/acge_text_embedding)。
+
+它特有的配置字段如下：
+
+| 名称                            | 数据类型 | 填写要求 | 默认值 | 描述                 | 填写值             |
+| ------------------------------- | -------- | -------- | ------ | -------------------- | ------------------ |
+| `embedding.textinAppId`         | string   | 必填     | -      | 应用 ID              | 获取的 app-id      |
+| `embedding.textinSecretCode`    | string   | 必填     | -      | 调用 API 所需 Secret | 获取的 secret-code |
+| `embedding.textinMatryoshkaDim` | int      | 必填     | -      | 返回的单个向量长度   |                    |
+
+### Hugging Face
+
+Hugging Face 所对应的 `embedding.type` 为 `huggingface`。它并无特有的配置字段。需要提前创建 [hf_token](https://huggingface.co/blog/getting-started-with-embeddings)，并将其填入`embedding.apiKey`，一个 hf_token 的示例为` hf_xxxxxxx`。
+
+`embedding.model`默认指定为`sentence-transformers/all-MiniLM-L6-v2`
+
+### Cohere
+
+Cohere 所对应的 `embedding.type` 为 `cohere`。它并无特有的配置字段。需要提前创建 [API Key](https://docs.cohere.com/reference/embed)，并将其填入`embedding.apiKey`。
+
+## 向量数据库提供商特有配置
+
+### Chroma
 Chroma 所对应的 `vector.type` 为 `chroma`。它并无特有的配置字段。需要提前创建 Collection，并填写 Collection ID 至配置项 `vector.collectionID`，一个 Collection ID 的示例为 `52bbb8b3-724c-477b-a4ce-d5b578214612`。
 
-## DashVector
+### DashVector
 DashVector 所对应的 `vector.type` 为 `dashvector`。它并无特有的配置字段。需要提前创建 Collection，并填写 `Collection 名称` 至配置项 `vector.collectionID`。
 
-## ElasticSearch
+### ElasticSearch
 ElasticSearch 所对应的 `vector.type` 为 `elasticsearch`。需要提前创建 Index 并填写 Index Name 至配置项 `vector.collectionID` 。
 
 当前依赖于 [KNN](https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html) 方法，请保证 ES 版本支持 `KNN`，当前已在 `8.16` 版本测试。
@@ -121,18 +165,18 @@ ElasticSearch 所对应的 `vector.type` 为 `elasticsearch`。需要提前创
 
 `vector.esUsername` 和 `vector.esPassword` 用于 Basic 认证。同时也支持 Api Key 认证，当填写了 `vector.apiKey` 时，则启用 Api Key 认证，如果使用 SaaS 版本需要填写 `encoded` 的值。
 
-## Milvus
+### Milvus
 Milvus 所对应的 `vector.type` 为 `milvus`。它并无特有的配置字段。需要提前创建 Collection，并填写 Collection Name 至配置项 `vector.collectionID`。
 
-## Pinecone
+### Pinecone
 Pinecone 所对应的 `vector.type` 为 `pinecone`。它并无特有的配置字段。需要提前创建 Index，并填写 Index 访问域名至 `vector.serviceHost`。
 
 Pinecone 中的 `Namespace` 参数通过插件的 `vector.collectionID` 进行配置，如果不填写 `vector.collectionID`，则默认为 Default Namespace。
 
-## Qdrant
+### Qdrant
 Qdrant 所对应的 `vector.type` 为 `qdrant`。它并无特有的配置字段。需要提前创建 Collection，并填写 Collection Name 至配置项 `vector.collectionID`。
 
-## Weaviate
+### Weaviate
 Weaviate 所对应的 `vector.type` 为 `weaviate`。它并无特有的配置字段。
 需要提前创建 Collection，并填写 Collection Name 至配置项 `vector.collectionID`。
 
diff --git a/plugins/wasm-go/extensions/ai-cache/embedding/provider.go b/plugins/wasm-go/extensions/ai-cache/embedding/provider.go
index 3f3c027b0f..62c3970fef 100644
--- a/plugins/wasm-go/extensions/ai-cache/embedding/provider.go
+++ b/plugins/wasm-go/extensions/ai-cache/embedding/provider.go
@@ -14,6 +14,7 @@ const (
 	PROVIDER_TYPE_OPENAI      = "openai"
 	PROVIDER_TYPE_OLLAMA      = "ollama"
 	PROVIDER_TYPE_HUGGINGFACE = "huggingface"
+	PROVIDER_TYPE_XFYUN       = "xfyun"
 )
 
 type providerInitializer interface {
@@ -30,6 +31,7 @@ var (
 		PROVIDER_TYPE_OPENAI:      &openAIProviderInitializer{},
 		PROVIDER_TYPE_OLLAMA:      &ollamaProviderInitializer{},
 		PROVIDER_TYPE_HUGGINGFACE: &HuggingFaceProviderInitializer{},
+		PROVIDER_TYPE_XFYUN:       &XfyunProviderInitializer{},
 	}
 )
 
diff --git a/plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go b/plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go
new file mode 100644
index 0000000000..a3410d3dda
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go
@@ -0,0 +1,310 @@
+package embedding
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+	"github.com/tidwall/gjson"
+	"math"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+const (
+	XFYUN_DOMAIN = "emb-cn-huabei-1.xf-yun.com"
+	XFYUN_PORT   = 443
+)
+
+type XfyunProviderInitializer struct {
+}
+
+var XfyunConfig XfyunProviderConfig
+
+type XfyunProviderConfig struct {
+	// @Title zh-CN 文本特征提取服务 API Key
+	// @Description zh-CN 文本特征提取服务 API Key。
+	apiKey string
+	// @Title zh-CN 文本特征提取服务 APPID
+	// @Description zh-CN 文本特征提取服务 APPID。仅适用与 Xfyun
+	xfyunAppID string
+	// @Title zh-CN 文本特征提取服务 APISecret
+	// @Description zh-CN 文本特征提取服务 APISecret。仅适用与 Xfyun
+	xfyunApiSecret string
+}
+
+func (c *XfyunProviderInitializer) InitConfig(json gjson.Result) {
+	XfyunConfig.xfyunAppID = json.Get("appId").String()
+	XfyunConfig.xfyunApiSecret = json.Get("apiSecret").String()
+	XfyunConfig.apiKey = json.Get("apiKey").String()
+}
+
+func (c *XfyunProviderInitializer) ValidateConfig() error {
+	if XfyunConfig.apiKey == "" {
+		return errors.New("[Xfyun] apiKey is required")
+	}
+	if XfyunConfig.xfyunAppID == "" {
+		return errors.New("[Xfyun] appId is required")
+	}
+	if XfyunConfig.xfyunApiSecret == "" {
+		return errors.New("[Xfyun] apiSecret is required")
+	}
+	return nil
+}
+
+func (t *XfyunProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
+	if c.servicePort == 0 {
+		c.servicePort = XFYUN_PORT
+	}
+	if c.serviceHost == "" {
+		c.serviceHost = XFYUN_DOMAIN
+	}
+
+	return &XfyunProvider{
+		config: c,
+		client: wrapper.NewClusterClient(wrapper.FQDNCluster{
+			FQDN: c.serviceName,
+			Host: c.serviceHost,
+			Port: c.servicePort,
+		}),
+	}, nil
+}
+
+func (t *XfyunProvider) GetProviderType() string {
+	return PROVIDER_TYPE_XFYUN
+}
+
+type XfyunProvider struct {
+	config ProviderConfig
+	client wrapper.HttpClient
+}
+
+type XfyunHeader struct {
+	AppID  string `json:"app_id"`
+	Status int    `json:"status"`
+}
+
+type ReqFeature struct {
+	Encoding string `json:"encoding"`
+}
+
+type XfyunEmb struct {
+	Domain  string     `json:"domain"`
+	Feature ReqFeature `json:"feature"`
+}
+
+type XfyunParameter struct {
+	Emb XfyunEmb `json:"emb"`
+}
+
+type XfyunPayload struct {
+	Messages struct {
+		Text string `json:"text"`
+	} `json:"messages"`
+}
+
+type XfyunText struct {
+	MainMessages []struct {
+		Content *string `json:"content"`
+		Role    *string `json:"role"`
+	} `json:"messages"`
+}
+
+type XfyunReqBody struct {
+	Header    XfyunHeader    `json:"header"`
+	Parameter XfyunParameter `json:"parameter"`
+	Payload   XfyunPayload   `json:"payload"`
+}
+
+type XfyunResponse struct {
+	Header  XfyunResHeader  `json:"header"`
+	Payload XfyunResPayload `json:"payload"`
+}
+
+type XfyunResHeader struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+	Sid     string `json:"sid"`
+}
+
+type XfyunResPayload struct {
+	Feature struct {
+		Text string `json:"text"`
+	} `json:"feature"`
+}
+
+func constructAuth(requestURL, method, apiKey, apiSecret string) (string, error) {
+	u, err := url.Parse(requestURL)
+	if err != nil {
+		return "", err
+	}
+	now := time.Now().UTC().Format(http.TimeFormat)
+	signatureOrigin := fmt.Sprintf("host: %s\ndate: %s\n%s %s HTTP/1.1", u.Host, now, method, u.Path)
+	h := hmac.New(sha256.New, []byte(apiSecret))
+	h.Write([]byte(signatureOrigin))
+	signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
+	authorizationOrigin := fmt.Sprintf("api_key=\"%s\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"%s\"", apiKey, signature)
+	authorization := base64.StdEncoding.EncodeToString([]byte(authorizationOrigin))
+
+	params := url.Values{}
+	params.Add("host", u.Host)
+	params.Add("date", now)
+	params.Add("authorization", authorization)
+
+	return "?" + params.Encode(), nil
+}
+
+func (t *XfyunProvider) constructParameters(text string, log wrapper.Log) (string, [][2]string, []byte, error) {
+	if text == "" {
+		err := errors.New("queryString text cannot be empty")
+		return "", nil, nil, err
+	}
+
+	host := "https://" + t.config.serviceHost + "/"
+	auth, err := constructAuth(host, "POST", XfyunConfig.apiKey, XfyunConfig.xfyunApiSecret)
+	if err != nil {
+		return "", nil, nil, err
+	}
+
+	role := "user"
+
+	xfyunText := XfyunText{
+		MainMessages: []struct {
+			Content *string `json:"content"`
+			Role    *string `json:"role"`
+		}{
+			{
+				Content: &text,
+				Role:    &role,
+			},
+		},
+	}
+
+	// 将 XfyunText 转换为 JSON
+	xfyunTextJSON, err := json.Marshal(xfyunText)
+	if err != nil {
+		log.Errorf("Error marshaling XfyunText: %v", err)
+		return "", nil, nil, err
+	}
+
+	// 将整个 XfyunText JSON 字符串转换为 Base64 编码
+	encodedText := base64.StdEncoding.EncodeToString(xfyunTextJSON)
+
+	// 构建请求体
+	data := XfyunReqBody{
+		Header: XfyunHeader{
+			AppID:  XfyunConfig.xfyunAppID,
+			Status: 3,
+		},
+		Parameter: XfyunParameter{
+			Emb: XfyunEmb{
+				Domain: "query",
+				Feature: ReqFeature{
+					Encoding: "utf8",
+				},
+			},
+		},
+		Payload: XfyunPayload{
+			Messages: struct {
+				Text string `json:"text"`
+			}{Text: encodedText}, // 填充经过 Base64 编码的文本
+		},
+	}
+
+	// 序列化请求数据
+	requestBody, err := json.Marshal(data)
+	if err != nil {
+		log.Errorf("failed to marshal request data: %v", err)
+		return "", nil, nil, err
+	}
+
+	// 构建请求头
+	headers := [][2]string{
+		{"Content-Type", "application/json"},
+	}
+
+	return "/" + auth, headers, requestBody, nil
+}
+
+func (t *XfyunProvider) parseTextEmbedding(responseBody []byte) ([]float32, error) {
+	var resp XfyunResponse
+	err := json.Unmarshal(responseBody, &resp)
+	if err != nil {
+		return nil, err
+	}
+
+	base64Text := resp.Payload.Feature.Text
+	decodedBytes, err := base64.StdEncoding.DecodeString(base64Text)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(decodedBytes) == 0 {
+		return nil, errors.New("decoded embedding is empty")
+	}
+
+	if len(decodedBytes)%4 != 0 {
+		return nil, errors.New("decoded data is not a valid float32 array")
+	}
+
+	floatArray := make([]float32, len(decodedBytes)/4)
+	for i := 0; i < len(floatArray); i++ {
+		bits := binary.LittleEndian.Uint32(decodedBytes[i*4 : (i+1)*4])
+		floatArray[i] = math.Float32frombits(bits)
+	}
+
+	return floatArray, nil
+}
+
+func (t *XfyunProvider) GetEmbedding(
+	queryString string,
+	ctx wrapper.HttpContext,
+	log wrapper.Log,
+	callback func(emb []float64, err error)) error {
+	embUrl, embHeaders, embRequestBody, err := t.constructParameters(queryString, log)
+	if err != nil {
+		log.Errorf("failed to construct parameters: %v", err)
+		return err
+	}
+
+	err = t.client.Post(embUrl, embHeaders, embRequestBody,
+		func(statusCode int, responseHeaders http.Header, responseBody []byte) {
+
+			if statusCode != http.StatusOK {
+				err = errors.New("failed to get embedding due to status code: " + strconv.Itoa(statusCode))
+				callback(nil, err)
+				return
+			}
+
+			var resp []float32
+			resp, err = t.parseTextEmbedding(responseBody)
+			if err != nil {
+				err = fmt.Errorf("failed to parse response: %v", err)
+				callback(nil, err)
+				return
+			}
+
+			log.Debugf("get embedding response: %d, %s", statusCode, responseBody)
+
+			if len(resp) == 0 {
+				err = errors.New("no embedding found in response")
+				callback(nil, err)
+				return
+			}
+
+			embedding := make([]float64, len(resp))
+			for i, v := range resp {
+				embedding[i] = float64(v)
+			}
+
+			callback(embedding, nil)
+
+		}, t.config.timeout)
+	return err
+}


Reviews:
user: login: CH3CHO
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T08:45:18Z
user: login: Beatrueman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T10:26:19Z
user: login: Beatrueman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T10:27:54Z
user: login: CH3CHO
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T12:48:36Z
user: login: CH3CHO
body: LGTM. Thanks.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T00:34:21Z
user: login: CH3CHO
body: LGTM. Thanks.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-20T03:04:37Z

Review comments:
diff_hunk: @@ -0,0 +1,328 @@
+package embedding
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+	"github.com/tidwall/gjson"
+	"math"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+const (
+	XFYUN_DOMAIN = "emb-cn-huabei-1.xf-yun.com"
+	XFYUN_PORT   = 443
+)
+
+type XfyunProviderInitializer struct {
+}
+
+var XfyunConfig XfyunProviderConfig
+
+type XfyunProviderConfig struct {
+	// @Title zh-CN 文本特征提取服务 API Key
+	// @Description zh-CN 文本特征提取服务 API Key。
+	apiKey string
+	// @Title zh-CN 文本特征提取服务 APPID
+	// @Description zh-CN 文本特征提取服务 APPID。仅适用与 Xfyun
+	xfyunAppID string
+	// @Title zh-CN 文本特征提取服务 APISecret
+	// @Description zh-CN 文本特征提取服务 APISecret。仅适用与 Xfyun
+	xfyunApiSecret string
+}
+
+func (c *XfyunProviderInitializer) InitConfig(json gjson.Result) {
+	XfyunConfig.xfyunAppID = json.Get("APPID").String()
+	XfyunConfig.xfyunApiSecret = json.Get("APISecret").String()
path: plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go
user: login: CH3CHO
body: 这两个 Key 的名字还是统一一下，大小写按规范来。
created_at: 2025-03-18T08:09:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,328 @@
+package embedding
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+	"github.com/tidwall/gjson"
+	"math"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+const (
+	XFYUN_DOMAIN = "emb-cn-huabei-1.xf-yun.com"
+	XFYUN_PORT   = 443
+)
+
+type XfyunProviderInitializer struct {
+}
+
+var XfyunConfig XfyunProviderConfig
+
+type XfyunProviderConfig struct {
+	// @Title zh-CN 文本特征提取服务 API Key
+	// @Description zh-CN 文本特征提取服务 API Key。
+	apiKey string
+	// @Title zh-CN 文本特征提取服务 APPID
+	// @Description zh-CN 文本特征提取服务 APPID。仅适用与 Xfyun
+	xfyunAppID string
+	// @Title zh-CN 文本特征提取服务 APISecret
+	// @Description zh-CN 文本特征提取服务 APISecret。仅适用与 Xfyun
+	xfyunApiSecret string
+}
+
+func (c *XfyunProviderInitializer) InitConfig(json gjson.Result) {
+	XfyunConfig.xfyunAppID = json.Get("APPID").String()
+	XfyunConfig.xfyunApiSecret = json.Get("APISecret").String()
+	XfyunConfig.apiKey = json.Get("apiKey").String()
+}
+
+func (c *XfyunProviderInitializer) ValidateConfig() error {
+	if XfyunConfig.apiKey == "" {
+		return errors.New("[Xfyun] apiKey is required")
+	}
+	if XfyunConfig.xfyunAppID == "" {
+		return errors.New("[Xfyun] APPID is required")
+	}
+	if XfyunConfig.xfyunApiSecret == "" {
+		return errors.New("[Xfyun] APISecret is required")
+	}
+	return nil
+}
+
+func (t *XfyunProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
+	if c.servicePort == 0 {
+		c.servicePort = XFYUN_PORT
+	}
+	if c.serviceHost == "" {
+		c.serviceHost = XFYUN_DOMAIN
+	}
+
+	return &XfyunProvider{
+		config: c,
+		client: wrapper.NewClusterClient(wrapper.FQDNCluster{
+			FQDN: c.serviceName,
+			Host: c.serviceHost,
+			Port: c.servicePort,
+		}),
+	}, nil
+}
+
+func (t *XfyunProvider) GetProviderType() string {
+	return PROVIDER_TYPE_XFYUN
+}
+
+type XfyunProvider struct {
+	config ProviderConfig
+	client wrapper.HttpClient
+}
+
+type XfyunHeader struct {
+	AppID  string `json:"app_id"`
+	Status int    `json:"status"`
+}
+
+type ReqFeature struct {
+	Encoding string `json:"encoding"`
+}
+
+type XfyunEmb struct {
+	Domain  string     `json:"domain"`
+	Feature ReqFeature `json:"feature"`
+}
+
+type XfyunParameter struct {
+	Emb XfyunEmb `json:"emb"`
+}
+
+type XfyunPayload struct {
+	Messages struct {
+		Text string `json:"text"`
+	} `json:"messages"`
+}
+
+type XfyunText struct {
+	MainMessages []struct {
+		Content *string `json:"content"`
+		Role    *string `json:"role"`
+	} `json:"messages"`
+}
+
+type XfyunReqBody struct {
+	Header    XfyunHeader    `json:"header"`
+	Parameter XfyunParameter `json:"parameter"`
+	Payload   XfyunPayload   `json:"payload"`
+}
+
+type XfyunResponse struct {
+	Header  XfyunResHeader  `json:"header"`
+	Payload XfyunResPayload `json:"payload"`
+}
+
+type XfyunResHeader struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+	Sid     string `json:"sid"`
+}
+
+type XfyunResPayload struct {
+	Feature struct {
+		Text string `json:"text"`
+	} `json:"feature"`
+}
+
+type Url struct {
+	Host   string
+	Path   string
+	Schema string
path: plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go
user: login: CH3CHO
body: URL 里的这个是叫 Scheme，不是 Schema
created_at: 2025-03-18T08:36:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,328 @@
+package embedding
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+	"github.com/tidwall/gjson"
+	"math"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+const (
+	XFYUN_DOMAIN = "emb-cn-huabei-1.xf-yun.com"
+	XFYUN_PORT   = 443
+)
+
+type XfyunProviderInitializer struct {
+}
+
+var XfyunConfig XfyunProviderConfig
+
+type XfyunProviderConfig struct {
+	// @Title zh-CN 文本特征提取服务 API Key
+	// @Description zh-CN 文本特征提取服务 API Key。
+	apiKey string
+	// @Title zh-CN 文本特征提取服务 APPID
+	// @Description zh-CN 文本特征提取服务 APPID。仅适用与 Xfyun
+	xfyunAppID string
+	// @Title zh-CN 文本特征提取服务 APISecret
+	// @Description zh-CN 文本特征提取服务 APISecret。仅适用与 Xfyun
+	xfyunApiSecret string
+}
+
+func (c *XfyunProviderInitializer) InitConfig(json gjson.Result) {
+	XfyunConfig.xfyunAppID = json.Get("APPID").String()
+	XfyunConfig.xfyunApiSecret = json.Get("APISecret").String()
+	XfyunConfig.apiKey = json.Get("apiKey").String()
+}
+
+func (c *XfyunProviderInitializer) ValidateConfig() error {
+	if XfyunConfig.apiKey == "" {
+		return errors.New("[Xfyun] apiKey is required")
+	}
+	if XfyunConfig.xfyunAppID == "" {
+		return errors.New("[Xfyun] APPID is required")
+	}
+	if XfyunConfig.xfyunApiSecret == "" {
+		return errors.New("[Xfyun] APISecret is required")
+	}
+	return nil
+}
+
+func (t *XfyunProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
+	if c.servicePort == 0 {
+		c.servicePort = XFYUN_PORT
+	}
+	if c.serviceHost == "" {
+		c.serviceHost = XFYUN_DOMAIN
+	}
+
+	return &XfyunProvider{
+		config: c,
+		client: wrapper.NewClusterClient(wrapper.FQDNCluster{
+			FQDN: c.serviceName,
+			Host: c.serviceHost,
+			Port: c.servicePort,
+		}),
+	}, nil
+}
+
+func (t *XfyunProvider) GetProviderType() string {
+	return PROVIDER_TYPE_XFYUN
+}
+
+type XfyunProvider struct {
+	config ProviderConfig
+	client wrapper.HttpClient
+}
+
+type XfyunHeader struct {
+	AppID  string `json:"app_id"`
+	Status int    `json:"status"`
+}
+
+type ReqFeature struct {
+	Encoding string `json:"encoding"`
+}
+
+type XfyunEmb struct {
+	Domain  string     `json:"domain"`
+	Feature ReqFeature `json:"feature"`
+}
+
+type XfyunParameter struct {
+	Emb XfyunEmb `json:"emb"`
+}
+
+type XfyunPayload struct {
+	Messages struct {
+		Text string `json:"text"`
+	} `json:"messages"`
+}
+
+type XfyunText struct {
+	MainMessages []struct {
+		Content *string `json:"content"`
+		Role    *string `json:"role"`
+	} `json:"messages"`
+}
+
+type XfyunReqBody struct {
+	Header    XfyunHeader    `json:"header"`
+	Parameter XfyunParameter `json:"parameter"`
+	Payload   XfyunPayload   `json:"payload"`
+}
+
+type XfyunResponse struct {
+	Header  XfyunResHeader  `json:"header"`
+	Payload XfyunResPayload `json:"payload"`
+}
+
+type XfyunResHeader struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+	Sid     string `json:"sid"`
+}
+
+type XfyunResPayload struct {
+	Feature struct {
+		Text string `json:"text"`
+	} `json:"feature"`
+}
+
+type Url struct {
+	Host   string
+	Path   string
+	Schema string
path: plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go
user: login: CH3CHO
body: 另外，为啥不直接用 `net/url` 里的 URL 呢？
created_at: 2025-03-18T08:36:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,328 @@
+package embedding
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+	"github.com/tidwall/gjson"
+	"math"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+const (
+	XFYUN_DOMAIN = "emb-cn-huabei-1.xf-yun.com"
+	XFYUN_PORT   = 443
+)
+
+type XfyunProviderInitializer struct {
+}
+
+var XfyunConfig XfyunProviderConfig
+
+type XfyunProviderConfig struct {
+	// @Title zh-CN 文本特征提取服务 API Key
+	// @Description zh-CN 文本特征提取服务 API Key。
+	apiKey string
+	// @Title zh-CN 文本特征提取服务 APPID
+	// @Description zh-CN 文本特征提取服务 APPID。仅适用与 Xfyun
+	xfyunAppID string
+	// @Title zh-CN 文本特征提取服务 APISecret
+	// @Description zh-CN 文本特征提取服务 APISecret。仅适用与 Xfyun
+	xfyunApiSecret string
+}
+
+func (c *XfyunProviderInitializer) InitConfig(json gjson.Result) {
+	XfyunConfig.xfyunAppID = json.Get("APPID").String()
+	XfyunConfig.xfyunApiSecret = json.Get("APISecret").String()
path: plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go
user: login: Beatrueman
body: 我是按照官方文档写的key
![image](https://github.com/user-attachments/assets/569ce25a-8564-45da-a1f0-527ae2158829)
需要修改为AppID、APISecret这样吗？
created_at: 2025-03-18T10:26:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,328 @@
+package embedding
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+	"github.com/tidwall/gjson"
+	"math"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+const (
+	XFYUN_DOMAIN = "emb-cn-huabei-1.xf-yun.com"
+	XFYUN_PORT   = 443
+)
+
+type XfyunProviderInitializer struct {
+}
+
+var XfyunConfig XfyunProviderConfig
+
+type XfyunProviderConfig struct {
+	// @Title zh-CN 文本特征提取服务 API Key
+	// @Description zh-CN 文本特征提取服务 API Key。
+	apiKey string
+	// @Title zh-CN 文本特征提取服务 APPID
+	// @Description zh-CN 文本特征提取服务 APPID。仅适用与 Xfyun
+	xfyunAppID string
+	// @Title zh-CN 文本特征提取服务 APISecret
+	// @Description zh-CN 文本特征提取服务 APISecret。仅适用与 Xfyun
+	xfyunApiSecret string
+}
+
+func (c *XfyunProviderInitializer) InitConfig(json gjson.Result) {
+	XfyunConfig.xfyunAppID = json.Get("APPID").String()
+	XfyunConfig.xfyunApiSecret = json.Get("APISecret").String()
+	XfyunConfig.apiKey = json.Get("apiKey").String()
+}
+
+func (c *XfyunProviderInitializer) ValidateConfig() error {
+	if XfyunConfig.apiKey == "" {
+		return errors.New("[Xfyun] apiKey is required")
+	}
+	if XfyunConfig.xfyunAppID == "" {
+		return errors.New("[Xfyun] APPID is required")
+	}
+	if XfyunConfig.xfyunApiSecret == "" {
+		return errors.New("[Xfyun] APISecret is required")
+	}
+	return nil
+}
+
+func (t *XfyunProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
+	if c.servicePort == 0 {
+		c.servicePort = XFYUN_PORT
+	}
+	if c.serviceHost == "" {
+		c.serviceHost = XFYUN_DOMAIN
+	}
+
+	return &XfyunProvider{
+		config: c,
+		client: wrapper.NewClusterClient(wrapper.FQDNCluster{
+			FQDN: c.serviceName,
+			Host: c.serviceHost,
+			Port: c.servicePort,
+		}),
+	}, nil
+}
+
+func (t *XfyunProvider) GetProviderType() string {
+	return PROVIDER_TYPE_XFYUN
+}
+
+type XfyunProvider struct {
+	config ProviderConfig
+	client wrapper.HttpClient
+}
+
+type XfyunHeader struct {
+	AppID  string `json:"app_id"`
+	Status int    `json:"status"`
+}
+
+type ReqFeature struct {
+	Encoding string `json:"encoding"`
+}
+
+type XfyunEmb struct {
+	Domain  string     `json:"domain"`
+	Feature ReqFeature `json:"feature"`
+}
+
+type XfyunParameter struct {
+	Emb XfyunEmb `json:"emb"`
+}
+
+type XfyunPayload struct {
+	Messages struct {
+		Text string `json:"text"`
+	} `json:"messages"`
+}
+
+type XfyunText struct {
+	MainMessages []struct {
+		Content *string `json:"content"`
+		Role    *string `json:"role"`
+	} `json:"messages"`
+}
+
+type XfyunReqBody struct {
+	Header    XfyunHeader    `json:"header"`
+	Parameter XfyunParameter `json:"parameter"`
+	Payload   XfyunPayload   `json:"payload"`
+}
+
+type XfyunResponse struct {
+	Header  XfyunResHeader  `json:"header"`
+	Payload XfyunResPayload `json:"payload"`
+}
+
+type XfyunResHeader struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+	Sid     string `json:"sid"`
+}
+
+type XfyunResPayload struct {
+	Feature struct {
+		Text string `json:"text"`
+	} `json:"feature"`
+}
+
+type Url struct {
+	Host   string
+	Path   string
+	Schema string
path: plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go
user: login: Beatrueman
body: 收到，随后更改
我忘记用net/url了，直接转换官方的Python demo了
![image](https://github.com/user-attachments/assets/ec4b139b-a8d2-4680-abce-04bcdd8786f2)

created_at: 2025-03-18T10:27:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,328 @@
+package embedding
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+	"github.com/tidwall/gjson"
+	"math"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+const (
+	XFYUN_DOMAIN = "emb-cn-huabei-1.xf-yun.com"
+	XFYUN_PORT   = 443
+)
+
+type XfyunProviderInitializer struct {
+}
+
+var XfyunConfig XfyunProviderConfig
+
+type XfyunProviderConfig struct {
+	// @Title zh-CN 文本特征提取服务 API Key
+	// @Description zh-CN 文本特征提取服务 API Key。
+	apiKey string
+	// @Title zh-CN 文本特征提取服务 APPID
+	// @Description zh-CN 文本特征提取服务 APPID。仅适用与 Xfyun
+	xfyunAppID string
+	// @Title zh-CN 文本特征提取服务 APISecret
+	// @Description zh-CN 文本特征提取服务 APISecret。仅适用与 Xfyun
+	xfyunApiSecret string
+}
+
+func (c *XfyunProviderInitializer) InitConfig(json gjson.Result) {
+	XfyunConfig.xfyunAppID = json.Get("APPID").String()
+	XfyunConfig.xfyunApiSecret = json.Get("APISecret").String()
path: plugins/wasm-go/extensions/ai-cache/embedding/xfyun.go
user: login: CH3CHO
body: 作为配置的命令，大小写还是要统一的。建议改成 appId、apiSecret。
created_at: 2025-03-18T12:48:35Z
author_association: COLLABORATOR

PR comments:
user: login: codecov-commenter
created_at: 2025-03-18T08:00:30Z
body: ## [Codecov](https://app.codecov.io/gh/alibaba/higress/pull/1921?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alibaba) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 43.76%. Comparing base ([`ef31e09`](https://app.codecov.io/gh/alibaba/higress/commit/ef31e0931063d7e54b72c043b77242bc593d96e9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alibaba)) to head ([`2f595a5`](https://app.codecov.io/gh/alibaba/higress/commit/2f595a5367d01b77132ee5fc1b9c9583e4602ab9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alibaba)).
:warning: Report is 703 commits behind head on main.

<details><summary>Additional details and impacted files</summary>



[![Impacted file tree graph](https://app.codecov.io/gh/alibaba/higress/pull/1921/graphs/tree.svg?width=650&height=150&src=pr&token=3ZFKS5X7H2&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alibaba)](https://app.codecov.io/gh/alibaba/higress/pull/1921?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alibaba)

```diff
@@            Coverage Diff             @@
##             main    #1921      +/-   ##
==========================================
+ Coverage   35.91%   43.76%   +7.85%     
==========================================
  Files          69       78       +9     
  Lines       11576    12479     +903     
==========================================
+ Hits         4157     5461    +1304     
+ Misses       7104     6677     -427     
- Partials      315      341      +26     
```
[see 73 files with indirect coverage changes](https://app.codecov.io/gh/alibaba/higress/pull/1921/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alibaba)
</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: Beatrueman
created_at: 2025-03-18T15:54:56Z
body: @CH3CHO 我做出了更改，麻烦老师review
- 更改凭据格式为`appId`和`apiSecret`
- 使用net/url进行url的解析。
author_association: CONTRIBUTOR
user: login: CH3CHO
created_at: 2025-03-19T00:37:33Z
body: 另外，能顺便更新一下 README 吗？
author_association: COLLABORATOR
user: login: Beatrueman
created_at: 2025-03-19T01:22:16Z
body: > 另外，能顺便更新一下 README 吗？

好的，我随后更新一下
author_association: CONTRIBUTOR
user: login: Beatrueman
created_at: 2025-03-20T02:04:20Z
body: > 另外，能顺便更新一下 README 吗？

@CH3CHO 我增加了文本向量化提供商特有配置说明部分，调整了一下整个README的格式和结构，麻烦老师review
author_association: CONTRIBUTOR