Documentation
¶
Overview ¶
Package contract:插件契约的**唯一定义** —— 字段类型、声明、与 Go 类型之间的绑定。
为什么单独一个包(而不是留在 go-sdk):契约有两个消费者——
- go-sdk:插件作者声明契约、按契约绑定入参/产出出参;
- server:平台按契约做类型归一与运行前校验、画布按契约渲染。
此前两边**各定义了一份**:SDK 的 Field 是全量的,平台那份只有 name/type/fields/valueType。 于是 SDK 声明了而平台那份没有的东西(联合类型、枚举、必填、oneOf、multiple), 平台就看不见。这类「同一件事两处定义」的漂移今天已经栽过一次 (入参绑定两侧不对称,嵌套 snake_case 字段静默绑空)。
本包不依赖传输,也不依赖平台类型:契约是数据,取字节/发请求都不在这里。
Index ¶
- func ApplyDefaultTag(v reflect.Value, sf reflect.StructField)
- func BindInput(input json.RawMessage, dst any) error
- func ParseTag(sf reflect.StructField) (string, bool)
- func RequireInputs(op Operation, cfg map[string]any) error
- func StructToVars(o any) map[string]any
- type AuthKind
- type AuthMeta
- type AuthSchema
- type AuthStep
- type CommonFieldsSchema
- type CredentialSchema
- type Event
- type EventMeta
- type EventSchema
- type Field
- type FieldSpec
- type FileRef
- type Meta
- type OneOfVariant
- type Operation
- type Option
- type ParamType
- type Schema
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 ¶
RequireInputs 按操作契约检查入参:必填的不能缺、不能是空串。
让契约**吃到实处**:执行器不再各写一遍 `if x == "" { return err }`——那份知识 与前端的 required 标记是两份手写副本,今天核对出前端漏标 6 个、后端有 32 处散落检查。 走同一份声明,两边就不可能分叉。
只查「有没有」,不查类型:类型归一在调用前统一做过(coerceContractTypes)。
func StructToVars ¶
StructToVars 是 structToVars 的导出视图(生成的代码与测试用)。
Types ¶
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 接实现」完全同形。
type AuthSchema ¶
type AuthSchema interface {
AuthMeta() AuthMeta
}
AuthSchema:声明凭证怎么拿到。挂在凭证声明上(同一个类型多一个方法)而不是另起一个—— 认证方式是**凭证的属性**,分成两处声明只会让「这条凭证怎么来的」要翻两个地方。
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 契约(注册握手时上报给平台)。
type EventSchema ¶
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 DeriveFields ¶
DeriveFields 从 Go 类型推导契约字段(供 sokel/field 的 .Shape() 用)。
这里用反射不违背「运行时零反射」:它只在**声明期**执行——sokel-gen 运行 schema 取声明的 那一刻——产物是生成的静态 Field 字面量。反射的问题从来不是反射本身,是运行时反射。
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)。
type Option ¶
Option:enum 的一个候选项。Label 为空时前端回退显示 Value—— 值本身可读时(asc/desc)不必再写一遍,值是代码时(发音人 xiaoyan)才需要显示名。