feat: major refactor for dynamic data-driven api orchestration and jsmod support

This commit is contained in:
AI Engineer 2026-05-31 00:14:26 +08:00
parent c0e0d7c280
commit c25a442b38
6 changed files with 268 additions and 67 deletions

View File

@ -1,9 +1,15 @@
# CHANGELOG
## v1.3.3 (2026-05-30)
- **新增**: 注册到 `jsmod`,提供 `call``load` 能力。支持在 JS 环境中通过简洁的对象传参发起 API 编排调用。
## v1.4.0 (2026-05-30)
- **重大重构**: 引入数据驱动的 API 编排模式。
- **新增**: `ActionRegistry` 支持注册 Go 结构体或 JSON 定义作为 API 模板。
- **新增**: `CallBy(name, payload)` 动态调用入口,自动处理模板实例化与动态参数合并。
- **新增**: `SetConfig` 支持内存配置热更新,并自动识别 `**` 前缀的加密字符串为 `SafeBuf`
- **新增**: 支持 `form``multipart` 请求格式。
- **新增**: 跨语言签名支持,提供 `SetJSRunner` 钩子允许执行 JS 签名逻辑。
- **JSMOD**: 注册 `call`, `setConfig`, `registerAction`, `registerSigner``jsmod`
## v1.0.3 (2026-05-09)
## v1.3.3 (2026-05-30)
### Security
- **Frictionless Memory Safety**: introduced `HttpRequest.SetHeader(key, values...)` with automatic secret extraction and buffer tracking.

View File

