feat(kol): expand variable schema and polish KOL UI

- Add length/range limits to KOL variable definitions: max_length and
  default_value for input/textarea, min_value/max_value/default_number
  for number. Backend validates and normalizes; frontend renders limit
  inputs in KolVariableConfig and applies limits at runtime in
  KolDynamicForm.
- Sort workspace cards by most recent prompt/package update, falling
  back to granted_at. Adds updated_at to KolWorkspaceCard.
- Polish TemplatesView, WorkspaceView, and KOL package management UI;
  route create/update/publish/archive/delete toasts through i18n and
  expand package form copy and status labels.
- Remove stale KnowledgeView.vue.patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 15:01:40 +08:00
parent 1dd316a34b
commit 79c65c1da7
26 changed files with 1506 additions and 235 deletions
@@ -6,7 +6,9 @@ import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
type KolVariableType string
@@ -17,16 +19,25 @@ const (
KolVariableTypeSelect KolVariableType = "select"
KolVariableTypeNumber KolVariableType = "number"
KolVariableTypeCheckbox KolVariableType = "checkbox"
kolDefaultTextMaxLength = 20
kolDefaultNumberMin = 0
kolDefaultNumberMax = 10000
)
type KolVariableDefinition struct {
ID string `json:"id"`
Key string `json:"key"`
Label string `json:"label"`
Type KolVariableType `json:"type"`
Required bool `json:"required"`
Placeholder *string `json:"placeholder,omitempty"`
Options []string `json:"options,omitempty"`
ID string `json:"id"`
Key string `json:"key"`
Label string `json:"label"`
Type KolVariableType `json:"type"`
Required bool `json:"required"`
Placeholder *string `json:"placeholder,omitempty"`
Options []string `json:"options,omitempty"`
MaxLength *int `json:"max_length,omitempty"`
DefaultValue *string `json:"default_value,omitempty"`
MinValue *float64 `json:"min_value,omitempty"`
MaxValue *float64 `json:"max_value,omitempty"`
DefaultNumber *float64 `json:"default_number,omitempty"`
}
type KolSchemaJSON struct {
@@ -63,7 +74,13 @@ func RenderPrompt(content string, schema KolSchemaJSON, values map[string]any) (
missing = append(missing, id)
}
var resolveErr error
rendered := kolPlaceholderRE.ReplaceAllStringFunc(content, func(match string) string {
if resolveErr != nil {
return match
}
submatches := kolPlaceholderRE.FindStringSubmatch(match)
if len(submatches) < 2 {
return match
@@ -76,7 +93,11 @@ func RenderPrompt(content string, schema KolSchemaJSON, values map[string]any) (
return match
}
value, hasValue := lookupKolVariableValue(values, variable, token)
value, hasValue, err := resolveKolVariableValue(values, variable, token)
if err != nil {
resolveErr = err
return match
}
if hasValue {
return fmt.Sprint(value)
}
@@ -86,6 +107,10 @@ func RenderPrompt(content string, schema KolSchemaJSON, values map[string]any) (
return ""
})
if resolveErr != nil {
return "", resolveErr
}
if len(missing) > 0 {
return "", fmt.Errorf("missing variables: %s", strings.Join(missing, ", "))
}
@@ -127,6 +152,26 @@ func ValidateSchema(schema KolSchemaJSON) error {
default:
return fmt.Errorf("unsupported variable type: %s", variable.Type)
}
switch variable.Type {
case KolVariableTypeInput, KolVariableTypeTextarea:
maxLength := kolVariableTextMaxLength(variable)
if maxLength <= 0 {
return fmt.Errorf("%s max length must be greater than 0", kolVariableDisplayName(variable))
}
if variable.DefaultValue != nil && utf8.RuneCountInString(*variable.DefaultValue) > maxLength {
return fmt.Errorf("%s default value exceeds max length %d", kolVariableDisplayName(variable), maxLength)
}
case KolVariableTypeNumber:
minValue := kolVariableNumberMin(variable)
maxValue := kolVariableNumberMax(variable)
if maxValue < minValue {
return fmt.Errorf("%s max value must be greater than or equal to min value", kolVariableDisplayName(variable))
}
if variable.DefaultNumber != nil && (*variable.DefaultNumber < minValue || *variable.DefaultNumber > maxValue) {
return fmt.Errorf("%s default number must be between %s and %s", kolVariableDisplayName(variable), formatKolNumber(minValue), formatKolNumber(maxValue))
}
}
}
return nil
@@ -139,11 +184,76 @@ func JSONEncodeSchema(schema KolSchemaJSON) ([]byte, error) {
func normalizeKolSchema(schema KolSchemaJSON) KolSchemaJSON {
if schema.Variables == nil {
schema.Variables = []KolVariableDefinition{}
return schema
}
normalizedVariables := make([]KolVariableDefinition, 0, len(schema.Variables))
for _, variable := range schema.Variables {
normalizedVariables = append(normalizedVariables, normalizeKolVariableDefinition(variable))
}
schema.Variables = normalizedVariables
return schema
}
func lookupKolVariableValue(values map[string]any, variable KolVariableDefinition, token string) (any, bool) {
func normalizeKolVariableDefinition(variable KolVariableDefinition) KolVariableDefinition {
variable.ID = strings.TrimSpace(variable.ID)
variable.Key = strings.TrimSpace(variable.Key)
variable.Label = strings.TrimSpace(variable.Label)
variable.Type = normalizeKolVariableType(variable.Type)
switch variable.Type {
case KolVariableTypeInput, KolVariableTypeTextarea:
maxLength := kolVariableTextMaxLength(variable)
variable.MaxLength = intPtr(maxLength)
variable.Options = nil
variable.MinValue = nil
variable.MaxValue = nil
variable.DefaultNumber = nil
case KolVariableTypeNumber:
minValue := kolVariableNumberMin(variable)
maxValue := kolVariableNumberMax(variable)
variable.MinValue = float64Ptr(minValue)
variable.MaxValue = float64Ptr(maxValue)
variable.Options = nil
variable.MaxLength = nil
variable.DefaultValue = nil
case KolVariableTypeSelect, KolVariableTypeCheckbox:
variable.MaxLength = nil
variable.DefaultValue = nil
variable.MinValue = nil
variable.MaxValue = nil
variable.DefaultNumber = nil
default:
variable.Type = KolVariableTypeInput
maxLength := kolVariableTextMaxLength(variable)
variable.MaxLength = intPtr(maxLength)
variable.Options = nil
variable.MinValue = nil
variable.MaxValue = nil
variable.DefaultNumber = nil
}
return variable
}
func normalizeKolVariableType(value KolVariableType) KolVariableType {
switch KolVariableType(strings.TrimSpace(string(value))) {
case KolVariableTypeTextarea:
return KolVariableTypeTextarea
case KolVariableTypeSelect:
return KolVariableTypeSelect
case KolVariableTypeNumber:
return KolVariableTypeNumber
case KolVariableTypeCheckbox:
return KolVariableTypeCheckbox
case KolVariableTypeInput:
fallthrough
default:
return KolVariableTypeInput
}
}
func resolveKolVariableValue(values map[string]any, variable KolVariableDefinition, token string) (any, bool, error) {
candidates := []string{
strings.TrimSpace(token),
strings.TrimSpace(variable.Key),
@@ -161,11 +271,160 @@ func lookupKolVariableValue(values map[string]any, variable KolVariableDefinitio
seen[candidate] = struct{}{}
if value, ok := values[candidate]; ok {
return value, true
return normalizeKolRuntimeValue(variable, value)
}
}
return nil, false
return kolVariableDefaultValue(variable)
}
func normalizeKolRuntimeValue(variable KolVariableDefinition, value any) (any, bool, error) {
switch variable.Type {
case KolVariableTypeInput, KolVariableTypeTextarea:
text := fmt.Sprint(value)
if strings.TrimSpace(text) == "" {
return "", false, nil
}
maxLength := kolVariableTextMaxLength(variable)
if utf8.RuneCountInString(text) > maxLength {
return nil, false, fmt.Errorf("%s exceeds max length %d", kolVariableDisplayName(variable), maxLength)
}
return text, true, nil
case KolVariableTypeNumber:
minValue := kolVariableNumberMin(variable)
maxValue := kolVariableNumberMax(variable)
if maxValue < minValue {
return nil, false, fmt.Errorf("invalid number range for %s", kolVariableDisplayName(variable))
}
number, ok := parseKolNumber(value)
if !ok {
return nil, false, fmt.Errorf("%s must be a valid number", kolVariableDisplayName(variable))
}
if number < minValue || number > maxValue {
return nil, false, fmt.Errorf("%s must be between %s and %s", kolVariableDisplayName(variable), formatKolNumber(minValue), formatKolNumber(maxValue))
}
return number, true, nil
case KolVariableTypeCheckbox:
switch typed := value.(type) {
case nil:
return nil, false, nil
case []string:
if len(typed) == 0 {
return typed, false, nil
}
return typed, true, nil
case []any:
if len(typed) == 0 {
return typed, false, nil
}
return typed, true, nil
default:
text := strings.TrimSpace(fmt.Sprint(value))
if text == "" {
return "", false, nil
}
return value, true, nil
}
case KolVariableTypeSelect:
fallthrough
default:
text := fmt.Sprint(value)
if strings.TrimSpace(text) == "" {
return "", false, nil
}
return text, true, nil
}
}
func kolVariableDefaultValue(variable KolVariableDefinition) (any, bool, error) {
switch variable.Type {
case KolVariableTypeInput, KolVariableTypeTextarea:
if variable.DefaultValue == nil {
return nil, false, nil
}
return normalizeKolRuntimeValue(variable, *variable.DefaultValue)
case KolVariableTypeNumber:
if variable.DefaultNumber == nil {
return nil, false, nil
}
return normalizeKolRuntimeValue(variable, *variable.DefaultNumber)
default:
return nil, false, nil
}
}
func kolVariableTextMaxLength(variable KolVariableDefinition) int {
if variable.MaxLength == nil || *variable.MaxLength <= 0 {
return kolDefaultTextMaxLength
}
return *variable.MaxLength
}
func kolVariableNumberMin(variable KolVariableDefinition) float64 {
if variable.MinValue == nil {
return kolDefaultNumberMin
}
return *variable.MinValue
}
func kolVariableNumberMax(variable KolVariableDefinition) float64 {
if variable.MaxValue == nil {
return kolDefaultNumberMax
}
return *variable.MaxValue
}
func parseKolNumber(value any) (float64, bool) {
switch typed := value.(type) {
case float64:
return typed, true
case float32:
return float64(typed), true
case int:
return float64(typed), true
case int8:
return float64(typed), true
case int16:
return float64(typed), true
case int32:
return float64(typed), true
case int64:
return float64(typed), true
case uint:
return float64(typed), true
case uint8:
return float64(typed), true
case uint16:
return float64(typed), true
case uint32:
return float64(typed), true
case uint64:
return float64(typed), true
case json.Number:
parsed, err := typed.Float64()
return parsed, err == nil
case string:
trimmed := strings.TrimSpace(typed)
if trimmed == "" {
return 0, false
}
parsed, err := strconv.ParseFloat(trimmed, 64)
return parsed, err == nil
default:
return 0, false
}
}
func formatKolNumber(value float64) string {
return strconv.FormatFloat(value, 'f', -1, 64)
}
func intPtr(value int) *int {
return &value
}
func float64Ptr(value float64) *float64 {
return &value
}
func kolVariableDisplayName(variable KolVariableDefinition) string {