contract

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package contract:插件契约的**唯一定义** —— 字段类型、声明、与 Go 类型之间的绑定。

为什么单独一个包(而不是留在 go-sdk):契约有两个消费者——

  • go-sdk:插件作者声明契约、按契约绑定入参/产出出参;
  • server:平台按契约做类型归一与运行前校验、画布按契约渲染。

此前两边**各定义了一份**:SDK 的 Field 是全量的,平台那份只有 name/type/fields/valueType。 于是 SDK 声明了而平台那份没有的东西(联合类型、枚举、必填、oneOf、multiple), 平台就看不见。这类「同一件事两处定义」的漂移今天已经栽过一次 (入参绑定两侧不对称,嵌套 snake_case 字段静默绑空)。

本包不依赖传输,也不依赖平台类型:契约是数据,取字节/发请求都不在这里。

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyDefaultTag

func ApplyDefaultTag(v reflect.Value, sf reflect.StructField)

ApplyDefaultTag 把 `default:"..."` 写进字段(导出给 SDK 复用)。

func BindInput

func BindInput(input json.RawMessage, dst any) error

bindInput 把平台传来的 input JSON 对象按 sokel tag 绑定进入参 struct(含文件字段)。 BindInput 把平台传来的 input JSON 绑进入参 struct,**按 sokel tag 递归**。

递归是必须的:出参那侧(StructToVars)一直按 sokel tag 递归展开,入参却曾只认顶层, 于是嵌套结构里的 snake_case 字段静默绑空——Go 的 json 大小写不敏感匹配跨不过下划线, `doc_id` 落不进 `DocID`,而且不报错。两侧必须互为逆运算。

func ParseTag

func ParseTag(sf reflect.StructField) (string, bool)

parseSokelTag 取字段的对外名与 optional 标记。无 sokel tag 时用字段名的下划线小写形式。 ParseTag 取字段的对外名与 optional 标记(导出给 SDK 复用)。

func RequireInputs

func RequireInputs(op Operation, cfg map[string]any) error

RequireInputs 按操作契约检查入参:必填的不能缺、不能是空串。

让契约**吃到实处**:执行器不再各写一遍 `if x == "" { return err }`——那份知识 与前端的 required 标记是两份手写副本,今天核对出前端漏标 6 个、后端有 32 处散落检查。 走同一份声明,两边就不可能分叉。

只查「有没有」,不查类型:类型归一在调用前统一做过(coerceContractTypes)。

func StructToVars

func StructToVars(o any) map[string]any

StructToVars 是 structToVars 的导出视图(生成的代码与测试用)。

Types

type AuthKind

type AuthKind string

AuthKind:认证形态。

const (
	AuthQR    AuthKind = "qr"    // 二维码(插件出题)
	AuthInput AuthKind = "input" // 用户回填,如短信验证码(插件出题)
	AuthOAuth AuthKind = "oauth" // 第三方同意页(**平台代答**)
)

type AuthMeta

type AuthMeta struct {
	// Kind:认证形态。用 auth.QR() / auth.Input() / auth.OAuth() 构造,别手写字面量。
	Kind AuthKind
	// Steps:插件实现哪几步。**由 Kind 决定**(见 contract/auth),不该手写——
	// 手写就是把「qr 要哪几步」这件事抄第二遍,而抄错的那份没人会发现。
	// 生成的 RegisterAuth 参数表照它长,缺一个就是编译错,而不是启动时才 panic。
	Steps []AuthStep
	// Provider / Scopes:kind=oauth 专用。作用域**由插件声明**,平台不写死——
	// 加一个新的 Google 服务插件时平台一行都不用改。
	Provider string
	Scopes   []string
}

AuthMeta:凭证的**获取方式**(协作式认证流的契约部分)。

只有契约,没有实现——Start/Poll/Submit 是函数,留在实现侧由生成的 RegisterAuth 接住, 与操作「schema 声明 + OnXxx 接实现」完全同形。

func AuthOf

func AuthOf(s AuthSchema) AuthMeta

AuthOf 取出认证声明。

type AuthSchema

type AuthSchema interface {
	AuthMeta() AuthMeta
}

AuthSchema:声明凭证怎么拿到。挂在凭证声明上(同一个类型多一个方法)而不是另起一个—— 认证方式是**凭证的属性**,分成两处声明只会让「这条凭证怎么来的」要翻两个地方。

type AuthStep

type AuthStep string

AuthStep:认证流的一步。

const (
	StepStart  AuthStep = "start"
	StepPoll   AuthStep = "poll"
	StepSubmit AuthStep = "submit"
)

type CommonFieldsSchema

type CommonFieldsSchema interface {
	CommonFields() []string
}

CommonFieldsSchema:可选声明「所有事件共有的字段」。