@ -1,6 +1,7 @@
package api
import (
"reflect"
"unsafe"
"apigo.cc/go/cast"
@ -37,6 +38,71 @@ type ValidatableAction interface {
Validate() error
}
// FormatAction 定义请求体格式
type FormatAction interface {
GetFormat() string // 返回: "json", "form", "multipart"
}
// ActionRegistry 存储已注册的 Action 模板或类型
var actionRegistry = make(map[string]any)
// RegisterAction 注册 API 动作。
// definition 可以是一个结构体实例(用作类型模板)或一个 map[string]any用作动态定义
func RegisterAction(name string, definition any) {
if definition == nil {
return
}
t := reflect.TypeOf(definition)
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() == reflect.Struct {
actionRegistry[name] = t
} else if m, ok := definition.(map[string]any); ok {
ga := &GenericAction{
name: name,
url: cast.String(m["url"]),
method: cast.String(m["method"]),
signer: cast.String(m["signer"]),
format: cast.String(m["format"]),
payload: make(map[string]any),
}
if p, ok := m["payload"].(map[string]any); ok {
ga.payload = p
}
actionRegistry[name] = ga
}
}
// GenericAction 是一个动态 Action 容器,实现了所有 API 相关接口
type GenericAction struct {
name string
url string
method string
signer string
format string
payload map[string]any
}
func (a *GenericAction) ActionName() string { return a.name }
func (a *GenericAction) SignerName() string { return a.signer }
func (a *GenericAction) GetURL() string { return a.url }
func (a *GenericAction) GetMethod() string { return a.method }
func (a *GenericAction) GetFormat() string { return a.format }
func (a *GenericAction) Config() map[string]any {
return map[string]any{
"url": a.url,
"method": a.method,
"signer": a.signer,
"format": a.format,
}
}
func (a *GenericAction) MarshalJSON() ([]byte, error) {
return cast.ToJSONBytes(a.payload)
}
// HttpRequest 内部使用的请求描述结构,供 Signer 使用
type HttpRequest struct {
Url string

67
api.go
View File

@ -2,6 +2,7 @@ package api
import (
"fmt"
"reflect"
"strings"
"apigo.cc/go/cast"
@ -85,16 +86,31 @@ func Call[T any](action Action) (*T, error) {
}
}
// 7. 发起底层 HTTP 调用
// 7. 处理请求格式 (Form/Multipart)
payload := httpReq.Payload
if fa, ok := action.(FormatAction); ok {
switch strings.ToLower(fa.GetFormat()) {
case "form":
var f http.Form
cast.Convert(&f, payload)
payload = f
case "multipart":
var m http.Multipart
cast.Convert(&m, payload)
payload = m
}
}
// 8. 发起底层 HTTP 调用
timeout := cast.Duration(actionConfig["timeout"])
client := http.NewClient(timeout)
res := client.Do(httpReq.Method, httpReq.Url, httpReq.Payload, headerSlice(httpReq)...)
res := client.Do(httpReq.Method, httpReq.Url, payload, headerSlice(httpReq)...)
if res.Error != nil {
return nil, res.Error
}
// 8. 解析响应
// 9. 解析响应
var response T
if err := res.To(&response); err != nil {
var temp any
@ -105,6 +121,51 @@ func Call[T any](action Action) (*T, error) {
return &response, nil
}
// CallBy 通过动作名称和动态 Payload 发起调用
func CallBy[T any](name string, payload any) (*T, error) {
tmpl, ok := actionRegistry[name]
if !ok {
return nil, fmt.Errorf("action not found: %s", name)
}
var action Action
if t, ok := tmpl.(reflect.Type); ok {
// Go 结构体模式
inst := reflect.New(t).Interface()
if payload != nil {
cast.Convert(inst, payload)
}
action = inst.(Action)
} else if ga, ok := tmpl.(*GenericAction); ok {
// JSON 定义模式 (深拷贝模板以防并发冲突)
newGA := &GenericAction{
name: ga.name,
url: ga.url,
method: ga.method,
signer: ga.signer,
format: ga.format,
payload: make(map[string]any),
}
// 复制模板 Payload
for k, v := range ga.payload {
newGA.payload[k] = v
}
// 合并动态 Payload
if payload != nil {
if m, ok := payload.(map[string]any); ok {
for k, v := range m {
newGA.payload[k] = v
}
} else {
cast.Convert(&newGA.payload, payload)
}
}
action = newGA
}
return Call[T](action)
}
func preprocessSecrets(m map[string]any, opened *[]*safe.SecretPlaintext) {
for k, v := range m {
if sb, ok := v.(*safe.SafeBuf); ok {

View File

@ -30,10 +30,17 @@ func SetEncryptKeys(key, iv []byte) {
crypto.SetDefaultAES(key, iv)
}
// AddConfig 允许在内存中动态添加或覆盖配置,适用于从数据库或知识库加载配置的场景
func AddConfig(name string, conf map[string]any) {
// SetConfig 允许在内存中动态添加或覆盖配置,适用于从数据库或知识库加载配置的场景。
// 它会自动扫描 map 中的字符串,识别并解密以 "**" 开头的加密内容并转换为 SafeBuf。
func SetConfig(name string, conf map[string]any) {
configMutex.Lock()
defer configMutex.Unlock()
// 1. 扫描并自动解密敏感数据
decryptMapWithPrefix(conf)
// 注意:解密出的 SafeBufs 是常驻内存的配置,由 GlobalConfigs 持有,不需要立即关闭
// 2. 合并到全局树
if GlobalConfigs[name] == nil {
GlobalConfigs[name] = conf
} else if dst, ok := GlobalConfigs[name].(map[string]any); ok {
@ -41,6 +48,9 @@ func AddConfig(name string, conf map[string]any) {
}
}
// AddConfig 是 SetConfig 的兼容性别名
func AddConfig(name string, conf map[string]any) { SetConfig(name, conf) }
// Load 加载指定的配置文件并合并到 GlobalConfigs
func Load(name string) error {
if name == "" {
@ -52,11 +62,7 @@ func Load(name string) error {
return err
}
configMutex.Lock()
defer configMutex.Unlock()
// 合并到全局树
MergeMap(GlobalConfigs, conf)
SetConfig(name, conf)
return nil
}
@ -71,7 +77,8 @@ func GetActionConfig(actionName string) (map[string]any, []*safe.SafeBuf) {
// 1. 获取 api 根节点
curr, ok := GlobalConfigs["api"].(map[string]any)
if !ok {
return res, nil
// 尝试直接从根开始找
curr = GlobalConfigs
}
// 2. 逐级导航并合并
@ -92,6 +99,7 @@ func GetActionConfig(actionName string) (map[string]any, []*safe.SafeBuf) {
}
}
// 这里的解密主要是为了处理文件中加载的 ENC() 格式 (兼容旧版)
safeBufs := decryptMap(res)
return res, safeBufs
}
@ -102,6 +110,19 @@ func fill(action any, actionConfig map[string]any) {
return
}
if ga, ok := action.(*GenericAction); ok {
// 对于通用动作,将配置合并到 payload map 中(仅合并 payload 不存在的 key
for k, v := range actionConfig {
if k == "url" || k == "method" || k == "signer" || k == "format" || k == "headers" || k == "timeout" {
continue
}
if _, exists := ga.payload[k]; !exists {
ga.payload[k] = v
}
}
return
}
v := reflect.ValueOf(action)
for v.Kind() == reflect.Ptr {
v = v.Elem()
@ -190,14 +211,46 @@ func MergeMap(dst, src map[string]any) {
}
}
func decryptMapWithPrefix(m map[string]any) []*safe.SafeBuf {
var safeBufs []*safe.SafeBuf
for k, v := range m {
if s, ok := v.(string); ok && strings.HasPrefix(s, "**") {
raw := s[2:]
var b64 []byte
var err error
if b64, err = encoding.UnUrlBase64FromString(raw); err != nil {
b64, err = encoding.UnBase64FromString(raw)
}
if err == nil && len(b64) > 0 {
if dec, err := confAES.DecryptBytes(b64); err == nil {
sb := safe.NewSafeBufAndErase(dec)
m[k] = sb
safeBufs = append(safeBufs, sb)
continue
}
}
} else if subMap, ok := v.(map[string]any); ok {
safeBufs = append(safeBufs, decryptMapWithPrefix(subMap)...)
}
}
return safeBufs
}
func decryptMap(m map[string]any) []*safe.SafeBuf {
var safeBufs []*safe.SafeBuf
for k, v := range m {
if s, ok := v.(string); ok {
var b64 []byte
var err error
if b64, err = encoding.UnUrlBase64FromString(s); err != nil {
b64, err = encoding.UnBase64FromString(s)
// 兼容旧版 ENC(...) 格式扫描
inner := s
if strings.HasPrefix(s, "ENC(") && strings.HasSuffix(s, ")") {
inner = s[4 : len(s)-1]
}
if b64, err = encoding.UnUrlBase64FromString(inner); err != nil {
b64, err = encoding.UnBase64FromString(inner)
}
if err == nil && len(b64) > 0 {

View File

@ -3,65 +3,27 @@ package api
import (
"context"
"apigo.cc/go/cast"
"apigo.cc/go/jsmod"
)
func init() {
jsmod.Register("api", map[string]any{
"call": call,
"addConfig": AddConfig,
"setConfig": SetConfig,
"registerAction": RegisterAction,
"registerSigner": registerSigner,
})
}
// call 提供给 JS 的私有入口,避免污染 Go 公开 API
func call(ctx context.Context, actionName string, payload any, options ...map[string]any) (any, error) {
var opt map[string]any
if len(options) > 0 {
opt = options[0]
}
if payload == nil {
payload = make(map[string]any)
}
// 1. 预填 Payload (JS 传来的 map 需要手动触发一次 fill 以获取全局配置)
actionConfig, safeBufs := GetActionConfig(actionName)
defer func() {
for _, sb := range safeBufs {
sb.Close()
}
}()
fill(payload, actionConfig)
// 2. 构造符合 Action 接口的私有包装器
ja := &jsAction{
name: actionName,
payload: payload,
options: opt,
}
return Call[any](ja)
// call 提供给 JS 的私有入口
func call(ctx context.Context, name string, payload any) (any, error) {
// 注意:在低代码环境中,我们可能需要将 ctx 传入以透传追踪信息给 JS 签名器。
// 虽然 api.CallBy[any] 目前不接收 ctx但底层的 jsRunner 钩子可以从 context 中获取信息。
// 如果未来 jsRunner 需要 ctx我们可以在这里通过 context.WithValue 注入。
return CallBy[any](name, payload)
}
type jsAction struct {
name string
payload any
options map[string]any
}
func (a *jsAction) ActionName() string { return a.name }
func (a *jsAction) SignerName() string {
if a.options != nil {
return cast.String(a.options["signer"])
}
return ""
}
func (a *jsAction) GetURL() string { return cast.String(a.options["url"]) }
func (a *jsAction) GetMethod() string { return cast.String(a.options["method"]) }
func (a *jsAction) Config() map[string]any {
return a.options
}
func (a *jsAction) MarshalJSON() ([]byte, error) {
return cast.ToJSONBytes(a.payload)
// registerSigner 允许从 JS 注册动态签名逻辑
func registerSigner(name string, code string) {
RegisterSigner(name, &jsSigner{code: code})
}

View File

@ -1,6 +1,7 @@
package api
import (
"context"
"errors"
"apigo.cc/go/safe"
@ -16,6 +17,14 @@ var signers = map[string]Signer{
"bearer": &bearerSigner{},
}
var jsRunner func(ctx context.Context, code string, args map[string]any) (map[string]any, error)
// SetJSRunner 设置 JS 执行器钩子,由 go/js 引擎在启动时注入。
// 这避免了 api 模块直接依赖 go/js 产生的循环引用。
func SetJSRunner(runner func(context.Context, string, map[string]any) (map[string]any, error)) {
jsRunner = runner
}
// RegisterSigner 注册全局签名器
func RegisterSigner(name string, s Signer) {
signers[name] = s
@ -26,6 +35,50 @@ func GetSigner(name string) Signer {
return signers[name]
}
// jsSigner 包装 JS 代码为 Go Signer 接口
type jsSigner struct {
code string
}
func (s *jsSigner) Sign(req *HttpRequest, config map[string]any) error {
if jsRunner == nil {
return errors.New("jsRunner is not set, cannot execute js signer")
}
// 准备传递给 JS 的参数
args := map[string]any{
"method": req.Method,
"url": req.Url,
"payload": req.Payload,
"headers": req.headers,
"config": config,
}
// 执行 JS 签名逻辑
// 注意:这里默认使用 Background context除非底层能透传低代码环境中会由 js_export 注入正确的 ctx
res, err := jsRunner(context.Background(), s.code, args)
if err != nil {
return err
}
// 将结果(通常是修改后的 headers写回请求对象
if newHeaders, ok := res["headers"].(map[string]any); ok {
for k, v := range newHeaders {
req.SetHeader(k, v)
}
}
// 如果 JS 返回了新的 URL 或 Method也支持修改
if newUrl, ok := res["url"].(string); ok {
req.Url = newUrl
}
if newMethod, ok := res["method"].(string); ok {
req.Method = newMethod
}
return nil
}
// 快速应用签名
func sign(name string, req *HttpRequest, config map[string]any) error {
if name == "" {