79c65c1da7
- 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>
439 lines
12 KiB
Go
439 lines
12 KiB
Go
package app
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type KolVariableType string
|
|
|
|
const (
|
|
KolVariableTypeInput KolVariableType = "input"
|
|
KolVariableTypeTextarea KolVariableType = "textarea"
|
|
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"`
|
|
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 {
|
|
Variables []KolVariableDefinition `json:"variables"`
|
|
}
|
|
|
|
var kolPlaceholderRE = regexp.MustCompile(`\{\{\s*([^{}]+?)\s*\}\}`)
|
|
|
|
func RenderPrompt(content string, schema KolSchemaJSON, values map[string]any) (string, error) {
|
|
schema = normalizeKolSchema(schema)
|
|
|
|
byToken := make(map[string]KolVariableDefinition, len(schema.Variables)*2)
|
|
for _, variable := range schema.Variables {
|
|
id := strings.TrimSpace(variable.ID)
|
|
key := strings.TrimSpace(variable.Key)
|
|
if id != "" {
|
|
byToken[id] = variable
|
|
}
|
|
if key != "" {
|
|
byToken[key] = variable
|
|
}
|
|
}
|
|
|
|
missingSet := make(map[string]struct{})
|
|
missing := make([]string, 0)
|
|
addMissing := func(id string) {
|
|
if id == "" {
|
|
return
|
|
}
|
|
if _, exists := missingSet[id]; exists {
|
|
return
|
|
}
|
|
missingSet[id] = struct{}{}
|
|
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
|
|
}
|
|
|
|
token := strings.TrimSpace(submatches[1])
|
|
variable, exists := byToken[token]
|
|
if !exists {
|
|
addMissing(token)
|
|
return match
|
|
}
|
|
|
|
value, hasValue, err := resolveKolVariableValue(values, variable, token)
|
|
if err != nil {
|
|
resolveErr = err
|
|
return match
|
|
}
|
|
if hasValue {
|
|
return fmt.Sprint(value)
|
|
}
|
|
if variable.Required {
|
|
addMissing(kolVariableDisplayName(variable))
|
|
}
|
|
return ""
|
|
})
|
|
|
|
if resolveErr != nil {
|
|
return "", resolveErr
|
|
}
|
|
|
|
if len(missing) > 0 {
|
|
return "", fmt.Errorf("missing variables: %s", strings.Join(missing, ", "))
|
|
}
|
|
|
|
return rendered, nil
|
|
}
|
|
|
|
func HashPrompt(rendered string) string {
|
|
sum := sha256.Sum256([]byte(rendered))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func ValidateSchema(schema KolSchemaJSON) error {
|
|
schema = normalizeKolSchema(schema)
|
|
|
|
seenIDs := make(map[string]struct{}, len(schema.Variables))
|
|
seenKeys := make(map[string]struct{}, len(schema.Variables))
|
|
for _, variable := range schema.Variables {
|
|
id := strings.TrimSpace(variable.ID)
|
|
if id == "" || !strings.HasPrefix(id, "var_") {
|
|
return fmt.Errorf("variable id must be non-empty and start with var_")
|
|
}
|
|
if _, exists := seenIDs[id]; exists {
|
|
return fmt.Errorf("duplicate variable id: %s", id)
|
|
}
|
|
seenIDs[id] = struct{}{}
|
|
|
|
key := strings.TrimSpace(variable.Key)
|
|
if key == "" {
|
|
return fmt.Errorf("variable key must be non-empty")
|
|
}
|
|
if _, exists := seenKeys[key]; exists {
|
|
return fmt.Errorf("duplicate variable key: %s", key)
|
|
}
|
|
seenKeys[key] = struct{}{}
|
|
|
|
switch KolVariableType(strings.TrimSpace(string(variable.Type))) {
|
|
case KolVariableTypeInput, KolVariableTypeTextarea, KolVariableTypeSelect, KolVariableTypeNumber, KolVariableTypeCheckbox:
|
|
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
|
|
}
|
|
|
|
func JSONEncodeSchema(schema KolSchemaJSON) ([]byte, error) {
|
|
return json.Marshal(normalizeKolSchema(schema))
|
|
}
|
|
|
|
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 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),
|
|
strings.TrimSpace(variable.ID),
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(candidates))
|
|
for _, candidate := range candidates {
|
|
if candidate == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[candidate]; exists {
|
|
continue
|
|
}
|
|
seen[candidate] = struct{}{}
|
|
|
|
if value, ok := values[candidate]; ok {
|
|
return normalizeKolRuntimeValue(variable, value)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if label := strings.TrimSpace(variable.Label); label != "" {
|
|
return label
|
|
}
|
|
if key := strings.TrimSpace(variable.Key); key != "" {
|
|
return key
|
|
}
|
|
return strings.TrimSpace(variable.ID)
|
|
}
|