平台触发时把这些字段从 payload 平铺到输入顶层({{节点.chat_id}}),各事件分支共享 同一变量。**必须显式声明**而不是从各事件里推断交集:推断的话,新增一个事件少写了 某字段,公共字段就悄悄缩水,存量工作流跟着断——而那时没人会想到是这里。

type CredentialSchema

type CredentialSchema interface {
	CredentialFields() []FieldSpec
}

CredentialSchema:本插件凭证契约的声明。

与操作/事件同一条路子(schema 里声明 → sokel-gen 生成类型 → 注册握手上报)。 早先凭证只能靠 main 包里的 struct tag 反射,那条路表达不出 enum 候选值与默认值—— 它是操作在 codegen 之前的写法,凭证只是没跟着迁过来。

type Event

type Event struct {
	ID     string  `json:"id"`
	Label  string  `json:"label,omitempty"`
	Desc   string  `json:"desc,omitempty"`
	Fields []Field `json:"fields"`
}

Event 一种事件及其 payload 契约(注册握手时上报给平台)。

func EventOf

func EventOf(e EventSchema) Event

EventOf 由声明产出事件契约。

type EventMeta

type EventMeta struct {
	ID    string
	Label string
	Desc  string
}

EventMeta 事件的身份。与 Meta 之于操作同位。

type EventSchema

type EventSchema interface {
	EventMeta() EventMeta
	Fields() []FieldSpec
}

EventSchema:一个事件的声明。与 Schema(操作)同构—— 方法名写错即编译失败,而不是等生成期才发现。

type Field

type Field struct {
	Name     string      `json:"name"`
	Label    string      `json:"label,omitempty"`
	Type     ParamType   `json:"type"`
	Types    []ParamType `json:"types,omitempty"` // 联合类型(如 number|string):变量绑定/校验接受其中任一;Type 为主类型
	Required bool        `json:"required,omitempty"`
	Default  any         `json:"default,omitempty"`
	Desc     string      `json:"desc,omitempty"`
	Options  []Option    `json:"options,omitempty"` // enum 候选值(`enum:"a,b"` 或带显示名 `enum:"a=甲,b=乙"`)
	Fields   []Field     `json:"fields,omitempty"`  // json 的子字段 / array 的元素字段

	// OneOf:结构联合,该字段接受列出的几种结构之一。
	// 注意:**运行时反射产不出它**——Go 没有联合类型,且反射拿不到「类型名字符串 → 类型」的映射。
	// 它由 sokel-gen 的 AST 解析从 `oneof:"TypeA,TypeB"` tag 产出(docs/plugin-sdk-multilang.md §1)。
	OneOf []OneOfVariant `json:"oneOf,omitempty"`
	// ValueType:动态键(JSON Schema 的 additionalProperties),键运行期才知道、值类型统一。
	// 由 map[string]T 推导:T 是 any → 不产出(opaque);T 是具体类型 → 递归展开。与 Fields 互斥。
	ValueType *Field `json:"valueType,omitempty"`
	// GoType:声明时给出的 Go 类型名(如 "OSInfo")。**仅代码生成用的提示**,
	// 其他语言的生成器忽略它即可(协议消费方也不需要)。
	//
	// 为什么必须记:field.Json("os", OSInfo{}) 已经交出了类型,生成 Out struct 时就该
	// 复用 OSInfo 本身,而不是照 Fields 重新生成一个等价结构——否则实现侧会出现两个
	// 形状相同的类型,赋值得逐字段转换,正是要杜绝的那种运行时转换。
	GoType string `json:"goType,omitempty"`
	// ItemType:数组元素的标量类型([]string 与 []number 在契约里得能区分;
	// Fields 只能表达"元素是对象时的字段",标量元素此前无处安放)。
	ItemType ParamType `json:"itemType,omitempty"`
	// Opaque:该字段没有可声明的结构(裸 map[string]any / any)。
	// 弱类型是合法选择,但要成为**看得见的决定**而不是默认路径(docs/type-system.md §3):
	// UI 据此标注「无结构约束」,平台侧据此跳过结构校验——
	// 否则「没声明结构」与「声明了但恰好为空」分不清。
	Opaque bool `json:"opaque,omitempty"`
}

Field 操作的一个入/出参契约项(形态对齐前端 ParamSpec)。

func BuildFields

func BuildFields(specs []FieldSpec) []Field

BuildFields 把一组声明交出为契约字段。

func CredentialOf

func CredentialOf(s CredentialSchema) []Field

CredentialOf 把凭证声明摊平成契约字段。

func DeriveFields

func DeriveFields(t reflect.Type) []Field

DeriveFields 从 Go 类型推导契约字段(供 sokel/field 的 .Shape() 用)。

