feat(canvas): 生成合同 - 统一节点参数并补充模型上下文能力
This commit is contained in:
@@ -23,8 +23,13 @@ type ModelCapabilityConfig struct {
|
||||
type TextCapabilityConfig struct {
|
||||
// Streaming controls whether this model accepts upstream SSE text responses.
|
||||
// A nil value is treated as true for backwards compatibility with older configs.
|
||||
Streaming *bool `json:"streaming,omitempty"`
|
||||
References TextReferenceConfig `json:"references"`
|
||||
Streaming *bool `json:"streaming,omitempty"`
|
||||
// ContextWindowTokens is the provider's total input plus output context window.
|
||||
// It is a model contract, not an application transport ceiling.
|
||||
ContextWindowTokens int `json:"contextWindowTokens"`
|
||||
// MaxOutputTokens is the provider's maximum completion/reasoning budget.
|
||||
MaxOutputTokens int `json:"maxOutputTokens"`
|
||||
References TextReferenceConfig `json:"references"`
|
||||
}
|
||||
|
||||
type TextReferenceConfig struct {
|
||||
@@ -209,7 +214,7 @@ func legacyImageSizeValues() []string {
|
||||
func DefaultModelCapabilityConfigForModel(protocol string, modelName string) *ModelCapabilityConfig {
|
||||
// 文本模型是否支持视觉输入不能从协议或模型名可靠推断,默认关闭,由管理员按真实上游能力开启。
|
||||
streaming := true
|
||||
text := &TextCapabilityConfig{Streaming: &streaming, References: TextReferenceConfig{PromptMaxChars: 32000}}
|
||||
text := &TextCapabilityConfig{Streaming: &streaming, ContextWindowTokens: 128000, MaxOutputTokens: 16384, References: TextReferenceConfig{PromptMaxChars: 32000}}
|
||||
video := &VideoCapabilityConfig{
|
||||
References: VideoReferenceConfig{PromptMaxChars: DefaultVideoPromptMaxChars, MinImages: 0, MaxImages: 9, MaxImageBytes: 30 * 1024 * 1024, MaxVideos: 0, MaxVideoBytes: 0, MaxVideoDuration: 0, MaxAudios: 0, MaxAudioBytes: 0, MaxAudioDuration: 0},
|
||||
Duration: VideoDurationConfig{Selection: "range", Min: 1, Max: 15, Step: 1, Default: 6},
|
||||
@@ -327,6 +332,12 @@ func NormalizeModelCapabilityConfigForModel(capability string, protocol string,
|
||||
streaming := true
|
||||
text.Streaming = &streaming
|
||||
}
|
||||
if text.ContextWindowTokens == 0 {
|
||||
text.ContextWindowTokens = 128000
|
||||
}
|
||||
if text.MaxOutputTokens == 0 {
|
||||
text.MaxOutputTokens = 16384
|
||||
}
|
||||
value := &ModelCapabilityConfig{Version: 1, Text: &text}
|
||||
if err := validateTextCapabilityConfig(value.Text); err != nil {
|
||||
return nil, err
|
||||
@@ -534,6 +545,12 @@ func addInputConstraint(inputs map[string]InputConstraint, name string, min int,
|
||||
}
|
||||
|
||||
func validateTextCapabilityConfig(value *TextCapabilityConfig) error {
|
||||
if value.ContextWindowTokens < 4096 || value.ContextWindowTokens > 10000000 {
|
||||
return BadAuthRequest("文本模型上下文窗口必须在 4096-10000000 Token 之间")
|
||||
}
|
||||
if value.MaxOutputTokens < 256 || value.MaxOutputTokens > 1000000 || value.MaxOutputTokens >= value.ContextWindowTokens {
|
||||
return BadAuthRequest("文本模型最大输出 Token 必须小于上下文窗口且在 256-1000000 之间")
|
||||
}
|
||||
if value.References.PromptMaxChars < 1 || value.References.PromptMaxChars > 1000000 {
|
||||
return BadAuthRequest("提示词最大字符数必须在 1-1000000 之间")
|
||||
}
|
||||
|
||||
@@ -352,6 +352,12 @@ func (s *Service) processCanvasGenerationTask(ctx context.Context, userID string
|
||||
return nil, err
|
||||
}
|
||||
input.Config = config
|
||||
if input.Mode == "text" && input.Config.CapabilityConfig != nil && input.Config.CapabilityConfig.Text != nil {
|
||||
// The same capability contract drives provider output limits, billing
|
||||
// estimates and Agent context budgeting. Never silently fall back to a
|
||||
// transport-specific fixed token count when the model declares one.
|
||||
input.MaxOutputTokens = input.Config.CapabilityConfig.Text.MaxOutputTokens
|
||||
}
|
||||
var textPublisher *taskTextStreamPublisher
|
||||
if input.Mode == "text" && strings.HasPrefix(taskType, "canvas_text") {
|
||||
requestedStream := input.TextOptions.Stream == nil || *input.TextOptions.Stream
|
||||
|
||||
15
backend/internal/canvas/contract/generate/main.go
Normal file
15
backend/internal/canvas/contract/generate/main.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"infinite-canvas/backend/internal/canvas/contract"
|
||||
)
|
||||
|
||||
func main() {
|
||||
path := filepath.Join("..", "..", "..", "..", "web", "src", "lib", "canvas", "generation-contract.generated.ts")
|
||||
if err := os.WriteFile(path, []byte(contract.TypeScriptDeclaration()), 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
317
backend/internal/canvas/contract/generation.go
Normal file
317
backend/internal/canvas/contract/generation.go
Normal file
@@ -0,0 +1,317 @@
|
||||
// Package contract owns the credential-free canvas generation contract.
|
||||
// Provider configuration remains an execution concern; it is never persisted here.
|
||||
package contract
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
//go:generate go run ./generate
|
||||
|
||||
const (
|
||||
GenerationVersion = 1
|
||||
MaxNodeTitleRunes = 240
|
||||
MaxPromptRunes = 16000
|
||||
MaxCanvasOperations = 20
|
||||
)
|
||||
|
||||
type ModelSelection struct {
|
||||
Kind string `json:"kind" enum:"logical,channel"`
|
||||
LogicalModelID string `json:"logicalModelId,omitempty"`
|
||||
ChannelID string `json:"channelId,omitempty"`
|
||||
ModelKey string `json:"modelKey,omitempty"`
|
||||
}
|
||||
|
||||
// Options contains public user choices, not resolved provider configuration.
|
||||
// node/task tags are the only mapping between the persisted canvas and task wire names.
|
||||
type Options struct {
|
||||
Size *string `json:"size,omitempty" node:"size" task:"size" modes:"image,video"`
|
||||
Quality *string `json:"quality,omitempty" node:"quality" task:"quality" modes:"image"`
|
||||
Count *int `json:"count,omitempty" node:"count" task:"count" modes:"image"`
|
||||
TransparentBackground *bool `json:"transparentBackground,omitempty" node:"transparentBackground" task:"transparentBackground" modes:"image"`
|
||||
DurationSeconds *int `json:"durationSeconds,omitempty" node:"seconds" task:"videoSeconds" modes:"video"`
|
||||
Resolution *string `json:"resolution,omitempty" node:"vquality" task:"vquality" modes:"video"`
|
||||
GenerateAudio *bool `json:"generateAudio,omitempty" node:"generateAudio" task:"videoGenerateAudio" modes:"video"`
|
||||
Watermark *bool `json:"watermark,omitempty" node:"watermark" task:"videoWatermark" modes:"video"`
|
||||
AudioVoice *string `json:"audioVoice,omitempty" node:"audioVoice" task:"audioVoice" modes:"audio"`
|
||||
AudioFormat *string `json:"audioFormat,omitempty" node:"audioFormat" task:"audioFormat" modes:"audio"`
|
||||
AudioSpeed *string `json:"audioSpeed,omitempty" node:"audioSpeed" task:"audioSpeed" modes:"audio"`
|
||||
AudioInstructions *string `json:"audioInstructions,omitempty" node:"audioInstructions" task:"audioInstructions" modes:"audio"`
|
||||
}
|
||||
|
||||
type ReferenceBinding struct {
|
||||
ID string `json:"id"`
|
||||
NodeID string `json:"nodeId,omitempty"`
|
||||
ResourceID string `json:"resourceId,omitempty"`
|
||||
TransientID string `json:"transientId,omitempty"`
|
||||
MediaType string `json:"mediaType" enum:"image,video,audio,text"`
|
||||
Role string `json:"role" enum:"reference,source-text,first-frame,last-frame,mask"`
|
||||
Order int `json:"order"`
|
||||
Resolution string `json:"resolution" enum:"latest,snapshot"`
|
||||
}
|
||||
|
||||
type GenerationSpec struct {
|
||||
Version int `json:"version"`
|
||||
Mode string `json:"mode" enum:"image,video,audio"`
|
||||
Prompt string `json:"prompt"`
|
||||
ModelSelection *ModelSelection `json:"modelSelection,omitempty"`
|
||||
Options Options `json:"options"`
|
||||
ReferenceBindings []ReferenceBinding `json:"referenceBindings"`
|
||||
TextInputMode string `json:"textInputMode" enum:"prompt-only,append-sources"`
|
||||
}
|
||||
|
||||
type FieldError struct{ Path, Message string }
|
||||
|
||||
func (e *FieldError) Error() string { return e.Path + ": " + e.Message }
|
||||
func invalid(path, message string) error { return &FieldError{Path: path, Message: message} }
|
||||
|
||||
func (s ModelSelection) Validate() error {
|
||||
switch s.Kind {
|
||||
case "logical":
|
||||
if strings.TrimSpace(s.LogicalModelID) == "" || s.ChannelID != "" || s.ModelKey != "" {
|
||||
return invalid("modelSelection", "逻辑模型必须且只能指定 logicalModelId")
|
||||
}
|
||||
case "channel":
|
||||
if strings.TrimSpace(s.ChannelID) == "" || strings.TrimSpace(s.ModelKey) == "" || s.LogicalModelID != "" {
|
||||
return invalid("modelSelection", "渠道模型必须且只能指定 channelId 和 modelKey")
|
||||
}
|
||||
default:
|
||||
return invalid("modelSelection.kind", "未知模型选择类型")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s GenerationSpec) Validate() error {
|
||||
if s.Version != GenerationVersion {
|
||||
return invalid("generationSpec.version", "生成合同版本不受支持")
|
||||
}
|
||||
if s.Mode != "image" && s.Mode != "video" && s.Mode != "audio" {
|
||||
return invalid("generationSpec.mode", "未知生成模式")
|
||||
}
|
||||
if utf8.RuneCountInString(s.Prompt) > MaxPromptRunes {
|
||||
return invalid("generationSpec.prompt", "提示词超过长度限制")
|
||||
}
|
||||
if s.ModelSelection != nil {
|
||||
if err := s.ModelSelection.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if s.TextInputMode != "prompt-only" && s.TextInputMode != "append-sources" {
|
||||
return invalid("generationSpec.textInputMode", "未知文本输入模式")
|
||||
}
|
||||
value, typ := reflect.ValueOf(s.Options), reflect.TypeOf(s.Options)
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
if value.Field(i).IsNil() {
|
||||
continue
|
||||
}
|
||||
field := typ.Field(i)
|
||||
if !strings.Contains(","+field.Tag.Get("modes")+",", ","+s.Mode+",") {
|
||||
return invalid("options."+strings.Split(field.Tag.Get("json"), ",")[0], "该选项不适用于当前生成模式")
|
||||
}
|
||||
}
|
||||
if s.Options.DurationSeconds != nil && *s.Options.DurationSeconds < 0 {
|
||||
return invalid("options.durationSeconds", "时长不能为负数")
|
||||
}
|
||||
if s.Options.Count != nil && *s.Options.Count < 1 {
|
||||
return invalid("options.count", "数量必须为正整数")
|
||||
}
|
||||
ids := map[string]bool{}
|
||||
orders := map[int]bool{}
|
||||
for i, binding := range s.ReferenceBindings {
|
||||
path := fmt.Sprintf("referenceBindings[%d]", i)
|
||||
if binding.ID == "" || ids[binding.ID] || binding.Order < 0 || orders[binding.Order] {
|
||||
return invalid(path, "引用身份和顺序必须有效且唯一")
|
||||
}
|
||||
ids[binding.ID], orders[binding.Order] = true, true
|
||||
if binding.NodeID == "" && binding.ResourceID == "" && binding.TransientID == "" {
|
||||
return invalid(path, "引用缺少来源")
|
||||
}
|
||||
if binding.TransientID != "" && (binding.NodeID != "" || binding.ResourceID != "") {
|
||||
return invalid(path, "临时引用不能混用其他来源")
|
||||
}
|
||||
if binding.Resolution != "latest" && binding.Resolution != "snapshot" {
|
||||
return invalid(path+".resolution", "未知引用解析策略")
|
||||
}
|
||||
if binding.MediaType != "image" && binding.MediaType != "video" && binding.MediaType != "audio" && binding.MediaType != "text" {
|
||||
return invalid(path+".mediaType", "未知参考类型")
|
||||
}
|
||||
if binding.Role != "reference" && binding.Role != "source-text" && binding.Role != "first-frame" && binding.Role != "last-frame" && binding.Role != "mask" {
|
||||
return invalid(path+".role", "未知参考角色")
|
||||
}
|
||||
if binding.Role == "source-text" && binding.MediaType != "text" {
|
||||
return invalid(path+".role", "文本来源必须引用文本")
|
||||
}
|
||||
if (binding.Role == "first-frame" || binding.Role == "last-frame" || binding.Role == "mask") && binding.MediaType != "image" {
|
||||
return invalid(path+".role", "帧和遮罩必须引用图片")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Decode(data []byte) (GenerationSpec, error) {
|
||||
var spec GenerationSpec
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&spec); err != nil {
|
||||
return spec, invalid("generationSpec", err.Error())
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
return spec, invalid("generationSpec", "必须是单个对象")
|
||||
}
|
||||
return spec, spec.Validate()
|
||||
}
|
||||
|
||||
// OptionsFromTaskConfig only copies public fields declared above. Credentials and
|
||||
// provider routing fields cannot be admitted by adding a task config key.
|
||||
func OptionsFromTaskConfig(mode string, config map[string]any) (Options, error) {
|
||||
options := Options{}
|
||||
value, typ := reflect.ValueOf(&options).Elem(), reflect.TypeOf(options)
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if !strings.Contains(","+field.Tag.Get("modes")+",", ","+mode+",") {
|
||||
continue
|
||||
}
|
||||
raw, exists := config[field.Tag.Get("task")]
|
||||
if !exists || raw == nil || raw == "" {
|
||||
continue
|
||||
}
|
||||
parsed := reflect.New(field.Type.Elem())
|
||||
switch field.Type.Elem().Kind() {
|
||||
case reflect.String:
|
||||
text, ok := raw.(string)
|
||||
if !ok {
|
||||
return options, invalid("options."+field.Tag.Get("task"), "必须是字符串")
|
||||
}
|
||||
parsed.Elem().SetString(text)
|
||||
case reflect.Bool:
|
||||
boolean, err := strconv.ParseBool(fmt.Sprint(raw))
|
||||
if err != nil {
|
||||
return options, invalid("options."+field.Tag.Get("task"), "必须是布尔值")
|
||||
}
|
||||
parsed.Elem().SetBool(boolean)
|
||||
case reflect.Int:
|
||||
number, err := strconv.Atoi(fmt.Sprint(raw))
|
||||
if err != nil {
|
||||
return options, invalid("options."+field.Tag.Get("task"), "必须是整数")
|
||||
}
|
||||
parsed.Elem().SetInt(int64(number))
|
||||
}
|
||||
value.Field(i).Set(parsed)
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func (o Options) TaskConfig() map[string]any {
|
||||
result := map[string]any{}
|
||||
value, typ := reflect.ValueOf(o), reflect.TypeOf(o)
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
if !value.Field(i).IsNil() {
|
||||
result[typ.Field(i).Tag.Get("task")] = fmt.Sprint(value.Field(i).Elem().Interface())
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NodeMetadata projects the editable specification into the existing canvas wire
|
||||
// format. The mirror fields keep existing renderers working; all conversions live here.
|
||||
func (s GenerationSpec) NodeMetadata() (map[string]any, error) {
|
||||
if err := s.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stored map[string]any
|
||||
if err := json.Unmarshal(encoded, &stored); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := map[string]any{"generationSpec": stored, "prompt": s.Prompt, "composerContent": s.Prompt}
|
||||
value, typ := reflect.ValueOf(s.Options), reflect.TypeOf(s.Options)
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
if value.Field(i).IsNil() {
|
||||
continue
|
||||
}
|
||||
fieldValue := value.Field(i).Elem().Interface()
|
||||
if typ.Field(i).Tag.Get("node") != "count" {
|
||||
fieldValue = fmt.Sprint(fieldValue)
|
||||
}
|
||||
result[typ.Field(i).Tag.Get("node")] = fieldValue
|
||||
}
|
||||
if selection := s.ModelSelection; selection != nil {
|
||||
if selection.Kind == "logical" {
|
||||
result["logicalModelId"] = selection.LogicalModelID
|
||||
} else {
|
||||
result["channelId"], result["channelModelKey"] = selection.ChannelID, selection.ModelKey
|
||||
result["model"] = selection.ChannelID + "::" + selection.ModelKey
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// NodeGenerationProjection is the explicit dependency read-set of generation.
|
||||
// Locks, task binding and authorization are checked separately by the caller.
|
||||
//
|
||||
// The projection keeps the canonical contract fields and also carries stable
|
||||
// extension metadata. Canvas presentation fields are deliberately excluded so
|
||||
// moving/resizing a node does not invalidate a prepared generation, while a
|
||||
// newly-added generation input cannot silently bypass admission.
|
||||
func NodeGenerationProjection(metadata map[string]any) (map[string]any, error) {
|
||||
result := map[string]any{}
|
||||
if raw, exists := metadata["generationSpec"]; exists {
|
||||
encoded, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec, err := Decode(encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result["generationSpec"] = spec
|
||||
}
|
||||
known := map[string]struct{}{
|
||||
"generationSpec": {}, "prompt": {}, "composerContent": {}, "model": {},
|
||||
"logicalModelId": {}, "channelId": {}, "channelModelKey": {},
|
||||
"videoStartFrameNodeId": {}, "videoEndFrameNodeId": {},
|
||||
"videoEditOperation": {}, "referenceNodeIds": {},
|
||||
}
|
||||
typ := reflect.TypeOf(Options{})
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
known[typ.Field(i).Tag.Get("node")] = struct{}{}
|
||||
}
|
||||
for key, value := range metadata {
|
||||
if _, exists := known[key]; exists {
|
||||
if key != "generationSpec" {
|
||||
result[key] = value
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isCanvasPresentationMetadataKey(key) {
|
||||
continue
|
||||
}
|
||||
extensions, _ := result["extensions"].(map[string]any)
|
||||
if extensions == nil {
|
||||
extensions = map[string]any{}
|
||||
result["extensions"] = extensions
|
||||
}
|
||||
extensions[key] = value
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isCanvasPresentationMetadataKey(key string) bool {
|
||||
switch key {
|
||||
case "position", "x", "y", "width", "height", "createdAt", "updatedAt", "selected", "collapsed", "zIndex", "color", "background", "displayName", "label", "ui", "presentation", "layout", "screenPosition", "isSelected", "dragging", "resizing":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
64
backend/internal/canvas/contract/typescript.go
Normal file
64
backend/internal/canvas/contract/typescript.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TypeScriptDeclaration is generated from the public Go contract, never from
|
||||
// provider configuration or an OpenAPI client schema.
|
||||
func TypeScriptDeclaration() string {
|
||||
var out strings.Builder
|
||||
out.WriteString("// Generated by go generate ./internal/canvas/contract. Do not edit.\n")
|
||||
fmt.Fprintf(&out, "export const GENERATION_CONTRACT_VERSION = %d;\nexport const GENERATION_LIMITS = { nodeTitle: %d, prompt: %d, canvasOperations: %d } as const;\n\n", GenerationVersion, MaxNodeTitleRunes, MaxPromptRunes, MaxCanvasOperations)
|
||||
out.WriteString("export type ModelSelection = { kind: \"logical\"; logicalModelId: string; channelId?: never; modelKey?: never } | { kind: \"channel\"; channelId: string; modelKey: string; logicalModelId?: never };\n\n")
|
||||
for _, typ := range []reflect.Type{reflect.TypeOf(Options{}), reflect.TypeOf(ReferenceBinding{}), reflect.TypeOf(GenerationSpec{})} {
|
||||
fmt.Fprintf(&out, "export type %s = {\n", typ.Name())
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
parts := strings.Split(field.Tag.Get("json"), ",")
|
||||
optional := ""
|
||||
if len(parts) > 1 && parts[1] == "omitempty" {
|
||||
optional = "?"
|
||||
}
|
||||
valueType := tsType(field.Type)
|
||||
if values := field.Tag.Get("enum"); values != "" {
|
||||
quoted := []string{}
|
||||
for _, value := range strings.Split(values, ",") {
|
||||
quoted = append(quoted, fmt.Sprintf("%q", value))
|
||||
}
|
||||
valueType = strings.Join(quoted, " | ")
|
||||
}
|
||||
fmt.Fprintf(&out, " %s%s: %s;\n", parts[0], optional, valueType)
|
||||
}
|
||||
out.WriteString("};\n\n")
|
||||
}
|
||||
fields := []map[string]any{}
|
||||
typ := reflect.TypeOf(Options{})
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
fields = append(fields, map[string]any{"name": strings.Split(field.Tag.Get("json"), ",")[0], "node": field.Tag.Get("node"), "task": field.Tag.Get("task"), "kind": tsType(field.Type), "modes": strings.Split(field.Tag.Get("modes"), ",")})
|
||||
}
|
||||
encoded, _ := json.MarshalIndent(fields, "", " ")
|
||||
fmt.Fprintf(&out, "export const GENERATION_OPTION_FIELDS = %s as const;\n", encoded)
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func tsType(typ reflect.Type) string {
|
||||
switch typ.Kind() {
|
||||
case reflect.Pointer:
|
||||
return tsType(typ.Elem())
|
||||
case reflect.Slice:
|
||||
return tsType(typ.Elem()) + "[]"
|
||||
case reflect.String:
|
||||
return "string"
|
||||
case reflect.Bool:
|
||||
return "boolean"
|
||||
case reflect.Int:
|
||||
return "number"
|
||||
default:
|
||||
return typ.Name()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user