mirror of
https://github.com/alibaba/higress.git
synced 2026-02-28 06:30:49 +08:00
Compare commits
20 Commits
wasm-go-ex
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2c2d1d521 | ||
|
|
a5a28aebf6 | ||
|
|
1c10f36369 | ||
|
|
7054f01a36 | ||
|
|
895f17f8d8 | ||
|
|
29fcd330d5 | ||
|
|
0e58042fa6 | ||
|
|
bdbfad8a8a | ||
|
|
4307f88645 | ||
|
|
25b085cb5e | ||
|
|
dcea483c61 | ||
|
|
8fa1224cba | ||
|
|
8f7c10ee5f | ||
|
|
5a854b990b | ||
|
|
dd11248e47 | ||
|
|
ba98f3a7ad | ||
|
|
d31c978ed3 | ||
|
|
daa374d9a4 | ||
|
|
6b9dabb489 | ||
|
|
6f04404edd |
5
.github/workflows/build-and-test-plugin.yaml
vendored
5
.github/workflows/build-and-test-plugin.yaml
vendored
@@ -49,6 +49,11 @@ jobs:
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
if: matrix.wasmPluginType == 'RUST'
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
32
README.md
32
README.md
@@ -1,8 +1,9 @@
|
||||
<h1 align="center">
|
||||
<img src="https://img.alicdn.com/imgextra/i2/O1CN01NwxLDd20nxfGBjxmZ_!!6000000006895-2-tps-960-290.png" alt="Higress" width="240" height="72.5">
|
||||
<br>
|
||||
AI Native API Gateway
|
||||
AI Gateway
|
||||
</h1>
|
||||
<h4 align="center"> AI Native API Gateway </h4>
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
Higress 是基于阿里内部多年的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。
|
||||
|
||||
Higress 是面向 AI 原生设计的 API 网关,在阿里内部,承载了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务的流量。
|
||||
Higress 在阿里内部作为 AI 网关,承载了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务的流量。
|
||||
|
||||
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
|
||||
|
||||
@@ -30,13 +31,36 @@ Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
- [**快速开始**](#快速开始)
|
||||
- [**功能展示**](#功能展示)
|
||||
- [**使用场景**](#使用场景)
|
||||
- [**核心优势**](#核心优势)
|
||||
- [**Quick Start**](https://higress.io/zh-cn/docs/user/quickstart)
|
||||
- [**社区**](#社区)
|
||||
|
||||
## 快速开始
|
||||
|
||||
Higress 只需 Docker 即可启动,方便个人开发者在本地搭建学习,或者用于搭建简易站点:
|
||||
|
||||
```bash
|
||||
# 创建一个工作目录
|
||||
mkdir higress; cd higress
|
||||
# 启动 higress,配置文件会写到工作目录下
|
||||
docker run -d --rm --name higress-ai -v ${PWD}:/data \
|
||||
-p 8001:8001 -p 8080:8080 -p 8443:8443 \
|
||||
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
|
||||
```
|
||||
|
||||
监听端口说明如下:
|
||||
|
||||
- 8001 端口:Higress UI 控制台入口
|
||||
- 8080 端口:网关 HTTP 协议入口
|
||||
- 8443 端口:网关 HTTPS 协议入口
|
||||
|
||||
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内不可访问的影响**
|
||||
|
||||
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.io/docs/latest/user/quickstart/)。
|
||||
|
||||
|
||||
## 使用场景
|
||||
|
||||
|
||||
@@ -431,11 +431,14 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
// If there is no matching secret, try to get it from configmap.
|
||||
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
namespace, secret := cert.ParseTLSSecret(secretName)
|
||||
if namespace != "" {
|
||||
secretNamespace = namespace
|
||||
matchSecretName := httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
if matchSecretName != "" {
|
||||
namespace, secret := cert.ParseTLSSecret(matchSecretName)
|
||||
if namespace == "" {
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
} else {
|
||||
secretNamespace = namespace
|
||||
}
|
||||
secretName = secret
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,11 +417,14 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
// If there is no matching secret, try to get it from configmap.
|
||||
secretName = httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
namespace, secret := cert.ParseTLSSecret(secretName)
|
||||
if namespace != "" {
|
||||
secretNamespace = namespace
|
||||
matchSecretName := httpsCredentialConfig.MatchSecretNameByDomain(rule.Host)
|
||||
if matchSecretName != "" {
|
||||
namespace, secret := cert.ParseTLSSecret(matchSecretName)
|
||||
if namespace == "" {
|
||||
secretNamespace = c.options.SystemNamespace
|
||||
} else {
|
||||
secretNamespace = namespace
|
||||
}
|
||||
secretName = secret
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,6 @@ func (c *controller) processNextWorkItem() bool {
|
||||
func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
event := model.EventUpdate
|
||||
ing, err := c.ingressLister.Ingresses(namespacedName.Namespace).Get(namespacedName.Name)
|
||||
ing.Status.InitializeConditions()
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
event = model.EventDelete
|
||||
@@ -181,6 +180,8 @@ func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ing.Status.InitializeConditions()
|
||||
|
||||
// we should check need process only when event is not delete,
|
||||
// if it is delete event, and previously processed, we need to process too.
|
||||
if event != model.EventDelete {
|
||||
|
||||
53
plugins/wasm-assemblyscript/README.md
Normal file
53
plugins/wasm-assemblyscript/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
## 介绍
|
||||
|
||||
此 SDK 用于使用 AssemblyScript 语言开发 Higress 的 Wasm 插件。
|
||||
|
||||
### 如何使用SDK
|
||||
|
||||
创建一个新的 AssemblyScript 项目。
|
||||
|
||||
```
|
||||
npm init
|
||||
npm install --save-dev assemblyscript
|
||||
npx asinit .
|
||||
```
|
||||
|
||||
在asconfig.json文件中,作为传递给asc编译器的选项之一,包含"use": "abort=abort_proc_exit"。
|
||||
|
||||
```
|
||||
{
|
||||
"options": {
|
||||
"use": "abort=abort_proc_exit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
将`"@higress/wasm-assemblyscript": "^0.0.4"`添加到你的依赖项中,然后运行`npm install`。
|
||||
|
||||
### 本地构建
|
||||
|
||||
```
|
||||
npm run asbuild
|
||||
```
|
||||
|
||||
构建结果将在`build`文件夹中。其中,`debug.wasm`和`release.wasm`是已编译的文件,在生产环境中建议使用`release.wasm`。
|
||||
|
||||
注:如果需要插件带有 name section 信息需要带上`"debug": true`,编译参数解释详见[using-the-compiler](https://www.assemblyscript.org/compiler.html#using-the-compiler)。
|
||||
|
||||
```json
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"textFile": "build/release.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false,
|
||||
"debug": true
|
||||
}
|
||||
```
|
||||
|
||||
### AssemblyScript 限制
|
||||
|
||||
此 SDK 使用的 AssemblyScript 版本为`0.27.29`,参考[AssemblyScript Status](https://www.assemblyscript.org/status.html)该版本尚未支持闭包、异常、迭代器等特性,并且JSON,正则表达式等功能还尚未在标准库中实现,暂时需要使用社区提供的实现。
|
||||
|
||||
23
plugins/wasm-assemblyscript/asconfig.json
Normal file
23
plugins/wasm-assemblyscript/asconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"targets": {
|
||||
"debug": {
|
||||
"outFile": "build/debug.wasm",
|
||||
"textFile": "build/debug.wat",
|
||||
"sourceMap": true,
|
||||
"debug": true
|
||||
},
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"textFile": "build/release.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"bindings": "esm",
|
||||
"use": "abort=abort_proc_exit"
|
||||
}
|
||||
}
|
||||
214
plugins/wasm-assemblyscript/assembly/cluster_wrapper.ts
Normal file
214
plugins/wasm-assemblyscript/assembly/cluster_wrapper.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
log,
|
||||
LogLevelValues,
|
||||
get_property,
|
||||
WasmResultValues,
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
import { getRequestHost } from "./request_wrapper";
|
||||
|
||||
export abstract class Cluster {
|
||||
abstract clusterName(): string;
|
||||
abstract hostName(): string;
|
||||
}
|
||||
|
||||
export class RouteCluster extends Cluster {
|
||||
host: string;
|
||||
constructor(host: string = "") {
|
||||
super();
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
let result = get_property("cluster_name");
|
||||
if (result.status != WasmResultValues.Ok) {
|
||||
log(LogLevelValues.error, "get route cluster failed");
|
||||
return "";
|
||||
}
|
||||
return String.UTF8.decode(result.returnValue);
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return getRequestHost();
|
||||
}
|
||||
}
|
||||
|
||||
export class K8sCluster extends Cluster {
|
||||
serviceName: string;
|
||||
namespace: string;
|
||||
port: i64;
|
||||
version: string;
|
||||
host: string;
|
||||
|
||||
constructor(
|
||||
serviceName: string,
|
||||
namespace: string,
|
||||
port: i64,
|
||||
version: string = "",
|
||||
host: string = ""
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.namespace = namespace;
|
||||
this.port = port;
|
||||
this.version = version;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
let namespace = this.namespace != "" ? this.namespace : "default";
|
||||
return `outbound|${this.port}|${this.version}|${this.serviceName}.${namespace}.svc.cluster.local`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return `${this.serviceName}.${this.namespace}.svc.cluster.local`;
|
||||
}
|
||||
}
|
||||
|
||||
export class NacosCluster extends Cluster {
|
||||
serviceName: string;
|
||||
group: string;
|
||||
namespaceID: string;
|
||||
port: i64;
|
||||
isExtRegistry: boolean;
|
||||
version: string;
|
||||
host: string;
|
||||
|
||||
constructor(
|
||||
serviceName: string,
|
||||
namespaceID: string,
|
||||
port: i64,
|
||||
// use DEFAULT-GROUP by default
|
||||
group: string = "DEFAULT-GROUP",
|
||||
// set true if use edas/sae registry
|
||||
isExtRegistry: boolean = false,
|
||||
version: string = "",
|
||||
host: string = ""
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.group = group.replace("_", "-");
|
||||
this.namespaceID = namespaceID;
|
||||
this.port = port;
|
||||
this.isExtRegistry = isExtRegistry;
|
||||
this.version = version;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
let tail = "nacos" + (this.isExtRegistry ? "-ext" : "");
|
||||
return `outbound|${this.port}|${this.version}|${this.serviceName}.${this.group}.${this.namespaceID}.${tail}`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return this.serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
export class StaticIpCluster extends Cluster {
|
||||
serviceName: string;
|
||||
port: i64;
|
||||
host: string;
|
||||
|
||||
constructor(serviceName: string, port: i64, host: string = "") {
|
||||
super()
|
||||
this.serviceName = serviceName;
|
||||
this.port = port;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
return `outbound|${this.port}||${this.serviceName}.static`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return this.serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
export class DnsCluster extends Cluster {
|
||||
serviceName: string;
|
||||
domain: string;
|
||||
port: i64;
|
||||
|
||||
constructor(serviceName: string, domain: string, port: i64) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.domain = domain;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
return `outbound|${this.port}||${this.serviceName}.dns`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
return this.domain;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConsulCluster extends Cluster {
|
||||
serviceName: string;
|
||||
datacenter: string;
|
||||
port: i64;
|
||||
host: string;
|
||||
|
||||
constructor(
|
||||
serviceName: string,
|
||||
datacenter: string,
|
||||
port: i64,
|
||||
host: string = ""
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.datacenter = datacenter;
|
||||
this.port = port;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
return `outbound|${this.port}||${this.serviceName}.${this.datacenter}.consul`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return this.serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
export class FQDNCluster extends Cluster {
|
||||
fqdn: string;
|
||||
host: string;
|
||||
port: i64;
|
||||
|
||||
constructor(fqdn: string, port: i64, host: string = "") {
|
||||
super();
|
||||
this.fqdn = fqdn;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
clusterName(): string {
|
||||
return `outbound|${this.port}||${this.fqdn}`;
|
||||
}
|
||||
|
||||
hostName(): string {
|
||||
if (this.host != "") {
|
||||
return this.host;
|
||||
}
|
||||
return this.fqdn;
|
||||
}
|
||||
}
|
||||
120
plugins/wasm-assemblyscript/assembly/http_wrapper.ts
Normal file
120
plugins/wasm-assemblyscript/assembly/http_wrapper.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
Cluster
|
||||
} from "./cluster_wrapper"
|
||||
|
||||
import {
|
||||
log,
|
||||
LogLevelValues,
|
||||
Headers,
|
||||
HeaderPair,
|
||||
root_context,
|
||||
BufferTypeValues,
|
||||
get_buffer_bytes,
|
||||
BaseContext,
|
||||
stream_context,
|
||||
WasmResultValues,
|
||||
RootContext,
|
||||
ResponseCallBack
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
|
||||
export interface HttpClient {
|
||||
get(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
head(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
options(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
post(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
put(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
patch(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
delete(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
connect(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
trace(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
|
||||
}
|
||||
|
||||
const methodArrayBuffer: ArrayBuffer = String.UTF8.encode(":method");
|
||||
const pathArrayBuffer: ArrayBuffer = String.UTF8.encode(":path");
|
||||
const authorityArrayBuffer: ArrayBuffer = String.UTF8.encode(":authority");
|
||||
|
||||
const StatusBadGateway: i32 = 502;
|
||||
|
||||
export class ClusterClient {
|
||||
cluster: Cluster;
|
||||
|
||||
constructor(cluster: Cluster) {
|
||||
this.cluster = cluster;
|
||||
}
|
||||
|
||||
private httpCall(method: string, path: string, headers: Headers, body: ArrayBuffer, callback: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
if (root_context == null) {
|
||||
log(LogLevelValues.error, "Root context is null");
|
||||
return false;
|
||||
}
|
||||
for (let i: i32 = headers.length - 1; i >= 0; i--) {
|
||||
const key = String.UTF8.decode(headers[i].key)
|
||||
if ((key == ":method") || (key == ":path") || (key == ":authority")) {
|
||||
headers.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
headers.push(new HeaderPair(methodArrayBuffer, String.UTF8.encode(method)));
|
||||
headers.push(new HeaderPair(pathArrayBuffer, String.UTF8.encode(path)));
|
||||
headers.push(new HeaderPair(authorityArrayBuffer, String.UTF8.encode(this.cluster.hostName())));
|
||||
|
||||
const result = (root_context as RootContext).httpCall(this.cluster.clusterName(), headers, body, [], timeoutMillisecond, root_context as BaseContext, callback,
|
||||
(_origin_context: BaseContext, _numHeaders: u32, body_size: usize, _trailers: u32, callback: ResponseCallBack): void => {
|
||||
const respBody = get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, 0, body_size as u32);
|
||||
const respHeaders = stream_context.headers.http_callback.get_headers()
|
||||
let code = StatusBadGateway;
|
||||
let headers = new Array<HeaderPair>();
|
||||
for (let i = 0; i < respHeaders.length; i++) {
|
||||
const h = respHeaders[i];
|
||||
if (String.UTF8.decode(h.key) == ":status") {
|
||||
code = <i32>parseInt(String.UTF8.decode(h.value))
|
||||
}
|
||||
headers.push(new HeaderPair(h.key, h.value));
|
||||
}
|
||||
log(LogLevelValues.debug, `http call end, code: ${code}, body: ${String.UTF8.decode(respBody)}`)
|
||||
callback(code, headers, respBody);
|
||||
})
|
||||
log(LogLevelValues.debug, `http call start, cluster: ${this.cluster.clusterName()}, method: ${method}, path: ${path}, body: ${String.UTF8.decode(body)}, timeout: ${timeoutMillisecond}`)
|
||||
if (result != WasmResultValues.Ok) {
|
||||
log(LogLevelValues.error, `http call failed, result: ${result}`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
get(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("GET", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
head(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("HEAD", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
options(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("OPTIONS", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
post(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("POST", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
put(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("PUT", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
patch(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("PATCH", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
delete(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("DELETE", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
connect(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("CONNECT", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
|
||||
trace(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
|
||||
return this.httpCall("TRACE", path, headers, body, cb, timeoutMillisecond);
|
||||
}
|
||||
}
|
||||
18
plugins/wasm-assemblyscript/assembly/index.ts
Normal file
18
plugins/wasm-assemblyscript/assembly/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export {RouteCluster,
|
||||
K8sCluster,
|
||||
NacosCluster,
|
||||
ConsulCluster,
|
||||
FQDNCluster,
|
||||
StaticIpCluster} from "./cluster_wrapper"
|
||||
export {HttpClient,
|
||||
ClusterClient} from "./http_wrapper"
|
||||
export {Log} from "./log_wrapper"
|
||||
export {SetCtx,
|
||||
HttpContext,
|
||||
ParseConfigBy,
|
||||
ProcessRequestBodyBy,
|
||||
ProcessRequestHeadersBy,
|
||||
ProcessResponseBodyBy,
|
||||
ProcessResponseHeadersBy,
|
||||
Logger, RegisteTickFunc} from "./plugin_wrapper"
|
||||
export {ParseResult} from "./rule_matcher"
|
||||
66
plugins/wasm-assemblyscript/assembly/log_wrapper.ts
Normal file
66
plugins/wasm-assemblyscript/assembly/log_wrapper.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { log, LogLevelValues } from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
|
||||
enum LogLevel {
|
||||
Trace = 0,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Critical,
|
||||
}
|
||||
|
||||
export class Log {
|
||||
private pluginName: string;
|
||||
|
||||
constructor(pluginName: string) {
|
||||
this.pluginName = pluginName;
|
||||
}
|
||||
|
||||
private log(level: LogLevel, msg: string): void {
|
||||
let formattedMsg = `[${this.pluginName}] ${msg}`;
|
||||
switch (level) {
|
||||
case LogLevel.Trace:
|
||||
log(LogLevelValues.trace, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
log(LogLevelValues.debug, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
log(LogLevelValues.info, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
log(LogLevelValues.warn, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
log(LogLevelValues.error, formattedMsg);
|
||||
break;
|
||||
case LogLevel.Critical:
|
||||
log(LogLevelValues.critical, formattedMsg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Trace(msg: string): void {
|
||||
this.log(LogLevel.Trace, msg);
|
||||
}
|
||||
|
||||
public Debug(msg: string): void {
|
||||
this.log(LogLevel.Debug, msg);
|
||||
}
|
||||
|
||||
public Info(msg: string): void {
|
||||
this.log(LogLevel.Info, msg);
|
||||
}
|
||||
|
||||
public Warn(msg: string): void {
|
||||
this.log(LogLevel.Warn, msg);
|
||||
}
|
||||
|
||||
public Error(msg: string): void {
|
||||
this.log(LogLevel.Error, msg);
|
||||
}
|
||||
|
||||
public Critical(msg: string): void {
|
||||
this.log(LogLevel.Critical, msg);
|
||||
}
|
||||
}
|
||||
445
plugins/wasm-assemblyscript/assembly/plugin_wrapper.ts
Normal file
445
plugins/wasm-assemblyscript/assembly/plugin_wrapper.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
import { Log } from "./log_wrapper";
|
||||
import {
|
||||
Context,
|
||||
FilterHeadersStatusValues,
|
||||
RootContext,
|
||||
setRootContext,
|
||||
proxy_set_effective_context,
|
||||
log,
|
||||
LogLevelValues,
|
||||
FilterDataStatusValues,
|
||||
get_buffer_bytes,
|
||||
BufferTypeValues,
|
||||
set_tick_period_milliseconds,
|
||||
get_current_time_nanoseconds
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
import {
|
||||
getRequestHost,
|
||||
getRequestMethod,
|
||||
getRequestPath,
|
||||
getRequestScheme,
|
||||
isBinaryRequestBody,
|
||||
} from "./request_wrapper";
|
||||
import { RuleMatcher, ParseResult } from "./rule_matcher";
|
||||
import { JSON } from "assemblyscript-json/assembly";
|
||||
|
||||
export function SetCtx<PluginConfig>(
|
||||
pluginName: string,
|
||||
setFuncs: usize[] = []
|
||||
): void {
|
||||
const rootContextId = 1
|
||||
setRootContext(new CommonRootCtx<PluginConfig>(rootContextId, pluginName, setFuncs));
|
||||
}
|
||||
|
||||
export interface HttpContext {
|
||||
Scheme(): string;
|
||||
Host(): string;
|
||||
Path(): string;
|
||||
Method(): string;
|
||||
SetContext(key: string, value: usize): void;
|
||||
GetContext(key: string): usize;
|
||||
DontReadRequestBody(): void;
|
||||
DontReadResponseBody(): void;
|
||||
}
|
||||
|
||||
type ParseConfigFunc<PluginConfig> = (
|
||||
json: JSON.Obj,
|
||||
) => ParseResult<PluginConfig>;
|
||||
type OnHttpHeadersFunc<PluginConfig> = (
|
||||
context: HttpContext,
|
||||
config: PluginConfig,
|
||||
) => FilterHeadersStatusValues;
|
||||
type OnHttpBodyFunc<PluginConfig> = (
|
||||
context: HttpContext,
|
||||
config: PluginConfig,
|
||||
body: ArrayBuffer,
|
||||
) => FilterDataStatusValues;
|
||||
|
||||
|
||||
export var Logger: Log = new Log("");
|
||||
|
||||
class CommonRootCtx<PluginConfig> extends RootContext {
|
||||
pluginName: string;
|
||||
hasCustomConfig: boolean;
|
||||
ruleMatcher: RuleMatcher<PluginConfig>;
|
||||
parseConfig: ParseConfigFunc<PluginConfig> | null;
|
||||
onHttpRequestHeaders: OnHttpHeadersFunc<PluginConfig> | null;
|
||||
onHttpRequestBody: OnHttpBodyFunc<PluginConfig> | null;
|
||||
onHttpResponseHeaders: OnHttpHeadersFunc<PluginConfig> | null;
|
||||
onHttpResponseBody: OnHttpBodyFunc<PluginConfig> | null;
|
||||
onTickFuncs: Array<TickFuncEntry>;
|
||||
|
||||
constructor(context_id: u32, pluginName: string, setFuncs: usize[]) {
|
||||
super(context_id);
|
||||
this.pluginName = pluginName;
|
||||
Logger = new Log(pluginName);
|
||||
this.hasCustomConfig = true;
|
||||
this.onHttpRequestHeaders = null;
|
||||
this.onHttpRequestBody = null;
|
||||
this.onHttpResponseHeaders = null;
|
||||
this.onHttpResponseBody = null;
|
||||
this.parseConfig = null;
|
||||
this.ruleMatcher = new RuleMatcher<PluginConfig>();
|
||||
this.onTickFuncs = new Array<TickFuncEntry>();
|
||||
for (let i = 0; i < setFuncs.length; i++) {
|
||||
changetype<Closure<PluginConfig>>(setFuncs[i]).lambdaFn(
|
||||
setFuncs[i],
|
||||
this
|
||||
);
|
||||
}
|
||||
if (this.parseConfig == null) {
|
||||
this.hasCustomConfig = false;
|
||||
this.parseConfig = (json: JSON.Obj): ParseResult<PluginConfig> =>{ return new ParseResult<PluginConfig>(null, true); };
|
||||
}
|
||||
}
|
||||
|
||||
createContext(context_id: u32): Context {
|
||||
return new CommonCtx<PluginConfig>(context_id, this);
|
||||
}
|
||||
|
||||
onConfigure(configuration_size: u32): boolean {
|
||||
super.onConfigure(configuration_size);
|
||||
const data = this.getConfiguration();
|
||||
let jsonData: JSON.Obj = new JSON.Obj();
|
||||
if (data == "{}") {
|
||||
if (this.hasCustomConfig) {
|
||||
log(LogLevelValues.warn, "config is empty, but has ParseConfigFunc");
|
||||
}
|
||||
} else {
|
||||
const parseData = JSON.parse(data);
|
||||
if (parseData.isObj) {
|
||||
jsonData = changetype<JSON.Obj>(JSON.parse(data));
|
||||
} else {
|
||||
log(LogLevelValues.error, "parse json data failed")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.ruleMatcher.parseRuleConfig(jsonData, this.parseConfig as ParseConfigFunc<PluginConfig>)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (globalOnTickFuncs.length > 0) {
|
||||
this.onTickFuncs = globalOnTickFuncs;
|
||||
set_tick_period_milliseconds(100);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onTick(): void {
|
||||
for (let i = 0; i < this.onTickFuncs.length; i++) {
|
||||
const tickFuncEntry = this.onTickFuncs[i];
|
||||
const now = getCurrentTimeMilliseconds();
|
||||
if (tickFuncEntry.lastExecuted + tickFuncEntry.tickPeriod <= now) {
|
||||
tickFuncEntry.tickFunc();
|
||||
tickFuncEntry.lastExecuted = getCurrentTimeMilliseconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentTimeMilliseconds(): u64 {
|
||||
return get_current_time_nanoseconds() / 1000000;
|
||||
}
|
||||
|
||||
class TickFuncEntry {
|
||||
lastExecuted: u64;
|
||||
tickPeriod: u64;
|
||||
tickFunc: () => void;
|
||||
|
||||
constructor(lastExecuted: u64, tickPeriod: u64, tickFunc: () => void) {
|
||||
this.lastExecuted = lastExecuted;
|
||||
this.tickPeriod = tickPeriod;
|
||||
this.tickFunc = tickFunc;
|
||||
}
|
||||
}
|
||||
|
||||
var globalOnTickFuncs = new Array<TickFuncEntry>();
|
||||
|
||||
export function RegisteTickFunc(tickPeriod: i64, tickFunc: () => void): void {
|
||||
globalOnTickFuncs.push(new TickFuncEntry(0, tickPeriod, tickFunc));
|
||||
}
|
||||
|
||||
class Closure<PluginConfig> {
|
||||
lambdaFn: (closure: usize, ctx: CommonRootCtx<PluginConfig>) => void;
|
||||
parseConfigFunc: ParseConfigFunc<PluginConfig> | null;
|
||||
onHttpHeadersFunc: OnHttpHeadersFunc<PluginConfig> | null;
|
||||
OnHttpBodyFunc: OnHttpBodyFunc<PluginConfig> | null;
|
||||
|
||||
constructor(
|
||||
lambdaFn: (closure: usize, ctx: CommonRootCtx<PluginConfig>) => void
|
||||
) {
|
||||
this.lambdaFn = lambdaFn;
|
||||
this.parseConfigFunc = null;
|
||||
this.onHttpHeadersFunc = null;
|
||||
this.OnHttpBodyFunc = null;
|
||||
}
|
||||
|
||||
setParseConfigFunc(f: ParseConfigFunc<PluginConfig>): void {
|
||||
this.parseConfigFunc = f;
|
||||
}
|
||||
|
||||
setHttpHeadersFunc(f: OnHttpHeadersFunc<PluginConfig>): void {
|
||||
this.onHttpHeadersFunc = f;
|
||||
}
|
||||
|
||||
setHttpBodyFunc(f: OnHttpBodyFunc<PluginConfig>): void {
|
||||
this.OnHttpBodyFunc = f;
|
||||
}
|
||||
}
|
||||
|
||||
export function ParseConfigBy<PluginConfig>(
|
||||
f: ParseConfigFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).parseConfigFunc;
|
||||
if (f != null) {
|
||||
ctx.parseConfig = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setParseConfigFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
export function ProcessRequestHeadersBy<PluginConfig>(
|
||||
f: OnHttpHeadersFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).onHttpHeadersFunc;
|
||||
if (f != null) {
|
||||
ctx.onHttpRequestHeaders = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setHttpHeadersFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
export function ProcessRequestBodyBy<PluginConfig>(
|
||||
f: OnHttpBodyFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).OnHttpBodyFunc;
|
||||
if (f != null) {
|
||||
ctx.onHttpRequestBody = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setHttpBodyFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
export function ProcessResponseHeadersBy<PluginConfig>(
|
||||
f: OnHttpHeadersFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).onHttpHeadersFunc;
|
||||
if (f != null) {
|
||||
ctx.onHttpResponseHeaders = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setHttpHeadersFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
export function ProcessResponseBodyBy<PluginConfig>(
|
||||
f: OnHttpBodyFunc<PluginConfig>
|
||||
): usize {
|
||||
const lambdaFn = function (
|
||||
closure: usize,
|
||||
ctx: CommonRootCtx<PluginConfig>
|
||||
): void {
|
||||
const f = changetype<Closure<PluginConfig>>(closure).OnHttpBodyFunc;
|
||||
if (f != null) {
|
||||
ctx.onHttpResponseBody = f;
|
||||
}
|
||||
};
|
||||
const closure = new Closure<PluginConfig>(lambdaFn);
|
||||
closure.setHttpBodyFunc(f);
|
||||
return changetype<usize>(closure);
|
||||
}
|
||||
|
||||
class CommonCtx<PluginConfig> extends Context implements HttpContext {
|
||||
commonRootCtx: CommonRootCtx<PluginConfig>;
|
||||
config: PluginConfig |null;
|
||||
needRequestBody: boolean;
|
||||
needResponseBody: boolean;
|
||||
requestBodySize: u32;
|
||||
responseBodySize: u32;
|
||||
contextID: u32;
|
||||
userContext: Map<string, usize>;
|
||||
|
||||
constructor(context_id: u32, root_context: CommonRootCtx<PluginConfig>) {
|
||||
super(context_id, root_context);
|
||||
this.userContext = new Map<string, usize>();
|
||||
this.commonRootCtx = root_context;
|
||||
this.contextID = context_id;
|
||||
this.requestBodySize = 0;
|
||||
this.responseBodySize = 0;
|
||||
this.config = null
|
||||
if (this.commonRootCtx.onHttpRequestHeaders != null) {
|
||||
this.needResponseBody = true;
|
||||
} else {
|
||||
this.needResponseBody = false;
|
||||
}
|
||||
if (this.commonRootCtx.onHttpRequestBody != null) {
|
||||
this.needRequestBody = true;
|
||||
} else {
|
||||
this.needRequestBody = false;
|
||||
}
|
||||
}
|
||||
|
||||
SetContext(key: string, value: usize): void {
|
||||
this.userContext.set(key, value);
|
||||
}
|
||||
|
||||
GetContext(key: string): usize {
|
||||
return this.userContext.get(key);
|
||||
}
|
||||
|
||||
Scheme(): string {
|
||||
proxy_set_effective_context(this.contextID);
|
||||
return getRequestScheme();
|
||||
}
|
||||
|
||||
Host(): string {
|
||||
proxy_set_effective_context(this.contextID);
|
||||
return getRequestHost();
|
||||
}
|
||||
|
||||
Path(): string {
|
||||
proxy_set_effective_context(this.contextID);
|
||||
return getRequestPath();
|
||||
}
|
||||
|
||||
Method(): string {
|
||||
proxy_set_effective_context(this.contextID);
|
||||
return getRequestMethod();
|
||||
}
|
||||
|
||||
DontReadRequestBody(): void {
|
||||
this.needRequestBody = false;
|
||||
}
|
||||
|
||||
DontReadResponseBody(): void {
|
||||
this.needResponseBody = false;
|
||||
}
|
||||
|
||||
onRequestHeaders(_a: u32, _end_of_stream: boolean): FilterHeadersStatusValues {
|
||||
const parseResult = this.commonRootCtx.ruleMatcher.getMatchConfig();
|
||||
if (parseResult.success == false) {
|
||||
log(LogLevelValues.error, "get match config failed");
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
this.config = parseResult.pluginConfig;
|
||||
|
||||
if (isBinaryRequestBody()) {
|
||||
this.needRequestBody = false;
|
||||
}
|
||||
|
||||
if (this.commonRootCtx.onHttpRequestHeaders == null) {
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
return this.commonRootCtx.onHttpRequestHeaders(
|
||||
this,
|
||||
this.config as PluginConfig
|
||||
);
|
||||
}
|
||||
|
||||
onRequestBody(
|
||||
body_buffer_length: usize,
|
||||
end_of_stream: boolean
|
||||
): FilterDataStatusValues {
|
||||
if (this.config == null || !this.needRequestBody) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
|
||||
if (this.commonRootCtx.onHttpRequestBody == null) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
this.requestBodySize += body_buffer_length as u32;
|
||||
|
||||
if (!end_of_stream) {
|
||||
return FilterDataStatusValues.StopIterationAndBuffer;
|
||||
}
|
||||
|
||||
const body = get_buffer_bytes(
|
||||
BufferTypeValues.HttpRequestBody,
|
||||
0,
|
||||
this.requestBodySize
|
||||
);
|
||||
|
||||
return this.commonRootCtx.onHttpRequestBody(
|
||||
this,
|
||||
this.config as PluginConfig,
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
onResponseHeaders(_a: u32, _end_of_stream: bool): FilterHeadersStatusValues {
|
||||
if (this.config == null) {
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
|
||||
if (isBinaryRequestBody()) {
|
||||
this.needResponseBody = false;
|
||||
}
|
||||
|
||||
if (this.commonRootCtx.onHttpResponseHeaders == null) {
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
|
||||
return this.commonRootCtx.onHttpResponseHeaders(
|
||||
this,
|
||||
this.config as PluginConfig
|
||||
);
|
||||
}
|
||||
|
||||
onResponseBody(
|
||||
body_buffer_length: usize,
|
||||
end_of_stream: bool
|
||||
): FilterDataStatusValues {
|
||||
if (this.config == null) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
|
||||
if (this.commonRootCtx.onHttpResponseBody == null) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
|
||||
if (!this.needResponseBody) {
|
||||
return FilterDataStatusValues.Continue;
|
||||
}
|
||||
|
||||
this.responseBodySize += body_buffer_length as u32;
|
||||
|
||||
if (!end_of_stream) {
|
||||
return FilterDataStatusValues.StopIterationAndBuffer;
|
||||
}
|
||||
const body = get_buffer_bytes(
|
||||
BufferTypeValues.HttpResponseBody,
|
||||
0,
|
||||
this.responseBodySize
|
||||
);
|
||||
|
||||
return this.commonRootCtx.onHttpResponseBody(
|
||||
this,
|
||||
this.config as PluginConfig,
|
||||
body
|
||||
);
|
||||
}
|
||||
}
|
||||
65
plugins/wasm-assemblyscript/assembly/request_wrapper.ts
Normal file
65
plugins/wasm-assemblyscript/assembly/request_wrapper.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
stream_context,
|
||||
log,
|
||||
LogLevelValues
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
|
||||
export function getRequestScheme(): string {
|
||||
let scheme: string = stream_context.headers.request.get(":scheme");
|
||||
if (scheme == "") {
|
||||
log(LogLevelValues.error, "Parse request scheme failed");
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
export function getRequestHost(): string {
|
||||
let host: string = stream_context.headers.request.get(":authority");
|
||||
if (host == "") {
|
||||
log(LogLevelValues.error, "Parse request host failed");
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
export function getRequestPath(): string {
|
||||
let path: string = stream_context.headers.request.get(":path");
|
||||
if (path == "") {
|
||||
log(LogLevelValues.error, "Parse request path failed");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function getRequestMethod(): string {
|
||||
let method: string = stream_context.headers.request.get(":method");
|
||||
if (method == "") {
|
||||
log(LogLevelValues.error, "Parse request method failed");
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
export function isBinaryRequestBody(): boolean {
|
||||
let contentType: string = stream_context.headers.request.get("content-type");
|
||||
if (contentType != "" && (contentType.includes("octet-stream") || contentType.includes("grpc"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let encoding: string = stream_context.headers.request.get("content-encoding");
|
||||
if (encoding != "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isBinaryResponseBody(): boolean {
|
||||
let contentType: string = stream_context.headers.response.get("content-type");
|
||||
if (contentType != "" && (contentType.includes("octet-stream") || contentType.includes("grpc"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let encoding: string = stream_context.headers.response.get("content-encoding");
|
||||
if (encoding != "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
346
plugins/wasm-assemblyscript/assembly/rule_matcher.ts
Normal file
346
plugins/wasm-assemblyscript/assembly/rule_matcher.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { getRequestHost } from "./request_wrapper";
|
||||
import {
|
||||
get_property,
|
||||
LogLevelValues,
|
||||
log,
|
||||
WasmResultValues,
|
||||
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
|
||||
import { JSON } from "assemblyscript-json/assembly";
|
||||
|
||||
enum Category {
|
||||
Route,
|
||||
Host,
|
||||
RoutePrefix,
|
||||
Service
|
||||
}
|
||||
|
||||
enum MatchType {
|
||||
Prefix,
|
||||
Exact,
|
||||
Suffix,
|
||||
}
|
||||
|
||||
const RULES_KEY: string = "_rules_";
|
||||
const MATCH_ROUTE_KEY: string = "_match_route_";
|
||||
const MATCH_DOMAIN_KEY: string = "_match_domain_";
|
||||
const MATCH_SERVICE_KEY: string = "_match_service_";
|
||||
const MATCH_ROUTE_PREFIX_KEY: string = "_match_route_prefix_"
|
||||
|
||||
class HostMatcher {
|
||||
matchType: MatchType;
|
||||
host: string;
|
||||
|
||||
constructor(matchType: MatchType, host: string) {
|
||||
this.matchType = matchType;
|
||||
this.host = host;
|
||||
}
|
||||
}
|
||||
|
||||
class RuleConfig<PluginConfig> {
|
||||
category: Category;
|
||||
routes!: Map<string, boolean>;
|
||||
services!: Map<string, boolean>;
|
||||
routePrefixs!: Map<string, boolean>;
|
||||
hosts!: Array<HostMatcher>;
|
||||
config: PluginConfig | null;
|
||||
|
||||
constructor() {
|
||||
this.category = Category.Route;
|
||||
this.config = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class ParseResult<PluginConfig> {
|
||||
pluginConfig: PluginConfig | null;
|
||||
success: boolean;
|
||||
constructor(pluginConfig: PluginConfig | null, success: boolean) {
|
||||
this.pluginConfig = pluginConfig;
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
|
||||
export class RuleMatcher<PluginConfig> {
|
||||
ruleConfig: Array<RuleConfig<PluginConfig>>;
|
||||
globalConfig: PluginConfig | null;
|
||||
hasGlobalConfig: boolean;
|
||||
|
||||
constructor() {
|
||||
this.ruleConfig = new Array<RuleConfig<PluginConfig>>();
|
||||
this.globalConfig = null;
|
||||
this.hasGlobalConfig = false;
|
||||
}
|
||||
|
||||
getMatchConfig(): ParseResult<PluginConfig> {
|
||||
const host = getRequestHost();
|
||||
if (host == "") {
|
||||
return new ParseResult<PluginConfig>(null, false);
|
||||
}
|
||||
let result = get_property("route_name");
|
||||
if (result.status != WasmResultValues.Ok && result.status != WasmResultValues.NotFound) {
|
||||
return new ParseResult<PluginConfig>(null, false);
|
||||
}
|
||||
const routeName = String.UTF8.decode(result.returnValue);
|
||||
|
||||
result = get_property("cluster_name");
|
||||
if (result.status != WasmResultValues.Ok && result.status != WasmResultValues.NotFound) {
|
||||
return new ParseResult<PluginConfig>(null, false);
|
||||
}
|
||||
const serviceName = String.UTF8.decode(result.returnValue);
|
||||
|
||||
for (let i = 0; i < this.ruleConfig.length; i++) {
|
||||
const rule = this.ruleConfig[i];
|
||||
// category == Host
|
||||
if (rule.category == Category.Host) {
|
||||
if (this.hostMatch(rule, host)) {
|
||||
log(LogLevelValues.debug, "getMatchConfig: match host " + host);
|
||||
return new ParseResult<PluginConfig>(rule.config, true);
|
||||
}
|
||||
}
|
||||
// category == Route
|
||||
if (rule.category == Category.Route) {
|
||||
if (rule.routes.has(routeName)) {
|
||||
log(LogLevelValues.debug, "getMatchConfig: match route " + routeName);
|
||||
return new ParseResult<PluginConfig>(rule.config, true);
|
||||
}
|
||||
}
|
||||
// category == RoutePrefix
|
||||
if (rule.category == Category.RoutePrefix) {
|
||||
for (let i = 0; i < rule.routePrefixs.keys().length; i++) {
|
||||
const routePrefix = rule.routePrefixs.keys()[i];
|
||||
if (routeName.startsWith(routePrefix)) {
|
||||
return new ParseResult<PluginConfig>(rule.config, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// category == Cluster
|
||||
if (this.serviceMatch(rule, serviceName)) {
|
||||
return new ParseResult<PluginConfig>(rule.config, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasGlobalConfig) {
|
||||
return new ParseResult<PluginConfig>(this.globalConfig, true);
|
||||
}
|
||||
return new ParseResult<PluginConfig>(null, false);
|
||||
}
|
||||
|
||||
parseRuleConfig(
|
||||
config: JSON.Obj,
|
||||
parsePluginConfig: (json: JSON.Obj) => ParseResult<PluginConfig>
|
||||
): boolean {
|
||||
const obj = config;
|
||||
let keyCount = obj.keys.length;
|
||||
if (keyCount == 0) {
|
||||
this.hasGlobalConfig = true;
|
||||
const parseResult = parsePluginConfig(config);
|
||||
if (parseResult.success) {
|
||||
this.globalConfig = parseResult.pluginConfig;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let rules: JSON.Arr | null = null;
|
||||
if (obj.has(RULES_KEY)) {
|
||||
rules = obj.getArr(RULES_KEY);
|
||||
keyCount--;
|
||||
}
|
||||
|
||||
if (keyCount > 0) {
|
||||
const parseResult = parsePluginConfig(config);
|
||||
if (parseResult.success) {
|
||||
this.globalConfig = parseResult.pluginConfig;
|
||||
this.hasGlobalConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rules) {
|
||||
if (this.hasGlobalConfig) {
|
||||
return true;
|
||||
}
|
||||
log(LogLevelValues.error, "parse config failed, no valid rules; global config parse error");
|
||||
return false;
|
||||
}
|
||||
|
||||
const rulesArray = rules.valueOf();
|
||||
for (let i = 0; i < rulesArray.length; i++) {
|
||||
if (!rulesArray[i].isObj) {
|
||||
log(LogLevelValues.error, "parse rule failed, rules must be an array of objects");
|
||||
continue;
|
||||
}
|
||||
const ruleJson = changetype<JSON.Obj>(rulesArray[i]);
|
||||
const rule = new RuleConfig<PluginConfig>();
|
||||
const parseResult = parsePluginConfig(ruleJson);
|
||||
if (parseResult.success) {
|
||||
rule.config = parseResult.pluginConfig;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
rule.routes = this.parseRouteMatchConfig(ruleJson);
|
||||
rule.hosts = this.parseHostMatchConfig(ruleJson);
|
||||
rule.services = this.parseServiceMatchConfig(ruleJson);
|
||||
rule.routePrefixs = this.parseRoutePrefixMatchConfig(ruleJson);
|
||||
|
||||
const noRoute = rule.routes.size == 0;
|
||||
const noHosts = rule.hosts.length == 0;
|
||||
const noServices = rule.services.size == 0;
|
||||
const noRoutePrefixs = rule.routePrefixs.size == 0;
|
||||
|
||||
if ((boolToInt(noRoute) + boolToInt(noHosts) + boolToInt(noServices) + boolToInt(noRoutePrefixs)) != 3) {
|
||||
log(LogLevelValues.error, "there is only one of '_match_route_', '_match_domain_', '_match_service_' and '_match_route_prefix_' can present in configuration.");
|
||||
return false;
|
||||
}
|
||||
if (!noRoute) {
|
||||
rule.category = Category.Route;
|
||||
} else if (!noHosts) {
|
||||
rule.category = Category.Host;
|
||||
} else if (!noServices) {
|
||||
rule.category = Category.Service;
|
||||
} else {
|
||||
rule.category = Category.RoutePrefix;
|
||||
}
|
||||
this.ruleConfig.push(rule);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
parseRouteMatchConfig(config: JSON.Obj): Map<string, boolean> {
|
||||
const keys = config.getArr(MATCH_ROUTE_KEY);
|
||||
const routes = new Map<string, boolean>();
|
||||
if (keys) {
|
||||
const array = keys.valueOf();
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const key = array[i].toString();
|
||||
if (key != "") {
|
||||
routes.set(key, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
parseRoutePrefixMatchConfig(config: JSON.Obj): Map<string, boolean> {
|
||||
const keys = config.getArr(MATCH_ROUTE_PREFIX_KEY);
|
||||
const routePrefixs = new Map<string, boolean>();
|
||||
if (keys) {
|
||||
const array = keys.valueOf();
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const key = array[i].toString();
|
||||
if (key != "") {
|
||||
routePrefixs.set(key, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return routePrefixs;
|
||||
}
|
||||
|
||||
parseServiceMatchConfig(config: JSON.Obj): Map<string, boolean> {
|
||||
const keys = config.getArr(MATCH_SERVICE_KEY);
|
||||
const clusters = new Map<string, boolean>();
|
||||
if (keys) {
|
||||
const array = keys.valueOf();
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const key = array[i].toString();
|
||||
if (key != "") {
|
||||
clusters.set(key, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return clusters;
|
||||
}
|
||||
|
||||
parseHostMatchConfig(config: JSON.Obj): Array<HostMatcher> {
|
||||
const hostMatchers = new Array<HostMatcher>();
|
||||
const keys = config.getArr(MATCH_DOMAIN_KEY);
|
||||
if (keys !== null) {
|
||||
const array = keys.valueOf();
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i].toString(); // Assuming the array has string elements
|
||||
let hostMatcher: HostMatcher;
|
||||
if (item.startsWith("*")) {
|
||||
hostMatcher = new HostMatcher(MatchType.Suffix, item.substr(1));
|
||||
} else if (item.endsWith("*")) {
|
||||
hostMatcher = new HostMatcher(
|
||||
MatchType.Prefix,
|
||||
item.substr(0, item.length - 1)
|
||||
);
|
||||
} else {
|
||||
hostMatcher = new HostMatcher(MatchType.Exact, item);
|
||||
}
|
||||
hostMatchers.push(hostMatcher);
|
||||
}
|
||||
}
|
||||
return hostMatchers;
|
||||
}
|
||||
|
||||
stripPortFromHost(reqHost: string): string {
|
||||
// Port removing code is inspired by
|
||||
// https://github.com/envoyproxy/envoy/blob/v1.17.0/source/common/http/header_utility.cc#L219
|
||||
let portStart: i32 = reqHost.lastIndexOf(":");
|
||||
if (portStart != -1) {
|
||||
// According to RFC3986 v6 address is always enclosed in "[]".
|
||||
// section 3.2.2.
|
||||
let v6EndIndex: i32 = reqHost.lastIndexOf("]");
|
||||
if (v6EndIndex == -1 || v6EndIndex < portStart) {
|
||||
if (portStart + 1 <= reqHost.length) {
|
||||
return reqHost.substring(0, portStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reqHost;
|
||||
}
|
||||
|
||||
hostMatch(rule: RuleConfig<PluginConfig>, reqHost: string): boolean {
|
||||
reqHost = this.stripPortFromHost(reqHost);
|
||||
for (let i = 0; i < rule.hosts.length; i++) {
|
||||
let hostMatch = rule.hosts[i];
|
||||
switch (hostMatch.matchType) {
|
||||
case MatchType.Suffix:
|
||||
if (reqHost.endsWith(hostMatch.host)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MatchType.Prefix:
|
||||
if (reqHost.startsWith(hostMatch.host)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MatchType.Exact:
|
||||
if (reqHost == hostMatch.host) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
serviceMatch(rule: RuleConfig<PluginConfig>, serviceName: string): boolean {
|
||||
const parts = serviceName.split('|');
|
||||
if (parts.length != 4) {
|
||||
return false;
|
||||
}
|
||||
const port = parts[1];
|
||||
const fqdn = parts[3];
|
||||
for (let i = 0; i < rule.services.keys().length; i++) {
|
||||
let configServiceName = rule.services.keys()[i];
|
||||
let colonIndex = configServiceName.lastIndexOf(':');
|
||||
if (colonIndex != -1) {
|
||||
let configFQDN = configServiceName.slice(0, colonIndex);
|
||||
let configPort = configServiceName.slice(colonIndex + 1);
|
||||
if (fqdn == configFQDN && port == configPort) return true;
|
||||
} else if (fqdn == configServiceName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function boolToInt(value: boolean): i32 {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
6
plugins/wasm-assemblyscript/assembly/tsconfig.json
Normal file
6
plugins/wasm-assemblyscript/assembly/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# 功能说明
|
||||
`custom-response`插件支持配置自定义的响应,包括自定义 HTTP 应答状态码、HTTP 应答头,以及 HTTP 应答 Body。可以用于 Mock 响应,也可以用于判断特定状态码后给出自定义应答,例如在触发网关限流策略时实现自定义响应。
|
||||
|
||||
# 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||
| headers | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||
| body | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||
| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
|
||||
|
||||
# 配置示例
|
||||
|
||||
## Mock 应答场景
|
||||
|
||||
```yaml
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
|
||||
```
|
||||
|
||||
根据该配置,请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Hello: World
|
||||
Content-Length: 17
|
||||
|
||||
{"hello":"world"}
|
||||
```
|
||||
|
||||
## 触发限流时自定义响应
|
||||
|
||||
```yaml
|
||||
enable_on_status:
|
||||
- 429
|
||||
status_code: 302
|
||||
headers:
|
||||
- Location=https://example.com
|
||||
```
|
||||
|
||||
触发网关限流时一般会返回 `429` 状态码,这时请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 302 Found
|
||||
Location: https://example.com
|
||||
```
|
||||
|
||||
从而实现基于浏览器 302 重定向机制,将限流后的用户引导到其他页面,比如可以是一个 CDN 上的静态页面。
|
||||
|
||||
如果希望触发限流时,正常返回其他应答,参考 Mock 应答场景配置相应的字段即可。
|
||||
|
||||
## 对特定路由或域名开启
|
||||
```yaml
|
||||
# 使用 matchRules 字段进行细粒度规则配置
|
||||
matchRules:
|
||||
# 规则一:按 Ingress 名称匹配生效
|
||||
- ingress:
|
||||
- default/foo
|
||||
- default/bar
|
||||
body: "{\"hello\":\"world\"}"
|
||||
# 规则二:按域名匹配生效
|
||||
- domain:
|
||||
- "*.example.com"
|
||||
- test.com
|
||||
enable_on_status:
|
||||
- 429
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
body: "{\"errmsg\": \"rate limited\"}"
|
||||
```
|
||||
此例 `ingress` 中指定的 `default/foo` 和 `default/bar` 对应 default 命名空间下名为 foo 和 bar 的 Ingress,当匹配到这两个 Ingress 时,将使用此段配置;
|
||||
此例 `domain` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置;
|
||||
配置的匹配生效顺序,将按照 `matchRules` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"targets": {
|
||||
"debug": {
|
||||
"outFile": "build/debug.wasm",
|
||||
"textFile": "build/debug.wat",
|
||||
"sourceMap": true,
|
||||
"debug": true
|
||||
},
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"textFile": "build/release.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false,
|
||||
"debug": true
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"bindings": "esm",
|
||||
"use": "abort=abort_proc_exit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
export * from "@higress/proxy-wasm-assemblyscript-sdk/assembly/proxy";
|
||||
import { SetCtx, HttpContext, ProcessRequestHeadersBy, Logger, ParseConfigBy, ParseResult, ProcessResponseHeadersBy } from "@higress/wasm-assemblyscript/assembly";
|
||||
import { FilterHeadersStatusValues, Headers, send_http_response, stream_context, HeaderPair } from "@higress/proxy-wasm-assemblyscript-sdk/assembly"
|
||||
import { JSON } from "assemblyscript-json/assembly";
|
||||
|
||||
class CustomResponseConfig {
|
||||
statusCode: u32;
|
||||
headers: Headers;
|
||||
body: ArrayBuffer;
|
||||
enableOnStatus: Array<u32>;
|
||||
contentType: string;
|
||||
constructor() {
|
||||
this.statusCode = 200;
|
||||
this.headers = [];
|
||||
this.body = new ArrayBuffer(0);
|
||||
this.enableOnStatus = [];
|
||||
this.contentType = "text/plain; charset=utf-8";
|
||||
}
|
||||
}
|
||||
|
||||
SetCtx<CustomResponseConfig>(
|
||||
"custom-response",
|
||||
[ParseConfigBy<CustomResponseConfig>(parseConfig),
|
||||
ProcessRequestHeadersBy<CustomResponseConfig>(onHttpRequestHeaders),
|
||||
ProcessResponseHeadersBy<CustomResponseConfig>(onHttpResponseHeaders),])
|
||||
|
||||
function parseConfig(json: JSON.Obj): ParseResult<CustomResponseConfig> {
|
||||
let headersArray = json.getArr("headers");
|
||||
let config = new CustomResponseConfig();
|
||||
if (headersArray != null) {
|
||||
for (let i = 0; i < headersArray.valueOf().length; i++) {
|
||||
let header = headersArray._arr[i];
|
||||
let jsonString = (<JSON.Str>header).toString()
|
||||
let kv = jsonString.split("=")
|
||||
if (kv.length == 2) {
|
||||
let key = kv[0].trim();
|
||||
let value = kv[1].trim();
|
||||
if (key.toLowerCase() == "content-type") {
|
||||
config.contentType = value;
|
||||
} else if (key.toLowerCase() == "content-length") {
|
||||
continue;
|
||||
} else {
|
||||
config.headers.push(new HeaderPair(String.UTF8.encode(key), String.UTF8.encode(value)));
|
||||
}
|
||||
} else {
|
||||
Logger.Error("parse header failed");
|
||||
return new ParseResult<CustomResponseConfig>(null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
let body = json.getString("body");
|
||||
if (body != null) {
|
||||
config.body = String.UTF8.encode(body.valueOf());
|
||||
}
|
||||
config.headers.push(new HeaderPair(String.UTF8.encode("content-type"), String.UTF8.encode(config.contentType)));
|
||||
|
||||
let statusCode = json.getInteger("statusCode");
|
||||
if (statusCode != null) {
|
||||
config.statusCode = statusCode.valueOf() as u32;
|
||||
}
|
||||
|
||||
let enableOnStatus = json.getArr("enableOnStatus");
|
||||
|
||||
if (enableOnStatus != null) {
|
||||
for (let i = 0; i < enableOnStatus.valueOf().length; i++) {
|
||||
let status = enableOnStatus._arr[i];
|
||||
if (status.isInteger) {
|
||||
config.enableOnStatus.push((<JSON.Integer>status).valueOf() as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ParseResult<CustomResponseConfig>(config, true);
|
||||
}
|
||||
|
||||
function onHttpRequestHeaders(context: HttpContext, config: CustomResponseConfig): FilterHeadersStatusValues {
|
||||
if (config.enableOnStatus.length != 0) {
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
send_http_response(config.statusCode, "custom-response", config.body, config.headers);
|
||||
return FilterHeadersStatusValues.StopIteration;
|
||||
}
|
||||
|
||||
function onHttpResponseHeaders(context: HttpContext, config: CustomResponseConfig): FilterHeadersStatusValues {
|
||||
let statusCodeStr = stream_context.headers.response.get(":status")
|
||||
if (statusCodeStr == "") {
|
||||
Logger.Error("get http response status code failed");
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
let statusCode = parseInt(statusCodeStr);
|
||||
for (let i = 0; i < config.enableOnStatus.length; i++) {
|
||||
if (statusCode == config.enableOnStatus[i]) {
|
||||
send_http_response(config.statusCode, "custom-response", config.body, config.headers);
|
||||
}
|
||||
}
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
68
plugins/wasm-assemblyscript/extensions/custom-response/package-lock.json
generated
Normal file
68
plugins/wasm-assemblyscript/extensions/custom-response/package-lock.json
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "custom-response",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "custom-response",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@higress/wasm-assemblyscript": "^0.0.4",
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@higress/wasm-assemblyscript": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@higress/wasm-assemblyscript/-/wasm-assemblyscript-0.0.4.tgz",
|
||||
"integrity": "sha512-F9m3fHBeM0OFWWHekTcmj3dVh7I4pbzf0oIioVdArD2oSUgpCZ8ur8E/9r7JR3WVwn2/l0A3LRSBOJTzQnHtMw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assemblyscript": {
|
||||
"version": "0.27.29",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
|
||||
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binaryen": "116.0.0-nightly.20240114",
|
||||
"long": "^5.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"asc": "bin/asc.js",
|
||||
"asinit": "bin/asinit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/assemblyscript"
|
||||
}
|
||||
},
|
||||
"node_modules/assemblyscript-json": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
|
||||
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/binaryen": {
|
||||
"version": "116.0.0-nightly.20240114",
|
||||
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
|
||||
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"wasm-opt": "bin/wasm-opt",
|
||||
"wasm2js": "bin/wasm2js"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "custom-response",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node tests",
|
||||
"asbuild:debug": "asc assembly/index.ts --target debug",
|
||||
"asbuild:release": "asc assembly/index.ts --target release",
|
||||
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
|
||||
"start": "npx serve ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0",
|
||||
"@higress/wasm-assemblyscript": "^0.0.4"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./build/release.js",
|
||||
"types": "./build/release.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"targets": {
|
||||
"debug": {
|
||||
"outFile": "build/debug.wasm",
|
||||
"textFile": "build/debug.wat",
|
||||
"sourceMap": true,
|
||||
"debug": true
|
||||
},
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"textFile": "build/release.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false,
|
||||
"debug": true
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"bindings": "esm",
|
||||
"use": "abort=abort_proc_exit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export * from "@higress/proxy-wasm-assemblyscript-sdk/assembly/proxy";
|
||||
import { SetCtx, HttpContext, ProcessRequestHeadersBy, Logger, ParseResult, ParseConfigBy, RegisteTickFunc, ProcessResponseHeadersBy } from "@higress/wasm-assemblyscript/assembly";
|
||||
import { FilterHeadersStatusValues, send_http_response, stream_context } from "@higress/proxy-wasm-assemblyscript-sdk/assembly"
|
||||
import { JSON } from "assemblyscript-json/assembly";
|
||||
class HelloWorldConfig {
|
||||
}
|
||||
|
||||
SetCtx<HelloWorldConfig>("hello-world",
|
||||
[ParseConfigBy<HelloWorldConfig>(parseConfig),
|
||||
ProcessRequestHeadersBy<HelloWorldConfig>(onHttpRequestHeaders),
|
||||
ProcessResponseHeadersBy<HelloWorldConfig>(onHttpResponseHeaders)
|
||||
])
|
||||
|
||||
function parseConfig(json: JSON.Obj): ParseResult<HelloWorldConfig> {
|
||||
RegisteTickFunc(2000, () => {
|
||||
Logger.Debug("tick 2s");
|
||||
})
|
||||
RegisteTickFunc(5000, () => {
|
||||
Logger.Debug("tick 5s");
|
||||
})
|
||||
return new ParseResult<HelloWorldConfig>(new HelloWorldConfig(), true);
|
||||
}
|
||||
|
||||
class TestContext{
|
||||
value: string
|
||||
constructor(value: string){
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
function onHttpRequestHeaders(context: HttpContext, config: HelloWorldConfig): FilterHeadersStatusValues {
|
||||
stream_context.headers.request.add("hello", "world");
|
||||
Logger.Debug("[hello-world] logger test");
|
||||
context.SetContext("test-set-context", changetype<usize>(new TestContext("value")))
|
||||
send_http_response(200, "hello-world", String.UTF8.encode("[wasm-assemblyscript]hello world"), []);
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
|
||||
function onHttpResponseHeaders(context: HttpContext, config: HelloWorldConfig): FilterHeadersStatusValues {
|
||||
const str = changetype<TestContext>(context.GetContext("test-set-context")).value;
|
||||
Logger.Debug("[hello-world] test-set-context: " + str);
|
||||
return FilterHeadersStatusValues.Continue;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
68
plugins/wasm-assemblyscript/extensions/hello-world/package-lock.json
generated
Normal file
68
plugins/wasm-assemblyscript/extensions/hello-world/package-lock.json
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "hello-world",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hello-world",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@higress/wasm-assemblyscript": "^0.0.4",
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@higress/wasm-assemblyscript": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@higress/wasm-assemblyscript/-/wasm-assemblyscript-0.0.4.tgz",
|
||||
"integrity": "sha512-F9m3fHBeM0OFWWHekTcmj3dVh7I4pbzf0oIioVdArD2oSUgpCZ8ur8E/9r7JR3WVwn2/l0A3LRSBOJTzQnHtMw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assemblyscript": {
|
||||
"version": "0.27.29",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
|
||||
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binaryen": "116.0.0-nightly.20240114",
|
||||
"long": "^5.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"asc": "bin/asc.js",
|
||||
"asinit": "bin/asinit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/assemblyscript"
|
||||
}
|
||||
},
|
||||
"node_modules/assemblyscript-json": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
|
||||
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/binaryen": {
|
||||
"version": "116.0.0-nightly.20240114",
|
||||
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
|
||||
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"wasm-opt": "bin/wasm-opt",
|
||||
"wasm2js": "bin/wasm2js"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "hello-world",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node tests",
|
||||
"asbuild:debug": "asc assembly/index.ts --target debug",
|
||||
"asbuild:release": "asc assembly/index.ts --target release",
|
||||
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
|
||||
"start": "npx serve ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0",
|
||||
"@higress/wasm-assemblyscript": "^0.0.4"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./build/release.js",
|
||||
"types": "./build/release.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
75
plugins/wasm-assemblyscript/package-lock.json
generated
Normal file
75
plugins/wasm-assemblyscript/package-lock.json
generated
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "@higress/wasm-assemblyscript",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@higress/wasm-assemblyscript",
|
||||
"version": "0.0.4",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2",
|
||||
"as-uuid": "^0.0.4",
|
||||
"assemblyscript": "^0.27.29",
|
||||
"assemblyscript-json": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@higress/proxy-wasm-assemblyscript-sdk": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@higress/proxy-wasm-assemblyscript-sdk/-/proxy-wasm-assemblyscript-sdk-0.0.2.tgz",
|
||||
"integrity": "sha512-0J1tFJMTE6o37JpGJBLq0wc5kBC/fpbISrP+KFb4bAEeshu6daXzD2P3bAfJXmW+oZdY0WGptTGXWx8pf9Fk+g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/as-uuid": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/as-uuid/-/as-uuid-0.0.4.tgz",
|
||||
"integrity": "sha512-ZHNv0ETSzg5ZD0IWWJVyip/73LWtrWeMmvRi+16xbkpU/nZ0O8EegvgS7bgZ5xRqrUbc2NqZqHOWMOtPqbLrhg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assemblyscript": {
|
||||
"version": "0.27.29",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
|
||||
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binaryen": "116.0.0-nightly.20240114",
|
||||
"long": "^5.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"asc": "bin/asc.js",
|
||||
"asinit": "bin/asinit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/assemblyscript"
|
||||
}
|
||||
},
|
||||
"node_modules/assemblyscript-json": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
|
||||
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/binaryen": {
|
||||
"version": "116.0.0-nightly.20240114",
|
||||
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
|
||||
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"wasm-opt": "bin/wasm-opt",
|
||||
"wasm2js": "bin/wasm2js"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
37
plugins/wasm-assemblyscript/package.json
Normal file
37
plugins/wasm-assemblyscript/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@higress/wasm-assemblyscript",
|
||||
"version": "0.0.4",
|
||||
"main": "assembly/index.ts",
|
||||
"scripts": {
|
||||
"test": "node tests",
|
||||
"asbuild:debug": "asc assembly/index.ts --target debug",
|
||||
"asbuild:release": "asc assembly/index.ts --target release",
|
||||
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
|
||||
"start": "npx serve ."
|
||||
},
|
||||
"author": "jingze.dai",
|
||||
"license": "Apache-2.0",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.27.29",
|
||||
"as-uuid": "^0.0.4",
|
||||
"assemblyscript-json": "^1.1.0",
|
||||
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./build/release.js",
|
||||
"types": "./build/release.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"/assembly",
|
||||
"package-lock.json",
|
||||
"index.js"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Jing-ze/wasm-assemblyscript.git"
|
||||
}
|
||||
}
|
||||
350
plugins/wasm-go/extensions/ai-agent/README.md
Normal file
350
plugins/wasm-go/extensions/ai-agent/README.md
Normal file
@@ -0,0 +1,350 @@
|
||||
---
|
||||
title: AI Agent
|
||||
keywords: [ AI网关, AI Agent ]
|
||||
description: AI Agent插件配置参考
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
一个可定制化的 API AI Agent,支持配置 http method 类型为 GET 与 POST 的 API,目前只支持非流式模式。
|
||||
agent流程图如下:
|
||||

|
||||
|
||||
|
||||
## 配置字段
|
||||
|
||||
### 基本配置
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------|-----------|---------|--------|----------------------------|
|
||||
| `llm` | object | 必填 | - | 配置 AI 服务提供商的信息 |
|
||||
| `apis` | object | 必填 | - | 配置外部 API 服务提供商的信息 |
|
||||
| `promptTemplate` | object | 非必填 | - | 配置 Agent ReAct 模板的信息 |
|
||||
|
||||
`llm`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------------------|-----------|---------|--------|-----------------------------------|
|
||||
| `apiKey` | string | 必填 | - | 用于在访问大模型服务时进行认证的令牌。|
|
||||
| `serviceName` | string | 必填 | - | 大模型服务名 |
|
||||
| `servicePort` | int | 必填 | - | 大模型服务端口 |
|
||||
| `domain` | string | 必填 | - | 访问大模型服务时域名 |
|
||||
| `path` | string | 必填 | - | 访问大模型服务时路径 |
|
||||
| `model` | string | 必填 | - | 访问大模型服务时模型名 |
|
||||
| `maxIterations` | int | 必填 | 15 | 结束执行循环前的最大步数 |
|
||||
| `maxExecutionTime` | int | 必填 | 50000 | 每一次请求大模型的超时时间,单位毫秒 |
|
||||
| `maxTokens` | int | 必填 | 1000 | 每一次请求大模型的输出token限制 |
|
||||
|
||||
`apis`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|---------|--------|-----------------------------------|
|
||||
| `apiProvider` | object | 必填 | - | 外部 API 服务信息 |
|
||||
| `api` | string | 必填 | - | 工具的 OpenAPI 文档 |
|
||||
|
||||
`apiProvider`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|---------|--------|------------------------------------------|
|
||||
| `apiKey` | object | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌。 |
|
||||
| `serviceName` | string | 必填 | - | 访问外部 API 服务名 |
|
||||
| `servicePort` | int | 必填 | - | 访问外部 API 服务端口 |
|
||||
| `domain` | string | 必填 | - | 访访问外部 API 时域名 |
|
||||
|
||||
`apiKey`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|---------|------------|--------|-------------------------------------------------------------------------------|
|
||||
| `in` | string | 非必填 | header | 在访问外部 API 服务时进行认证的令牌是放在 header 中还是放在 query 中,默认是 header。
|
||||
| `name` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的名称。 |
|
||||
| `value` | string | 非必填 | - | 用于在访问外部 API 服务时进行认证的令牌的值。 |
|
||||
|
||||
`promptTemplate`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|-----------|--------|--------------------------------------------|
|
||||
| `language` | string | 非必填 | EN | Agent ReAct 模板的语言类型,包括 CH 和 EN 两种|
|
||||
| `chTemplate` | object | 非必填 | - | Agent ReAct 中文模板 |
|
||||
| `enTemplate` | object | 非必填 | - | Agent ReAct 英文模板 |
|
||||
|
||||
`chTemplate`和`enTemplate`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------|-----------|-----------|--------|---------------------------------------------|
|
||||
| `question` | string | 非必填 | - | Agent ReAct 模板的 question 部分 |
|
||||
| `thought1` | string | 非必填 | - | Agent ReAct 模板的 thought1 部分 |
|
||||
| `actionInput` | string | 非必填 | - | Agent ReAct 模板的 actionInput 部分 |
|
||||
| `observation` | string | 非必填 | - | Agent ReAct 模板的 observation 部分 |
|
||||
| `thought2` | string | 非必填 | - | Agent ReAct 模板的 thought2 部分 |
|
||||
| `finalAnswer` | string | 非必填 | - | Agent ReAct 模板的 finalAnswer 部分 |
|
||||
| `begin` | string | 非必填 | - | Agent ReAct 模板的 begin 部分 |
|
||||
|
||||
## 用法示例
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
apiKey: xxxxxxxxxxxxxxxxxx
|
||||
domain: dashscope.aliyuncs.com
|
||||
serviceName: dashscope.dns
|
||||
servicePort: 443
|
||||
path: /compatible-mode/v1/chat/completions
|
||||
model: qwen-max-0403
|
||||
maxIterations: 2
|
||||
promptTemplate:
|
||||
language: CH
|
||||
apis:
|
||||
- apiProvider:
|
||||
domain: restapi.amap.com
|
||||
serviceName: geo.dns
|
||||
servicePort: 80
|
||||
apiKey:
|
||||
in: query
|
||||
name: key
|
||||
value: xxxxxxxxxxxxxxx
|
||||
api: |
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: 高德地图
|
||||
description: 获取 POI 的相关信息
|
||||
version: v1.0.0
|
||||
servers:
|
||||
- url: https://restapi.amap.com
|
||||
paths:
|
||||
/v5/place/text:
|
||||
get:
|
||||
description: 根据POI名称,获得POI的经纬度坐标
|
||||
operationId: get_location_coordinate
|
||||
parameters:
|
||||
- name: keywords
|
||||
in: query
|
||||
description: POI名称,必须是中文
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: region
|
||||
in: query
|
||||
description: POI所在的区域名,必须是中文
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
deprecated: false
|
||||
/v5/place/around:
|
||||
get:
|
||||
description: 搜索给定坐标附近的POI
|
||||
operationId: search_nearby_pois
|
||||
parameters:
|
||||
- name: keywords
|
||||
in: query
|
||||
description: 目标POI的关键字
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: location
|
||||
in: query
|
||||
description: 中心点的经度和纬度,用逗号隔开
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
deprecated: false
|
||||
components:
|
||||
schemas: {}
|
||||
- apiProvider:
|
||||
domain: api.seniverse.com
|
||||
serviceName: seniverse.dns
|
||||
servicePort: 80
|
||||
apiKey:
|
||||
in: query
|
||||
name: key
|
||||
value: xxxxxxxxxxxxxxx
|
||||
api: |
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: 心知天气
|
||||
description: 获取 天气预办相关信息
|
||||
version: v1.0.0
|
||||
servers:
|
||||
- url: https://api.seniverse.com
|
||||
paths:
|
||||
/v3/weather/now.json:
|
||||
get:
|
||||
description: 获取指定城市的天气实况
|
||||
operationId: get_weather_now
|
||||
parameters:
|
||||
- name: location
|
||||
in: query
|
||||
description: 所查询的城市
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: language
|
||||
in: query
|
||||
description: 返回天气查询结果所使用的语言
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
default: zh-Hans
|
||||
enum:
|
||||
- zh-Hans
|
||||
- en
|
||||
- ja
|
||||
- name: unit
|
||||
in: query
|
||||
description: 表示温度的的单位,有摄氏度和华氏度两种
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
default: c
|
||||
enum:
|
||||
- c
|
||||
- f
|
||||
deprecated: false
|
||||
components:
|
||||
schemas: {}
|
||||
- apiProvider:
|
||||
apiKey:
|
||||
in: "header"
|
||||
name: "DeepL-Auth-Key"
|
||||
value: "73xxxxxxxxxxxxxxx:fx"
|
||||
domain: "api-free.deepl.com"
|
||||
serviceName: "deepl.dns"
|
||||
servicePort: 443
|
||||
api: |
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: DeepL API Documentation
|
||||
description: The DeepL API provides programmatic access to DeepL’s machine translation technology.
|
||||
version: v1.0.0
|
||||
servers:
|
||||
- url: https://api-free.deepl.com/v2
|
||||
paths:
|
||||
/translate:
|
||||
post:
|
||||
summary: Request Translation
|
||||
operationId: translateText
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
- target_lang
|
||||
properties:
|
||||
text:
|
||||
description: |
|
||||
Text to be translated. Only UTF-8-encoded plain text is supported. The parameter may be specified
|
||||
up to 50 times in a single request. Translations are returned in the same order as they are requested.
|
||||
type: array
|
||||
maxItems: 50
|
||||
items:
|
||||
type: string
|
||||
example: Hello, World!
|
||||
target_lang:
|
||||
description: The language into which the text should be translated.
|
||||
type: string
|
||||
enum:
|
||||
- BG
|
||||
- CS
|
||||
- DA
|
||||
- DE
|
||||
- EL
|
||||
- EN-GB
|
||||
- EN-US
|
||||
- ES
|
||||
- ET
|
||||
- FI
|
||||
- FR
|
||||
- HU
|
||||
- ID
|
||||
- IT
|
||||
- JA
|
||||
- KO
|
||||
- LT
|
||||
- LV
|
||||
- NB
|
||||
- NL
|
||||
- PL
|
||||
- PT-BR
|
||||
- PT-PT
|
||||
- RO
|
||||
- RU
|
||||
- SK
|
||||
- SL
|
||||
- SV
|
||||
- TR
|
||||
- UK
|
||||
- ZH
|
||||
- ZH-HANS
|
||||
example: DE
|
||||
components:
|
||||
schemas: {}
|
||||
```
|
||||
|
||||
本示例配置了三个服务,演示了get与post两种类型的工具。其中get类型的工具包括高德地图与心知天气,post类型的工具是deepl翻译。三个服务都需要现在Higress的服务中以DNS域名的方式配置好,并确保健康。
|
||||
高德地图提供了两个工具,分别是获取指定地点的坐标,以及搜索坐标附近的感兴趣的地点。文档:https://lbs.amap.com/api/webservice/guide/api-advanced/newpoisearch
|
||||
心知天气提供了一个工具,用于获取指定城市的实时天气情况,支持中文,英文,日语返回,以及摄氏度和华氏度的表示。文档:https://seniverse.yuque.com/hyper_data/api_v3/nyiu3t
|
||||
deepl提供了一个工具,用于翻译给定的句子,支持多语言。。文档:https://developers.deepl.com/docs/v/zh/api-reference/translate?fallback=true
|
||||
|
||||
|
||||
以下为测试用例,为了效果的稳定性,建议保持大模型版本的稳定,本例子中使用的qwen-max-0403:
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"我想在济南市鑫盛大厦附近喝咖啡,给我推荐几个"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"139487e7-96a0-9b13-91b4-290fb79ac992","choices":[{"index":0,"message":{"role":"assistant","content":" 在济南市鑫盛大厦附近,您可以选择以下咖啡店:\n1. luckin coffee 瑞幸咖啡(鑫盛大厦店),位于新泺大街1299号鑫盛大厦2号楼大堂;\n2. 三庆齐盛广场挪瓦咖啡(三庆·齐盛广场店),位于新泺大街与颖秀路交叉口西南60米;\n3. luckin coffee 瑞幸咖啡(三庆·齐盛广场店),位于颖秀路1267号;\n4. 库迪咖啡(齐鲁软件园店),位于新泺大街三庆齐盛广场4号楼底商;\n5. 库迪咖啡(美莲广场店),位于高新区新泺大街1166号美莲广场L117号;以及其他一些选项。希望这些建议对您有所帮助!"},"finish_reason":"stop"}],"created":1723172296,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":886,"completion_tokens":50,"total_tokens":936}}
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"济南市现在的天气情况如何?"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":" 济南市现在的天气状况为阴天,温度为31℃。此信息最后更新于2024年8月9日15时12分(北京时间)。"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"济南市现在的天气情况如何?用华氏度表示,用日语回答"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"ebd6ea91-8e38-9e14-9a5b-90178d2edea4","choices":[{"index":0,"message":{"role":"assistant","content":" 济南市の現在の天気は雨曇りで、気温は88°Fです。この情報は2024年8月9日15時12分(東京時間)に更新されました。"},"finish_reason":"stop"}],"created":1723187991,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":890,"completion_tokens":56,"total_tokens":946}}
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```shell
|
||||
curl 'http://<这里换成网关公网IP>/api/openai/v1/chat/completions' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{"model":"qwen","frequency_penalty":0,"max_tokens":800,"stream":false,"messages":[{"role":"user","content":"帮我用德语翻译以下句子:九头蛇万岁!"}],"presence_penalty":0,"temperature":0,"top_p":0}'
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{"id":"65dcf12c-61ff-9e68-bffa-44fc9e6070d5","choices":[{"index":0,"message":{"role":"assistant","content":" “九头蛇万岁!”的德语翻译为“Hoch lebe Hydra!”。"},"finish_reason":"stop"}],"created":1724043865,"model":"qwen-max-0403","object":"chat.completion","usage":{"prompt_tokens":908,"completion_tokens":52,"total_tokens":960}}
|
||||
```
|
||||
424
plugins/wasm-go/extensions/ai-agent/config.go
Normal file
424
plugins/wasm-go/extensions/ai-agent/config.go
Normal file
@@ -0,0 +1,424 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty"`
|
||||
PresencePenalty float64 `json:"presence_penalty"`
|
||||
Stream bool `json:"stream"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Topp int32 `json:"top_p"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Index int `json:"index"`
|
||||
Message Message `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
ID string `json:"id"`
|
||||
Choices []Choice `json:"choices"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Object string `json:"object"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
// 用于存放拆解出来的工具相关信息
|
||||
type Tool_Param struct {
|
||||
ToolName string `yaml:"toolName"`
|
||||
Path string `yaml:"path"`
|
||||
Method string `yaml:"method"`
|
||||
ParamName []string `yaml:"paramName"`
|
||||
Parameter string `yaml:"parameter"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
// 用于存放拆解出来的api相关信息
|
||||
type APIParam struct {
|
||||
APIKey APIKey `yaml:"apiKey"`
|
||||
URL string `yaml:"url"`
|
||||
Tool_Param []Tool_Param `yaml:"tool_Param"`
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
// 给OpenAPI的get方法用的
|
||||
type Parameter struct {
|
||||
Name string `yaml:"name"`
|
||||
In string `yaml:"in"`
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
Schema struct {
|
||||
Type string `yaml:"type"`
|
||||
Default string `yaml:"default"`
|
||||
Enum []string `yaml:"enum"`
|
||||
} `yaml:"schema"`
|
||||
}
|
||||
|
||||
type Items struct {
|
||||
Type string `yaml:"type"`
|
||||
Example string `yaml:"example"`
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
Description string `yaml:"description"`
|
||||
Type string `yaml:"type"`
|
||||
Enum []string `yaml:"enum,omitempty"`
|
||||
Items *Items `yaml:"items,omitempty"`
|
||||
MaxItems int `yaml:"maxItems,omitempty"`
|
||||
Example string `yaml:"example,omitempty"`
|
||||
}
|
||||
|
||||
type Schema struct {
|
||||
Type string `yaml:"type"`
|
||||
Required []string `yaml:"required"`
|
||||
Properties map[string]Property `yaml:"properties"`
|
||||
}
|
||||
|
||||
type MediaType struct {
|
||||
Schema Schema `yaml:"schema"`
|
||||
}
|
||||
|
||||
// 给OpenAPI的post方法用的
|
||||
type RequestBody struct {
|
||||
Required bool `yaml:"required"`
|
||||
Content map[string]MediaType `yaml:"content"`
|
||||
}
|
||||
|
||||
type PathItem struct {
|
||||
Description string `yaml:"description"`
|
||||
Summary string `yaml:"summary"`
|
||||
OperationID string `yaml:"operationId"`
|
||||
RequestBody RequestBody `yaml:"requestBody"`
|
||||
Parameters []Parameter `yaml:"parameters"`
|
||||
Deprecated bool `yaml:"deprecated"`
|
||||
}
|
||||
|
||||
type Paths map[string]map[string]PathItem
|
||||
|
||||
type Components struct {
|
||||
Schemas map[string]interface{} `yaml:"schemas"`
|
||||
}
|
||||
|
||||
type API struct {
|
||||
OpenAPI string `yaml:"openapi"`
|
||||
Info Info `yaml:"info"`
|
||||
Servers []Server `yaml:"servers"`
|
||||
Paths Paths `yaml:"paths"`
|
||||
Components Components `yaml:"components"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
In string `yaml:"in" json:"in"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Value string `yaml:"value" json:"value"`
|
||||
}
|
||||
|
||||
type APIProvider struct {
|
||||
// @Title zh-CN 服务名称
|
||||
// @Description zh-CN 带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local
|
||||
ServiceName string `required:"true" yaml:"serviceName" json:"serviceName"`
|
||||
// @Title zh-CN 服务端口
|
||||
// @Description zh-CN 服务端口
|
||||
ServicePort int64 `required:"true" yaml:"servicePort" json:"servicePort"`
|
||||
// @Title zh-CN 服务域名
|
||||
// @Description zh-CN 服务域名,例如 restapi.amap.com
|
||||
Domin string `required:"true" yaml:"domain" json:"domain"`
|
||||
// @Title zh-CN 通义千问大模型服务的key
|
||||
// @Description zh-CN 通义千问大模型服务的key
|
||||
APIKey APIKey `required:"true" yaml:"apiKey" json:"apiKey"`
|
||||
}
|
||||
|
||||
type APIs struct {
|
||||
APIProvider APIProvider `required:"true" yaml:"apiProvider" json:"apiProvider"`
|
||||
API string `required:"true" yaml:"api" json:"api"`
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
Question string `yaml:"question" json:"question"`
|
||||
Thought1 string `yaml:"thought1" json:"thought1"`
|
||||
ActionInput string `yaml:"actionInput" json:"actionInput"`
|
||||
Observation string `yaml:"observation" json:"observation"`
|
||||
Thought2 string `yaml:"thought2" json:"thought2"`
|
||||
FinalAnswer string `yaml:"finalAnswer" json:"finalAnswer"`
|
||||
Begin string `yaml:"begin" json:"begin"`
|
||||
}
|
||||
|
||||
type PromptTemplate struct {
|
||||
Language string `required:"true" yaml:"language" json:"language"`
|
||||
CHTemplate Template `yaml:"chTemplate" json:"chTemplate"`
|
||||
ENTemplate Template `yaml:"enTemplate" json:"enTemplate"`
|
||||
}
|
||||
|
||||
type LLMInfo struct {
|
||||
// @Title zh-CN 大模型服务名称
|
||||
// @Description zh-CN 带服务类型的完整 FQDN 名称
|
||||
ServiceName string `required:"true" yaml:"serviceName" json:"serviceName"`
|
||||
// @Title zh-CN 大模型服务端口
|
||||
// @Description zh-CN 服务端口
|
||||
ServicePort int64 `required:"true" yaml:"servicePort" json:"servicePort"`
|
||||
// @Title zh-CN 大模型服务域名
|
||||
// @Description zh-CN 大模型服务域名,例如 dashscope.aliyuncs.com
|
||||
Domin string `required:"true" yaml:"domin" json:"domin"`
|
||||
// @Title zh-CN 大模型服务的key
|
||||
// @Description zh-CN 大模型服务的key
|
||||
APIKey string `required:"true" yaml:"apiKey" json:"apiKey"`
|
||||
// @Title zh-CN 大模型服务的请求路径
|
||||
// @Description zh-CN 大模型服务的请求路径,如"/compatible-mode/v1/chat/completions"
|
||||
Path string `required:"true" yaml:"path" json:"path"`
|
||||
// @Title zh-CN 大模型服务的模型名称
|
||||
// @Description zh-CN 大模型服务的模型名称,如"qwen-max-0403"
|
||||
Model string `required:"true" yaml:"model" json:"model"`
|
||||
// @Title zh-CN 结束执行循环前的最大步数
|
||||
// @Description zh-CN 结束执行循环前的最大步数,比如2,设置为0,可能会无限循环,直到超时退出,默认15
|
||||
MaxIterations int64 `yaml:"maxIterations" json:"maxIterations"`
|
||||
// @Title zh-CN 每一次请求大模型的超时时间
|
||||
// @Description zh-CN 每一次请求大模型的超时时间,单位毫秒,默认50000
|
||||
MaxExecutionTime int64 `yaml:"maxExecutionTime" json:"maxExecutionTime"`
|
||||
// @Title zh-CN
|
||||
// @Description zh-CN 每一次请求大模型的输出token限制,默认1000
|
||||
MaxTokens int64 `yaml:"maxToken" json:"maxTokens"`
|
||||
}
|
||||
|
||||
type PluginConfig struct {
|
||||
// @Title zh-CN 返回 HTTP 响应的模版
|
||||
// @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
|
||||
ReturnResponseTemplate string `required:"true" yaml:"returnResponseTemplate" json:"returnResponseTemplate"`
|
||||
// @Title zh-CN 工具服务商以及工具信息
|
||||
// @Description zh-CN 用于存储工具服务商以及工具信息
|
||||
APIs []APIs `required:"true" yaml:"apis" json:"apis"`
|
||||
APIClient []wrapper.HttpClient `yaml:"-" json:"-"`
|
||||
// @Title zh-CN llm信息
|
||||
// @Description zh-CN 用于存储llm使用信息
|
||||
LLMInfo LLMInfo `required:"true" yaml:"llm" json:"llm"`
|
||||
LLMClient wrapper.HttpClient `yaml:"-" json:"-"`
|
||||
APIParam []APIParam `yaml:"-" json:"-"`
|
||||
PromptTemplate PromptTemplate `yaml:"promptTemplate" json:"promptTemplate"`
|
||||
}
|
||||
|
||||
func initResponsePromptTpl(gjson gjson.Result, c *PluginConfig) {
|
||||
//设置回复模板
|
||||
c.ReturnResponseTemplate = gjson.Get("returnResponseTemplate").String()
|
||||
if c.ReturnResponseTemplate == "" {
|
||||
c.ReturnResponseTemplate = `{"id":"error","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}}`
|
||||
}
|
||||
}
|
||||
|
||||
func initAPIs(gjson gjson.Result, c *PluginConfig) error {
|
||||
//从插件配置中获取apis信息
|
||||
apis := gjson.Get("apis")
|
||||
if !apis.Exists() {
|
||||
return errors.New("apis is required")
|
||||
}
|
||||
if len(apis.Array()) == 0 {
|
||||
return errors.New("apis cannot be empty")
|
||||
}
|
||||
|
||||
for _, item := range apis.Array() {
|
||||
serviceName := item.Get("apiProvider.serviceName")
|
||||
if !serviceName.Exists() || serviceName.String() == "" {
|
||||
return errors.New("apiProvider serviceName is required")
|
||||
}
|
||||
|
||||
servicePort := item.Get("apiProvider.servicePort")
|
||||
if !servicePort.Exists() || servicePort.Int() == 0 {
|
||||
return errors.New("apiProvider servicePort is required")
|
||||
}
|
||||
|
||||
domain := item.Get("apiProvider.domain")
|
||||
if !domain.Exists() || domain.String() == "" {
|
||||
return errors.New("apiProvider domain is required")
|
||||
}
|
||||
|
||||
apiKeyIn := item.Get("apiProvider.apiKey.in").String()
|
||||
if apiKeyIn != "query" {
|
||||
apiKeyIn = "header"
|
||||
}
|
||||
|
||||
apiKeyName := item.Get("apiProvider.apiKey.name")
|
||||
|
||||
apiKeyValue := item.Get("apiProvider.apiKey.value")
|
||||
|
||||
//根据多个toolsClientInfo的信息,分别初始化toolsClient
|
||||
apiClient := wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName.String(),
|
||||
Port: servicePort.Int(),
|
||||
Host: domain.String(),
|
||||
})
|
||||
|
||||
c.APIClient = append(c.APIClient, apiClient)
|
||||
|
||||
api := item.Get("api")
|
||||
if !api.Exists() || api.String() == "" {
|
||||
return errors.New("api is required")
|
||||
}
|
||||
|
||||
var apiStruct API
|
||||
err := yaml.Unmarshal([]byte(api.String()), &apiStruct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var allTool_param []Tool_Param
|
||||
//拆除服务下面的每个api的path
|
||||
for path, pathmap := range apiStruct.Paths {
|
||||
//拆解出每个api对应的参数
|
||||
for method, submap := range pathmap {
|
||||
//把参数列表存起来
|
||||
var param Tool_Param
|
||||
param.Path = path
|
||||
param.ToolName = submap.OperationID
|
||||
if method == "get" {
|
||||
param.Method = "GET"
|
||||
paramName := make([]string, 0)
|
||||
for _, parammeter := range submap.Parameters {
|
||||
paramName = append(paramName, parammeter.Name)
|
||||
}
|
||||
param.ParamName = paramName
|
||||
out, _ := json.Marshal(submap.Parameters)
|
||||
param.Parameter = string(out)
|
||||
param.Description = submap.Description
|
||||
} else if method == "post" {
|
||||
param.Method = "POST"
|
||||
schema := submap.RequestBody.Content["application/json"].Schema
|
||||
param.ParamName = schema.Required
|
||||
param.Description = submap.Summary
|
||||
out, _ := json.Marshal(schema.Properties)
|
||||
param.Parameter = string(out)
|
||||
}
|
||||
allTool_param = append(allTool_param, param)
|
||||
}
|
||||
}
|
||||
apiParam := APIParam{
|
||||
APIKey: APIKey{In: apiKeyIn, Name: apiKeyName.String(), Value: apiKeyValue.String()},
|
||||
URL: apiStruct.Servers[0].URL,
|
||||
Tool_Param: allTool_param,
|
||||
}
|
||||
|
||||
c.APIParam = append(c.APIParam, apiParam)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initReActPromptTpl(gjson gjson.Result, c *PluginConfig) {
|
||||
c.PromptTemplate.Language = gjson.Get("promptTemplate.language").String()
|
||||
if c.PromptTemplate.Language != "EN" && c.PromptTemplate.Language != "CH" {
|
||||
c.PromptTemplate.Language = "EN"
|
||||
}
|
||||
if c.PromptTemplate.Language == "EN" {
|
||||
c.PromptTemplate.ENTemplate.Question = gjson.Get("promptTemplate.enTemplate.question").String()
|
||||
if c.PromptTemplate.ENTemplate.Question == "" {
|
||||
c.PromptTemplate.ENTemplate.Question = "the input question you must answer"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.Thought1 = gjson.Get("promptTemplate.enTemplate.thought1").String()
|
||||
if c.PromptTemplate.ENTemplate.Thought1 == "" {
|
||||
c.PromptTemplate.ENTemplate.Thought1 = "you should always think about what to do"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.ActionInput = gjson.Get("promptTemplate.enTemplate.actionInput").String()
|
||||
if c.PromptTemplate.ENTemplate.ActionInput == "" {
|
||||
c.PromptTemplate.ENTemplate.ActionInput = "the input to the action"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.Observation = gjson.Get("promptTemplate.enTemplate.observation").String()
|
||||
if c.PromptTemplate.ENTemplate.Observation == "" {
|
||||
c.PromptTemplate.ENTemplate.Observation = "the result of the action"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.Thought1 = gjson.Get("promptTemplate.enTemplate.thought2").String()
|
||||
if c.PromptTemplate.ENTemplate.Thought1 == "" {
|
||||
c.PromptTemplate.ENTemplate.Thought1 = "I now know the final answer"
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.FinalAnswer = gjson.Get("promptTemplate.enTemplate.finalAnswer").String()
|
||||
if c.PromptTemplate.ENTemplate.FinalAnswer == "" {
|
||||
c.PromptTemplate.ENTemplate.FinalAnswer = "the final answer to the original input question, please give the most direct answer directly in Chinese, not English, and do not add extra content."
|
||||
}
|
||||
c.PromptTemplate.ENTemplate.Begin = gjson.Get("promptTemplate.enTemplate.begin").String()
|
||||
if c.PromptTemplate.ENTemplate.Begin == "" {
|
||||
c.PromptTemplate.ENTemplate.Begin = "Begin! Remember to speak as a pirate when giving your final answer. Use lots of \"Arg\"s"
|
||||
}
|
||||
} else if c.PromptTemplate.Language == "CH" {
|
||||
c.PromptTemplate.CHTemplate.Question = gjson.Get("promptTemplate.chTemplate.question").String()
|
||||
if c.PromptTemplate.CHTemplate.Question == "" {
|
||||
c.PromptTemplate.CHTemplate.Question = "你需要回答的输入问题"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Thought1 = gjson.Get("promptTemplate.chTemplate.thought1").String()
|
||||
if c.PromptTemplate.CHTemplate.Thought1 == "" {
|
||||
c.PromptTemplate.CHTemplate.Thought1 = "你应该总是思考该做什么"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.ActionInput = gjson.Get("promptTemplate.chTemplate.actionInput").String()
|
||||
if c.PromptTemplate.CHTemplate.ActionInput == "" {
|
||||
c.PromptTemplate.CHTemplate.ActionInput = "行动的输入,必须出现在Action后"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Observation = gjson.Get("promptTemplate.chTemplate.observation").String()
|
||||
if c.PromptTemplate.CHTemplate.Observation == "" {
|
||||
c.PromptTemplate.CHTemplate.Observation = "行动的结果"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Thought1 = gjson.Get("promptTemplate.chTemplate.thought2").String()
|
||||
if c.PromptTemplate.CHTemplate.Thought1 == "" {
|
||||
c.PromptTemplate.CHTemplate.Thought1 = "我现在知道最终答案"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.FinalAnswer = gjson.Get("promptTemplate.chTemplate.finalAnswer").String()
|
||||
if c.PromptTemplate.CHTemplate.FinalAnswer == "" {
|
||||
c.PromptTemplate.CHTemplate.FinalAnswer = "对原始输入问题的最终答案"
|
||||
}
|
||||
c.PromptTemplate.CHTemplate.Begin = gjson.Get("promptTemplate.chTemplate.begin").String()
|
||||
if c.PromptTemplate.CHTemplate.Begin == "" {
|
||||
c.PromptTemplate.CHTemplate.Begin = "再次重申,不要修改以上模板的字段名称,开始吧!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initLLMClient(gjson gjson.Result, c *PluginConfig) {
|
||||
c.LLMInfo.APIKey = gjson.Get("llm.apiKey").String()
|
||||
c.LLMInfo.ServiceName = gjson.Get("llm.serviceName").String()
|
||||
c.LLMInfo.ServicePort = gjson.Get("llm.servicePort").Int()
|
||||
c.LLMInfo.Domin = gjson.Get("llm.domain").String()
|
||||
c.LLMInfo.Path = gjson.Get("llm.path").String()
|
||||
c.LLMInfo.Model = gjson.Get("llm.model").String()
|
||||
c.LLMInfo.MaxIterations = gjson.Get("llm.maxIterations").Int()
|
||||
if c.LLMInfo.MaxIterations == 0 {
|
||||
c.LLMInfo.MaxIterations = 15
|
||||
}
|
||||
c.LLMInfo.MaxExecutionTime = gjson.Get("llm.maxExecutionTime").Int()
|
||||
if c.LLMInfo.MaxExecutionTime == 0 {
|
||||
c.LLMInfo.MaxExecutionTime = 50000
|
||||
}
|
||||
c.LLMInfo.MaxTokens = gjson.Get("llm.maxTokens").Int()
|
||||
if c.LLMInfo.MaxTokens == 0 {
|
||||
c.LLMInfo.MaxTokens = 1000
|
||||
}
|
||||
|
||||
c.LLMClient = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.LLMInfo.ServiceName,
|
||||
Port: c.LLMInfo.ServicePort,
|
||||
Host: c.LLMInfo.Domin,
|
||||
})
|
||||
}
|
||||
46
plugins/wasm-go/extensions/ai-agent/dashscope/message.go
Normal file
46
plugins/wasm-go/extensions/ai-agent/dashscope/message.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package dashscope
|
||||
|
||||
var MessageStore ChatMessages
|
||||
|
||||
func init() {
|
||||
MessageStore = make(ChatMessages, 0)
|
||||
MessageStore.Clear() //清理和初始化
|
||||
|
||||
}
|
||||
|
||||
type ChatMessages []Message
|
||||
|
||||
// 枚举出角色
|
||||
const (
|
||||
RoleUser = "user"
|
||||
RoleAssistant = "assistant"
|
||||
RoleSystem = "system"
|
||||
)
|
||||
|
||||
func (cm *ChatMessages) Clear() {
|
||||
*cm = make([]Message, 0) //重新初始化
|
||||
}
|
||||
|
||||
// 添加角色和对应的prompt
|
||||
func (cm *ChatMessages) AddFor(msg string, role string) {
|
||||
*cm = append(*cm, Message{
|
||||
Role: role,
|
||||
Content: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// 添加Assistant角色的prompt
|
||||
func (cm *ChatMessages) AddForAssistant(msg string) {
|
||||
cm.AddFor(msg, RoleAssistant)
|
||||
|
||||
}
|
||||
|
||||
// 添加System角色的prompt
|
||||
func (cm *ChatMessages) AddForSystem(msg string) {
|
||||
cm.AddFor(msg, RoleSystem)
|
||||
}
|
||||
|
||||
// 添加User角色的prompt
|
||||
func (cm *ChatMessages) AddForUser(msg string) {
|
||||
cm.AddFor(msg, RoleUser)
|
||||
}
|
||||
70
plugins/wasm-go/extensions/ai-agent/dashscope/types.go
Normal file
70
plugins/wasm-go/extensions/ai-agent/dashscope/types.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package dashscope
|
||||
|
||||
// DashScope embedding service: Request
|
||||
type Request struct {
|
||||
Model string `json:"model"`
|
||||
Input Input `json:"input"`
|
||||
Parameter Parameter `json:"parameters"`
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Texts []string `json:"texts"`
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
TextType string `json:"text_type"`
|
||||
}
|
||||
|
||||
// DashScope embedding service: Response
|
||||
type Response struct {
|
||||
Output Output `json:"output"`
|
||||
Usage Usage `json:"usage"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Embeddings []Embedding `json:"embeddings"`
|
||||
}
|
||||
|
||||
type Embedding struct {
|
||||
Embedding []float32 `json:"embedding"`
|
||||
TextIndex int32 `json:"text_index"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
TotalTokens int32 `json:"total_tokens"`
|
||||
}
|
||||
|
||||
// completion
|
||||
type Completion struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
MaxTokens int64 `json:"max_tokens"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type CompletionResponse struct {
|
||||
Choices []Choice `json:"choices"`
|
||||
Object string `json:"object"`
|
||||
Usage CompletionUsage `json:"usage"`
|
||||
Created string `json:"created"`
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
Model string `json:"model"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Message Message `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
type CompletionUsage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
19
plugins/wasm-go/extensions/ai-agent/go.mod
Normal file
19
plugins/wasm-go/extensions/ai-agent/go.mod
Normal file
@@ -0,0 +1,19 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
)
|
||||
26
plugins/wasm-go/extensions/ai-agent/go.sum
Normal file
26
plugins/wasm-go/extensions/ai-agent/go.sum
Normal file
@@ -0,0 +1,26 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2 h1:gH7OIGXm4wtW5Vo7L2deMPqF7OVWNESDHv1CaaTGu6s=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2/go.mod h1:359don/ahMxpfeLMzr29Cjwcu8IywTTDUzWlBPRNLHw=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
372
plugins/wasm-go/extensions/ai-agent/main.go
Normal file
372
plugins/wasm-go/extensions/ai-agent/main.go
Normal file
@@ -0,0 +1,372 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent/dashscope"
|
||||
prompttpl "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent/promptTpl"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// 用于统计函数的递归调用次数
|
||||
const ToolCallsCount = "ToolCallsCount"
|
||||
|
||||
// react的正则规则
|
||||
const ActionPattern = `Action:\s*(.*?)[.\n]`
|
||||
const ActionInputPattern = `Action Input:\s*(.*)`
|
||||
const FinalAnswerPattern = `Final Answer:(.*)`
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"ai-agent",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
|
||||
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(gjson gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
initResponsePromptTpl(gjson, c)
|
||||
|
||||
err := initAPIs(gjson, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
initReActPromptTpl(gjson, c)
|
||||
|
||||
initLLMClient(gjson, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func firstReq(config PluginConfig, prompt string, rawRequest Request, log wrapper.Log) types.Action {
|
||||
log.Debugf("[onHttpRequestBody] firstreq:%s", prompt)
|
||||
|
||||
var userMessage Message
|
||||
userMessage.Role = "user"
|
||||
userMessage.Content = prompt
|
||||
|
||||
newMessages := []Message{userMessage}
|
||||
rawRequest.Messages = newMessages
|
||||
|
||||
//replace old message and resume request qwen
|
||||
newbody, err := json.Marshal(rawRequest)
|
||||
if err != nil {
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
log.Debugf("[onHttpRequestBody] newRequestBody: ", string(newbody))
|
||||
err := proxywasm.ReplaceHttpRequestBody(newbody)
|
||||
if err != nil {
|
||||
log.Debug("替换失败")
|
||||
proxywasm.SendHttpResponse(200, [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, "替换失败"+err.Error())), -1)
|
||||
}
|
||||
log.Debug("[onHttpRequestBody] request替换成功")
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
log.Debug("onHttpRequestBody start")
|
||||
defer log.Debug("onHttpRequestBody end")
|
||||
|
||||
//拿到请求
|
||||
var rawRequest Request
|
||||
err := json.Unmarshal(body, &rawRequest)
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpRequestBody] body json umarshal err: ", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Debugf("onHttpRequestBody rawRequest: %v", rawRequest)
|
||||
|
||||
//获取用户query
|
||||
var query string
|
||||
messageLength := len(rawRequest.Messages)
|
||||
log.Debugf("[onHttpRequestBody] messageLength: %s\n", messageLength)
|
||||
if messageLength > 0 {
|
||||
query = rawRequest.Messages[messageLength-1].Content
|
||||
log.Debugf("[onHttpRequestBody] query: %s\n", query)
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
if query == "" {
|
||||
log.Debug("parse query from request body failed")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
//拼装agent prompt模板
|
||||
tool_desc := make([]string, 0)
|
||||
tool_names := make([]string, 0)
|
||||
for _, apiParam := range config.APIParam {
|
||||
for _, tool_param := range apiParam.Tool_Param {
|
||||
tool_desc = append(tool_desc, fmt.Sprintf(prompttpl.TOOL_DESC, tool_param.ToolName, tool_param.Description, tool_param.Description, tool_param.Description, tool_param.Parameter), "\n")
|
||||
tool_names = append(tool_names, tool_param.ToolName)
|
||||
}
|
||||
}
|
||||
|
||||
var prompt string
|
||||
if config.PromptTemplate.Language == "CH" {
|
||||
prompt = fmt.Sprintf(prompttpl.CH_Template,
|
||||
tool_desc,
|
||||
config.PromptTemplate.CHTemplate.Question,
|
||||
config.PromptTemplate.CHTemplate.Thought1,
|
||||
tool_names,
|
||||
config.PromptTemplate.CHTemplate.ActionInput,
|
||||
config.PromptTemplate.CHTemplate.Observation,
|
||||
config.PromptTemplate.CHTemplate.Thought2,
|
||||
config.PromptTemplate.CHTemplate.FinalAnswer,
|
||||
config.PromptTemplate.CHTemplate.Begin,
|
||||
query)
|
||||
} else {
|
||||
prompt = fmt.Sprintf(prompttpl.EN_Template,
|
||||
tool_desc,
|
||||
config.PromptTemplate.ENTemplate.Question,
|
||||
config.PromptTemplate.ENTemplate.Thought1,
|
||||
tool_names,
|
||||
config.PromptTemplate.ENTemplate.ActionInput,
|
||||
config.PromptTemplate.ENTemplate.Observation,
|
||||
config.PromptTemplate.ENTemplate.Thought2,
|
||||
config.PromptTemplate.ENTemplate.FinalAnswer,
|
||||
config.PromptTemplate.ENTemplate.Begin,
|
||||
query)
|
||||
}
|
||||
|
||||
ctx.SetContext(ToolCallsCount, 0)
|
||||
|
||||
//清理历史对话记录
|
||||
dashscope.MessageStore.Clear()
|
||||
|
||||
//将请求加入到历史对话存储器中
|
||||
dashscope.MessageStore.AddForUser(prompt)
|
||||
|
||||
//开始第一次请求
|
||||
ret := firstReq(config, prompt, rawRequest, log)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
log.Debug("onHttpResponseHeaders start")
|
||||
defer log.Debug("onHttpResponseHeaders end")
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func toolsCallResult(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log, statusCode int, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
log.Debugf("statusCode: %d\n", statusCode)
|
||||
}
|
||||
log.Info("========函数返回结果========")
|
||||
log.Infof(string(responseBody))
|
||||
|
||||
observation := "Observation: " + string(responseBody)
|
||||
|
||||
dashscope.MessageStore.AddForUser(observation)
|
||||
|
||||
completion := dashscope.Completion{
|
||||
Model: config.LLMInfo.Model,
|
||||
Messages: dashscope.MessageStore,
|
||||
MaxTokens: config.LLMInfo.MaxTokens,
|
||||
}
|
||||
|
||||
headers := [][2]string{{"Content-Type", "application/json"}, {"Authorization", "Bearer " + config.LLMInfo.APIKey}}
|
||||
completionSerialized, _ := json.Marshal(completion)
|
||||
err := config.LLMClient.Post(
|
||||
config.LLMInfo.Path,
|
||||
headers,
|
||||
completionSerialized,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
//得到gpt的返回结果
|
||||
var responseCompletion dashscope.CompletionResponse
|
||||
_ = json.Unmarshal(responseBody, &responseCompletion)
|
||||
log.Infof("[toolsCall] content: %s\n", responseCompletion.Choices[0].Message.Content)
|
||||
|
||||
if responseCompletion.Choices[0].Message.Content != "" {
|
||||
retType := toolsCall(ctx, config, responseCompletion.Choices[0].Message.Content, rawResponse, log)
|
||||
if retType == types.ActionContinue {
|
||||
//得到了Final Answer
|
||||
var assistantMessage Message
|
||||
assistantMessage.Role = "assistant"
|
||||
startIndex := strings.Index(responseCompletion.Choices[0].Message.Content, "Final Answer:")
|
||||
if startIndex != -1 {
|
||||
startIndex += len("Final Answer:") // 移动到"Final Answer:"之后的位置
|
||||
extractedText := responseCompletion.Choices[0].Message.Content[startIndex:]
|
||||
assistantMessage.Content = extractedText
|
||||
}
|
||||
|
||||
rawResponse.Choices[0].Message = assistantMessage
|
||||
|
||||
newbody, err := json.Marshal(rawResponse)
|
||||
if err != nil {
|
||||
proxywasm.ResumeHttpResponse()
|
||||
return
|
||||
} else {
|
||||
log.Infof("[onHttpResponseBody] newResponseBody: ", string(newbody))
|
||||
proxywasm.ReplaceHttpResponseBody(newbody)
|
||||
|
||||
log.Debug("[onHttpResponseBody] response替换成功")
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}, uint32(config.LLMInfo.MaxExecutionTime))
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpRequestBody] completion err: %s", err.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}
|
||||
|
||||
func toolsCall(ctx wrapper.HttpContext, config PluginConfig, content string, rawResponse Response, log wrapper.Log) types.Action {
|
||||
dashscope.MessageStore.AddForAssistant(content)
|
||||
|
||||
//得到最终答案
|
||||
regexPattern := regexp.MustCompile(FinalAnswerPattern)
|
||||
finalAnswer := regexPattern.FindStringSubmatch(content)
|
||||
if len(finalAnswer) > 1 {
|
||||
return types.ActionContinue
|
||||
}
|
||||
count := ctx.GetContext(ToolCallsCount).(int)
|
||||
count++
|
||||
log.Debugf("toolCallsCount:%d, config.LLMInfo.MaxIterations=%d\n", count, config.LLMInfo.MaxIterations)
|
||||
//函数递归调用次数,达到了预设的循环次数,强制结束
|
||||
if int64(count) > config.LLMInfo.MaxIterations {
|
||||
ctx.SetContext(ToolCallsCount, 0)
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
ctx.SetContext(ToolCallsCount, count)
|
||||
}
|
||||
|
||||
//没得到最终答案
|
||||
regexAction := regexp.MustCompile(ActionPattern)
|
||||
regexActionInput := regexp.MustCompile(ActionInputPattern)
|
||||
|
||||
action := regexAction.FindStringSubmatch(content)
|
||||
actionInput := regexActionInput.FindStringSubmatch(content)
|
||||
|
||||
if len(action) > 1 && len(actionInput) > 1 {
|
||||
var url string
|
||||
var headers [][2]string
|
||||
var apiClient wrapper.HttpClient
|
||||
var method string
|
||||
var reqBody []byte
|
||||
var key string
|
||||
|
||||
for i, apiParam := range config.APIParam {
|
||||
for _, tool_param := range apiParam.Tool_Param {
|
||||
if action[1] == tool_param.ToolName {
|
||||
log.Infof("calls %s\n", tool_param.ToolName)
|
||||
log.Infof("actionInput[1]: %s", actionInput[1])
|
||||
|
||||
//将大模型需要的参数反序列化
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(actionInput[1]), &data); err != nil {
|
||||
log.Debugf("Error: %s\n", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
method = tool_param.Method
|
||||
|
||||
//key or header组装
|
||||
if apiParam.APIKey.Name != "" {
|
||||
if apiParam.APIKey.In == "query" { //query类型的key要放到url中
|
||||
headers = nil
|
||||
key = "?" + apiParam.APIKey.Name + "=" + apiParam.APIKey.Value
|
||||
} else if apiParam.APIKey.In == "header" { //header类型的key放在header中
|
||||
headers = [][2]string{{"Content-Type", "application/json"}, {"Authorization", apiParam.APIKey.Name + " " + apiParam.APIKey.Value}}
|
||||
}
|
||||
}
|
||||
|
||||
if method == "GET" {
|
||||
//query组装
|
||||
var args string
|
||||
for i, param := range tool_param.ParamName { //从参数列表中取出参数
|
||||
if i == 0 && apiParam.APIKey.In != "query" {
|
||||
args = "?" + param + "=%s"
|
||||
args = fmt.Sprintf(args, data[param])
|
||||
} else {
|
||||
args = args + "&" + param + "=%s"
|
||||
args = fmt.Sprintf(args, data[param])
|
||||
}
|
||||
}
|
||||
|
||||
//url组装
|
||||
url = apiParam.URL + tool_param.Path + key + args
|
||||
} else if method == "POST" {
|
||||
reqBody = nil
|
||||
//json参数组装
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Debugf("Error: %s\n", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
reqBody = jsonData
|
||||
|
||||
//url组装
|
||||
url = apiParam.URL + tool_param.Path + key
|
||||
}
|
||||
|
||||
log.Infof("url: %s\n", url)
|
||||
|
||||
apiClient = config.APIClient[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if apiClient != nil {
|
||||
err := apiClient.Call(
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
reqBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
toolsCallResult(ctx, config, content, rawResponse, log, statusCode, responseBody)
|
||||
}, 50000)
|
||||
if err != nil {
|
||||
log.Debugf("tool calls error: %s\n", err.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
// 从response接收到firstreq的大模型返回
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
log.Debugf("onHttpResponseBody start")
|
||||
defer log.Debugf("onHttpResponseBody end")
|
||||
|
||||
//初始化接收gpt返回内容的结构体
|
||||
var rawResponse Response
|
||||
err := json.Unmarshal(body, &rawResponse)
|
||||
if err != nil {
|
||||
log.Debugf("[onHttpResponseBody] body to json err: %s", err.Error())
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Infof("first content: %s\n", rawResponse.Choices[0].Message.Content)
|
||||
//如果gpt返回的内容不是空的
|
||||
if rawResponse.Choices[0].Message.Content != "" {
|
||||
//进入agent的循环思考,工具调用的过程中
|
||||
return toolsCall(ctx, config, rawResponse.Choices[0].Message.Content, rawResponse, log)
|
||||
} else {
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
93
plugins/wasm-go/extensions/ai-agent/promptTpl/prompt.go
Normal file
93
plugins/wasm-go/extensions/ai-agent/promptTpl/prompt.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package prompttpl
|
||||
|
||||
// input param
|
||||
// {name_for_model}
|
||||
// {description_for_model}
|
||||
// {description_for_model}
|
||||
// {description_for_model}
|
||||
// {parameters}
|
||||
const TOOL_DESC = `
|
||||
%s: Call this tool to interact with the %s API. What is the %s API useful for? %s
|
||||
Parameters:
|
||||
%s
|
||||
Format the arguments as a JSON object.`
|
||||
|
||||
/*
|
||||
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
|
||||
|
||||
%s
|
||||
|
||||
Use the following format:
|
||||
|
||||
Question: the input question you must answer
|
||||
Thought: you should always think about what to do
|
||||
Action: the action to take, should be one of %s
|
||||
Action Input: the input to the action
|
||||
Observation: the result of the action
|
||||
... (this Thought/Action/Action Input/Observation can repeat N times)
|
||||
Thought: I now know the final answer
|
||||
Final Answer: the final answer to the original input question, please give the most direct answer directly in Chinese, not English, and do not add extra content.
|
||||
|
||||
Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Arg"s
|
||||
|
||||
Question: %s
|
||||
*/
|
||||
const EN_Template = `
|
||||
Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:
|
||||
|
||||
%s
|
||||
|
||||
Use the following format:
|
||||
|
||||
Question: %s
|
||||
Thought: %s
|
||||
Action: the action to take, should be one of %s
|
||||
Action Input: %s
|
||||
Observation: %s
|
||||
... (this Thought/Action/Action Input/Observation can repeat N times)
|
||||
Thought: %s
|
||||
Final Answer: %s
|
||||
|
||||
%s
|
||||
|
||||
Question: %s
|
||||
`
|
||||
|
||||
/*
|
||||
尽你所能回答以下问题。你可以使用以下工具:
|
||||
|
||||
%s
|
||||
|
||||
请使用以下格式,其中Action字段后必须跟着Action Input字段,并且不要将Action Input替换成Input或者tool等字段,不能出现格式以外的字段名,每个字段在每个轮次只出现一次:
|
||||
Question: 你需要回答的输入问题
|
||||
Thought: 你应该总是思考该做什么
|
||||
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
|
||||
Action Input: 行动的输入,必须出现在Action后。
|
||||
Observation: 行动的结果
|
||||
...(这个Thought/Action/Action Input/Observation可以重复N次)
|
||||
Thought: 我现在知道最终答案
|
||||
Final Answer: 对原始输入问题的最终答案
|
||||
|
||||
再次重申,不要修改以上模板的字段名称,开始吧!
|
||||
|
||||
Question: %s
|
||||
*/
|
||||
const CH_Template = `
|
||||
尽你所能回答以下问题。你可以使用以下工具:
|
||||
|
||||
%s
|
||||
|
||||
请使用以下格式,其中Action字段后必须跟着Action Input字段,并且不要将Action Input替换成Input或者tool等字段,不能出现格式以外的字段名,每个字段在每个轮次只出现一次:
|
||||
Question: %s
|
||||
Thought: %s
|
||||
Action: 要采取的动作,动作只能是%s中的一个 ,一定不要加入其它内容
|
||||
Action Input: %s
|
||||
Observation: %s
|
||||
...(这个Thought/Action/Action Input/Observation可以重复N次)
|
||||
Thought: %s
|
||||
Final Answer: %s
|
||||
|
||||
%s
|
||||
|
||||
Question: %s
|
||||
`
|
||||
@@ -66,4 +66,70 @@ curl http://localhost/test \
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
# 基于geo-ip插件的能力,扩展AI提示词装饰器插件携带用户地理位置信息
|
||||
如果需要在LLM的请求前后加入用户地理位置信息,请确保同时开启geo-ip插件和AI提示词装饰器插件。并且在相同的请求处理阶段里,geo-ip插件的优先级必须高于AI提示词装饰器插件。首先geo-ip插件会根据用户ip计算出用户的地理位置信息,然后通过请求属性传递给后续插件。比如在默认阶段里,geo-ip插件的priority配置1000,ai-prompt-decorator插件的priority配置500。
|
||||
|
||||
geo-ip插件配置示例:
|
||||
```yaml
|
||||
ipProtocal: "ipv4"
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
AI提示词装饰器插件的配置示例如下:
|
||||
```yaml
|
||||
prepend:
|
||||
- role: system
|
||||
content: "提问用户当前的地理位置信息是,国家:${geo-country},省份:${geo-province}, 城市:${geo-city}"
|
||||
append:
|
||||
- role: user
|
||||
content: "每次回答完问题,尝试进行反问"
|
||||
```
|
||||
|
||||
使用以上配置发起请求:
|
||||
|
||||
```bash
|
||||
curl http://localhost/test \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-forwarded-for: 87.254.207.100,4.5.6.7" \
|
||||
-d '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "今天天气怎么样?"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
经过插件处理后,实际请求为:
|
||||
|
||||
```bash
|
||||
curl http://localhost/test \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-forwarded-for: 87.254.207.100,4.5.6.7" \
|
||||
-d '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "提问用户当前的地理位置信息是,国家:中国,省份:北京, 城市:北京"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "今天天气怎么样?"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "每次回答完问题,尝试进行反问"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
@@ -38,10 +40,42 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIPromptDecoratorConfi
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func replaceVariable(variable string, entry *Message) (*Message, error) {
|
||||
key := fmt.Sprintf("${%s}", variable)
|
||||
if strings.Contains(entry.Content, key) {
|
||||
value, err := proxywasm.GetProperty([]string{variable})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.Content = strings.ReplaceAll(entry.Content, key, string(value))
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func decorateGeographicPrompt(entry *Message) (*Message, error) {
|
||||
geoArr := []string{"geo-country", "geo-province", "geo-city", "geo-isp"}
|
||||
|
||||
var err error
|
||||
for _, geo := range geoArr {
|
||||
entry, err = replaceVariable(geo, entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config AIPromptDecoratorConfig, body []byte, log wrapper.Log) types.Action {
|
||||
messageJson := `{"messages":[]}`
|
||||
|
||||
for _, entry := range config.Prepend {
|
||||
entry, err := decorateGeographicPrompt(&entry)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to decorate geographic prompt in prepend, error: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add prepend message, error: %v", err)
|
||||
@@ -60,6 +94,12 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AIPromptDecoratorConfig,
|
||||
}
|
||||
|
||||
for _, entry := range config.Append {
|
||||
entry, err := decorateGeographicPrompt(&entry)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to decorate geographic prompt in append, error: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add prepend message, error: %v", err)
|
||||
|
||||
@@ -34,6 +34,7 @@ description: AI 代理插件配置参考
|
||||
| `modelMapping` | map of string | 非必填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `protocol` | string | 非必填 | - | 插件对外提供的 API 接口契约。目前支持以下取值:openai(默认值,使用 OpenAI 的接口契约)、original(使用目标服务提供商的原始接口契约) |
|
||||
| `context` | object | 非必填 | - | 配置 AI 对话上下文信息 |
|
||||
| `customSettings` | array of customSetting | 非必填 | - | 为AI请求指定覆盖或者填充参数 |
|
||||
|
||||
`context`的配置字段说明如下:
|
||||
|
||||
@@ -43,6 +44,33 @@ description: AI 代理插件配置参考
|
||||
| `serviceName` | string | 必填 | - | URL 所对应的 Higress 后端服务完整名称 |
|
||||
| `servicePort` | number | 必填 | - | URL 所对应的 Higress 后端服务访问端口 |
|
||||
|
||||
|
||||
`customSettings`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------------- | -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `name` | string | 必填 | - | 想要设置的参数的名称,例如`max_tokens` |
|
||||
| `value` | string/int/float/bool | 必填 | - | 想要设置的参数的值,例如0 |
|
||||
| `mode` | string | 非必填 | "auto" | 参数设置的模式,可以设置为"auto"或者"raw",如果为"auto"则会自动根据协议对参数名做改写,如果为"raw"则不会有任何改写和限制检查 |
|
||||
| `overwrite` | bool | 非必填 | true | 如果为false则只在用户没有设置这个参数时填充参数,否则会直接覆盖用户原有的参数设置 |
|
||||
|
||||
|
||||
custom-setting会遵循如下表格,根据`name`和协议来替换对应的字段,用户需要填写表格中`settingName`列中存在的值。例如用户将`name`设置为`max_tokens`,在openai协议中会替换`max_tokens`,在gemini中会替换`maxOutputTokens`。
|
||||
`none`表示该协议不支持此参数。如果`name`不在此表格中或者对应协议不支持此参数,同时没有设置raw模式,则配置不会生效。
|
||||
|
||||
|
||||
| settingName | openai | baidu | spark | qwen | gemini | hunyuan | claude | minimax |
|
||||
| ----------- | ----------- | ----------------- | ----------- | ----------- | --------------- | ----------- | ----------- | ------------------ |
|
||||
| max_tokens | max_tokens | max_output_tokens | max_tokens | max_tokens | maxOutputTokens | none | max_tokens | tokens_to_generate |
|
||||
| temperature | temperature | temperature | temperature | temperature | temperature | Temperature | temperature | temperature |
|
||||
| top_p | top_p | top_p | none | top_p | topP | TopP | top_p | top_p |
|
||||
| top_k | none | none | top_k | none | topK | none | top_k | none |
|
||||
| seed | seed | none | none | seed | none | none | none | none |
|
||||
|
||||
如果启用了raw模式,custom-setting会直接用输入的`name`和`value`去更改请求中的json内容,而不对参数名称做任何限制和修改。
|
||||
对于大多数协议,custom-setting都会在json内容的根路径修改或者填充参数。对于`qwen`协议,ai-proxy会在json的`parameters`子路径下做配置。对于`gemini`协议,则会在`generation_config`子路径下做配置。
|
||||
|
||||
|
||||
### 提供商特有配置
|
||||
|
||||
#### OpenAI
|
||||
@@ -52,6 +80,7 @@ OpenAI 所对应的 `type` 为 `openai`。它特有的配置字段如下:
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|----------|----------|--------|-------------------------------------------------------------------------------|
|
||||
| `openaiCustomUrl` | string | 非必填 | - | 基于OpenAI协议的自定义后端URL,例如: www.example.com/myai/v1/chat/completions |
|
||||
| `responseJsonSchema` | object | 非必填 | - | 预先定义OpenAI响应需满足的Json Schema, 注意目前仅特定的几种模型支持该用法|
|
||||
|
||||
|
||||
#### Azure OpenAI
|
||||
@@ -105,6 +134,10 @@ Groq 所对应的 `type` 为 `groq`。它并无特有的配置字段。
|
||||
|
||||
文心一言所对应的 `type` 为 `baidu`。它并无特有的配置字段。
|
||||
|
||||
#### 360智脑
|
||||
|
||||
360智脑所对应的 `type` 为 `ai360`。它并无特有的配置字段。
|
||||
|
||||
#### MiniMax
|
||||
|
||||
MiniMax所对应的 `type` 为 `minimax`。它特有的配置字段如下:
|
||||
@@ -165,6 +198,14 @@ Gemini 所对应的 `type` 为 `gemini`。它特有的配置字段如下:
|
||||
| --------------------- | -------- | -------- |-----|-------------------------------------------------------------------------------------------------|
|
||||
| `geminiSafetySetting` | map of string | 非必填 | - | Gemini AI内容过滤和安全级别设定。参考[Safety settings](https://ai.google.dev/gemini-api/docs/safety-settings) |
|
||||
|
||||
#### DeepL
|
||||
|
||||
DeepL 所对应的 `type` 为 `deepl`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ------------ | -------- | -------- | ------ | ---------------------------- |
|
||||
| `targetLang` | string | 必填 | - | DeepL 翻译服务需要的目标语种 |
|
||||
|
||||
## 用法示例
|
||||
|
||||
### 使用 OpenAI 协议代理 Azure OpenAI 服务
|
||||
@@ -281,6 +322,7 @@ provider:
|
||||
'gpt-35-turbo': "qwen-plus"
|
||||
'gpt-4-turbo': "qwen-max"
|
||||
'gpt-4-*': "qwen-max"
|
||||
'gpt-4o': "qwen-vl-plus"
|
||||
'text-embedding-v1': 'text-embedding-v1'
|
||||
'*': "qwen-turbo"
|
||||
```
|
||||
@@ -289,7 +331,111 @@ provider:
|
||||
|
||||
URL: http://your-domain/v1/chat/completions
|
||||
|
||||
请求体:
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-3",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"temperature": 0.3
|
||||
}
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "c2518bd3-0f46-97d1-be34-bb5777cb3108",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "我是通义千问,由阿里云开发的AI助手。我可以回答各种问题、提供信息和与用户进行对话。有什么我可以帮助你的吗?"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1715175072,
|
||||
"model": "qwen-turbo",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 24,
|
||||
"completion_tokens": 33,
|
||||
"total_tokens": 57
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**多模态模型 API 请求示例(适用于 `qwen-vl-plus` 和 `qwen-vl-max` 模型)**
|
||||
|
||||
URL: http://your-domain/v1/chat/completions
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这个图片是哪里?"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"temperature": 0.3
|
||||
}
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "17c5955d-af9c-9f28-bbde-293a9c9a3515",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"text": "这张照片显示的是一位女士和一只狗在海滩上。由于我无法获取具体的地理位置信息,所以不能确定这是哪个地方的海滩。但是从视觉内容来看,它可能是一个位于沿海地区的沙滩海岸线,并且有海浪拍打着岸边。这样的场景在全球许多美丽的海滨地区都可以找到。如果您需要更精确的信息,请提供更多的背景或细节描述。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1723949230,
|
||||
"model": "qwen-vl-plus",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 1279,
|
||||
"completion_tokens": 78
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**文本向量请求示例**
|
||||
|
||||
URL: http://your-domain/v1/embeddings
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -298,7 +444,7 @@ URL: http://your-domain/v1/chat/completions
|
||||
}
|
||||
```
|
||||
|
||||
响应体示例:
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -330,47 +476,6 @@ URL: http://your-domain/v1/chat/completions
|
||||
}
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
URL: http://your-domain/v1/embeddings
|
||||
|
||||
示例请求内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "text-embedding-v1",
|
||||
"input": [
|
||||
"Hello world!"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
示例响应内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "c2518bd3-0f46-97d1-be34-bb5777cb3108",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "我是通义千问,由阿里云开发的AI助手。我可以回答各种问题、提供信息和与用户进行对话。有什么我可以帮助你的吗?"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1715175072,
|
||||
"model": "qwen-turbo",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 24,
|
||||
"completion_tokens": 33,
|
||||
"total_tokens": 57
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用通义千问配合纯文本上下文信息
|
||||
|
||||
使用通义千问服务,同时配置纯文本上下文信息。
|
||||
@@ -839,6 +944,77 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理360智脑服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: ai360
|
||||
apiTokens:
|
||||
- "YOUR_MINIMAX_API_TOKEN"
|
||||
modelMapping:
|
||||
"gpt-4o": "360gpt-turbo-responsibility-8k"
|
||||
"gpt-4": "360gpt2-pro"
|
||||
"gpt-3.5": "360gpt-turbo"
|
||||
"*": "360gpt-pro"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一个专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好,我是360智脑,一个大型语言模型。我可以帮助回答各种问题、提供信息、进行对话等。有什么可以帮助你的吗?"
|
||||
},
|
||||
"finish_reason": "",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"created": 1724257207,
|
||||
"id": "5e5c94a2-d989-40b5-9965-5b971db941fe",
|
||||
"model": "360gpt-turbo",
|
||||
"object": "",
|
||||
"usage": {
|
||||
"completion_tokens": 33,
|
||||
"prompt_tokens": 24,
|
||||
"total_tokens": 57
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一个专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"context": null
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 Cloudflare Workers AI 服务
|
||||
|
||||
**配置信息**
|
||||
@@ -1008,6 +1184,59 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 DeepL 文本翻译服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: deepl
|
||||
apiTokens:
|
||||
- "YOUR_DEEPL_API_TOKEN"
|
||||
targetLang: "ZH"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
此处 `model` 表示 DeepL 的服务类型,只能填 `Free` 或 `Pro`。`content` 中设置需要翻译的文本;在 `role: system` 的 `content` 中可以包含可能影响翻译但本身不会被翻译的上下文,例如翻译产品名称时,可以将产品描述作为上下文传递,这种额外的上下文可能会提高翻译的质量。
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "Free",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "money"
|
||||
},
|
||||
{
|
||||
"content": "sit by the bank"
|
||||
},
|
||||
{
|
||||
"content": "a bank in China"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": { "name": "EN", "role": "assistant", "content": "坐庄" }
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"message": { "name": "EN", "role": "assistant", "content": "中国银行" }
|
||||
}
|
||||
],
|
||||
"created": 1722747752,
|
||||
"model": "Free",
|
||||
"object": "chat.completion",
|
||||
"usage": {}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整配置示例
|
||||
|
||||
### Kubernetes 示例
|
||||
|
||||
@@ -50,3 +50,7 @@ func (c *PluginConfig) Complete() error {
|
||||
func (c *PluginConfig) GetProvider() provider.Provider {
|
||||
return c.provider
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetProviderConfig() provider.ProviderConfig {
|
||||
return c.providerConfig
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@ require (
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -20,6 +21,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -75,15 +75,15 @@ func onHttpRequestHeader(ctx wrapper.HttpContext, pluginConfig config.PluginConf
|
||||
// Disable the route re-calculation since the plugin may modify some headers related to the chosen route.
|
||||
ctx.DisableReroute()
|
||||
|
||||
action, err := handler.OnRequestHeaders(ctx, apiName, log)
|
||||
_, err := handler.OnRequestHeaders(ctx, apiName, log)
|
||||
if err == nil {
|
||||
if contentType, err := proxywasm.GetHttpRequestHeader("Content-Type"); err == nil && contentType != "" {
|
||||
if wrapper.HasRequestBody() {
|
||||
ctx.SetRequestBodyBufferLimit(defaultMaxBodyBytes)
|
||||
// Always return types.HeaderStopIteration to support fallback routing,
|
||||
// as long as onHttpRequestBody can be called.
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
return action
|
||||
return types.ActionContinue
|
||||
}
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_headers_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process request headers: %v", err))
|
||||
return types.ActionContinue
|
||||
@@ -104,12 +104,20 @@ func onHttpRequestBody(ctx wrapper.HttpContext, pluginConfig config.PluginConfig
|
||||
|
||||
if handler, ok := activeProvider.(provider.RequestBodyHandler); ok {
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
|
||||
newBody, settingErr := pluginConfig.GetProviderConfig().ReplaceByCustomSettings(body)
|
||||
if settingErr != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_body_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to rewrite request body by custom settings: %v", settingErr))
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
log.Debugf("[onHttpRequestBody] newBody=%s", newBody)
|
||||
body = newBody
|
||||
action, err := handler.OnRequestBody(ctx, apiName, body, log)
|
||||
if err == nil {
|
||||
return action
|
||||
}
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_body_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process request body: %v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -140,24 +148,18 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, pluginConfig config.PluginCo
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
contentType, err := proxywasm.GetHttpResponseHeader("Content-Type")
|
||||
if err != nil || !strings.HasPrefix(contentType, "text/event-stream") {
|
||||
if err != nil {
|
||||
log.Errorf("unable to load content-type header from response: %v", err)
|
||||
}
|
||||
ctx.BufferResponseBody()
|
||||
}
|
||||
|
||||
if handler, ok := activeProvider.(provider.ResponseHeadersHandler); ok {
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
action, err := handler.OnResponseHeaders(ctx, apiName, log)
|
||||
if err == nil {
|
||||
checkStream(&ctx, &log)
|
||||
return action
|
||||
}
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_resp_headers_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process response headers: %v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
checkStream(&ctx, &log)
|
||||
_, needHandleBody := activeProvider.(provider.ResponseBodyHandler)
|
||||
_, needHandleStreamingBody := activeProvider.(provider.StreamingResponseBodyHandler)
|
||||
if !needHandleBody && !needHandleStreamingBody {
|
||||
@@ -223,3 +225,13 @@ func getOpenAiApiName(path string) provider.ApiName {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func checkStream(ctx *wrapper.HttpContext, log *wrapper.Log) {
|
||||
contentType, err := proxywasm.GetHttpResponseHeader("Content-Type")
|
||||
if err != nil || !strings.HasPrefix(contentType, "text/event-stream") {
|
||||
if err != nil {
|
||||
log.Errorf("unable to load content-type header from response: %v", err)
|
||||
}
|
||||
(*ctx).BufferResponseBody()
|
||||
}
|
||||
}
|
||||
|
||||
74
plugins/wasm-go/extensions/ai-proxy/provider/ai360.go
Normal file
74
plugins/wasm-go/extensions/ai-proxy/provider/ai360.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
// ai360Provider is the provider for 360 OpenAI service.
|
||||
const (
|
||||
ai360Domain = "api.360.cn"
|
||||
)
|
||||
|
||||
type ai360ProviderInitializer struct {
|
||||
}
|
||||
|
||||
type ai360Provider struct {
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (m *ai360ProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ai360ProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
return &ai360Provider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *ai360Provider) GetProviderType() string {
|
||||
return providerTypeAi360
|
||||
}
|
||||
|
||||
func (m *ai360Provider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
_ = util.OverwriteRequestHost(ai360Domain)
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
_ = proxywasm.ReplaceHttpRequestHeader("Authorization", m.config.GetRandomToken())
|
||||
// Delay the header processing to allow changing streaming mode in OnRequestBody
|
||||
return types.HeaderStopIteration, nil
|
||||
}
|
||||
|
||||
func (m *ai360Provider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
request := &chatCompletionRequest{}
|
||||
if err := decodeChatCompletionRequest(body, request); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
if request.Model == "" {
|
||||
return types.ActionContinue, errors.New("missing model in chat completion request")
|
||||
}
|
||||
// 映射模型
|
||||
mappedModel := getMappedModel(request.Model, m.config.modelMapping, log)
|
||||
if mappedModel == "" {
|
||||
return types.ActionContinue, errors.New("model becomes empty after applying the configured mapping")
|
||||
}
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, mappedModel)
|
||||
request.Model = mappedModel
|
||||
return types.ActionContinue, replaceJsonRequestBody(request, log)
|
||||
}
|
||||
@@ -253,7 +253,7 @@ func (b *baiduProvider) baiduTextGenRequest(request *chatCompletionRequest) *bai
|
||||
}
|
||||
for _, message := range request.Messages {
|
||||
if message.Role == roleSystem {
|
||||
baiduRequest.System = message.Content
|
||||
baiduRequest.System = message.StringContent()
|
||||
} else {
|
||||
baiduRequest.Messages = append(baiduRequest.Messages, chatMessage{
|
||||
Role: message.Role,
|
||||
|
||||
@@ -274,7 +274,7 @@ func (c *claudeProvider) buildClaudeTextGenRequest(origRequest *chatCompletionRe
|
||||
|
||||
for _, message := range origRequest.Messages {
|
||||
if message.Role == roleSystem {
|
||||
claudeRequest.System = message.Content
|
||||
claudeRequest.System = message.StringContent()
|
||||
continue
|
||||
}
|
||||
claudeMessage := chatMessage{
|
||||
|
||||
137
plugins/wasm-go/extensions/ai-proxy/provider/custom_setting.go
Normal file
137
plugins/wasm-go/extensions/ai-proxy/provider/custom_setting.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
nameMaxTokens = "max_tokens"
|
||||
nameTemperature = "temperature"
|
||||
nameTopP = "top_p"
|
||||
nameTopK = "top_k"
|
||||
nameSeed = "seed"
|
||||
)
|
||||
|
||||
var maxTokensMapping = map[string]string{
|
||||
"openai": "max_tokens",
|
||||
"baidu": "max_output_tokens",
|
||||
"spark": "max_tokens",
|
||||
"qwen": "max_tokens",
|
||||
"gemini": "maxOutputTokens",
|
||||
"claude": "max_tokens",
|
||||
"minimax": "tokens_to_generate",
|
||||
}
|
||||
|
||||
var temperatureMapping = map[string]string{
|
||||
"openai": "temperature",
|
||||
"baidu": "temperature",
|
||||
"spark": "temperature",
|
||||
"qwen": "temperature",
|
||||
"gemini": "temperature",
|
||||
"hunyuan": "Temperature",
|
||||
"claude": "temperature",
|
||||
"minimax": "temperature",
|
||||
}
|
||||
|
||||
var topPMapping = map[string]string{
|
||||
"openai": "top_p",
|
||||
"baidu": "top_p",
|
||||
"qwen": "top_p",
|
||||
"gemini": "topP",
|
||||
"hunyuan": "TopP",
|
||||
"claude": "top_p",
|
||||
"minimax": "top_p",
|
||||
}
|
||||
|
||||
var topKMapping = map[string]string{
|
||||
"spark": "top_k",
|
||||
"gemini": "topK",
|
||||
"claude": "top_k",
|
||||
}
|
||||
|
||||
var seedMapping = map[string]string{
|
||||
"openai": "seed",
|
||||
"qwen": "seed",
|
||||
}
|
||||
|
||||
var settingMapping = map[string]map[string]string{
|
||||
nameMaxTokens: maxTokensMapping,
|
||||
nameTemperature: temperatureMapping,
|
||||
nameTopP: topPMapping,
|
||||
nameTopK: topKMapping,
|
||||
nameSeed: seedMapping,
|
||||
}
|
||||
|
||||
type CustomSetting struct {
|
||||
// @Title zh-CN 参数名称
|
||||
// @Description zh-CN 想要设置的参数的名称,例如max_tokens
|
||||
name string
|
||||
// @Title zh-CN 参数值
|
||||
// @Description zh-CN 想要设置的参数的值,例如0
|
||||
value string
|
||||
// @Title zh-CN 设置模式
|
||||
// @Description zh-CN 参数设置的模式,可以设置为"auto"或者"raw",如果为"auto"则会根据 /plugins/wasm-go/extensions/ai-proxy/README.md中关于custom-setting部分的表格自动按照协议对参数名做改写,如果为"raw"则不会有任何改写和限制检查
|
||||
mode string
|
||||
// @Title zh-CN json edit 模式
|
||||
// @Description zh-CN 如果为false则只在用户没有设置这个参数时填充参数,否则会直接覆盖用户原有的参数设置
|
||||
overwrite bool
|
||||
}
|
||||
|
||||
func (c *CustomSetting) FromJson(json gjson.Result) {
|
||||
c.name = json.Get("name").String()
|
||||
c.value = json.Get("value").Raw
|
||||
if obj := json.Get("mode"); obj.Exists() {
|
||||
c.mode = obj.String()
|
||||
} else {
|
||||
c.mode = "auto"
|
||||
}
|
||||
if obj := json.Get("overwrite"); obj.Exists() {
|
||||
c.overwrite = obj.Bool()
|
||||
} else {
|
||||
c.overwrite = true
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CustomSetting) Validate() bool {
|
||||
return c.name != ""
|
||||
}
|
||||
|
||||
func (c *CustomSetting) setInvalid() {
|
||||
c.name = "" // set empty to represent invalid
|
||||
}
|
||||
|
||||
func (c *CustomSetting) AdjustWithProtocol(protocol string) {
|
||||
if !(c.mode == "raw") {
|
||||
mapping, ok := settingMapping[c.name]
|
||||
if ok {
|
||||
c.name, ok = mapping[protocol]
|
||||
}
|
||||
if !ok {
|
||||
c.setInvalid()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if protocol == providerTypeQwen {
|
||||
c.name = "parameters." + c.name
|
||||
}
|
||||
if protocol == providerTypeGemini {
|
||||
c.name = "generation_config." + c.name
|
||||
}
|
||||
}
|
||||
|
||||
func ReplaceByCustomSettings(body []byte, settings []CustomSetting) ([]byte, error) {
|
||||
var err error
|
||||
strBody := string(body)
|
||||
for _, setting := range settings {
|
||||
if !setting.overwrite && gjson.Get(strBody, setting.name).Exists() {
|
||||
continue
|
||||
}
|
||||
strBody, err = sjson.SetRaw(strBody, setting.name, setting.value)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return []byte(strBody), err
|
||||
}
|
||||
176
plugins/wasm-go/extensions/ai-proxy/provider/deepl.go
Normal file
176
plugins/wasm-go/extensions/ai-proxy/provider/deepl.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
)
|
||||
|
||||
// deeplProvider is the provider for DeepL service.
|
||||
const (
|
||||
deeplHostPro = "api.deepl.com"
|
||||
deeplHostFree = "api-free.deepl.com"
|
||||
deeplChatCompletionPath = "/v2/translate"
|
||||
)
|
||||
|
||||
type deeplProviderInitializer struct {
|
||||
}
|
||||
|
||||
type deeplProvider struct {
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
// spec reference: https://developers.deepl.com/docs/v/zh/api-reference/translate/openapi-spec-for-text-translation
|
||||
type deeplRequest struct {
|
||||
// "Model" parameter is used to distinguish which service to use
|
||||
Model string `json:"model,omitempty"`
|
||||
Text []string `json:"text"`
|
||||
SourceLang string `json:"source_lang,omitempty"`
|
||||
TargetLang string `json:"target_lang"`
|
||||
Context string `json:"context,omitempty"`
|
||||
SplitSentences string `json:"split_sentences,omitempty"`
|
||||
PreserveFormatting bool `json:"preserve_formatting,omitempty"`
|
||||
Formality string `json:"formality,omitempty"`
|
||||
GlossaryId string `json:"glossary_id,omitempty"`
|
||||
TagHandling string `json:"tag_handling,omitempty"`
|
||||
OutlineDetection bool `json:"outline_detection,omitempty"`
|
||||
NonSplittingTags []string `json:"non_splitting_tags,omitempty"`
|
||||
SplittingTags []string `json:"splitting_tags,omitempty"`
|
||||
IgnoreTags []string `json:"ignore_tags,omitempty"`
|
||||
}
|
||||
|
||||
type deeplResponse struct {
|
||||
Translations []deeplResponseTranslation `json:"translations,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type deeplResponseTranslation struct {
|
||||
DetectedSourceLanguage string `json:"detected_source_language"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (d *deeplProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.targetLang == "" {
|
||||
return errors.New("missing targetLang in deepl provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deeplProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
return &deeplProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *deeplProvider) GetProviderType() string {
|
||||
return providerTypeDeepl
|
||||
}
|
||||
|
||||
func (d *deeplProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
_ = util.OverwriteRequestPath(deeplChatCompletionPath)
|
||||
_ = util.OverwriteRequestAuthorization("DeepL-Auth-Key " + d.config.GetRandomToken())
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
return types.HeaderStopIteration, nil
|
||||
}
|
||||
|
||||
func (d *deeplProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
if d.config.protocol == protocolOriginal {
|
||||
request := &deeplRequest{}
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
if err := d.overwriteRequestHost(request.Model); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, request.Model)
|
||||
return types.ActionContinue, replaceJsonRequestBody(request, log)
|
||||
} else {
|
||||
originRequest := &chatCompletionRequest{}
|
||||
if err := decodeChatCompletionRequest(body, originRequest); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
if err := d.overwriteRequestHost(originRequest.Model); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, originRequest.Model)
|
||||
deeplRequest := &deeplRequest{
|
||||
Text: make([]string, 0),
|
||||
TargetLang: d.config.targetLang,
|
||||
}
|
||||
for _, msg := range originRequest.Messages {
|
||||
if msg.Role == roleSystem {
|
||||
deeplRequest.Context = msg.StringContent()
|
||||
} else {
|
||||
deeplRequest.Text = append(deeplRequest.Text, msg.StringContent())
|
||||
}
|
||||
}
|
||||
return types.ActionContinue, replaceJsonRequestBody(deeplRequest, log)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deeplProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
_ = proxywasm.RemoveHttpResponseHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
func (d *deeplProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
deeplResponse := &deeplResponse{}
|
||||
if err := json.Unmarshal(body, deeplResponse); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal deepl response: %v", err)
|
||||
}
|
||||
response := d.responseDeepl2OpenAI(ctx, deeplResponse)
|
||||
return types.ActionContinue, replaceJsonResponseBody(response, log)
|
||||
}
|
||||
|
||||
func (d *deeplProvider) responseDeepl2OpenAI(ctx wrapper.HttpContext, deeplResponse *deeplResponse) *chatCompletionResponse {
|
||||
var choices []chatCompletionChoice
|
||||
// Fail
|
||||
if deeplResponse.Message != "" {
|
||||
choices = make([]chatCompletionChoice, 1)
|
||||
choices[0] = chatCompletionChoice{
|
||||
Message: &chatMessage{Role: roleAssistant, Content: deeplResponse.Message},
|
||||
Index: 0,
|
||||
}
|
||||
} else {
|
||||
// Success
|
||||
choices = make([]chatCompletionChoice, len(deeplResponse.Translations))
|
||||
for idx, t := range deeplResponse.Translations {
|
||||
choices[idx] = chatCompletionChoice{
|
||||
Index: idx,
|
||||
Message: &chatMessage{Role: roleAssistant, Content: t.Text, Name: t.DetectedSourceLanguage},
|
||||
}
|
||||
}
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Object: objectChatCompletion,
|
||||
Choices: choices,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deeplProvider) overwriteRequestHost(model string) error {
|
||||
if model == "Pro" {
|
||||
_ = util.OverwriteRequestHost(deeplHostPro)
|
||||
} else if model == "Free" {
|
||||
_ = util.OverwriteRequestHost(deeplHostFree)
|
||||
} else {
|
||||
return errors.New(`deepl model should be "Free" or "Pro"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/google/uuid"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// geminiProvider is the provider for google gemini/gemini flash service.
|
||||
@@ -378,7 +379,7 @@ func (g *geminiProvider) buildGeminiChatRequest(request *chatCompletionRequest)
|
||||
Role: message.Role,
|
||||
Parts: []geminiPart{
|
||||
{
|
||||
Text: message.Content,
|
||||
Text: message.StringContent(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ func convertMessagesFromOpenAIToHunyuan(openAIMessages []chatMessage) []hunyuanC
|
||||
for _, msg := range openAIMessages {
|
||||
hunyuanChatMessages = append(hunyuanChatMessages, hunyuanChatMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
Content: msg.StringContent(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -404,19 +404,19 @@ func (m *minimaxProvider) buildMinimaxChatCompletionV2Request(request *chatCompl
|
||||
botName = determineName(message.Name, defaultBotName)
|
||||
botSetting = append(botSetting, minimaxBotSetting{
|
||||
BotName: botName,
|
||||
Content: message.Content,
|
||||
Content: message.StringContent(),
|
||||
})
|
||||
case roleAssistant:
|
||||
messages = append(messages, minimaxMessage{
|
||||
SenderType: senderTypeBot,
|
||||
SenderName: determineName(message.Name, defaultBotName),
|
||||
Text: message.Content,
|
||||
Text: message.StringContent(),
|
||||
})
|
||||
case roleUser:
|
||||
messages = append(messages, minimaxMessage{
|
||||
SenderType: senderTypeUser,
|
||||
SenderName: determineName(message.Name, defaultSenderName),
|
||||
Text: message.Content,
|
||||
Text: message.StringContent(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ const (
|
||||
eventResult = "result"
|
||||
|
||||
httpStatus200 = "200"
|
||||
|
||||
contentTypeText = "text"
|
||||
contentTypeImageUrl = "image_url"
|
||||
)
|
||||
|
||||
type chatCompletionRequest struct {
|
||||
@@ -31,6 +34,7 @@ type chatCompletionRequest struct {
|
||||
ToolChoice *toolChoice `json:"tool_choice,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Stop []string `json:"stop,omitempty"`
|
||||
ResponseFormat map[string]interface{} `json:"response_format,omitempty"`
|
||||
}
|
||||
|
||||
type streamOptions struct {
|
||||
@@ -79,12 +83,27 @@ type usage struct {
|
||||
type chatMessage struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Content any `json:"content,omitempty"`
|
||||
ToolCalls []toolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
type messageContent struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Text string `json:"text"`
|
||||
ImageUrl *imageUrl `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
type imageUrl struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
func (m *chatMessage) IsEmpty() bool {
|
||||
if m.Content != "" {
|
||||
if m.IsStringContent() && m.Content != "" {
|
||||
return false
|
||||
}
|
||||
anyList, ok := m.Content.([]any)
|
||||
if ok && len(anyList) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(m.ToolCalls) != 0 {
|
||||
@@ -102,6 +121,76 @@ func (m *chatMessage) IsEmpty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *chatMessage) IsStringContent() bool {
|
||||
_, ok := m.Content.(string)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *chatMessage) StringContent() string {
|
||||
content, ok := m.Content.(string)
|
||||
if ok {
|
||||
return content
|
||||
}
|
||||
contentList, ok := m.Content.([]any)
|
||||
if ok {
|
||||
var contentStr string
|
||||
for _, contentItem := range contentList {
|
||||
contentMap, ok := contentItem.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if contentMap["type"] == contentTypeText {
|
||||
if subStr, ok := contentMap[contentTypeText].(string); ok {
|
||||
contentStr += subStr + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return contentStr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *chatMessage) ParseContent() []messageContent {
|
||||
var contentList []messageContent
|
||||
content, ok := m.Content.(string)
|
||||
if ok {
|
||||
contentList = append(contentList, messageContent{
|
||||
Type: contentTypeText,
|
||||
Text: content,
|
||||
})
|
||||
return contentList
|
||||
}
|
||||
anyList, ok := m.Content.([]any)
|
||||
if ok {
|
||||
for _, contentItem := range anyList {
|
||||
contentMap, ok := contentItem.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch contentMap["type"] {
|
||||
case contentTypeText:
|
||||
if subStr, ok := contentMap[contentTypeText].(string); ok {
|
||||
contentList = append(contentList, messageContent{
|
||||
Type: contentTypeText,
|
||||
Text: subStr,
|
||||
})
|
||||
}
|
||||
case contentTypeImageUrl:
|
||||
if subObj, ok := contentMap[contentTypeImageUrl].(map[string]any); ok {
|
||||
contentList = append(contentList, messageContent{
|
||||
Type: contentTypeImageUrl,
|
||||
ImageUrl: &imageUrl{
|
||||
Url: subObj["url"].(string),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return contentList
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type toolCall struct {
|
||||
Index int `json:"index"`
|
||||
Id string `json:"id"`
|
||||
|
||||
@@ -89,6 +89,10 @@ func (m *openaiProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName,
|
||||
if err := decodeChatCompletionRequest(body, request); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
if m.config.responseJsonSchema != nil {
|
||||
log.Debugf("[ai-proxy] set response format to %s", m.config.responseJsonSchema)
|
||||
request.ResponseFormat = m.config.responseJsonSchema
|
||||
}
|
||||
if request.Stream {
|
||||
// For stream requests, we need to include usage in the response.
|
||||
if request.StreamOptions == nil {
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
|
||||
providerTypeMoonshot = "moonshot"
|
||||
providerTypeAzure = "azure"
|
||||
providerTypeAi360 = "ai360"
|
||||
providerTypeQwen = "qwen"
|
||||
providerTypeOpenAI = "openai"
|
||||
providerTypeGroq = "groq"
|
||||
@@ -35,6 +36,7 @@ const (
|
||||
providerTypeCloudflare = "cloudflare"
|
||||
providerTypeSpark = "spark"
|
||||
providerTypeGemini = "gemini"
|
||||
providerTypeDeepl = "deepl"
|
||||
|
||||
protocolOpenAI = "openai"
|
||||
protocolOriginal = "original"
|
||||
@@ -72,6 +74,7 @@ var (
|
||||
providerInitializers = map[string]providerInitializer{
|
||||
providerTypeMoonshot: &moonshotProviderInitializer{},
|
||||
providerTypeAzure: &azureProviderInitializer{},
|
||||
providerTypeAi360: &ai360ProviderInitializer{},
|
||||
providerTypeQwen: &qwenProviderInitializer{},
|
||||
providerTypeOpenAI: &openaiProviderInitializer{},
|
||||
providerTypeGroq: &groqProviderInitializer{},
|
||||
@@ -88,6 +91,7 @@ var (
|
||||
providerTypeCloudflare: &cloudflareProviderInitializer{},
|
||||
providerTypeSpark: &sparkProviderInitializer{},
|
||||
providerTypeGemini: &geminiProviderInitializer{},
|
||||
providerTypeDeepl: &deeplProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,6 +144,9 @@ type ProviderConfig struct {
|
||||
// @Title zh-CN 启用通义千问搜索服务
|
||||
// @Description zh-CN 仅适用于通义千问服务,表示是否启用通义千问的互联网搜索功能。
|
||||
qwenEnableSearch bool `required:"false" yaml:"qwenEnableSearch" json:"qwenEnableSearch"`
|
||||
// @Title zh-CN 开启通义千问兼容模式
|
||||
// @Description zh-CN 启用通义千问兼容模式后,将调用千问的兼容模式接口,同时对请求/响应不做修改。
|
||||
qwenEnableCompatible bool `required:"false" yaml:"qwenEnableCompatible" json:"qwenEnableCompatible"`
|
||||
// @Title zh-CN Ollama Server IP/Domain
|
||||
// @Description zh-CN 仅适用于 Ollama 服务。Ollama 服务器的主机地址。
|
||||
ollamaServerHost string `required:"false" yaml:"ollamaServerHost" json:"ollamaServerHost"`
|
||||
@@ -173,6 +180,15 @@ type ProviderConfig struct {
|
||||
// @Title zh-CN Gemini AI内容过滤和安全级别设定
|
||||
// @Description zh-CN 仅适用于 Gemini AI 服务。参考:https://ai.google.dev/gemini-api/docs/safety-settings
|
||||
geminiSafetySetting map[string]string `required:"false" yaml:"geminiSafetySetting" json:"geminiSafetySetting"`
|
||||
// @Title zh-CN 翻译服务需指定的目标语种
|
||||
// @Description zh-CN 翻译结果的语种,目前仅适用于DeepL服务。
|
||||
targetLang string `required:"false" yaml:"targetLang" json:"targetLang"`
|
||||
// @Title zh-CN 指定服务返回的响应需满足的JSON Schema
|
||||
// @Description zh-CN 目前仅适用于OpenAI部分模型服务。参考:https://platform.openai.com/docs/guides/structured-outputs
|
||||
responseJsonSchema map[string]interface{} `required:"false" yaml:"responseJsonSchema" json:"responseJsonSchema"`
|
||||
// @Title zh-CN 自定义大模型参数配置
|
||||
// @Description zh-CN 用于填充或者覆盖大模型调用时的参数
|
||||
customSettings []CustomSetting
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
@@ -193,6 +209,7 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.qwenFileIds = append(c.qwenFileIds, fileId.String())
|
||||
}
|
||||
c.qwenEnableSearch = json.Get("qwenEnableSearch").Bool()
|
||||
c.qwenEnableCompatible = json.Get("qwenEnableCompatible").Bool()
|
||||
c.ollamaServerHost = json.Get("ollamaServerHost").String()
|
||||
c.ollamaServerPort = uint32(json.Get("ollamaServerPort").Uint())
|
||||
c.modelMapping = make(map[string]string)
|
||||
@@ -219,6 +236,32 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.geminiSafetySetting[k] = v.String()
|
||||
}
|
||||
}
|
||||
c.targetLang = json.Get("targetLang").String()
|
||||
|
||||
if schemaValue, ok := json.Get("responseJsonSchema").Value().(map[string]interface{}); ok {
|
||||
c.responseJsonSchema = schemaValue
|
||||
} else {
|
||||
c.responseJsonSchema = nil
|
||||
}
|
||||
|
||||
c.customSettings = make([]CustomSetting, 0)
|
||||
customSettingsJson := json.Get("customSettings")
|
||||
if customSettingsJson.Exists() {
|
||||
protocol := protocolOpenAI
|
||||
if c.protocol == protocolOriginal {
|
||||
// use provider name to represent original protocol name
|
||||
protocol = c.typ
|
||||
}
|
||||
for _, settingJson := range customSettingsJson.Array() {
|
||||
setting := CustomSetting{}
|
||||
setting.FromJson(settingJson)
|
||||
// use protocol info to rewrite setting
|
||||
setting.AdjustWithProtocol(protocol)
|
||||
if setting.Validate() {
|
||||
c.customSettings = append(c.customSettings, setting)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) Validate() error {
|
||||
@@ -304,3 +347,7 @@ func doGetMappedModel(model string, modelMapping map[string]string, log wrapper.
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c ProviderConfig) ReplaceByCustomSettings(body []byte) ([]byte, error) {
|
||||
return ReplaceByCustomSettings(body, c.customSettings)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// qwenProvider is the provider for Qwen service.
|
||||
@@ -20,16 +22,19 @@ import (
|
||||
const (
|
||||
qwenResultFormatMessage = "message"
|
||||
|
||||
qwenDomain = "dashscope.aliyuncs.com"
|
||||
qwenChatCompletionPath = "/api/v1/services/aigc/text-generation/generation"
|
||||
qwenTextEmbeddingPath = "/api/v1/services/embeddings/text-embedding/text-embedding"
|
||||
qwenDomain = "dashscope.aliyuncs.com"
|
||||
qwenChatCompletionPath = "/api/v1/services/aigc/text-generation/generation"
|
||||
qwenTextEmbeddingPath = "/api/v1/services/embeddings/text-embedding/text-embedding"
|
||||
qwenCompatiblePath = "/compatible-mode/v1/chat/completions"
|
||||
qwenMultimodalGenerationPath = "/api/v1/services/aigc/multimodal-generation/generation"
|
||||
|
||||
qwenTopPMin = 0.000001
|
||||
qwenTopPMax = 0.999999
|
||||
|
||||
qwenDummySystemMessageContent = "You are a helpful assistant."
|
||||
|
||||
qwenLongModelName = "qwen-long"
|
||||
qwenLongModelName = "qwen-long"
|
||||
qwenVlModelPrefixName = "qwen-vl"
|
||||
)
|
||||
|
||||
type qwenProviderInitializer struct {
|
||||
@@ -63,7 +68,9 @@ func (m *qwenProvider) GetProviderType() string {
|
||||
}
|
||||
|
||||
func (m *qwenProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
if m.config.qwenEnableCompatible {
|
||||
_ = util.OverwriteRequestPath(qwenCompatiblePath)
|
||||
} else if apiName == ApiNameChatCompletion {
|
||||
_ = util.OverwriteRequestPath(qwenChatCompletionPath)
|
||||
} else if apiName == ApiNameEmbeddings {
|
||||
_ = util.OverwriteRequestPath(qwenTextEmbeddingPath)
|
||||
@@ -85,6 +92,23 @@ func (m *qwenProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName
|
||||
}
|
||||
|
||||
func (m *qwenProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if m.config.qwenEnableCompatible {
|
||||
if gjson.GetBytes(body, "model").Exists() {
|
||||
rawModel := gjson.GetBytes(body, "model").String()
|
||||
mappedModel := getMappedModel(rawModel, m.config.modelMapping, log)
|
||||
newBody, err := sjson.SetBytes(body, "model", mappedModel)
|
||||
if err != nil {
|
||||
log.Errorf("Replace model error: %v", err)
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
err = proxywasm.ReplaceHttpRequestBody(newBody)
|
||||
if err != nil {
|
||||
log.Errorf("Replace request body error: %v", err)
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
}
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
if apiName == ApiNameChatCompletion {
|
||||
return m.onChatCompletionRequestBody(ctx, body, log)
|
||||
}
|
||||
@@ -141,6 +165,10 @@ func (m *qwenProvider) onChatCompletionRequestBody(ctx wrapper.HttpContext, body
|
||||
}
|
||||
request.Model = mappedModel
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, request.Model)
|
||||
// Use the qwen multimodal model generation API
|
||||
if strings.HasPrefix(request.Model, qwenVlModelPrefixName) {
|
||||
_ = util.OverwriteRequestPath(qwenMultimodalGenerationPath)
|
||||
}
|
||||
|
||||
streaming := request.Stream
|
||||
if streaming {
|
||||
@@ -220,7 +248,7 @@ func (m *qwenProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiNam
|
||||
}
|
||||
|
||||
func (m *qwenProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool, log wrapper.Log) ([]byte, error) {
|
||||
if name != ApiNameChatCompletion {
|
||||
if m.config.qwenEnableCompatible || name != ApiNameChatCompletion {
|
||||
return chunk, nil
|
||||
}
|
||||
|
||||
@@ -305,6 +333,9 @@ func (m *qwenProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name Api
|
||||
}
|
||||
|
||||
func (m *qwenProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if m.config.qwenEnableCompatible {
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
if apiName == ApiNameChatCompletion {
|
||||
return m.onChatCompletionResponseBody(ctx, body, log)
|
||||
}
|
||||
@@ -425,8 +456,29 @@ func (m *qwenProvider) buildChatCompletionStreamingResponse(ctx wrapper.HttpCont
|
||||
if pushedMessage, ok := ctx.GetContext(ctxKeyPushedMessage).(qwenMessage); ok {
|
||||
if message.Content == "" {
|
||||
message.Content = pushedMessage.Content
|
||||
} else if message.IsStringContent() {
|
||||
deltaContentMessage.Content = util.StripPrefix(deltaContentMessage.StringContent(), pushedMessage.StringContent())
|
||||
} else if strings.HasPrefix(baseMessage.Model, qwenVlModelPrefixName) {
|
||||
// Use the Qwen multimodal model generation API
|
||||
deltaContentList, ok := deltaContentMessage.Content.([]qwenVlMessageContent)
|
||||
if !ok {
|
||||
log.Warnf("unexpected deltaContentMessage content type: %T", deltaContentMessage.Content)
|
||||
} else {
|
||||
pushedContentList, ok := pushedMessage.Content.([]qwenVlMessageContent)
|
||||
if !ok {
|
||||
log.Warnf("unexpected pushedMessage content type: %T", pushedMessage.Content)
|
||||
} else {
|
||||
for i, content := range deltaContentList {
|
||||
if i >= len(pushedContentList) {
|
||||
break
|
||||
}
|
||||
pushedText := pushedContentList[i].Text
|
||||
content.Text = util.StripPrefix(content.Text, pushedText)
|
||||
deltaContentList[i] = content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
deltaContentMessage.Content = util.StripPrefix(deltaContentMessage.Content, pushedMessage.Content)
|
||||
if len(deltaToolCallsMessage.ToolCalls) > 0 && pushedMessage.ToolCalls != nil {
|
||||
for i, tc := range deltaToolCallsMessage.ToolCalls {
|
||||
if i >= len(pushedMessage.ToolCalls) {
|
||||
@@ -532,7 +584,7 @@ func (m *qwenProvider) insertContextMessage(request *qwenTextGenRequest, content
|
||||
if builder.Len() != 0 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString(message.Content)
|
||||
builder.WriteString(message.StringContent())
|
||||
}
|
||||
request.Input.Messages = append([]qwenMessage{{Role: roleSystem, Content: builder.String()}, fileMessage}, request.Input.Messages[firstNonSystemMessageIndex:]...)
|
||||
return 1
|
||||
@@ -637,10 +689,15 @@ type qwenUsage struct {
|
||||
type qwenMessage struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Content any `json:"content"`
|
||||
ToolCalls []toolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
type qwenVlMessageContent struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type qwenTextEmbeddingRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input qwenTextEmbeddingInput `json:"input"`
|
||||
@@ -680,11 +737,58 @@ func qwenMessageToChatMessage(qwenMessage qwenMessage) chatMessage {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *qwenMessage) IsStringContent() bool {
|
||||
_, ok := m.Content.(string)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *qwenMessage) StringContent() string {
|
||||
content, ok := m.Content.(string)
|
||||
if ok {
|
||||
return content
|
||||
}
|
||||
contentList, ok := m.Content.([]any)
|
||||
if ok {
|
||||
var contentStr string
|
||||
for _, contentItem := range contentList {
|
||||
contentMap, ok := contentItem.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if text, ok := contentMap["text"].(string); ok {
|
||||
contentStr += text
|
||||
}
|
||||
}
|
||||
return contentStr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func chatMessage2QwenMessage(chatMessage chatMessage) qwenMessage {
|
||||
return qwenMessage{
|
||||
Name: chatMessage.Name,
|
||||
Role: chatMessage.Role,
|
||||
Content: chatMessage.Content,
|
||||
ToolCalls: chatMessage.ToolCalls,
|
||||
if chatMessage.IsStringContent() {
|
||||
return qwenMessage{
|
||||
Name: chatMessage.Name,
|
||||
Role: chatMessage.Role,
|
||||
Content: chatMessage.StringContent(),
|
||||
ToolCalls: chatMessage.ToolCalls,
|
||||
}
|
||||
} else {
|
||||
var contents []qwenVlMessageContent
|
||||
openaiContent := chatMessage.ParseContent()
|
||||
for _, part := range openaiContent {
|
||||
var content qwenVlMessageContent
|
||||
if part.Type == contentTypeText {
|
||||
content.Text = part.Text
|
||||
} else if part.Type == contentTypeImageUrl {
|
||||
content.Image = part.ImageUrl.Url
|
||||
}
|
||||
contents = append(contents, content)
|
||||
}
|
||||
return qwenMessage{
|
||||
Name: chatMessage.Name,
|
||||
Role: chatMessage.Role,
|
||||
Content: contents,
|
||||
ToolCalls: chatMessage.ToolCalls,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
plugins/wasm-go/extensions/ai-quota/README.md
Normal file
58
plugins/wasm-go/extensions/ai-quota/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 功能说明
|
||||
|
||||
`ai-qutoa` 插件实现给特定 consumer 根据分配固定的 quota 进行 quota 策略限流,同时支持 quota 管理能力,包括查询 quota 、刷新 quota、增减 quota。
|
||||
|
||||
`ai-quota` 插件需要配合 认证插件比如 `key-auth`、`jwt-auth` 等插件获取认证身份的 consumer 名称,同时需要配合 `ai-statatistics` 插件获取 AI Token 统计信息。
|
||||
|
||||
# 配置说明
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------------------|-----------------|--------------------------------------| ---- |--------------------------------------------|
|
||||
| `redis_key_prefix` | string | 选填 | chat_quota: | qutoa redis key 前缀 |
|
||||
| `admin_consumer` | string | 必填 | | 管理 quota 管理身份的 consumer 名称 |
|
||||
| `admin_path` | string | 选填 | /quota | 管理 quota 请求 path 前缀 |
|
||||
| `redis` | object | 是 | | redis相关配置 |
|
||||
|
||||
`redis`中每一项的配置字段说明
|
||||
|
||||
| 配置项 | 类型 | 必填 | 默认值 | 说明 |
|
||||
| ------------ | ------ | ---- | ---------------------------------------------------------- | --------------------------- |
|
||||
| service_name | string | 必填 | - | redis 服务名称,带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local |
|
||||
| service_port | int | 否 | 服务类型为固定地址(static service)默认值为80,其他为6379 | 输入redis服务的服务端口 |
|
||||
| username | string | 否 | - | redis用户名 |
|
||||
| password | string | 否 | - | redis密码 |
|
||||
| timeout | int | 否 | 1000 | redis连接超时时间,单位毫秒 |
|
||||
|
||||
|
||||
|
||||
# 配置示例
|
||||
|
||||
## 识别请求参数 apikey,进行区别限流
|
||||
```yaml
|
||||
redis_key_prefix: "chat_quota:"
|
||||
admin_consumer: consumer3
|
||||
admin_path: /quota
|
||||
redis:
|
||||
service_name: redis-service.default.svc.cluster.local
|
||||
service_port: 6379
|
||||
timeout: 2000
|
||||
```
|
||||
|
||||
|
||||
## 刷新 quota
|
||||
|
||||
如果当前请求 url 的后缀符合 admin_path,例如插件在 example.com/v1/chat/completions 这个路由上生效,那么更新 quota 可以通过
|
||||
curl https://example.com/v1/chat/completions/quota/refresh -H "Authorization: Bearer credential3" -d "consumer=consumer1"a=10000"
|
||||
|
||||
Redis 中 key 为 chat_quota:consumer1 的值就会被刷新为 10000
|
||||
|
||||
## 查询 quota
|
||||
|
||||
查询特定用户的 quota 可以通过 curl https://example.com/v1/chat/completions/quota?consumer=consumer1 -H "Authorization: Bearer credential3"
|
||||
将返回: {"quota": 10000, "consumer": "consumer1"}
|
||||
|
||||
## 增减 quota
|
||||
|
||||
增减特定用户的 quota 可以通过 curl https://example.com/v1/chat/completions/quota/delta -d "consumer=consumer1&value=100" -H "Authorization: Bearer credential3"
|
||||
这样 Redis 中 Key 为 chat_quota:consumer1 的值就会增加100,可以支持负数,则减去对应值。
|
||||
|
||||
20
plugins/wasm-go/extensions/ai-quota/go.mod
Normal file
20
plugins/wasm-go/extensions/ai-quota/go.mod
Normal file
@@ -0,0 +1,20 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/ai-quota
|
||||
|
||||
go 1.19
|
||||
|
||||
//replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.3-0.20240808022948-34f5722d93de
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/tidwall/resp v0.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
)
|
||||
22
plugins/wasm-go/extensions/ai-quota/go.sum
Normal file
22
plugins/wasm-go/extensions/ai-quota/go.sum
Normal file
@@ -0,0 +1,22 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.3-0.20240808022948-34f5722d93de h1:lDLqj7Hw41ox8VdsP7oCTPhjPa3+QJUCKApcLh2a45Y=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.3-0.20240808022948-34f5722d93de/go.mod h1:359don/ahMxpfeLMzr29Cjwcu8IywTTDUzWlBPRNLHw=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
399
plugins/wasm-go/extensions/ai-quota/main.go
Normal file
399
plugins/wasm-go/extensions/ai-quota/main.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-quota/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/resp"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginName = "ai-quota"
|
||||
)
|
||||
|
||||
type ChatMode string
|
||||
|
||||
const (
|
||||
ChatModeCompletion ChatMode = "completion"
|
||||
ChatModeAdmin ChatMode = "admin"
|
||||
ChatModeNone ChatMode = "none"
|
||||
)
|
||||
|
||||
type AdminMode string
|
||||
|
||||
const (
|
||||
AdminModeRefresh AdminMode = "refresh"
|
||||
AdminModeQuery AdminMode = "query"
|
||||
AdminModeDelta AdminMode = "delta"
|
||||
AdminModeNone AdminMode = "none"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
pluginName,
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
type QuotaConfig struct {
|
||||
redisInfo RedisInfo `yaml:"redis"`
|
||||
RedisKeyPrefix string `yaml:"redis_key_prefix"`
|
||||
AdminConsumer string `yaml:"admin_consumer"`
|
||||
AdminPath string `yaml:"admin_path"`
|
||||
credential2Name map[string]string `yaml:"-"`
|
||||
redisClient wrapper.RedisClient
|
||||
}
|
||||
|
||||
type Consumer struct {
|
||||
Name string `yaml:"name"`
|
||||
Credential string `yaml:"credential"`
|
||||
}
|
||||
|
||||
type RedisInfo struct {
|
||||
ServiceName string `required:"true" yaml:"service_name" json:"service_name"`
|
||||
ServicePort int `required:"false" yaml:"service_port" json:"service_port"`
|
||||
Username string `required:"false" yaml:"username" json:"username"`
|
||||
Password string `required:"false" yaml:"password" json:"password"`
|
||||
Timeout int `required:"false" yaml:"timeout" json:"timeout"`
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *QuotaConfig, log wrapper.Log) error {
|
||||
log.Debugf("parse config()")
|
||||
// admin
|
||||
config.AdminPath = json.Get("admin_path").String()
|
||||
config.AdminConsumer = json.Get("admin_consumer").String()
|
||||
if config.AdminPath == "" {
|
||||
config.AdminPath = "/quota"
|
||||
}
|
||||
if config.AdminConsumer == "" {
|
||||
return errors.New("missing admin_consumer in config")
|
||||
}
|
||||
// Redis
|
||||
config.RedisKeyPrefix = json.Get("redis_key_prefix").String()
|
||||
if config.RedisKeyPrefix == "" {
|
||||
config.RedisKeyPrefix = "chat_quota:"
|
||||
}
|
||||
redisConfig := json.Get("redis")
|
||||
if !redisConfig.Exists() {
|
||||
return errors.New("missing redis in config")
|
||||
}
|
||||
serviceName := redisConfig.Get("service_name").String()
|
||||
if serviceName == "" {
|
||||
return errors.New("redis service name must not be empty")
|
||||
}
|
||||
servicePort := int(redisConfig.Get("service_port").Int())
|
||||
if servicePort == 0 {
|
||||
if strings.HasSuffix(serviceName, ".static") {
|
||||
// use default logic port which is 80 for static service
|
||||
servicePort = 80
|
||||
} else {
|
||||
servicePort = 6379
|
||||
}
|
||||
}
|
||||
username := redisConfig.Get("username").String()
|
||||
password := redisConfig.Get("password").String()
|
||||
timeout := int(redisConfig.Get("timeout").Int())
|
||||
if timeout == 0 {
|
||||
timeout = 1000
|
||||
}
|
||||
config.redisInfo.ServiceName = serviceName
|
||||
config.redisInfo.ServicePort = servicePort
|
||||
config.redisInfo.Username = username
|
||||
config.redisInfo.Password = password
|
||||
config.redisInfo.Timeout = timeout
|
||||
config.redisClient = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName,
|
||||
Port: int64(servicePort),
|
||||
})
|
||||
|
||||
return config.redisClient.Init(username, password, int64(timeout))
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(context wrapper.HttpContext, config QuotaConfig, log wrapper.Log) types.Action {
|
||||
log.Debugf("onHttpRequestHeaders()")
|
||||
// get tokens
|
||||
consumer, err := proxywasm.GetHttpRequestHeader("x-mse-consumer")
|
||||
if err != nil {
|
||||
return deniedNoKeyAuthData()
|
||||
}
|
||||
if consumer == "" {
|
||||
return deniedUnauthorizedConsumer()
|
||||
}
|
||||
|
||||
rawPath := context.Path()
|
||||
path, _ := url.Parse(rawPath)
|
||||
chatMode, adminMode := getOperationMode(path.Path, config.AdminPath, log)
|
||||
context.SetContext("chatMode", chatMode)
|
||||
context.SetContext("adminMode", adminMode)
|
||||
context.SetContext("consumer", consumer)
|
||||
log.Debugf("chatMode:%s, adminMode:%s, consumer:%s", chatMode, adminMode, consumer)
|
||||
if chatMode == ChatModeNone {
|
||||
return types.ActionContinue
|
||||
}
|
||||
if chatMode == ChatModeAdmin {
|
||||
// query quota
|
||||
if adminMode == AdminModeQuery {
|
||||
return queryQuota(context, config, consumer, path, log)
|
||||
}
|
||||
if adminMode == AdminModeRefresh || adminMode == AdminModeDelta {
|
||||
context.BufferRequestBody()
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
// there is no need to read request body when it is on chat completion mode
|
||||
context.DontReadRequestBody()
|
||||
// check quota here
|
||||
config.redisClient.Get(config.RedisKeyPrefix+consumer, func(response resp.Value) {
|
||||
isDenied := false
|
||||
if err := response.Error(); err != nil {
|
||||
isDenied = true
|
||||
}
|
||||
if response.IsNull() {
|
||||
isDenied = true
|
||||
}
|
||||
if response.Integer() <= 0 {
|
||||
isDenied = true
|
||||
}
|
||||
log.Debugf("get consumer:%s quota:%d isDenied:%t", consumer, response.Integer(), isDenied)
|
||||
if isDenied {
|
||||
util.SendResponse(http.StatusForbidden, "ai-quota.noquota", "text/plain", "Request denied by ai quota check, No quota left")
|
||||
return
|
||||
}
|
||||
proxywasm.ResumeHttpRequest()
|
||||
})
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config QuotaConfig, body []byte, log wrapper.Log) types.Action {
|
||||
log.Debugf("onHttpRequestBody()")
|
||||
chatMode, ok := ctx.GetContext("chatMode").(ChatMode)
|
||||
if !ok {
|
||||
return types.ActionContinue
|
||||
}
|
||||
if chatMode == ChatModeNone || chatMode == ChatModeCompletion {
|
||||
return types.ActionContinue
|
||||
}
|
||||
adminMode, ok := ctx.GetContext("adminMode").(AdminMode)
|
||||
if !ok {
|
||||
return types.ActionContinue
|
||||
}
|
||||
adminConsumer, ok := ctx.GetContext("consumer").(string)
|
||||
if !ok {
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
if adminMode == AdminModeRefresh {
|
||||
return refreshQuota(ctx, config, adminConsumer, string(body), log)
|
||||
}
|
||||
if adminMode == AdminModeDelta {
|
||||
return deltaQuota(ctx, config, adminConsumer, string(body), log)
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpStreamingResponseBody(ctx wrapper.HttpContext, config QuotaConfig, data []byte, endOfStream bool, log wrapper.Log) []byte {
|
||||
chatMode, ok := ctx.GetContext("chatMode").(ChatMode)
|
||||
if !ok {
|
||||
return data
|
||||
}
|
||||
if chatMode == ChatModeNone || chatMode == ChatModeAdmin {
|
||||
return data
|
||||
}
|
||||
// chat completion mode
|
||||
if !endOfStream {
|
||||
return data
|
||||
}
|
||||
inputTokenStr, err := proxywasm.GetProperty([]string{"filter_state", "wasm.input_token"})
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
outputTokenStr, err := proxywasm.GetProperty([]string{"filter_state", "wasm.output_token"})
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
inputToken, err := strconv.Atoi(string(inputTokenStr))
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
outputToken, err := strconv.Atoi(string(outputTokenStr))
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
consumer, ok := ctx.GetContext("consumer").(string)
|
||||
if ok {
|
||||
totalToken := int(inputToken + outputToken)
|
||||
log.Debugf("update consumer:%s, totalToken:%d", consumer, totalToken)
|
||||
config.redisClient.DecrBy(config.RedisKeyPrefix+consumer, totalToken, nil)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func deniedNoKeyAuthData() types.Action {
|
||||
util.SendResponse(http.StatusUnauthorized, "ai-quota.no_key", "text/plain", "Request denied by ai quota check. No Key Authentication information found.")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func deniedUnauthorizedConsumer() types.Action {
|
||||
util.SendResponse(http.StatusForbidden, "ai-quota.unauthorized", "text/plain", "Request denied by ai quota check. Unauthorized consumer.")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func getOperationMode(path string, adminPath string, log wrapper.Log) (ChatMode, AdminMode) {
|
||||
fullAdminPath := "/v1/chat/completions" + adminPath
|
||||
if strings.HasSuffix(path, fullAdminPath+"/refresh") {
|
||||
return ChatModeAdmin, AdminModeRefresh
|
||||
}
|
||||
if strings.HasSuffix(path, fullAdminPath+"/delta") {
|
||||
return ChatModeAdmin, AdminModeDelta
|
||||
}
|
||||
if strings.HasSuffix(path, fullAdminPath) {
|
||||
return ChatModeAdmin, AdminModeQuery
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/chat/completions") {
|
||||
return ChatModeCompletion, AdminModeNone
|
||||
}
|
||||
return ChatModeNone, AdminModeNone
|
||||
}
|
||||
|
||||
func refreshQuota(ctx wrapper.HttpContext, config QuotaConfig, adminConsumer string, body string, log wrapper.Log) types.Action {
|
||||
// check consumer
|
||||
if adminConsumer != config.AdminConsumer {
|
||||
util.SendResponse(http.StatusForbidden, "ai-quota.unauthorized", "text/plain", "Request denied by ai quota check. Unauthorized admin consumer.")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
queryValues, _ := url.ParseQuery(body)
|
||||
values := make(map[string]string, len(queryValues))
|
||||
for k, v := range queryValues {
|
||||
values[k] = v[0]
|
||||
}
|
||||
queryConsumer := values["consumer"]
|
||||
quota, err := strconv.Atoi(values["quota"])
|
||||
if queryConsumer == "" || err != nil {
|
||||
util.SendResponse(http.StatusForbidden, "ai-quota.unauthorized", "text/plain", "Request denied by ai quota check. consumer can't be empty and quota must be integer.")
|
||||
return types.ActionContinue
|
||||
}
|
||||
err2 := config.redisClient.Set(config.RedisKeyPrefix+queryConsumer, quota, func(response resp.Value) {
|
||||
log.Debugf("Redis set key = %s quota = %d", config.RedisKeyPrefix+queryConsumer, quota)
|
||||
if err := response.Error(); err != nil {
|
||||
util.SendResponse(http.StatusServiceUnavailable, "ai-quota.error", "text/plain", fmt.Sprintf("redis error:%v", err))
|
||||
return
|
||||
}
|
||||
util.SendResponse(http.StatusOK, "ai-quota.refreshquota", "text/plain", "refresh quota successful")
|
||||
})
|
||||
|
||||
if err2 != nil {
|
||||
util.SendResponse(http.StatusServiceUnavailable, "ai-quota.error", "text/plain", fmt.Sprintf("redis error:%v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
return types.ActionPause
|
||||
}
|
||||
func queryQuota(ctx wrapper.HttpContext, config QuotaConfig, adminConsumer string, url *url.URL, log wrapper.Log) types.Action {
|
||||
// check consumer
|
||||
if adminConsumer != config.AdminConsumer {
|
||||
util.SendResponse(http.StatusForbidden, "ai-quota.unauthorized", "text/plain", "Request denied by ai quota check. Unauthorized admin consumer.")
|
||||
return types.ActionContinue
|
||||
}
|
||||
// check url
|
||||
queryValues := url.Query()
|
||||
values := make(map[string]string, len(queryValues))
|
||||
for k, v := range queryValues {
|
||||
values[k] = v[0]
|
||||
}
|
||||
if values["consumer"] == "" {
|
||||
util.SendResponse(http.StatusForbidden, "ai-quota.unauthorized", "text/plain", "Request denied by ai quota check. consumer can't be empty.")
|
||||
return types.ActionContinue
|
||||
}
|
||||
queryConsumer := values["consumer"]
|
||||
err := config.redisClient.Get(config.RedisKeyPrefix+queryConsumer, func(response resp.Value) {
|
||||
quota := 0
|
||||
if err := response.Error(); err != nil {
|
||||
util.SendResponse(http.StatusServiceUnavailable, "ai-quota.error", "text/plain", fmt.Sprintf("redis error:%v", err))
|
||||
return
|
||||
} else if response.IsNull() {
|
||||
quota = 0
|
||||
} else {
|
||||
quota = response.Integer()
|
||||
}
|
||||
result := struct {
|
||||
Consumer string `json:"consumer"`
|
||||
Quota int `json:"quota"`
|
||||
}{
|
||||
Consumer: queryConsumer,
|
||||
Quota: quota,
|
||||
}
|
||||
body, _ := json.Marshal(result)
|
||||
util.SendResponse(http.StatusOK, "ai-quota.queryquota", "application/json", string(body))
|
||||
})
|
||||
if err != nil {
|
||||
util.SendResponse(http.StatusServiceUnavailable, "ai-quota.error", "text/plain", fmt.Sprintf("redis error:%v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
return types.ActionPause
|
||||
}
|
||||
func deltaQuota(ctx wrapper.HttpContext, config QuotaConfig, adminConsumer string, body string, log wrapper.Log) types.Action {
|
||||
// check consumer
|
||||
if adminConsumer != config.AdminConsumer {
|
||||
util.SendResponse(http.StatusForbidden, "ai-quota.unauthorized", "text/plain", "Request denied by ai quota check. Unauthorized admin consumer.")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
queryValues, _ := url.ParseQuery(body)
|
||||
values := make(map[string]string, len(queryValues))
|
||||
for k, v := range queryValues {
|
||||
values[k] = v[0]
|
||||
}
|
||||
queryConsumer := values["consumer"]
|
||||
value, err := strconv.Atoi(values["value"])
|
||||
if queryConsumer == "" || err != nil {
|
||||
util.SendResponse(http.StatusForbidden, "ai-quota.unauthorized", "text/plain", "Request denied by ai quota check. consumer can't be empty and value must be integer.")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
if value >= 0 {
|
||||
err := config.redisClient.IncrBy(config.RedisKeyPrefix+queryConsumer, value, func(response resp.Value) {
|
||||
log.Debugf("Redis Incr key = %s value = %d", config.RedisKeyPrefix+queryConsumer, value)
|
||||
if err := response.Error(); err != nil {
|
||||
util.SendResponse(http.StatusServiceUnavailable, "ai-quota.error", "text/plain", fmt.Sprintf("redis error:%v", err))
|
||||
return
|
||||
}
|
||||
util.SendResponse(http.StatusOK, "ai-quota.deltaquota", "text/plain", "delta quota successful")
|
||||
})
|
||||
if err != nil {
|
||||
util.SendResponse(http.StatusServiceUnavailable, "ai-quota.error", "text/plain", fmt.Sprintf("redis error:%v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
} else {
|
||||
err := config.redisClient.DecrBy(config.RedisKeyPrefix+queryConsumer, 0-value, func(response resp.Value) {
|
||||
log.Debugf("Redis Decr key = %s value = %d", config.RedisKeyPrefix+queryConsumer, 0-value)
|
||||
if err := response.Error(); err != nil {
|
||||
util.SendResponse(http.StatusServiceUnavailable, "ai-quota.error", "text/plain", fmt.Sprintf("redis error:%v", err))
|
||||
return
|
||||
}
|
||||
util.SendResponse(http.StatusOK, "ai-quota.deltaquota", "text/plain", "delta quota successful")
|
||||
})
|
||||
if err != nil {
|
||||
util.SendResponse(http.StatusServiceUnavailable, "ai-quota.error", "text/plain", fmt.Sprintf("redis error:%v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionPause
|
||||
}
|
||||
61
plugins/wasm-go/extensions/ai-quota/plugin.yaml
Normal file
61
plugins/wasm-go/extensions/ai-quota/plugin.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-quota
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig: {}
|
||||
defaultConfigDisable: true
|
||||
matchRules:
|
||||
- config:
|
||||
redis_key_prefix: "chat_quota:"
|
||||
admin_consumer: consumer3
|
||||
admin_path: /quota
|
||||
redis:
|
||||
service_name: redis-service.default.svc.cluster.local
|
||||
service_port: 6379
|
||||
timeout: 2000
|
||||
configDisable: false
|
||||
ingress:
|
||||
- qwen
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 280
|
||||
url: oci://registry.cn-hangzhou.aliyuncs.com/2456868764/ai-quota:1.0.8
|
||||
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-statistics
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
enable: true
|
||||
defaultConfigDisable: false
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 250
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-statistics:1.0.0
|
||||
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: wasm-keyauth
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
consumers:
|
||||
- credential: "Bearer credential1"
|
||||
name: consumer1
|
||||
- credential: "Bearer credential2"
|
||||
name: consumer2
|
||||
- credential: "Bearer credential3"
|
||||
name: consumer3
|
||||
global_auth: true
|
||||
keys:
|
||||
- authorization
|
||||
in_header: true
|
||||
defaultConfigDisable: false
|
||||
priority: 300
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/key-auth:1.0.0
|
||||
imagePullPolicy: Always
|
||||
22
plugins/wasm-go/extensions/ai-quota/util/http.go
Normal file
22
plugins/wasm-go/extensions/ai-quota/util/http.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package util
|
||||
|
||||
import "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
|
||||
const (
|
||||
HeaderContentType = "Content-Type"
|
||||
|
||||
MimeTypeTextPlain = "text/plain"
|
||||
MimeTypeApplicationJson = "application/json"
|
||||
)
|
||||
|
||||
func SendResponse(statusCode uint32, statusCodeDetails string, contentType, body string) error {
|
||||
return proxywasm.SendHttpResponseWithDetail(statusCode, statusCodeDetails, CreateHeaders(HeaderContentType, contentType), []byte(body), -1)
|
||||
}
|
||||
|
||||
func CreateHeaders(kvs ...string) [][2]string {
|
||||
headers := make([][2]string, 0, len(kvs)/2)
|
||||
for i := 0; i < len(kvs); i += 2 {
|
||||
headers = append(headers, [2]string{kvs[i], kvs[i+1]})
|
||||
}
|
||||
return headers
|
||||
}
|
||||
@@ -3,11 +3,12 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
re "github.com/wasilibs/go-re2"
|
||||
"github.com/zmap/go-iptree/iptree"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 限流规则项类型
|
||||
|
||||
@@ -5,8 +5,7 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
|
||||
@@ -16,15 +16,16 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/resp"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -88,12 +89,10 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ClusterKeyRateLimitCon
|
||||
args := []interface{}{configItem.count, configItem.timeWindow}
|
||||
// 执行限流逻辑
|
||||
err := config.redisClient.Eval(FixedWindowScript, 1, keys, args, func(response resp.Value) {
|
||||
defer func() {
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
}()
|
||||
resultArray := response.Array()
|
||||
if len(resultArray) != 3 {
|
||||
log.Errorf("redis response parse error, response: %v", response)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
}
|
||||
context := LimitContext{
|
||||
@@ -106,6 +105,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ClusterKeyRateLimitCon
|
||||
rejected(config, context)
|
||||
} else {
|
||||
ctx.SetContext(LimitContextKey, context)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -2,9 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/zmap/go-iptree/iptree"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zmap/go-iptree/iptree"
|
||||
)
|
||||
|
||||
// parseIPNet 解析Ip段配置
|
||||
|
||||
@@ -271,3 +271,14 @@ Content-Length: 0
|
||||
```
|
||||
|
||||
`ext-auth` 服务返回响应头中如果包含 `x-user-id` 和 `x-auth-version`,网关调用upstream时的请求中会带上这两个请求头
|
||||
|
||||
#### x-forwarded-* header
|
||||
在endpoint_mode为forward_auth时,higress会自动生成并发送以下header至鉴权服务。
|
||||
|
||||
| Header | 说明 |
|
||||
|--------------------|-------------------------------------|
|
||||
| x-forwarded-proto | 原始请求的scheme,比如http/https |
|
||||
| x-forwarded-method | 原始请求的方法,比如get/post/delete/patch |
|
||||
| x-forwarded-host | 原始请求的host |
|
||||
| x-forwarded-uri | 原始请求的path,包含路径参数,比如/v1/app?test=true |
|
||||
| x-forwarded-for | 原始请求的客户端IP地址 |
|
||||
@@ -2,12 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"ext-auth/expr"
|
||||
"fmt"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"ext-auth/expr"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -2,9 +2,10 @@ package expr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringMatcher(t *testing.T) {
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -36,6 +37,12 @@ const (
|
||||
HeaderFailureModeAllow string = "x-envoy-auth-failure-mode-allowed"
|
||||
HeaderOriginalMethod string = "x-original-method"
|
||||
HeaderOriginalUri string = "x-original-uri"
|
||||
|
||||
// Currently, x-forwarded-xxx headers only apply for forward_auth.
|
||||
HeaderXForwardedProto = "x-forwarded-proto"
|
||||
HeaderXForwardedMethod = "x-forwarded-method"
|
||||
HeaderXForwardedUri = "x-Forwarded-uri"
|
||||
HeaderXForwardedHost = "x-Forwarded-host"
|
||||
)
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config ExtAuthConfig, log wrapper.Log) types.Action {
|
||||
@@ -94,6 +101,10 @@ func checkExtAuth(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, lo
|
||||
if httpServiceConfig.endpointMode == EndpointModeForwardAuth {
|
||||
extAuthReqHeaders.Set(HeaderOriginalMethod, ctx.Method())
|
||||
extAuthReqHeaders.Set(HeaderOriginalUri, ctx.Path())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedProto, ctx.Scheme())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedMethod, ctx.Method())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedUri, ctx.Path())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedHost, ctx.Host())
|
||||
}
|
||||
|
||||
requestMethod := httpServiceConfig.requestMethod
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
func sendResponse(statusCode uint32, statusCodeDetailData string, headers http.Header) error {
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
`frontend-gray`插件实现了前端用户灰度的的功能,通过此插件,不但可以用于业务`A/B实验`,同时通过`可灰度`配合`可监控`,`可回滚`策略保证系统发布运维的稳定性。
|
||||
|
||||
## 配置字段
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
|
||||
| `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 |
|
||||
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
|
||||
| `rules` | array of object | 非必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
|
||||
| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |
|
||||
| `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|--------------|----|-----|----------------------------------------------------------------------------------------------------|
|
||||
| `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 |
|
||||
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
|
||||
| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
|
||||
| `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 |
|
||||
| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |
|
||||
| `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 |
|
||||
|
||||
`rules`字段配置说明:
|
||||
|
||||
@@ -20,6 +21,19 @@
|
||||
| `grayTagKey` | string | 非必填 | - | 用户分类打标的标签key值,来自Cookie |
|
||||
| `grayTagValue` | array of string | 非必填 | - | 用户分类打标的标签value值,来自Cookie |
|
||||
|
||||
`rewrite`字段配置说明:
|
||||
> `indexRouting`首页重写和`fileRouting`文件重写,本质都是前缀匹配,比如`/app1`: `/mfe/app1/{version}/index.html`代表/app1为前缀的请求,路由到`/mfe/app1/{version}/index.html`页面上,其中`{version}`代表版本号,在运行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换。
|
||||
|
||||
> `{version}` 作为保留字段,在执行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换前端版本。
|
||||
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------|--------------|------|-----|------------------------------|
|
||||
| `host` | string | 非必填 | - | host地址,如果是OSS则设置为 VPC 内网访问地址 |
|
||||
| `notFoundUri` | string | 非必填 | - | 404 页面配置 |
|
||||
| `indexRouting` | map of string to string | 非必填 | - | 用于定义首页重写路由规则。每个键 (Key) 表示首页的路由路径,值 (Value) 则指向重定向的目标文件。例如,键为 `/app1` 对应的值为 `/mfe/app1/{version}/index.html`。生效version为`0.0.1`, 访问路径为 `/app1`,则重定向到 `/mfe/app1/0.0.1/index.html`。 |
|
||||
| `fileRouting` | map of string to string | 非必填 | - | 用于定义资源文件重写路由规则。每个键 (Key) 表示资源访问路径,值 (Value) 则指向重定向的目标文件。例如,键为 `/app1/` 对应的值为 `/mfe/app1/{version}`。生效version为`0.0.1`,访问路径为 `/app1/js/a.js`,则重定向到 `/mfe/app1/0.0.1/js/a.js`。 |
|
||||
|
||||
`baseDeployment`字段配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
@@ -28,11 +42,12 @@
|
||||
|
||||
`grayDeployments`字段配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------|--------|------|-----|----------------------------|
|
||||
| `version` | string | 必填 | - | Gray版本的版本号,如果命中灰度规则,则使用此版本 |
|
||||
| `name` | string | 必填 | - | 规则名称和`rules[].name`关联, |
|
||||
| `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------|--------|------|-----|-------------------------------------------------|
|
||||
| `version` | string | 必填 | - | Gray版本的版本号,如果命中灰度规则,则使用此版本。如果是非CDN部署,在header添加`x-higress-tag` |
|
||||
| `backendVersion` | string | 必填 | - | 后端灰度版本,会在`XHR/Fetch`请求的header头添加 `x-mse-tag`到后端 |
|
||||
| `name` | string | 必填 | - | 规则名称和`rules[].name`关联, |
|
||||
| `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 |
|
||||
|
||||
## 配置示例
|
||||
### 基础配置
|
||||
@@ -100,4 +115,51 @@ cookie存在`appInfo`的JSON数据,其中包含`userId`字段为当前的唯
|
||||
- cookie中`userid`等于`00000002`或者`00000003`
|
||||
- cookie中`level`等于`level3`或者`level5`的用户
|
||||
|
||||
否则使用`version: base`版本
|
||||
否则使用`version: base`版本
|
||||
|
||||
### rewrite重写配置
|
||||
> 一般用于CDN部署场景
|
||||
```yml
|
||||
grayKey: userid
|
||||
rules:
|
||||
- name: inner-user
|
||||
grayKeyValue:
|
||||
- '00000001'
|
||||
- '00000005'
|
||||
- name: beta-user
|
||||
grayKeyValue:
|
||||
- '00000002'
|
||||
- '00000003'
|
||||
grayTagKey: level
|
||||
grayTagValue:
|
||||
- level3
|
||||
- level5
|
||||
rewrite:
|
||||
host: frontend-gray.oss-cn-shanghai-internal.aliyuncs.com
|
||||
notFoundUri: /mfe/app1/dev/404.html
|
||||
indexRouting:
|
||||
/app1: '/mfe/app1/{version}/index.html'
|
||||
/: '/mfe/app1/{version}/index.html',
|
||||
fileRouting:
|
||||
/: '/mfe/app1/{version}'
|
||||
/app1/: '/mfe/app1/{version}'
|
||||
baseDeployment:
|
||||
version: base
|
||||
grayDeployments:
|
||||
- name: beta-user
|
||||
version: gray
|
||||
enabled: true
|
||||
```
|
||||
|
||||
`{version}`会在运行过程中动态替换为真正的版本
|
||||
|
||||
#### indexRouting:首页路由配置
|
||||
访问 `/app1`, `/app123`,`/app1/index.html`, `/app1/xxx`, `/xxxx` 都会路由到'/mfe/app1/{version}/index.html'
|
||||
|
||||
#### fileRouting:文件路由配置
|
||||
下面文件映射均生效
|
||||
- `/js/a.js` => `/mfe/app1/v1.0.0/js/a.js`
|
||||
- `/js/template/a.js` => `/mfe/app1/v1.0.0/js/template/a.js`
|
||||
- `/app1/js/a.js` => `/mfe/app1/v1.0.0/js/a.js`
|
||||
- `/app1/js/template/a.js` => `/mfe/app1/v1.0.0/js/template/a.js`
|
||||
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
XHigressTag = "x-higress-tag"
|
||||
XPreHigressTag = "x-pre-higress-tag"
|
||||
XMseTag = "x-mse-tag"
|
||||
IsHTML = "is_html"
|
||||
IsIndex = "is_index"
|
||||
NotFound = "not_found"
|
||||
)
|
||||
|
||||
type LogInfo func(format string, args ...interface{})
|
||||
|
||||
type GrayRule struct {
|
||||
Name string
|
||||
GrayKeyValue []interface{}
|
||||
GrayKeyValue []string
|
||||
GrayTagKey string
|
||||
GrayTagValue []interface{}
|
||||
GrayTagValue []string
|
||||
}
|
||||
|
||||
type BaseDeployment struct {
|
||||
@@ -18,35 +27,46 @@ type BaseDeployment struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type GrayDeployments struct {
|
||||
Name string
|
||||
Version string
|
||||
Enabled bool
|
||||
type GrayDeployment struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
Version string
|
||||
BackendVersion string
|
||||
}
|
||||
|
||||
type Rewrite struct {
|
||||
Host string
|
||||
NotFound string
|
||||
Index map[string]string
|
||||
File map[string]string
|
||||
}
|
||||
|
||||
type GrayConfig struct {
|
||||
GrayKey string
|
||||
GraySubKey string
|
||||
Rules []*GrayRule
|
||||
Rewrite *Rewrite
|
||||
BaseDeployment *BaseDeployment
|
||||
GrayDeployments []*GrayDeployments
|
||||
GrayDeployments []*GrayDeployment
|
||||
}
|
||||
|
||||
func interfacesFromJSONResult(results []gjson.Result) []interface{} {
|
||||
var interfaces []interface{}
|
||||
for _, result := range results {
|
||||
switch v := result.Value().(type) {
|
||||
case float64:
|
||||
// 当 v 是 float64 时,将其转换为字符串
|
||||
interfaces = append(interfaces, strconv.FormatFloat(v, 'f', -1, 64))
|
||||
default:
|
||||
// 其它类型不改变,直接追加
|
||||
interfaces = append(interfaces, v)
|
||||
}
|
||||
func convertToStringList(results []gjson.Result) []string {
|
||||
interfaces := make([]string, len(results)) // 预分配切片容量
|
||||
for i, result := range results {
|
||||
interfaces[i] = result.String() // 使用 String() 方法直接获取字符串
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func convertToStringMap(result gjson.Result) map[string]string {
|
||||
m := make(map[string]string)
|
||||
result.ForEach(func(key, value gjson.Result) bool {
|
||||
m[key.String()] = value.String()
|
||||
return true // keep iterating
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
// 解析 GrayKey
|
||||
grayConfig.GrayKey = json.Get("grayKey").String()
|
||||
@@ -57,14 +77,20 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
for _, rule := range rules {
|
||||
grayRule := GrayRule{
|
||||
Name: rule.Get("name").String(),
|
||||
GrayKeyValue: interfacesFromJSONResult(rule.Get("grayKeyValue").Array()), // 使用辅助函数将 []gjson.Result 转换为 []interface{}
|
||||
GrayKeyValue: convertToStringList(rule.Get("grayKeyValue").Array()),
|
||||
GrayTagKey: rule.Get("grayTagKey").String(),
|
||||
GrayTagValue: interfacesFromJSONResult(rule.Get("grayTagValue").Array()),
|
||||
GrayTagValue: convertToStringList(rule.Get("grayTagValue").Array()),
|
||||
}
|
||||
grayConfig.Rules = append(grayConfig.Rules, &grayRule)
|
||||
}
|
||||
grayConfig.Rewrite = &Rewrite{
|
||||
Host: json.Get("rewrite.host").String(),
|
||||
NotFound: json.Get("rewrite.notFoundUri").String(),
|
||||
Index: convertToStringMap(json.Get("rewrite.indexRouting")),
|
||||
File: convertToStringMap(json.Get("rewrite.fileRouting")),
|
||||
}
|
||||
|
||||
// 解析 deploy
|
||||
// 解析 deployment
|
||||
baseDeployment := json.Get("baseDeployment")
|
||||
grayDeployments := json.Get("grayDeployments").Array()
|
||||
|
||||
@@ -73,10 +99,11 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
Version: baseDeployment.Get("version").String(),
|
||||
}
|
||||
for _, item := range grayDeployments {
|
||||
grayConfig.GrayDeployments = append(grayConfig.GrayDeployments, &GrayDeployments{
|
||||
Name: item.Get("name").String(),
|
||||
Version: item.Get("version").String(),
|
||||
Enabled: item.Get("enabled").Bool(),
|
||||
grayConfig.GrayDeployments = append(grayConfig.GrayDeployments, &GrayDeployment{
|
||||
Name: item.Get("name").String(),
|
||||
Enabled: item.Get("enabled").Bool(),
|
||||
Version: item.Get("version").String(),
|
||||
BackendVersion: item.Get("backendVersion").String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,7 @@ static_resources:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: |
|
||||
{
|
||||
"grayKey": "UserInfo",
|
||||
"graySubKey": "userCode",
|
||||
"grayKey": "userId",
|
||||
"rules": [
|
||||
{
|
||||
"name": "inner-user",
|
||||
@@ -70,13 +69,26 @@ static_resources:
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrite": {
|
||||
"host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com",
|
||||
"notFoundUri": "/mfe/app1/dev/404.html",
|
||||
"indexRouting": {
|
||||
"/app1": "/mfe/app1/{version}/index.html",
|
||||
"/": "/mfe/app1/{version}/index.html"
|
||||
},
|
||||
"fileRouting": {
|
||||
"/": "/mfe/app1/{version}",
|
||||
"/app1": "/mfe/app1/{version}"
|
||||
}
|
||||
},
|
||||
"baseDeployment": {
|
||||
"version": "base"
|
||||
"version": "dev"
|
||||
},
|
||||
"grayDeployments": [
|
||||
{
|
||||
"name": "beta-user",
|
||||
"version": "gray",
|
||||
"version": "0.0.1",
|
||||
"backendVersion": "beta",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
@@ -98,5 +110,5 @@ static_resources:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin.org
|
||||
address: frontend-gray-cn-shanghai.oss-cn-shanghai.aliyuncs.com
|
||||
port_value: 80
|
||||
@@ -5,9 +5,9 @@ go 1.18
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.3-0.20240727022514-bccfbde62188
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e h1:0b2UXrEpotHwWgwvgvkXnyKWuxTXtzfKu6c2YpRV+zw=
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e/go.mod h1:10jQXKsYFUF7djs+Oy7t82f4dbie9pISfP9FJwpPLuk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5 h1:VOLL3m442IHCSu8mR5AZ4sc6LVT9X0w1hdqDI7oB9jY=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5/go.mod h1:kr3V9Ntbspj1eSrX8rgjBsdMXkGupYEf+LM72caGPQc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1+incompatible/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226065437-8f7a0b3c9071 h1:STb5rOHRZOzoiAa+gTz2LFqO1nYj7U/1eIVUJJadU4A=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226065437-8f7a0b3c9071/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/pmezard/go-difflib v1.0.0+incompatible/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -27,7 +23,6 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/util"
|
||||
|
||||
@@ -15,6 +19,9 @@ func main() {
|
||||
"frontend-gray",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeader),
|
||||
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
|
||||
wrapper.ProcessStreamingResponseBodyBy(onStreamingResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,55 +31,146 @@ func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.L
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterGrayRule 过滤灰度规则
|
||||
func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, log wrapper.Log) *config.GrayDeployments {
|
||||
for _, grayDeployment := range grayConfig.GrayDeployments {
|
||||
if !grayDeployment.Enabled {
|
||||
// 跳过Enabled=false
|
||||
continue
|
||||
}
|
||||
grayRule := util.GetRule(grayConfig.Rules, grayDeployment.Name)
|
||||
// 首先:先校验用户名单ID
|
||||
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
|
||||
if util.Contains(grayRule.GrayKeyValue, grayKeyValue) {
|
||||
log.Infof("x-mse-tag: %s, grayKeyValue: %s", grayDeployment.Version, grayKeyValue)
|
||||
return grayDeployment
|
||||
}
|
||||
}
|
||||
// 第二:校验Cookie中的 GrayTagKey
|
||||
if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 {
|
||||
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
grayTagValue := util.GetValueByCookie(cookieStr, grayRule.GrayTagKey)
|
||||
if util.Contains(grayRule.GrayTagValue, grayTagValue) {
|
||||
log.Infof("x-mse-tag: %s, grayTag: %s=%s", grayDeployment.Version, grayRule.GrayTagKey, grayTagValue)
|
||||
return grayDeployment
|
||||
}
|
||||
}
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||
if !util.IsGrayEnabled(grayConfig) {
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Infof("x-mse-tag: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue)
|
||||
return nil
|
||||
|
||||
cookies, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
path, _ := proxywasm.GetHttpRequestHeader(":path")
|
||||
fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
|
||||
|
||||
isIndex := util.IsIndexRequest(fetchMode, path)
|
||||
hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0
|
||||
grayKeyValue := util.GetGrayKey(util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey), grayConfig.GraySubKey)
|
||||
|
||||
// 如果有重写的配置,则进行重写
|
||||
if hasRewrite {
|
||||
// 禁止重新路由,要在更改Header之前操作,否则会失效
|
||||
ctx.DisableReroute()
|
||||
}
|
||||
|
||||
// 删除Accept-Encoding,避免压缩, 如果是压缩的内容,后续插件就没法处理了
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
|
||||
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue, log.Infof)
|
||||
frontendVersion := util.GetVersion(grayConfig.BaseDeployment.Version, cookies, isIndex)
|
||||
backendVersion := ""
|
||||
|
||||
// 命中灰度规则
|
||||
if grayDeployment != nil {
|
||||
frontendVersion = util.GetVersion(grayDeployment.Version, cookies, isIndex)
|
||||
backendVersion = grayDeployment.BackendVersion
|
||||
}
|
||||
|
||||
proxywasm.AddHttpRequestHeader(config.XHigressTag, frontendVersion)
|
||||
|
||||
ctx.SetContext(config.XPreHigressTag, frontendVersion)
|
||||
ctx.SetContext(config.XMseTag, backendVersion)
|
||||
ctx.SetContext(config.IsIndex, isIndex)
|
||||
|
||||
rewrite := grayConfig.Rewrite
|
||||
if rewrite.Host != "" {
|
||||
proxywasm.ReplaceHttpRequestHeader("HOST", rewrite.Host)
|
||||
}
|
||||
|
||||
if hasRewrite {
|
||||
rewritePath := path
|
||||
if isIndex {
|
||||
rewritePath = util.IndexRewrite(path, frontendVersion, grayConfig.Rewrite.Index)
|
||||
} else {
|
||||
rewritePath = util.PrefixFileRewrite(path, frontendVersion, grayConfig.Rewrite.File)
|
||||
}
|
||||
log.Infof("rewrite path: %s %s %v", path, frontendVersion, rewritePath)
|
||||
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||
// 优先从cookie中获取,如果拿不到再从header中获取
|
||||
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
grayHeaderKey, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey)
|
||||
grayKeyValue := util.GetValueByCookie(cookieStr, grayConfig.GrayKey)
|
||||
proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
// 优先从Cookie中获取,否则从header中获取
|
||||
if grayKeyValue == "" {
|
||||
grayKeyValue = grayHeaderKey
|
||||
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||
if !util.IsGrayEnabled(grayConfig) {
|
||||
return types.ActionContinue
|
||||
}
|
||||
// 如果有子key, 尝试从子key中获取值
|
||||
if grayConfig.GraySubKey != "" {
|
||||
subKeyValue := util.GetBySubKey(grayKeyValue, grayConfig.GraySubKey)
|
||||
if subKeyValue != "" {
|
||||
grayKeyValue = subKeyValue
|
||||
status, err := proxywasm.GetHttpResponseHeader(":status")
|
||||
contentType, _ := proxywasm.GetHttpResponseHeader("Content-Type")
|
||||
if err != nil || status != "200" {
|
||||
isIndex := ctx.GetContext(config.IsIndex)
|
||||
if status == "404" {
|
||||
if grayConfig.Rewrite.NotFound != "" && isIndex != nil && isIndex.(bool) {
|
||||
ctx.SetContext(config.NotFound, true)
|
||||
responseHeaders, _ := proxywasm.GetHttpResponseHeaders()
|
||||
headersMap := util.ConvertHeaders(responseHeaders)
|
||||
headersMap[":status"][0] = "200"
|
||||
headersMap["content-type"][0] = "text/html"
|
||||
delete(headersMap, "content-length")
|
||||
proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap))
|
||||
ctx.BufferResponseBody()
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
ctx.DontReadResponseBody()
|
||||
}
|
||||
}
|
||||
log.Errorf("error status: %s, error message: %v", status, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
grayDeployment := FilterGrayRule(&grayConfig, grayKeyValue, log)
|
||||
if grayDeployment != nil {
|
||||
proxywasm.AddHttpRequestHeader("x-mse-tag", grayDeployment.Version)
|
||||
|
||||
// 删除content-length,可能要修改Response返回值
|
||||
proxywasm.RemoveHttpResponseHeader("Content-Length")
|
||||
|
||||
// 删除Content-Disposition,避免自动下载文件
|
||||
proxywasm.RemoveHttpResponseHeader("Content-Disposition")
|
||||
|
||||
if strings.HasPrefix(contentType, "text/html") {
|
||||
ctx.SetContext(config.IsHTML, true)
|
||||
// 不会进去Streaming 的Body处理
|
||||
ctx.BufferResponseBody()
|
||||
|
||||
// 添加Cache-Control 头部,禁止缓存
|
||||
proxywasm.ReplaceHttpRequestHeader("Cache-Control", "no-cache, no-store")
|
||||
|
||||
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
|
||||
backendVersion := ctx.GetContext(config.XMseTag).(string)
|
||||
|
||||
// 设置当前的前端版本
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", config.XPreHigressTag, frontendVersion))
|
||||
// 设置后端的前端版本
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", config.XMseTag, backendVersion))
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action {
|
||||
if !util.IsGrayEnabled(grayConfig) {
|
||||
return types.ActionContinue
|
||||
}
|
||||
backendVersion := ctx.GetContext(config.XMseTag)
|
||||
isHtml := ctx.GetContext(config.IsHTML)
|
||||
isIndex := ctx.GetContext(config.IsIndex)
|
||||
notFoundUri := ctx.GetContext(config.NotFound)
|
||||
if isIndex != nil && isIndex.(bool) && notFoundUri != nil && notFoundUri.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
|
||||
client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host})
|
||||
client.Get(grayConfig.Rewrite.NotFound, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
proxywasm.ReplaceHttpResponseBody(responseBody)
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}, 1500)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
// 以text/html 开头,将 cookie转到cookie
|
||||
if isHtml != nil && isHtml.(bool) && backendVersion != nil && backendVersion.(string) != "" {
|
||||
newText := strings.ReplaceAll(string(body), "</head>", `<script>
|
||||
!function(e,t){function n(e){var n="; "+t.cookie,r=n.split("; "+e+"=");return 2===r.length?r.pop().split(";").shift():null}var r=n("x-mse-tag");if(!r)return null;var s=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(e,t,n,a,i){return this._XHR=!0,this.addEventListener("readystatechange",function(){1===this.readyState&&r&&this.setRequestHeader("x-mse-tag",r)}),s.apply(this,arguments)};var a=e.fetch;e.fetch=function(e,t){return"undefined"==typeof t&&(t={}),"undefined"==typeof t.headers&&(t.headers={}),r&&(t.headers["x-mse-tag"]=r),a.apply(this,[e,t])}}(window,document);
|
||||
</script>
|
||||
</head>`)
|
||||
if err := proxywasm.ReplaceHttpResponseBody([]byte(newText)); err != nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.GrayConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
|
||||
return chunk
|
||||
}
|
||||
|
||||
@@ -2,39 +2,56 @@ package util
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// GetValueByCookie 根据 cookieStr 和 cookieName 获取 cookie 值
|
||||
func GetValueByCookie(cookieStr string, cookieName string) string {
|
||||
if cookieStr == "" {
|
||||
func IsGrayEnabled(grayConfig config.GrayConfig) bool {
|
||||
// 检查是否存在重写主机
|
||||
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查灰度部署是否为 nil 或空
|
||||
grayDeployments := grayConfig.GrayDeployments
|
||||
if grayDeployments != nil && len(grayDeployments) > 0 {
|
||||
for _, grayDeployment := range grayDeployments {
|
||||
if grayDeployment.Enabled {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ExtractCookieValueByKey 根据 cookie 和 key 获取 cookie 值
|
||||
func ExtractCookieValueByKey(cookie string, key string) string {
|
||||
if cookie == "" {
|
||||
return ""
|
||||
}
|
||||
cookies := strings.Split(cookieStr, ";")
|
||||
curCookieName := cookieName + "="
|
||||
var foundCookieValue string
|
||||
var found bool
|
||||
// 遍历找到 cookie 对并处理
|
||||
for _, cookie := range cookies {
|
||||
cookie = strings.TrimSpace(cookie) // 清理空白符
|
||||
if strings.HasPrefix(cookie, curCookieName) {
|
||||
foundCookieValue = cookie[len(curCookieName):]
|
||||
found = true
|
||||
value := ""
|
||||
pairs := strings.Split(cookie, ";")
|
||||
for _, pair := range pairs {
|
||||
pair = strings.TrimSpace(pair)
|
||||
kv := strings.Split(pair, "=")
|
||||
if kv[0] == key {
|
||||
value = kv[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
return foundCookieValue
|
||||
return value
|
||||
}
|
||||
|
||||
// contains 检查切片 slice 中是否含有元素 value。
|
||||
func Contains(slice []interface{}, value string) bool {
|
||||
func ContainsValue(slice []string, value string) bool {
|
||||
for _, item := range slice {
|
||||
if item == value {
|
||||
return true
|
||||
@@ -43,6 +60,30 @@ func Contains(slice []interface{}, value string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// headers: [][2]string -> map[string][]string
|
||||
func ConvertHeaders(hs [][2]string) map[string][]string {
|
||||
ret := make(map[string][]string)
|
||||
for _, h := range hs {
|
||||
k, v := strings.ToLower(h[0]), h[1]
|
||||
ret[k] = append(ret[k], v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// headers: map[string][]string -> [][2]string
|
||||
func ReconvertHeaders(hs map[string][]string) [][2]string {
|
||||
var ret [][2]string
|
||||
for k, vs := range hs {
|
||||
for _, v := range vs {
|
||||
ret = append(ret, [2]string{k, v})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(ret, func(i, j int) bool {
|
||||
return ret[i][0] < ret[j][0]
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func GetRule(rules []*config.GrayRule, name string) *config.GrayRule {
|
||||
for _, rule := range rules {
|
||||
if rule.Name == name {
|
||||
@@ -52,7 +93,66 @@ func GetRule(rules []*config.GrayRule, name string) *config.GrayRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 检查是否是页面
|
||||
var indexSuffixes = []string{
|
||||
".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig",
|
||||
}
|
||||
|
||||
// IsIndexRequest determines if the request is an index request
|
||||
func IsIndexRequest(fetchMode string, p string) bool {
|
||||
if fetchMode == "cors" {
|
||||
return false
|
||||
}
|
||||
ext := path.Ext(p)
|
||||
return ext == "" || ContainsValue(indexSuffixes, ext)
|
||||
}
|
||||
|
||||
// 首页Rewrite
|
||||
func IndexRewrite(path, version string, matchRules map[string]string) string {
|
||||
for prefix, rewrite := range matchRules {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
newPath := strings.Replace(rewrite, "{version}", version, -1)
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func PrefixFileRewrite(path, version string, matchRules map[string]string) string {
|
||||
var matchedPrefix, replacement string
|
||||
for prefix, template := range matchRules {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
if len(prefix) > len(matchedPrefix) { // 找到更长的前缀
|
||||
matchedPrefix = prefix
|
||||
replacement = strings.Replace(template, "{version}", version, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 将path 中的前缀部分用 replacement 替换掉
|
||||
newPath := strings.Replace(path, matchedPrefix, replacement+"/", 1)
|
||||
return filepath.Clean(newPath)
|
||||
}
|
||||
|
||||
func GetVersion(version string, cookies string, isIndex bool) string {
|
||||
if isIndex {
|
||||
return version
|
||||
}
|
||||
// 来自Cookie中的版本
|
||||
cookieVersion := ExtractCookieValueByKey(cookies, config.XPreHigressTag)
|
||||
// cookie 中为空,返回当前版本
|
||||
if cookieVersion == "" {
|
||||
return version
|
||||
}
|
||||
|
||||
// cookie 中和当前版本不相同,返回cookie中值
|
||||
if cookieVersion != version {
|
||||
return cookieVersion
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// 从cookie中解析出灰度信息
|
||||
func getBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 首先对 URL 编码的字符串进行解码
|
||||
jsonStr, err := url.QueryUnescape(grayInfoStr)
|
||||
if err != nil {
|
||||
@@ -68,3 +168,43 @@ func GetBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 返回字符串形式的值
|
||||
return value.String()
|
||||
}
|
||||
|
||||
func GetGrayKey(grayKeyValue string, graySubKey string) string {
|
||||
// 如果有子key, 尝试从子key中获取值
|
||||
if graySubKey != "" {
|
||||
subKeyValue := getBySubKey(grayKeyValue, graySubKey)
|
||||
if subKeyValue != "" {
|
||||
grayKeyValue = subKeyValue
|
||||
}
|
||||
}
|
||||
return grayKeyValue
|
||||
}
|
||||
|
||||
// FilterGrayRule 过滤灰度规则
|
||||
func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, logInfof func(format string, args ...interface{})) *config.GrayDeployment {
|
||||
for _, grayDeployment := range grayConfig.GrayDeployments {
|
||||
if !grayDeployment.Enabled {
|
||||
// 跳过Enabled=false
|
||||
continue
|
||||
}
|
||||
grayRule := GetRule(grayConfig.Rules, grayDeployment.Name)
|
||||
// 首先:先校验用户名单ID
|
||||
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
|
||||
if ContainsValue(grayRule.GrayKeyValue, grayKeyValue) {
|
||||
logInfof("frontendVersion: %s, grayKeyValue: %s", grayDeployment.Version, grayKeyValue)
|
||||
return grayDeployment
|
||||
}
|
||||
}
|
||||
// 第二:校验Cookie中的 GrayTagKey
|
||||
if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 {
|
||||
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
grayTagValue := ExtractCookieValueByKey(cookieStr, grayRule.GrayTagKey)
|
||||
if ContainsValue(grayRule.GrayTagValue, grayTagValue) {
|
||||
logInfof("frontendVersion: %s, grayTag: %s=%s", grayDeployment.Version, grayRule.GrayTagKey, grayTagValue)
|
||||
return grayDeployment
|
||||
}
|
||||
}
|
||||
}
|
||||
logInfof("frontendVersion: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetValueByCookie(t *testing.T) {
|
||||
func TestExtractCookieValueByKey(t *testing.T) {
|
||||
var tests = []struct {
|
||||
cookie, cookieKey, output string
|
||||
}{
|
||||
@@ -19,23 +19,85 @@ func TestGetValueByCookie(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
testName := test.cookie
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := GetValueByCookie(test.cookie, test.cookieKey)
|
||||
output := ExtractCookieValueByKey(test.cookie, test.cookieKey)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeJsonCookie(t *testing.T) {
|
||||
// 测试首页Rewrite重写
|
||||
func TestIndexRewrite(t *testing.T) {
|
||||
matchRules := map[string]string{
|
||||
"/app1": "/mfe/app1/{version}/index.html",
|
||||
"/": "/mfe/app1/{version}/index.html",
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
userInfoStr, grayJsonKey, output string
|
||||
path, output string
|
||||
}{
|
||||
{"{%22password%22:%22$2a$10$YAvYjA6783YeCi44/M395udIZ4Ll2iyKkQCzePaYx5NNG/aIWgICG%22%2C%22username%22:%22%E8%B0%A2%E6%99%AE%E8%80%80%22%2C%22authorities%22:[]%2C%22accountNonExpired%22:true%2C%22accountNonLocked%22:true%2C%22credentialsNonExpired%22:true%2C%22enabledd%22:true%2C%22id%22:838925798835720200%2C%22mobile%22:%22%22%2C%22userCode%22:%22noah%22%2C%22userName%22:%22%E8%B0%A2%E6%99%AE%E8%80%80%22%2C%22orgId%22:10%2C%22ocId%22:87%2C%22userType%22:%22OWN%22%2C%22firstLogin%22:false%2C%22ownOrgId%22:null%2C%22clientCode%22:%22%22%2C%22clientType%22:null%2C%22country%22:%22UAE%22%2C%22isGuide%22:null%2C%22acctId%22:null%2C%22userToken%22:null%2C%22deviceId%22:%223a47fec00a59d140%22%2C%22ocCode%22:%2299990002%22%2C%22secondType%22:%22dtl%22%2C%22vendorCode%22:%2210000001%22%2C%22status%22:%22ACTIVE%22%2C%22isDelete%22:false%2C%22email%22:%22%22%2C%22deleteStatus%22:null%2C%22deleteRequestDate%22:null%2C%22wechatId%22:null%2C%22userMfaInfoDTO%22:{%22checkMfa%22:false%2C%22checkSuccess%22:false%2C%22mobile%22:null%2C%22email%22:null%2C%22wechatId%22:null%2C%22totpSecret%22:null}}",
|
||||
"userCode", "noah"},
|
||||
{"/app1/", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app123", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/index.html", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/index.jsp", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/xxx", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/xxxx", "/mfe/app1/v1.0.0/index.html"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.userInfoStr
|
||||
testName := test.path
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := GetBySubKey(test.userInfoStr, test.grayJsonKey)
|
||||
output := IndexRewrite(testName, "v1.0.0", matchRules)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixFileRewrite(t *testing.T) {
|
||||
matchRules := map[string]string{
|
||||
// 前缀匹配
|
||||
"/": "/mfe/app1/{version}",
|
||||
"/app2/": "/mfe/app1/{version}",
|
||||
"/app1/": "/mfe/app1/{version}",
|
||||
"/app1/prefix2": "/mfe/app1/{version}",
|
||||
"/mfe/app1": "/mfe/app1/{version}",
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
path, output string
|
||||
}{
|
||||
{"/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/prefix2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/prefix2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/mfe/app1/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.path
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := PrefixFileRewrite(testName, "v1.0.0", matchRules)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIndexRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
fetchMode string
|
||||
p string
|
||||
output bool
|
||||
}{
|
||||
{"cors", "/js/a.js", false},
|
||||
{"no-cors", "/js/a.js", false},
|
||||
{"no-cors", "/images/a.png", false},
|
||||
{"no-cors", "/index", true},
|
||||
{"cors", "/inde", false},
|
||||
{"no-cors", "/index.html", true},
|
||||
{"no-cors", "/demo.php", true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testPath := test.p
|
||||
t.Run(testPath, func(t *testing.T) {
|
||||
output := IsIndexRequest(test.fetchMode, testPath)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
|
||||
144
plugins/wasm-rust/Cargo.lock
generated
144
plugins/wasm-rust/Cargo.lock
generated
@@ -4,15 +4,22 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -21,9 +28,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.9"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@@ -32,11 +39,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.13.2"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -52,24 +60,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.6"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.144"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
@@ -82,60 +93,129 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.1"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxy-wasm"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "823b744520cd4a54ba7ebacbffe4562e839d6dcd8f89209f96a1ace4f5229cd4"
|
||||
version = "0.2.2"
|
||||
source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.13"
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.163"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.96"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.3.3"
|
||||
name = "syn"
|
||||
version = "2.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proxy-wasm = "0.2.1"
|
||||
proxy-wasm = { git="https://github.com/higress-group/proxy-wasm-rust-sdk", branch="main", version="0.2.2" }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "1.3.3", features = ["v4"] }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
FROM rust:1.69 as builder
|
||||
FROM rust:1.80 as builder
|
||||
WORKDIR /workspace
|
||||
RUN apt update && apt-get install gcc gcc-multilib llvm clang -y && apt clean
|
||||
RUN rustup target add wasm32-wasi
|
||||
ARG PLUGIN_NAME="say-hello"
|
||||
ARG BUILD_OPTS="--release"
|
||||
@@ -9,4 +10,4 @@ RUN cargo build --target wasm32-wasi $BUILD_OPTS \
|
||||
&& cp target/wasm32-wasi/release/*.wasm /main.wasm
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /main.wasm plugin.wasm
|
||||
COPY --from=builder /main.wasm plugin.wasm
|
||||
|
||||
@@ -6,6 +6,12 @@ IMAGE_TAG = $(if $(strip $(PLUGIN_VERSION)),${PLUGIN_VERSION},${BUILD_TIME}-${CO
|
||||
IMG ?= ${REGISTRY}${PLUGIN_NAME}:${IMAGE_TAG}
|
||||
|
||||
.DEFAULT:
|
||||
lint-base:
|
||||
cargo fmt --all --check
|
||||
cargo clippy --workspace --all-features --all-targets
|
||||
lint:
|
||||
cargo fmt --all --check --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml
|
||||
cargo clippy --workspace --all-features --all-targets --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml
|
||||
build:
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--build-arg PLUGIN_NAME=${PLUGIN_NAME} \
|
||||
@@ -13,4 +19,4 @@ build:
|
||||
--output extensions/${PLUGIN_NAME} \
|
||||
.
|
||||
@echo ""
|
||||
@echo "output wasm file: extensions/${PLUGIN_NAME}/plugin.wasm"
|
||||
@echo "output wasm file: extensions/${PLUGIN_NAME}/plugin.wasm"
|
||||
|
||||
851
plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock
generated
Normal file
851
plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock
generated
Normal file
@@ -0,0 +1,851 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ai-data-masking"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fancy-regex",
|
||||
"grok",
|
||||
"higress-wasm-rust",
|
||||
"jieba-rs",
|
||||
"jsonpath-rust",
|
||||
"lazy_static",
|
||||
"md5",
|
||||
"proxy-wasm",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fb8dd288a69fc53a1996d7ecfbf4a20d59065bff137ce7e56bbd620de191189"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cedarwood"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d910bedd62c24733263d0bed247460853c9d22e8956bd4cd964302095e04e90"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "grok"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "273797968160270573071022613fc4aa28b91fe68f3eef6c96a1b2a1947ddfbd"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"onig",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "higress-wasm-rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"multimap",
|
||||
"proxy-wasm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "jieba-rs"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1e2b0210dc78b49337af9e49d7ae41a39dceac6e5985613f1cf7763e2f76a25"
|
||||
dependencies = [
|
||||
"cedarwood",
|
||||
"derive_builder",
|
||||
"fxhash",
|
||||
"lazy_static",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonpath-rust"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d64f9886fc067a709ab27faf63b7d3f4d1ec570a700705408b0b0683e2f43897"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "onig"
|
||||
version = "6.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"onig_sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onig_sys"
|
||||
version = "69.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxy-wasm"
|
||||
version = "0.2.2"
|
||||
source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
22
plugins/wasm-rust/extensions/ai-data-masking/Cargo.toml
Normal file
22
plugins/wasm-rust/extensions/ai-data-masking/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "ai-data-masking"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
higress-wasm-rust = { path = "../../", version = "0.1.0" }
|
||||
proxy-wasm = { git="https://github.com/higress-group/proxy-wasm-rust-sdk", branch="main", version="0.2.2" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
fancy-regex = "0"
|
||||
md5 = "0"
|
||||
grok = "2"
|
||||
lazy_static = "1"
|
||||
jieba-rs = "0"
|
||||
rust-embed="8.5.0"
|
||||
jsonpath-rust = "0"
|
||||
155
plugins/wasm-rust/extensions/ai-data-masking/README.md
Normal file
155
plugins/wasm-rust/extensions/ai-data-masking/README.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 功能说明
|
||||
|
||||
对请求/返回中的敏感词拦截、替换
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 用户
|
||||
participant 敏感词插件
|
||||
participant 后端服务
|
||||
|
||||
用户->>敏感词插件: 请求数据(如:包含admin@gmail.com)
|
||||
敏感词插件->>敏感词插件: 数据解析
|
||||
opt 如果包含拦截词
|
||||
敏感词插件-->>用户: 返回预设错误消息 (拦截)
|
||||
end
|
||||
opt 替换敏感词
|
||||
敏感词插件->>后端服务: 关键词替换后的请求数据 (将admin@gmail.com替换为****@gmail.com)
|
||||
后端服务->>敏感词插件: 原始返回响应(包含 ****@gmail.com)
|
||||
敏感词插件->>用户: 数据恢复后的相应数据(将****@gmail.com恢复为admin@gmail.com)
|
||||
end
|
||||
```
|
||||
## 处理数据范围
|
||||
- openai协议:请求/返回对话内容
|
||||
- jsonpath:只处理指定字段
|
||||
- raw:整个请求/返回body
|
||||
|
||||
## 敏感词拦截
|
||||
- 处理数据范围中出现敏感词直接拦截,返回预设错误信息
|
||||
- 支持系统内置敏感词库和自定义敏感词
|
||||
|
||||
## 敏感词替换
|
||||
- 将请求数据中出现的敏感词替换为脱敏字符串,传递给后端服务。可保证敏感数据不出域
|
||||
- 部分脱敏数据在后端服务返回后可进行还原
|
||||
- 自定义规则支持标准正则和grok规则,替换字符串支持变量替换
|
||||
|
||||
|
||||
# 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- |
|
||||
| deny_openai | bool | true | 对openai协议进行拦截 |
|
||||
| deny_jsonpath | string | [] | 对指定jsonpath拦截 |
|
||||
| deny_raw | bool | false | 对原始body拦截 |
|
||||
| system_deny | bool | true | 开启内置拦截规则 |
|
||||
| deny_code | int | 200 | 拦截时http状态码 |
|
||||
| deny_message | string | 提问或回答中包含敏感词,已被屏蔽 | 拦截时ai返回消息 |
|
||||
| deny_raw_message | string | {"errmsg":"提问或回答中包含敏感词,已被屏蔽"} | 非openai拦截时返回内容 |
|
||||
| deny_content_type | string | application/json | 非openai拦截时返回content_type头 |
|
||||
| deny_words | array of string | [] | 自定义敏感词列表 |
|
||||
| replace_roles | array | - | 自定义敏感词正则替换 |
|
||||
| replace_roles.regex | string | - | 规则正则(内置GROK规则) |
|
||||
| replace_roles.type | [replace, hash] | - | 替换类型 |
|
||||
| replace_roles.restore | bool | false | 是否恢复 |
|
||||
| replace_roles.value | string | - | 替换值(支持正则变量) |
|
||||
|
||||
# 配置示例
|
||||
|
||||
```yaml
|
||||
system_deny: true
|
||||
deny_openai: true
|
||||
deny_jsonpath:
|
||||
- "$.messages[*].content"
|
||||
deny_raw: true
|
||||
deny_code: 200
|
||||
deny_message: "提问或回答中包含敏感词,已被屏蔽"
|
||||
deny_raw_message: "{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}"
|
||||
deny_content_type: "application/json"
|
||||
deny_words:
|
||||
- "自定义敏感词1"
|
||||
- "自定义敏感词2"
|
||||
replace_roles:
|
||||
- regex: "%{MOBILE}"
|
||||
type: "replace"
|
||||
value: "****"
|
||||
# 手机号 13800138000 -> ****
|
||||
- regex: "%{EMAILLOCALPART}@%{HOSTNAME:domain}"
|
||||
type: "replace"
|
||||
restore: true
|
||||
value: "****@$domain"
|
||||
# 电子邮箱 admin@gmail.com -> ****@gmail.com
|
||||
- regex: "%{IP}"
|
||||
type: "replace"
|
||||
restore: true
|
||||
value: "***.***.***.***"
|
||||
# ip 192.168.0.1 -> ***.***.***.***
|
||||
- regex: "%{IDCARD}"
|
||||
type: "replace"
|
||||
value: "****"
|
||||
# 身份证号 110000000000000000 -> ****
|
||||
- regex: "sk-[0-9a-zA-Z]*"
|
||||
restore: true
|
||||
type: "hash"
|
||||
# hash sk-12345 -> 9cb495455da32f41567dab1d07f1973d
|
||||
# hash后的值提供给大模型,从大模型返回的数据中会将hash值还原为原始值
|
||||
```
|
||||
|
||||
# 敏感词替换样例
|
||||
|
||||
## 用户请求内容
|
||||
|
||||
请将 `curl http://172.20.5.14/api/openai/v1/chat/completions -H "Authorization: sk-12345" -H "Auth: test@gmail.com"` 改成post方式
|
||||
|
||||
## 处理后请求大模型内容
|
||||
|
||||
`curl http://***.***.***.***/api/openai/v1/chat/completions -H "Authorization: 48a7e98a91d93896d8dac522c5853948" -H "Auth: ****@gmail.com"` 改成post方式
|
||||
|
||||
## 大模型返回内容
|
||||
|
||||
您想要将一个 `curl` 的 GET 请求转换为 POST 请求,并且这个请求是向一个特定的 API 发送数据。下面是修改后的 `curl` 命令,以 POST 方式发送:
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H "Authorization: 48a7e98a91d93896d8dac522c5853948" \
|
||||
-H "Auth: ****@gmail.com" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"key":"value"}' \
|
||||
http://***.***.***.***/api/openai/v1/chat/completions
|
||||
```
|
||||
|
||||
这里做了如下几个修改:
|
||||
|
||||
- `-X POST` 设置请求方式为 POST。
|
||||
- `-H "Content-Type: application/json"` 设置请求头中的 `Content-Type` 为 `application/json`,这通常用来告诉服务器您发送的数据格式是 JSON。
|
||||
- `-d '{"key":"value"}'` 这里设置了要发送的数据,`'{"key":"value"}'` 是一个简单的 JSON 对象示例。您需要将其替换为您实际想要发送的数据。
|
||||
|
||||
请注意,您需要将 `"key":"value"` 替换为您实际要发送的数据内容。如果您的 API 接受不同的数据结构或者需要特定的字段,请根据实际情况调整这部分内容。
|
||||
|
||||
## 处理后返回用户内容
|
||||
|
||||
您想要将一个 `curl` 的 GET 请求转换为 POST 请求,并且这个请求是向一个特定的 API 发送数据。下面是修改后的 `curl` 命令,以 POST 方式发送:
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H "Authorization: sk-12345" \
|
||||
-H "Auth: test@gmail.com" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"key":"value"}' \
|
||||
http://172.20.5.14/api/openai/v1/chat/completions
|
||||
```
|
||||
|
||||
这里做了如下几个修改:
|
||||
|
||||
- `-X POST` 设置请求方式为 POST。
|
||||
- `-H "Content-Type: application/json"` 设置请求头中的 `Content-Type` 为 `application/json`,这通常用来告诉服务器您发送的数据格式是 JSON。
|
||||
- `-d '{"key":"value"}'` 这里设置了要发送的数据,`'{"key":"value"}'` 是一个简单的 JSON 对象示例。您需要将其替换为您实际想要发送的数据。
|
||||
|
||||
请注意,您需要将 `"key":"value"` 替换为您实际要发送的数据内容。如果您的 API 接受不同的数据结构或者需要特定的字段,请根据实际情况调整这部分内容。
|
||||
|
||||
|
||||
# 相关说明
|
||||
|
||||
- 流模式中如果脱敏后的词被多个chunk拆分,可能无法进行还原
|
||||
- 流模式中,如果敏感词语被多个chunk拆分,可能会有敏感词的一部分返回给用户的情况
|
||||
- grok 内置规则列表 https://help.aliyun.com/zh/sls/user-guide/grok-patterns
|
||||
- 内置拦截规则数据来源 https://github.com/houbb/sensitive-word/tree/master/src/main/resources
|
||||
|
||||
64430
plugins/wasm-rust/extensions/ai-data-masking/res/sensitive_word_dict.txt
Normal file
64430
plugins/wasm-rust/extensions/ai-data-masking/res/sensitive_word_dict.txt
Normal file
File diff suppressed because it is too large
Load Diff
732
plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
Normal file
732
plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
Normal file
@@ -0,0 +1,732 @@
|
||||
// Copyright (c) 2023 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use fancy_regex::Regex;
|
||||
use grok::patterns;
|
||||
use higress_wasm_rust::log::Log;
|
||||
use higress_wasm_rust::plugin_wrapper::{HttpContextWrapper, RootContextWrapper};
|
||||
use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher};
|
||||
use jieba_rs::Jieba;
|
||||
use jsonpath_rust::{JsonPath, JsonPathValue};
|
||||
use lazy_static::lazy_static;
|
||||
use proxy_wasm::traits::{Context, HttpContext, RootContext};
|
||||
use proxy_wasm::types::{Bytes, ContextType, DataAction, HeaderAction, LogLevel};
|
||||
use rust_embed::Embed;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde_json::{json, Value};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use std::ops::DerefMut;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
use std::vec;
|
||||
|
||||
proxy_wasm::main! {{
|
||||
proxy_wasm::set_log_level(LogLevel::Trace);
|
||||
proxy_wasm::set_root_context(|_|Box::new(AiDataMaskingRoot::new()));
|
||||
}}
|
||||
|
||||
const PLUGIN_NAME: &str = "ai-data-masking";
|
||||
const GROK_PATTERN: &str = r"%\{(?<name>(?<pattern>[A-z0-9]+)(?::(?<alias>[A-z0-9_:;\/\s\.]+))?)\}";
|
||||
|
||||
#[derive(Embed)]
|
||||
#[folder = "res/"]
|
||||
struct Asset;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct DenyWord {
|
||||
jieba: Jieba,
|
||||
words: HashSet<String>,
|
||||
}
|
||||
struct System {
|
||||
deny_word: DenyWord,
|
||||
grok_regex: Regex,
|
||||
grok_patterns: BTreeMap<String, String>,
|
||||
}
|
||||
lazy_static! {
|
||||
static ref SYSTEM: System = System::new();
|
||||
}
|
||||
|
||||
struct AiDataMaskingRoot {
|
||||
log: Log,
|
||||
rule_matcher: SharedRuleMatcher<AiDataMaskingConfig>,
|
||||
}
|
||||
struct AiDataMasking {
|
||||
config: Option<AiDataMaskingConfig>,
|
||||
mask_map: HashMap<String, Option<String>>,
|
||||
is_openai: bool,
|
||||
stream: bool,
|
||||
res_body: Bytes,
|
||||
}
|
||||
fn deserialize_regexp<'de, D>(deserializer: D) -> Result<Regex, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value: Value = Deserialize::deserialize(deserializer)?;
|
||||
if let Some(pattern) = value.as_str() {
|
||||
let (p, _) = SYSTEM.grok_to_pattern(pattern);
|
||||
if let Ok(reg) = Regex::new(&p) {
|
||||
Ok(reg)
|
||||
} else if let Ok(reg) = Regex::new(pattern) {
|
||||
Ok(reg)
|
||||
} else {
|
||||
Err(Error::custom(format!("regexp error field {}", pattern)))
|
||||
}
|
||||
} else {
|
||||
Err(Error::custom("regexp error not string".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_type<'de, D>(deserializer: D) -> Result<Type, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value: Value = Deserialize::deserialize(deserializer)?;
|
||||
if let Some(_type) = value.as_str() {
|
||||
if _type == "replace" {
|
||||
Ok(Type::Replace)
|
||||
} else if _type == "hash" {
|
||||
Ok(Type::Hash)
|
||||
} else {
|
||||
Err(Error::custom(format!("regexp error value {}", _type)))
|
||||
}
|
||||
} else {
|
||||
Err(Error::custom("type error not string".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_denyword<'de, D>(deserializer: D) -> Result<DenyWord, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value: Vec<String> = Deserialize::deserialize(deserializer)?;
|
||||
Ok(DenyWord::from_iter(value))
|
||||
}
|
||||
|
||||
fn deserialize_jsonpath<'de, D>(deserializer: D) -> Result<Vec<JsonPath>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value: Vec<String> = Deserialize::deserialize(deserializer)?;
|
||||
let mut ret = Vec::new();
|
||||
for v in value {
|
||||
if v.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match JsonPath::from_str(&v) {
|
||||
Ok(jp) => ret.push(jp),
|
||||
Err(_) => return Err(Error::custom(format!("jsonpath error value {}", v))),
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum Type {
|
||||
Replace,
|
||||
Hash,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct Rule {
|
||||
#[serde(deserialize_with = "deserialize_regexp")]
|
||||
regex: Regex,
|
||||
#[serde(deserialize_with = "deserialize_type", alias = "type")]
|
||||
type_: Type,
|
||||
#[serde(default)]
|
||||
restore: bool,
|
||||
#[serde(default)]
|
||||
value: String,
|
||||
}
|
||||
fn default_deny_openai() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_deny_raw() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_system_deny() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_deny_code() -> u16 {
|
||||
200
|
||||
}
|
||||
fn default_deny_content_type() -> String {
|
||||
"application/json".to_string()
|
||||
}
|
||||
fn default_deny_raw_message() -> String {
|
||||
"{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}".to_string()
|
||||
}
|
||||
fn default_deny_message() -> String {
|
||||
"提问或回答中包含敏感词,已被屏蔽".to_string()
|
||||
}
|
||||
#[derive(Default, Debug, Deserialize, Clone)]
|
||||
pub struct AiDataMaskingConfig {
|
||||
#[serde(default = "default_deny_openai")]
|
||||
deny_openai: bool,
|
||||
#[serde(default = "default_deny_raw")]
|
||||
deny_raw: bool,
|
||||
#[serde(default, deserialize_with = "deserialize_jsonpath")]
|
||||
deny_jsonpath: Vec<JsonPath>,
|
||||
#[serde(default = "default_system_deny")]
|
||||
system_deny: bool,
|
||||
#[serde(default = "default_deny_code")]
|
||||
deny_code: u16,
|
||||
#[serde(default = "default_deny_message")]
|
||||
deny_message: String,
|
||||
#[serde(default = "default_deny_raw_message")]
|
||||
deny_raw_message: String,
|
||||
#[serde(default = "default_deny_content_type")]
|
||||
deny_content_type: String,
|
||||
#[serde(default)]
|
||||
replace_roles: Vec<Rule>,
|
||||
#[serde(deserialize_with = "deserialize_denyword", default = "DenyWord::empty")]
|
||||
deny_words: DenyWord,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct Message {
|
||||
content: String,
|
||||
}
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct Req {
|
||||
#[serde(default)]
|
||||
stream: bool,
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
struct ResMessage {
|
||||
#[serde(default)]
|
||||
message: Option<Message>,
|
||||
#[serde(default)]
|
||||
delta: Option<Message>,
|
||||
}
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
struct Res {
|
||||
#[serde(default)]
|
||||
choices: Vec<ResMessage>,
|
||||
}
|
||||
|
||||
static SYSTEM_PATTERNS: &[(&str, &str)] = &[
|
||||
("MOBILE", r#"\d{8,11}"#),
|
||||
("IDCARD", r#"\d{17}[0-9xX]|\d{15}"#),
|
||||
];
|
||||
|
||||
impl DenyWord {
|
||||
fn empty() -> Self {
|
||||
DenyWord {
|
||||
jieba: Jieba::empty(),
|
||||
words: HashSet::new(),
|
||||
}
|
||||
}
|
||||
fn from_iter<T: IntoIterator<Item = impl Into<String>>>(words: T) -> Self {
|
||||
let mut deny_word = DenyWord::empty();
|
||||
|
||||
for word in words {
|
||||
let _w = word.into();
|
||||
let w = _w.trim();
|
||||
if w.is_empty() {
|
||||
continue;
|
||||
}
|
||||
deny_word.jieba.add_word(w, None, None);
|
||||
deny_word.words.insert(w.to_string());
|
||||
}
|
||||
|
||||
deny_word
|
||||
}
|
||||
fn default() -> Self {
|
||||
if let Some(file) = Asset::get("sensitive_word_dict.txt") {
|
||||
if let Ok(data) = std::str::from_utf8(file.data.as_ref()) {
|
||||
return DenyWord::from_iter(data.split('\n'));
|
||||
}
|
||||
}
|
||||
DenyWord::empty()
|
||||
}
|
||||
|
||||
fn check(&self, message: &str) -> bool {
|
||||
for word in self.jieba.cut(message, true) {
|
||||
if self.words.contains(word) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
impl System {
|
||||
fn new() -> Self {
|
||||
let grok_regex = Regex::new(GROK_PATTERN).unwrap();
|
||||
let grok_patterns = BTreeMap::new();
|
||||
let mut system = System {
|
||||
deny_word: DenyWord::default(),
|
||||
grok_regex,
|
||||
grok_patterns,
|
||||
};
|
||||
system.init();
|
||||
system
|
||||
}
|
||||
fn init(&mut self) {
|
||||
let mut grok_temp_patterns = VecDeque::new();
|
||||
for patterns in [patterns(), SYSTEM_PATTERNS] {
|
||||
for &(key, value) in patterns {
|
||||
if self.grok_regex.is_match(value).is_ok_and(|r| r) {
|
||||
grok_temp_patterns.push_back((String::from(key), String::from(value)));
|
||||
} else {
|
||||
self.grok_patterns
|
||||
.insert(String::from(key), String::from(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut last_ok: Option<String> = None;
|
||||
|
||||
while let Some((key, value)) = grok_temp_patterns.pop_front() {
|
||||
if let Some(k) = &last_ok {
|
||||
if k == &key {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (v, ok) = self.grok_to_pattern(&value);
|
||||
if ok {
|
||||
self.grok_patterns.insert(key, v);
|
||||
last_ok = None;
|
||||
} else {
|
||||
if last_ok.is_none() {
|
||||
last_ok = Some(key.clone());
|
||||
}
|
||||
grok_temp_patterns.push_back((key, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn grok_to_pattern(&self, pattern: &str) -> (String, bool) {
|
||||
let mut ok = true;
|
||||
let mut ret = pattern.to_string();
|
||||
for _c in self.grok_regex.captures_iter(pattern) {
|
||||
if _c.is_err() {
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
let c = _c.unwrap();
|
||||
if let (Some(full), Some(name)) = (c.get(0), c.name("pattern")) {
|
||||
if let Some(p) = self.grok_patterns.get(name.as_str()) {
|
||||
if let Some(alias) = c.name("alias") {
|
||||
ret = ret.replace(full.as_str(), &format!("(?P<{}>{})", alias.as_str(), p));
|
||||
} else {
|
||||
ret = ret.replace(full.as_str(), p);
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
(ret, ok)
|
||||
}
|
||||
}
|
||||
impl AiDataMaskingRoot {
|
||||
fn new() -> Self {
|
||||
AiDataMaskingRoot {
|
||||
log: Log::new(PLUGIN_NAME.to_string()),
|
||||
rule_matcher: Rc::new(RefCell::new(RuleMatcher::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AiDataMaskingRoot {}
|
||||
|
||||
impl RootContext for AiDataMaskingRoot {
|
||||
fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool {
|
||||
on_configure(
|
||||
self,
|
||||
_plugin_configuration_size,
|
||||
self.rule_matcher.borrow_mut().deref_mut(),
|
||||
&self.log,
|
||||
)
|
||||
}
|
||||
fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> {
|
||||
self.create_http_context_use_wrapper(_context_id)
|
||||
}
|
||||
fn get_type(&self) -> Option<ContextType> {
|
||||
Some(ContextType::HttpContext)
|
||||
}
|
||||
}
|
||||
|
||||
impl RootContextWrapper<AiDataMaskingConfig> for AiDataMaskingRoot {
|
||||
fn rule_matcher(&self) -> &SharedRuleMatcher<AiDataMaskingConfig> {
|
||||
&self.rule_matcher
|
||||
}
|
||||
|
||||
fn create_http_context_wrapper(
|
||||
&self,
|
||||
_context_id: u32,
|
||||
) -> Option<Box<dyn HttpContextWrapper<AiDataMaskingConfig>>> {
|
||||
Some(Box::new(AiDataMasking {
|
||||
mask_map: HashMap::new(),
|
||||
config: None,
|
||||
is_openai: false,
|
||||
stream: false,
|
||||
res_body: Bytes::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
impl AiDataMasking {
|
||||
fn check_message(&self, message: &str) -> bool {
|
||||
if let Some(config) = &self.config {
|
||||
config.deny_words.check(message)
|
||||
|| (config.system_deny && SYSTEM.deny_word.check(message))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
fn msg_to_response(&self, msg: &str, raw_msg: &str, content_type: &str) -> (String, String) {
|
||||
if !self.is_openai {
|
||||
(raw_msg.to_string(), content_type.to_string())
|
||||
} else if self.stream {
|
||||
(
|
||||
format!(
|
||||
"data:{}\n\n",
|
||||
json!({"choices": [{"index": 0, "delta": {"role": "assistant", "content": msg}}], "usage": {}})
|
||||
),
|
||||
"text/event-stream;charset=UTF-8".to_string(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
json!({"choices": [{"index": 0, "message": {"role": "assistant", "content": msg}}], "usage": {}}).to_string(),
|
||||
"application/json".to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
fn deny(&mut self, in_response: bool) -> DataAction {
|
||||
if in_response && self.stream {
|
||||
self.replace_http_response_body(&[]);
|
||||
return DataAction::Continue;
|
||||
}
|
||||
let (deny_code, (deny_message, content_type)) = if let Some(config) = &self.config {
|
||||
(
|
||||
config.deny_code,
|
||||
self.msg_to_response(
|
||||
&config.deny_message,
|
||||
&config.deny_raw_message,
|
||||
&config.deny_content_type,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
default_deny_code(),
|
||||
self.msg_to_response(
|
||||
&default_deny_message(),
|
||||
&default_deny_raw_message(),
|
||||
&default_deny_content_type(),
|
||||
),
|
||||
)
|
||||
};
|
||||
if in_response {
|
||||
self.replace_http_response_body(deny_message.as_bytes());
|
||||
return DataAction::Continue;
|
||||
}
|
||||
self.send_http_response(
|
||||
deny_code as u32,
|
||||
vec![("Content-Type", &content_type)],
|
||||
Some(deny_message.as_bytes()),
|
||||
);
|
||||
DataAction::StopIterationAndBuffer
|
||||
}
|
||||
|
||||
fn process_sse_message(&mut self, sse_message: &str) -> Vec<String> {
|
||||
let mut messages = Vec::new();
|
||||
for msg in sse_message.split('\n') {
|
||||
if !msg.starts_with("data:") {
|
||||
continue;
|
||||
}
|
||||
let res: Res = if let Some(m) = msg.strip_prefix("data:") {
|
||||
match serde_json::from_str(m) {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if res.choices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
for choice in &res.choices {
|
||||
if let Some(delta) = &choice.delta {
|
||||
messages.push(delta.content.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
messages
|
||||
}
|
||||
fn replace_request_msg(&mut self, message: &str) -> String {
|
||||
let config = self.config.as_ref().unwrap();
|
||||
let mut msg = message.to_string();
|
||||
for rule in &config.replace_roles {
|
||||
let mut replace_pair = Vec::new();
|
||||
if rule.type_ == Type::Replace && !rule.restore {
|
||||
msg = rule.regex.replace_all(&msg, &rule.value).to_string();
|
||||
} else {
|
||||
for _m in rule.regex.find_iter(&msg) {
|
||||
if _m.is_err() {
|
||||
continue;
|
||||
}
|
||||
let m = _m.unwrap();
|
||||
let from_word = m.as_str();
|
||||
|
||||
let to_word = match rule.type_ {
|
||||
Type::Hash => {
|
||||
let digest = md5::compute(from_word.as_bytes());
|
||||
format!("{:x}", digest)
|
||||
}
|
||||
Type::Replace => rule.regex.replace(from_word, &rule.value).to_string(),
|
||||
};
|
||||
replace_pair.push((from_word.to_string(), to_word.clone()));
|
||||
|
||||
if rule.restore && !to_word.is_empty() {
|
||||
match self.mask_map.entry(to_word) {
|
||||
std::collections::hash_map::Entry::Occupied(mut e) => {
|
||||
e.insert(None);
|
||||
}
|
||||
std::collections::hash_map::Entry::Vacant(e) => {
|
||||
e.insert(Some(from_word.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (from_word, to_word) in replace_pair {
|
||||
msg = msg.replace(&from_word, &to_word);
|
||||
}
|
||||
}
|
||||
}
|
||||
msg
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AiDataMasking {}
|
||||
impl HttpContext for AiDataMasking {
|
||||
fn on_http_request_headers(
|
||||
&mut self,
|
||||
_num_headers: usize,
|
||||
_end_of_stream: bool,
|
||||
) -> HeaderAction {
|
||||
HeaderAction::StopIteration
|
||||
}
|
||||
fn on_http_response_headers(
|
||||
&mut self,
|
||||
_num_headers: usize,
|
||||
_end_of_stream: bool,
|
||||
) -> HeaderAction {
|
||||
self.set_http_response_header("Content-Length", None);
|
||||
HeaderAction::Continue
|
||||
}
|
||||
fn on_http_response_body(&mut self, body_size: usize, _end_of_stream: bool) -> DataAction {
|
||||
if !self.stream {
|
||||
return DataAction::Continue;
|
||||
}
|
||||
if let Some(body) = self.get_http_response_body(0, body_size) {
|
||||
self.res_body.extend(&body);
|
||||
|
||||
if let Ok(body_str) = String::from_utf8(self.res_body.clone()) {
|
||||
if self.is_openai {
|
||||
let messages = self.process_sse_message(&body_str);
|
||||
|
||||
if self.check_message(&messages.join("")) {
|
||||
return self.deny(true);
|
||||
}
|
||||
} else if self.check_message(&body_str) {
|
||||
return self.deny(true);
|
||||
}
|
||||
}
|
||||
if self.mask_map.is_empty() {
|
||||
return DataAction::Continue;
|
||||
}
|
||||
if let Ok(body_str) = std::str::from_utf8(&body) {
|
||||
let mut new_str = body_str.to_string();
|
||||
if self.is_openai {
|
||||
let messages = self.process_sse_message(body_str);
|
||||
|
||||
for message in messages {
|
||||
let mut new_message = message.clone();
|
||||
for (from_word, to_word) in self.mask_map.iter() {
|
||||
if let Some(to) = to_word {
|
||||
new_message = new_message.replace(from_word, to);
|
||||
}
|
||||
}
|
||||
if new_message != message {
|
||||
new_str = new_str.replace(
|
||||
&json!(message).to_string(),
|
||||
&json!(new_message).to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (from_word, to_word) in self.mask_map.iter() {
|
||||
if let Some(to) = to_word {
|
||||
new_str = new_str.replace(from_word, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
if new_str != body_str {
|
||||
self.replace_http_response_body(new_str.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
DataAction::Continue
|
||||
}
|
||||
}
|
||||
impl HttpContextWrapper<AiDataMaskingConfig> for AiDataMasking {
|
||||
fn on_config(&mut self, config: &AiDataMaskingConfig) {
|
||||
self.config = Some(config.clone());
|
||||
}
|
||||
fn cache_request_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn cache_response_body(&self) -> bool {
|
||||
!self.stream
|
||||
}
|
||||
fn on_http_request_complete_body(&mut self, req_body: &Bytes) -> DataAction {
|
||||
if self.config.is_none() {
|
||||
return DataAction::Continue;
|
||||
}
|
||||
let config = self.config.as_ref().unwrap();
|
||||
|
||||
let mut req_body = match String::from_utf8(req_body.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return DataAction::Continue,
|
||||
};
|
||||
if config.deny_openai {
|
||||
if let Ok(r) = serde_json::from_str(req_body.as_str()) {
|
||||
let req: Req = r;
|
||||
self.is_openai = true;
|
||||
self.stream = req.stream;
|
||||
for msg in req.messages {
|
||||
if self.check_message(&msg.content) {
|
||||
return self.deny(false);
|
||||
}
|
||||
let new_content = self.replace_request_msg(&msg.content);
|
||||
if new_content != msg.content {
|
||||
if let (Ok(from), Ok(to)) = (
|
||||
serde_json::to_string(&msg.content),
|
||||
serde_json::to_string(&new_content),
|
||||
) {
|
||||
req_body = req_body.replace(&from, &to);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.replace_http_request_body(req_body.as_bytes());
|
||||
return DataAction::Continue;
|
||||
}
|
||||
}
|
||||
if !config.deny_jsonpath.is_empty() {
|
||||
if let Ok(r) = serde_json::from_str(req_body.as_str()) {
|
||||
let json: Value = r;
|
||||
for jsonpath in config.deny_jsonpath.clone() {
|
||||
for v in jsonpath.find_slice(&json) {
|
||||
if let JsonPathValue::Slice(d, _) = v {
|
||||
if let Some(s) = d.as_str() {
|
||||
if self.check_message(s) {
|
||||
return self.deny(false);
|
||||
}
|
||||
let content = s.to_string();
|
||||
let new_content = self.replace_request_msg(&content);
|
||||
if new_content != content {
|
||||
if let (Ok(from), Ok(to)) = (
|
||||
serde_json::to_string(&content),
|
||||
serde_json::to_string(&new_content),
|
||||
) {
|
||||
req_body = req_body.replace(&from, &to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.replace_http_request_body(req_body.as_bytes());
|
||||
return DataAction::Continue;
|
||||
}
|
||||
}
|
||||
if config.deny_raw {
|
||||
if self.check_message(&req_body) {
|
||||
return self.deny(false);
|
||||
}
|
||||
let new_body = self.replace_request_msg(&req_body);
|
||||
if new_body != req_body {
|
||||
self.replace_http_request_body(new_body.as_bytes())
|
||||
}
|
||||
return DataAction::Continue;
|
||||
}
|
||||
DataAction::Continue
|
||||
}
|
||||
fn on_http_response_complete_body(&mut self, res_body: &Bytes) -> DataAction {
|
||||
if self.config.is_none() {
|
||||
self.reset_http_response();
|
||||
return DataAction::Continue;
|
||||
}
|
||||
let config = self.config.as_ref().unwrap();
|
||||
let mut res_body = match String::from_utf8(res_body.clone()) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
self.reset_http_response();
|
||||
return DataAction::Continue;
|
||||
}
|
||||
};
|
||||
if config.deny_openai && self.is_openai {
|
||||
if let Ok(r) = serde_json::from_str(res_body.as_str()) {
|
||||
let res: Res = r;
|
||||
for msg in res.choices {
|
||||
if let Some(meesage) = msg.message {
|
||||
if self.check_message(&meesage.content) {
|
||||
return self.deny(true);
|
||||
}
|
||||
|
||||
if self.mask_map.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut m = meesage.content.clone();
|
||||
for (from_word, to_word) in self.mask_map.iter() {
|
||||
if let Some(to) = to_word {
|
||||
m = m.replace(from_word, to);
|
||||
}
|
||||
}
|
||||
if m != meesage.content {
|
||||
if let (Ok(from), Ok(to)) = (
|
||||
serde_json::to_string(&meesage.content),
|
||||
serde_json::to_string(&m),
|
||||
) {
|
||||
res_body = res_body.replace(&from, &to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.replace_http_response_body(res_body.as_bytes());
|
||||
|
||||
return DataAction::Continue;
|
||||
}
|
||||
}
|
||||
if config.deny_raw {
|
||||
if self.check_message(&res_body) {
|
||||
return self.deny(true);
|
||||
}
|
||||
if !self.mask_map.is_empty() {
|
||||
for (from_word, to_word) in self.mask_map.iter() {
|
||||
if let Some(to) = to_word {
|
||||
res_body = res_body.replace(from_word, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.replace_http_response_body(res_body.as_bytes());
|
||||
return DataAction::Continue;
|
||||
}
|
||||
DataAction::Continue
|
||||
}
|
||||
}
|
||||
106
plugins/wasm-rust/extensions/request-block/Cargo.lock
generated
106
plugins/wasm-rust/extensions/request-block/Cargo.lock
generated
@@ -4,13 +4,14 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -22,6 +23,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -30,9 +37,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.9"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@@ -41,11 +48,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.13.2"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -61,21 +69,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.6"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.144"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.18"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@@ -94,24 +102,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.2"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.59"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxy-wasm"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "823b744520cd4a54ba7ebacbffe4562e839d6dcd8f89209f96a1ace4f5229cd4"
|
||||
version = "0.2.2"
|
||||
source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"log",
|
||||
@@ -119,18 +126,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.28"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.5"
|
||||
version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -169,24 +176,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.13"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.163"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.163"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
||||
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -195,20 +202,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.96"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.18"
|
||||
version = "2.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -217,27 +225,47 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.9"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.3.3"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -10,8 +10,8 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
higress-wasm-rust = { path = "../../", version = "0.1.0" }
|
||||
proxy-wasm = "0.2.1"
|
||||
proxy-wasm = { git="https://github.com/higress-group/proxy-wasm-rust-sdk", branch="main", version="0.2.2" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
regex = "1"
|
||||
multimap = "0"
|
||||
multimap = "0"
|
||||
|
||||
@@ -17,7 +17,7 @@ use higress_wasm_rust::plugin_wrapper::{HttpContextWrapper, RootContextWrapper};
|
||||
use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher};
|
||||
use multimap::MultiMap;
|
||||
use proxy_wasm::traits::{Context, HttpContext, RootContext};
|
||||
use proxy_wasm::types::{Action, Bytes, ContextType, LogLevel};
|
||||
use proxy_wasm::types::{Bytes, ContextType, DataAction, HeaderAction, LogLevel};
|
||||
use regex::Regex;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
@@ -148,9 +148,12 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
fn cache_request_body(&self) -> bool {
|
||||
self.cache_request
|
||||
}
|
||||
fn on_http_request_headers_ok(&mut self, headers: &MultiMap<String, String>) -> Action {
|
||||
fn on_http_request_complete_headers(
|
||||
&mut self,
|
||||
headers: &MultiMap<String, String>,
|
||||
) -> HeaderAction {
|
||||
if self.config.is_none() {
|
||||
return Action::Continue;
|
||||
return HeaderAction::Continue;
|
||||
}
|
||||
let config = self.config.as_ref().unwrap();
|
||||
if !config.block_urls.is_empty()
|
||||
@@ -161,7 +164,7 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
|
||||
if value.is_none() {
|
||||
self.log.warn("get path failed");
|
||||
return Action::Continue;
|
||||
return HeaderAction::Continue;
|
||||
}
|
||||
let mut request_url = value.unwrap().clone();
|
||||
|
||||
@@ -175,7 +178,7 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
Vec::new(),
|
||||
Some(config.blocked_message.as_bytes()),
|
||||
);
|
||||
return Action::Pause;
|
||||
return HeaderAction::StopIteration;
|
||||
}
|
||||
}
|
||||
for block_url in &config.block_urls {
|
||||
@@ -185,7 +188,7 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
Vec::new(),
|
||||
Some(config.blocked_message.as_bytes()),
|
||||
);
|
||||
return Action::Pause;
|
||||
return HeaderAction::StopIteration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +199,7 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
Vec::new(),
|
||||
Some(config.blocked_message.as_bytes()),
|
||||
);
|
||||
return Action::Pause;
|
||||
return HeaderAction::StopIteration;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,19 +217,19 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
Vec::new(),
|
||||
Some(config.blocked_message.as_bytes()),
|
||||
);
|
||||
return Action::Pause;
|
||||
return HeaderAction::StopIteration;
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Continue
|
||||
HeaderAction::Continue
|
||||
}
|
||||
fn on_http_request_body_ok(&mut self, req_body: &Bytes) -> Action {
|
||||
fn on_http_request_complete_body(&mut self, req_body: &Bytes) -> DataAction {
|
||||
if self.config.is_none() {
|
||||
return Action::Continue;
|
||||
return DataAction::Continue;
|
||||
}
|
||||
let config = self.config.as_ref().unwrap();
|
||||
if config.block_bodies.is_empty() {
|
||||
return Action::Continue;
|
||||
return DataAction::Continue;
|
||||
}
|
||||
let mut body = req_body.clone();
|
||||
if !config.case_sensitive {
|
||||
@@ -240,9 +243,9 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
Vec::new(),
|
||||
Some(config.blocked_message.as_bytes()),
|
||||
);
|
||||
return Action::Pause;
|
||||
return DataAction::StopIterationAndBuffer;
|
||||
}
|
||||
}
|
||||
Action::Continue
|
||||
DataAction::Continue
|
||||
}
|
||||
}
|
||||
|
||||
118
plugins/wasm-rust/extensions/say-hello/Cargo.lock
generated
118
plugins/wasm-rust/extensions/say-hello/Cargo.lock
generated
@@ -4,15 +4,22 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -21,9 +28,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.9"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@@ -32,17 +39,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.13.2"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "higress-wasm-rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"multimap",
|
||||
"proxy-wasm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -51,42 +60,56 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.6"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.144"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.18"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.2"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.59"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxy-wasm"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "823b744520cd4a54ba7ebacbffe4562e839d6dcd8f89209f96a1ace4f5229cd4"
|
||||
version = "0.2.2"
|
||||
source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"log",
|
||||
@@ -94,18 +117,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.28"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.13"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "say-hello"
|
||||
@@ -119,18 +142,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.163"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.163"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
||||
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -139,20 +162,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.96"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.18"
|
||||
version = "2.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -161,27 +185,47 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.9"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.3.3"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -10,6 +10,6 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
higress-wasm-rust = { path = "../../", version = "0.1.0" }
|
||||
proxy-wasm = "0.2.1"
|
||||
proxy-wasm = { git="https://github.com/higress-group/proxy-wasm-rust-sdk", branch="main", version="0.2.2" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use higress_wasm_rust::log::Log;
|
||||
use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher};
|
||||
use proxy_wasm::traits::{Context, HttpContext, RootContext};
|
||||
use proxy_wasm::types::{Action, ContextType, LogLevel};
|
||||
use proxy_wasm::types::{ContextType, HeaderAction, LogLevel};
|
||||
use serde::Deserialize;
|
||||
use std::cell::RefCell;
|
||||
use std::ops::DerefMut;
|
||||
@@ -75,12 +75,16 @@ impl RootContext for SayHelloRoot {
|
||||
impl Context for SayHello {}
|
||||
|
||||
impl HttpContext for SayHello {
|
||||
fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
|
||||
fn on_http_request_headers(
|
||||
&mut self,
|
||||
_num_headers: usize,
|
||||
_end_of_stream: bool,
|
||||
) -> HeaderAction {
|
||||
let binding = self.rule_matcher.borrow();
|
||||
let config = match binding.get_match_config() {
|
||||
None => {
|
||||
self.send_http_response(200, vec![], Some("Hello, World!".as_bytes()));
|
||||
return Action::Continue;
|
||||
return HeaderAction::Continue;
|
||||
}
|
||||
Some(config) => config.1,
|
||||
};
|
||||
@@ -90,6 +94,6 @@ impl HttpContext for SayHello {
|
||||
vec![],
|
||||
Some(format!("Hello, {}!", config.name).as_bytes()),
|
||||
);
|
||||
Action::Continue
|
||||
HeaderAction::Continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use crate::rule_matcher::SharedRuleMatcher;
|
||||
use multimap::MultiMap;
|
||||
use proxy_wasm::traits::{Context, HttpContext, RootContext};
|
||||
use proxy_wasm::types::{Action, Bytes};
|
||||
use proxy_wasm::types::{Action, Bytes, DataAction, HeaderAction};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub trait RootContextWrapper<PluginConfig>: RootContext
|
||||
@@ -43,8 +43,17 @@ where
|
||||
}
|
||||
pub trait HttpContextWrapper<PluginConfig>: HttpContext {
|
||||
fn on_config(&mut self, _config: &PluginConfig) {}
|
||||
fn on_http_request_headers_ok(&mut self, _headers: &MultiMap<String, String>) -> Action {
|
||||
Action::Continue
|
||||
fn on_http_request_complete_headers(
|
||||
&mut self,
|
||||
_headers: &MultiMap<String, String>,
|
||||
) -> HeaderAction {
|
||||
HeaderAction::Continue
|
||||
}
|
||||
fn on_http_response_complete_headers(
|
||||
&mut self,
|
||||
_headers: &MultiMap<String, String>,
|
||||
) -> HeaderAction {
|
||||
HeaderAction::Continue
|
||||
}
|
||||
fn cache_request_body(&self) -> bool {
|
||||
false
|
||||
@@ -52,11 +61,11 @@ pub trait HttpContextWrapper<PluginConfig>: HttpContext {
|
||||
fn cache_response_body(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn on_http_request_body_ok(&mut self, _req_body: &Bytes) -> Action {
|
||||
Action::Continue
|
||||
fn on_http_request_complete_body(&mut self, _req_body: &Bytes) -> DataAction {
|
||||
DataAction::Continue
|
||||
}
|
||||
fn on_http_response_body_ok(&mut self, _res_body: &Bytes) -> Action {
|
||||
Action::Continue
|
||||
fn on_http_response_complete_body(&mut self, _res_body: &Bytes) -> DataAction {
|
||||
DataAction::Continue
|
||||
}
|
||||
fn replace_http_request_body(&mut self, body: &[u8]) {
|
||||
self.set_http_request_body(0, i32::MAX as usize, body)
|
||||
@@ -67,6 +76,7 @@ pub trait HttpContextWrapper<PluginConfig>: HttpContext {
|
||||
}
|
||||
pub struct PluginHttpWrapper<PluginConfig> {
|
||||
req_headers: MultiMap<String, String>,
|
||||
res_headers: MultiMap<String, String>,
|
||||
req_body_len: usize,
|
||||
res_body_len: usize,
|
||||
config: Option<PluginConfig>,
|
||||
@@ -80,6 +90,7 @@ impl<PluginConfig> PluginHttpWrapper<PluginConfig> {
|
||||
) -> Self {
|
||||
PluginHttpWrapper {
|
||||
req_headers: MultiMap::new(),
|
||||
res_headers: MultiMap::new(),
|
||||
req_body_len: 0,
|
||||
res_body_len: 0,
|
||||
config: None,
|
||||
@@ -129,7 +140,7 @@ impl<PluginConfig> HttpContext for PluginHttpWrapper<PluginConfig>
|
||||
where
|
||||
PluginConfig: Default + DeserializeOwned + Clone,
|
||||
{
|
||||
fn on_http_request_headers(&mut self, num_headers: usize, end_of_stream: bool) -> Action {
|
||||
fn on_http_request_headers(&mut self, num_headers: usize, end_of_stream: bool) -> HeaderAction {
|
||||
let binding = self.rule_matcher.borrow();
|
||||
self.config = binding.get_match_config().map(|config| config.1.clone());
|
||||
for (k, v) in self.get_http_request_headers() {
|
||||
@@ -141,23 +152,22 @@ where
|
||||
let ret = self
|
||||
.http_content
|
||||
.on_http_request_headers(num_headers, end_of_stream);
|
||||
if ret != Action::Continue {
|
||||
if ret != HeaderAction::Continue {
|
||||
return ret;
|
||||
}
|
||||
self.http_content
|
||||
.on_http_request_headers_ok(&self.req_headers)
|
||||
.on_http_request_complete_headers(&self.req_headers)
|
||||
}
|
||||
|
||||
fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
|
||||
fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
|
||||
if !self.http_content.cache_request_body() {
|
||||
return self
|
||||
.http_content
|
||||
.on_http_request_body(body_size, end_of_stream);
|
||||
}
|
||||
self.req_body_len += body_size;
|
||||
if !end_of_stream {
|
||||
return Action::Pause;
|
||||
}
|
||||
let ret = self
|
||||
.http_content
|
||||
.on_http_request_body(self.req_body_len, end_of_stream);
|
||||
if ret != Action::Continue || !self.http_content.cache_request_body() {
|
||||
return ret;
|
||||
return DataAction::StopIterationAndBuffer;
|
||||
}
|
||||
let mut req_body = Bytes::new();
|
||||
if self.req_body_len > 0 {
|
||||
@@ -165,30 +175,42 @@ where
|
||||
req_body = body;
|
||||
}
|
||||
}
|
||||
self.http_content.on_http_request_body_ok(&req_body)
|
||||
self.http_content.on_http_request_complete_body(&req_body)
|
||||
}
|
||||
|
||||
fn on_http_request_trailers(&mut self, num_trailers: usize) -> Action {
|
||||
self.http_content.on_http_request_trailers(num_trailers)
|
||||
}
|
||||
|
||||
fn on_http_response_headers(&mut self, num_headers: usize, end_of_stream: bool) -> Action {
|
||||
self.http_content
|
||||
.on_http_response_headers(num_headers, end_of_stream)
|
||||
}
|
||||
|
||||
fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
|
||||
self.res_body_len += body_size;
|
||||
|
||||
if !end_of_stream {
|
||||
return Action::Pause;
|
||||
fn on_http_response_headers(
|
||||
&mut self,
|
||||
num_headers: usize,
|
||||
end_of_stream: bool,
|
||||
) -> HeaderAction {
|
||||
for (k, v) in self.get_http_response_headers() {
|
||||
self.res_headers.insert(k, v);
|
||||
}
|
||||
let ret = self
|
||||
.http_content
|
||||
.on_http_response_body(self.res_body_len, end_of_stream);
|
||||
if ret != Action::Continue || !self.http_content.cache_response_body() {
|
||||
.on_http_response_headers(num_headers, end_of_stream);
|
||||
if ret != HeaderAction::Continue {
|
||||
return ret;
|
||||
}
|
||||
self.http_content
|
||||
.on_http_response_complete_headers(&self.res_headers)
|
||||
}
|
||||
|
||||
fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
|
||||
if !self.http_content.cache_response_body() {
|
||||
return self
|
||||
.http_content
|
||||
.on_http_response_body(body_size, end_of_stream);
|
||||
}
|
||||
self.res_body_len += body_size;
|
||||
|
||||
if !end_of_stream {
|
||||
return DataAction::StopIterationAndBuffer;
|
||||
}
|
||||
|
||||
let mut res_body = Bytes::new();
|
||||
if self.res_body_len > 0 {
|
||||
@@ -196,7 +218,7 @@ where
|
||||
res_body = body;
|
||||
}
|
||||
}
|
||||
self.http_content.on_http_response_body_ok(&res_body)
|
||||
self.http_content.on_http_response_complete_body(&res_body)
|
||||
}
|
||||
|
||||
fn on_http_response_trailers(&mut self, num_trailers: usize) -> Action {
|
||||
|
||||
115
test/e2e/conformance/tests/go-wasm-ai-proxy.go
Normal file
115
test/e2e/conformance/tests/go-wasm-ai-proxy.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(WasmPluginsAiProxy)
|
||||
}
|
||||
|
||||
var WasmPluginsAiProxy = suite.ConformanceTest{
|
||||
ShortName: "WasmPluginAiProxy",
|
||||
Description: "The Ingress in the higress-conformance-infra namespace test the ai-proxy WASM plugin.",
|
||||
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
|
||||
Manifests: []string{"tests/go-wasm-ai-proxy.yaml"},
|
||||
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
|
||||
testcases := []http.Assertion{
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 1: openai",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "openai.ai.com",
|
||||
Path: "/v1/chat/completions",
|
||||
Method:"POST",
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
Body: []byte(`{
|
||||
"model": "gpt-3",
|
||||
"messages": [{"role":"user","content":"hi"}]}`),
|
||||
},
|
||||
ExpectedRequest: &http.ExpectedRequest{
|
||||
Request: http.Request{
|
||||
Host: "api.openai.com",
|
||||
Path: "/v1/chat/completions",
|
||||
Method: "POST",
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
Body: []byte(`{
|
||||
"model": "gpt-3",
|
||||
"messages": [{"role":"user","content":"hi"}],
|
||||
"max_tokens": 123,
|
||||
"temperature": 0.66}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 2: qwen",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "qwen.ai.com",
|
||||
Path: "/v1/chat/completions",
|
||||
Method:"POST",
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
Body: []byte(`{
|
||||
"model": "qwen-long",
|
||||
"input": {"messages": [{"role":"user","content":"hi"}]},
|
||||
"parameters": {"max_tokens": 321, "temperature": 0.7}}`),
|
||||
},
|
||||
ExpectedRequest: &http.ExpectedRequest{
|
||||
Request: http.Request{
|
||||
Host: "dashscope.aliyuncs.com",
|
||||
Path: "/api/v1/services/aigc/text-generation/generation",
|
||||
Method: "POST",
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
Body: []byte(`{
|
||||
"model": "qwen-long",
|
||||
"input": {"messages": [{"role":"user","content":"hi"}]},
|
||||
"parameters": {"max_tokens": 321, "temperature": 0.66}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
t.Run("WasmPlugins ai-proxy", func(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
87
test/e2e/conformance/tests/go-wasm-ai-proxy.yaml
Normal file
87
test/e2e/conformance/tests/go-wasm-ai-proxy.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
name: wasmplugin-ai-proxy-openai
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "openai.ai.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v1
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
name: wasmplugin-ai-proxy-qwen
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "qwen.ai.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v1
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-proxy
|
||||
namespace: higress-system
|
||||
spec:
|
||||
priority: 200
|
||||
matchRules:
|
||||
- config:
|
||||
provider:
|
||||
type: "openai"
|
||||
customSettings:
|
||||
- name: "max_tokens"
|
||||
value: 123
|
||||
overwrite: false
|
||||
- name: "temperature"
|
||||
value: 0.66
|
||||
overwrite: true
|
||||
ingress:
|
||||
- higress-conformance-infra/wasmplugin-ai-proxy-openai
|
||||
- config:
|
||||
provider:
|
||||
type: "qwen"
|
||||
apiTokens: "fake-token"
|
||||
customSettings:
|
||||
- name: "max_tokens"
|
||||
value: 123
|
||||
overwrite: false
|
||||
- name: "temperature"
|
||||
value: 0.66
|
||||
overwrite: true
|
||||
ingress:
|
||||
- higress-conformance-infra/wasmplugin-ai-proxy-qwen
|
||||
url: file:///opt/plugins/wasm-go/extensions/ai-proxy/plugin.wasm
|
||||
189
test/e2e/conformance/tests/rust-wasm-ai-data-masking.go
Normal file
189
test/e2e/conformance/tests/rust-wasm-ai-data-masking.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(RustWasmPluginsAiDataMasking)
|
||||
}
|
||||
|
||||
func gen_assertion(host string, req_is_json bool, req_body []byte, res_body []byte) http.Assertion {
|
||||
var content_type string
|
||||
if req_is_json {
|
||||
content_type = http.ContentTypeApplicationJson
|
||||
} else {
|
||||
content_type = http.ContentTypeTextPlain
|
||||
}
|
||||
return http.Assertion{
|
||||
Meta: http.AssertionMeta{
|
||||
CompareTarget: http.CompareTargetResponse,
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: host,
|
||||
Path: "/",
|
||||
Method: "POST",
|
||||
ContentType: content_type,
|
||||
Body: req_body,
|
||||
UnfollowRedirect: true,
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
Body: res_body,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var RustWasmPluginsAiDataMasking = suite.ConformanceTest{
|
||||
ShortName: "RustWasmPluginsAiDataMasking",
|
||||
Description: "The Ingress in the higress-conformance-infra namespace test the rust ai-data-masking wasmplugins.",
|
||||
Manifests: []string{"tests/rust-wasm-ai-data-masking.yaml"},
|
||||
Features: []suite.SupportedFeature{suite.WASMRustConformanceFeature},
|
||||
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
|
||||
var testcases []http.Assertion
|
||||
//openai
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"replace.openai.com",
|
||||
true,
|
||||
[]byte("{\"messages\":[{\"role\":\"user\",\"content\":\"127.0.0.1 admin@gmail.com sk-12345\"}]}"),
|
||||
[]byte("{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"127.0.0.1 sk-12345 admin@gmail.com\"}}],\"usage\":{}}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"replace.openai.com",
|
||||
true,
|
||||
[]byte("{\"messages\":[{\"role\":\"user\",\"content\":\"192.168.0.1 root@gmail.com sk-12345\"}]}"),
|
||||
[]byte("{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"192.168.0.1 sk-12345 root@gmail.com\"}}],\"usage\":{}}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"ok.openai.com",
|
||||
true,
|
||||
[]byte("{\"messages\":[{\"role\":\"user\",\"content\":\"fuck\"}]}"),
|
||||
[]byte("{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"提问或回答中包含敏感词,已被屏蔽\"}}],\"usage\":{}}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"ok.openai.com",
|
||||
true,
|
||||
[]byte("{\"messages\":[{\"role\":\"user\",\"content\":\"costom_word1\"}]}"),
|
||||
[]byte("{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"提问或回答中包含敏感词,已被屏蔽\"}}],\"usage\":{}}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"ok.openai.com",
|
||||
true,
|
||||
[]byte("{\"messages\":[{\"role\":\"user\",\"content\":\"costom_word\"}]}"),
|
||||
[]byte("{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"ok\"}}],\"usage\":{}}"),
|
||||
))
|
||||
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"system_deny.openai.com",
|
||||
true,
|
||||
[]byte("{\"messages\":[{\"role\":\"user\",\"content\":\"test\"}]}"),
|
||||
[]byte("{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"提问或回答中包含敏感词,已被屏蔽\"}}],\"usage\":{}}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"costom_word1.openai.com",
|
||||
true,
|
||||
[]byte("{\"messages\":[{\"role\":\"user\",\"content\":\"test\"}]}"),
|
||||
[]byte("{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"提问或回答中包含敏感词,已被屏蔽\"}}],\"usage\":{}}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"costom_word.openai.com",
|
||||
true,
|
||||
[]byte("{\"messages\":[{\"role\":\"user\",\"content\":\"test\"}]}"),
|
||||
[]byte("{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"costom_word\"}}],\"usage\":{}}"),
|
||||
))
|
||||
|
||||
//raw
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"replace.raw.com",
|
||||
false,
|
||||
[]byte("127.0.0.1 admin@gmail.com sk-12345"),
|
||||
[]byte("{\"res\":\"127.0.0.1 sk-12345 admin@gmail.com\"}"),
|
||||
))
|
||||
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"replace.raw.com",
|
||||
false,
|
||||
[]byte("192.168.0.1 root@gmail.com sk-12345"),
|
||||
[]byte("{\"res\":\"192.168.0.1 sk-12345 root@gmail.com\"}"),
|
||||
))
|
||||
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"ok.raw.com",
|
||||
false,
|
||||
[]byte("fuck"),
|
||||
[]byte("{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"ok.raw.com",
|
||||
false,
|
||||
[]byte("costom_word1"),
|
||||
[]byte("{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"ok.raw.com",
|
||||
false,
|
||||
[]byte("costom_word"),
|
||||
[]byte("{\"res\":\"ok\"}"),
|
||||
))
|
||||
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"system_deny.raw.com",
|
||||
false,
|
||||
[]byte("test"),
|
||||
[]byte("{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"costom_word1.raw.com",
|
||||
false,
|
||||
[]byte("test"),
|
||||
[]byte("{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"costom_word.raw.com",
|
||||
false,
|
||||
[]byte("test"),
|
||||
[]byte("{\"res\":\"costom_word\"}"),
|
||||
))
|
||||
|
||||
//jsonpath
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"replace.raw.com",
|
||||
true,
|
||||
[]byte("{\"test\":[{\"test\":\"127.0.0.1 admin@gmail.com sk-12345\"}]}"),
|
||||
[]byte("{\"res\":\"127.0.0.1 sk-12345 admin@gmail.com\"}"),
|
||||
))
|
||||
testcases = append(testcases, gen_assertion(
|
||||
"replace.raw.com",
|
||||
true,
|
||||
[]byte("{\"test\":[{\"test\":\"test\", \"test1\":\"127.0.0.1 admin@gmail.com sk-12345\"}]}"),
|
||||
[]byte("{\"res\":\"***.***.***.*** 48a7e98a91d93896d8dac522c5853948 ****@gmail.com\"}"),
|
||||
))
|
||||
|
||||
t.Run("WasmPlugins ai-data-masking", func(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
150
test/e2e/conformance/tests/rust-wasm-ai-data-masking.yaml
Normal file
150
test/e2e/conformance/tests/rust-wasm-ai-data-masking.yaml
Normal file
@@ -0,0 +1,150 @@
|
||||
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: wasmplugin-ai-data-masking
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "*.openai.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v1
|
||||
port:
|
||||
number: 8080
|
||||
- host: "*.raw.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v1
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: custom-response
|
||||
namespace: higress-system
|
||||
spec:
|
||||
priority: 200
|
||||
defaultConfig:
|
||||
"body": "ok"
|
||||
matchRules:
|
||||
- domain:
|
||||
- ok.openai.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"ok\"}}],\"usage\":{}}"
|
||||
- domain:
|
||||
- replace.openai.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"***.***.***.*** 48a7e98a91d93896d8dac522c5853948 ****@gmail.com\"}}],\"usage\":{}}"
|
||||
|
||||
- domain:
|
||||
- system_deny.openai.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"fuck\"}}],\"usage\":{}}"
|
||||
- domain:
|
||||
- costom_word1.openai.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"costom_word1\"}}],\"usage\":{}}"
|
||||
- domain:
|
||||
- costom_word.openai.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"costom_word\"}}],\"usage\":{}}"
|
||||
- domain:
|
||||
- ok.raw.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"res\":\"ok\"}"
|
||||
- domain:
|
||||
- replace.raw.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"res\":\"***.***.***.*** 48a7e98a91d93896d8dac522c5853948 ****@gmail.com\"}"
|
||||
- domain:
|
||||
- system_deny.raw.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"res\":\"fuck\"}"
|
||||
- domain:
|
||||
- costom_word1.raw.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"res\":\"costom_word1\"}"
|
||||
- domain:
|
||||
- costom_word.raw.com
|
||||
config:
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
"body": "{\"res\":\"costom_word\"}"
|
||||
|
||||
url: file:///opt/plugins/wasm-go/extensions/custom-response/plugin.wasm
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-data-masking
|
||||
namespace: higress-system
|
||||
spec:
|
||||
priority: 300
|
||||
defaultConfig:
|
||||
system_deny: true
|
||||
deny_openai: true
|
||||
deny_jsonpath:
|
||||
- "$.test[*].test"
|
||||
deny_raw: true
|
||||
deny_code: 200
|
||||
deny_message: "提问或回答中包含敏感词,已被屏蔽"
|
||||
deny_raw_message: "{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}"
|
||||
deny_content_type: "application/json"
|
||||
deny_words:
|
||||
- "costom_word1"
|
||||
replace_roles:
|
||||
- regex: "%{EMAILLOCALPART}@%{HOSTNAME:domain}"
|
||||
type: "replace"
|
||||
restore: true
|
||||
value: "****@$domain"
|
||||
- regex: "%{IP}"
|
||||
type: "replace"
|
||||
restore: true
|
||||
value: "***.***.***.***"
|
||||
- regex: "sk-[0-9a-zA-Z]*"
|
||||
restore: true
|
||||
type: "hash"
|
||||
|
||||
url: file:///opt/plugins/wasm-rust/extensions/ai-data-masking/plugin.wasm
|
||||
@@ -32,6 +32,7 @@ then
|
||||
elif [ "$TYPE" == "RUST" ]
|
||||
then
|
||||
cd ./plugins/wasm-rust/
|
||||
make lint-base
|
||||
if [ ! -n "$INNER_PLUGIN_NAME" ]; then
|
||||
EXTENSIONS_DIR=$(pwd)"/extensions/"
|
||||
echo "🚀 Build all Rust WasmPlugins under folder of $EXTENSIONS_DIR"
|
||||
@@ -40,12 +41,21 @@ then
|
||||
if [ -d $EXTENSIONS_DIR$file ]; then
|
||||
name=${file##*/}
|
||||
echo "🚀 Build Rust WasmPlugin: $name"
|
||||
PLUGIN_NAME=${name} make lint
|
||||
PLUGIN_NAME=${name} BUILDER_REGISTRY="docker.io/alihigress/plugins-rust-" make build
|
||||
fi
|
||||
done
|
||||
cd ../wasm-go/
|
||||
PLUGIN_NAME=custom-response make build
|
||||
else
|
||||
echo "🚀 Build Rust WasmPlugin: $INNER_PLUGIN_NAME"
|
||||
PLUGIN_NAME=${INNER_PLUGIN_NAME} make lint
|
||||
PLUGIN_NAME=${INNER_PLUGIN_NAME} make build
|
||||
if [ "$INNER_PLUGIN_NAME" == "ai-data-masking" ]; then
|
||||
cd ../wasm-go/
|
||||
PLUGIN_NAME=custom-response make build
|
||||
fi
|
||||
|
||||
fi
|
||||
else
|
||||
echo "Not specify plugin language, so just compile wasm-go as default"
|
||||
|
||||
Reference in New Issue
Block a user