这里用反射不违背「运行时零反射」:它只在**声明期**执行——sokel-gen 运行 schema 取声明的 那一刻——产物是生成的静态 Field 字面量。反射的问题从来不是反射本身,是运行时反射。

func ValidateCommonFields

func ValidateCommonFields(events []Event, names []string) ([]Field, error)

ValidateCommonFields 校验公共字段:每个都必须在**所有**事件里存在且类型一致。

fail fast 而不是静默取交集——理由同 CommonFieldsSchema 的注释。

type FieldSpec

type FieldSpec interface{ Field() Field }

FieldSpec:字段声明。由 sokel/field 的 builder 实现——契约层只认这个接口, 不依赖 builder 的具体类型(否则 sokel ↔ field 成环)。

type FileRef

type FileRef interface{ FileRef() }

FileRef:标记「这个类型是平台文件引用」。SDK 的 sokel.File 实现它。

契约包只需要**认出**文件字段(报 type=file、绑定时交给标准库); 取字节与上传属于运行时,那是 SDK 的事,不该为了识别一个字段把运行时拖进来。

type Meta

type Meta struct {
	ID         string
	Label      string
	Desc       string
	TimeoutSec int  // 插件最清楚自己这个操作要跑多久,让它自报,用户不必拖出节点时猜
	Stream     bool // 流式(多帧回复)
	Internal   bool // 内部操作(认证流等):不上画布,仅平台面板调用
}

Meta:操作的元信息(不含入/出参——那是 Inputs/Outputs 的事)。

type OneOfVariant

type OneOfVariant struct {
	Name  string    `json:"name"`
	Label string    `json:"label,omitempty"`
	Type  ParamType `json:"type"`
	// GoType:该分支的 Go 类型名——**数组分支指的是元素类型**([]Block → "Block")。
	// 与 Name 分开是因为匿名切片没有名字:reflect.TypeOf([]Block{}).Name() 返回空串,
	// 只用 Name 会生成出 `[]schema.` 这种坏代码(实测踩到)。
	GoType string  `json:"goType,omitempty"`
	Fields []Field `json:"fields,omitempty"`
}

OneOfVariant:oneOf 的一个分支。Name 是分支标识(报错定位用),不落盘到运行值里—— 运行值就是该分支本身的形状,不带 discriminator 包装。

type Operation

type Operation struct {
	ID       string `json:"id"`
	Label    string `json:"label"`
	Desc     string `json:"desc,omitempty"`
	Stream   bool   `json:"stream,omitempty"`   // 是否流式(多帧回复)
	Internal bool   `json:"internal,omitempty"` // 内部操作(如 auth_start/submit/poll 认证流):不出现在画布节点,仅平台面板调用
	// TimeoutSec 该操作的建议超时(秒)。平台取超时的优先级:节点显式配置 > 本字段 > 平台默认 60s。
	// 重活(转写/长文本合成/大文件解析)务必声明,否则 60s 抢跑,而用户拖出节点时并不知道该填多少。
	TimeoutSec int     `json:"timeoutSec,omitempty"`
	Inputs     []Field `json:"inputs"`
	Outputs    []Field `json:"outputs"`
}

Operation 操作声明。Inputs/Outputs 留空时由 Register 的 In/Out 类型反射推导。 json tag 用于注册握手时上报契约给平台(形态对齐前端 PluginOperation)。

func OperationOf

func OperationOf(s Schema) Operation

OperationOf 把声明摊平成线协议的 Operation(注册握手用)。

type Option

type Option struct {
	Value string `json:"value"`
	Label string `json:"label,omitempty"`
}

Option:enum 的一个候选项。Label 为空时前端回退显示 Value—— 值本身可读时(asc/desc)不必再写一遍,值是代码时(发音人 xiaoyan)才需要显示名。

type ParamType

type ParamType string

ParamType 与平台/前端一致的字段类型(画布据此渲染入/出参绑定)。

const (
	TString ParamType = "string"
	TNumber ParamType = "number"
	TBool   ParamType = "boolean"
	TFile   ParamType = "file"
	TJSON   ParamType = "json"
	TArray  ParamType = "array"
	TEnum   ParamType = "enum"
)

type Schema

type Schema interface {
	Meta() Meta
	Inputs() []FieldSpec
	Outputs() []FieldSpec
}

Schema:一个操作的完整声明。**只声明,不含实现**—— 实现由生成的专用注册函数接住(签名完全具体,零泛型零 any)。

先定义后实现是刻意的:契约是对外接口,应该能被单独评审、单独导出成 JSON 供其他语言 SDK 生成对应类型,而不是从实现代码里反推出来的副产品。

Directories

Path Synopsis
Package auth 构造凭证的获取方式声明。
Package auth 构造凭证的获取方式声明。
Package field 提供声明插件契约的 builder。
Package field 提供声明插件契约的 builder。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL