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:
@@ -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 {
|
||||
|
||||
@@ -69,6 +69,39 @@ func TestKolVariableRenderPrompt(t *testing.T) {
|
||||
values: map[string]any{},
|
||||
wantErr: "missing variables",
|
||||
},
|
||||
{
|
||||
name: "uses default text value when missing",
|
||||
content: "Hello {{city}}",
|
||||
schema: KolSchemaJSON{
|
||||
Variables: []KolVariableDefinition{
|
||||
{ID: "var_city", Key: "city", Label: "城市", Type: KolVariableTypeInput, Required: true, DefaultValue: stringPtr("合肥")},
|
||||
},
|
||||
},
|
||||
values: map[string]any{},
|
||||
want: "Hello 合肥",
|
||||
},
|
||||
{
|
||||
name: "rejects text exceeding max length",
|
||||
content: "Topic: {{topic}}",
|
||||
schema: KolSchemaJSON{
|
||||
Variables: []KolVariableDefinition{
|
||||
{ID: "var_topic", Key: "topic", Label: "主题", Type: KolVariableTypeInput, Required: true, MaxLength: intPtr(4)},
|
||||
},
|
||||
},
|
||||
values: map[string]any{"topic": "超过四个字"},
|
||||
wantErr: "exceeds max length",
|
||||
},
|
||||
{
|
||||
name: "rejects number outside configured range",
|
||||
content: "Top {{count}} results",
|
||||
schema: KolSchemaJSON{
|
||||
Variables: []KolVariableDefinition{
|
||||
{ID: "var_count", Key: "count", Label: "数量", Type: KolVariableTypeNumber, Required: true, MinValue: float64Ptr(1), MaxValue: float64Ptr(5)},
|
||||
},
|
||||
},
|
||||
values: map[string]any{"count": 6},
|
||||
wantErr: "must be between 1 and 5",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -136,3 +169,46 @@ func TestKolVariableValidateSchemaRejectsDuplicateKey(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "duplicate variable key")
|
||||
}
|
||||
|
||||
func TestKolVariableValidateSchemaRejectsLongDefaultValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := ValidateSchema(KolSchemaJSON{
|
||||
Variables: []KolVariableDefinition{
|
||||
{ID: "var_topic", Key: "topic", Label: "主题", Type: KolVariableTypeInput, MaxLength: intPtr(2), DefaultValue: stringPtr("过长默认值")},
|
||||
},
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "default value exceeds max length")
|
||||
}
|
||||
|
||||
func TestKolVariableValidateSchemaRejectsInvalidNumberRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := ValidateSchema(KolSchemaJSON{
|
||||
Variables: []KolVariableDefinition{
|
||||
{ID: "var_count", Key: "count", Label: "数量", Type: KolVariableTypeNumber, MinValue: float64Ptr(10), MaxValue: float64Ptr(5)},
|
||||
},
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "max value must be greater than or equal to min value")
|
||||
}
|
||||
|
||||
func TestKolVariableValidateSchemaRejectsDefaultNumberOutOfRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := ValidateSchema(KolSchemaJSON{
|
||||
Variables: []KolVariableDefinition{
|
||||
{ID: "var_count", Key: "count", Label: "数量", Type: KolVariableTypeNumber, MinValue: float64Ptr(1), MaxValue: float64Ptr(5), DefaultNumber: float64Ptr(6)},
|
||||
},
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "default number must be between 1 and 5")
|
||||
}
|
||||
|
||||
func stringPtr(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ func (s *WorkspaceService) KolCards(ctx context.Context) ([]domain.KolWorkspaceC
|
||||
cards = append(cards, domain.KolWorkspaceCard{
|
||||
SubscriptionPromptID: row.SubscriptionPromptID,
|
||||
GrantedAt: row.GrantedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
PromptName: row.PromptName,
|
||||
PlatformHint: row.PlatformHint,
|
||||
PackageName: row.PackageName,
|
||||
|
||||
@@ -34,6 +34,7 @@ type QuotaSummary struct {
|
||||
type KolWorkspaceCard struct {
|
||||
SubscriptionPromptID int64 `json:"subscription_prompt_id"`
|
||||
GrantedAt time.Time `json:"granted_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PromptName string `json:"prompt_name"`
|
||||
PlatformHint *string `json:"platform_hint"`
|
||||
PackageName string `json:"package_name"`
|
||||
|
||||
@@ -288,6 +288,9 @@ type KolPrompt struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
PromptAssetKey pgtype.Text `json:"prompt_asset_key"`
|
||||
SchemaJson []byte `json:"schema_json"`
|
||||
CardConfigJson []byte `json:"card_config_json"`
|
||||
}
|
||||
|
||||
type KolPromptRevision struct {
|
||||
|
||||
@@ -163,6 +163,7 @@ func (q *Queries) GetRecentArticles(ctx context.Context, tenantID int64) ([]GetR
|
||||
|
||||
const listKolCardsForTenant = `-- name: ListKolCardsForTenant :many
|
||||
SELECT sp.id AS subscription_prompt_id, sp.granted_at,
|
||||
GREATEST(p.updated_at, k.updated_at)::timestamptz AS updated_at,
|
||||
p.name AS prompt_name, p.platform_hint,
|
||||
k.name AS package_name, k.cover_url AS package_cover,
|
||||
pf.display_name AS kol_display_name
|
||||
@@ -174,13 +175,14 @@ WHERE sp.tenant_id = $1::bigint
|
||||
AND sp.status = 'active'
|
||||
AND p.status = 'active'
|
||||
AND p.published_revision_no IS NOT NULL
|
||||
ORDER BY sp.granted_at DESC
|
||||
ORDER BY GREATEST(p.updated_at, k.updated_at)::timestamptz DESC, sp.granted_at DESC
|
||||
LIMIT 8
|
||||
`
|
||||
|
||||
type ListKolCardsForTenantRow struct {
|
||||
SubscriptionPromptID int64 `json:"subscription_prompt_id"`
|
||||
GrantedAt pgtype.Timestamptz `json:"granted_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
PromptName string `json:"prompt_name"`
|
||||
PlatformHint pgtype.Text `json:"platform_hint"`
|
||||
PackageName string `json:"package_name"`
|
||||
@@ -200,6 +202,7 @@ func (q *Queries) ListKolCardsForTenant(ctx context.Context, tenantID int64) ([]
|
||||
if err := rows.Scan(
|
||||
&i.SubscriptionPromptID,
|
||||
&i.GrantedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.PromptName,
|
||||
&i.PlatformHint,
|
||||
&i.PackageName,
|
||||
|
||||
@@ -49,6 +49,7 @@ LIMIT 1;
|
||||
|
||||
-- name: ListKolCardsForTenant :many
|
||||
SELECT sp.id AS subscription_prompt_id, sp.granted_at,
|
||||
GREATEST(p.updated_at, k.updated_at)::timestamptz AS updated_at,
|
||||
p.name AS prompt_name, p.platform_hint,
|
||||
k.name AS package_name, k.cover_url AS package_cover,
|
||||
pf.display_name AS kol_display_name
|
||||
@@ -60,5 +61,5 @@ WHERE sp.tenant_id = sqlc.arg(tenant_id)::bigint
|
||||
AND sp.status = 'active'
|
||||
AND p.status = 'active'
|
||||
AND p.published_revision_no IS NOT NULL
|
||||
ORDER BY sp.granted_at DESC
|
||||
ORDER BY GREATEST(p.updated_at, k.updated_at)::timestamptz DESC, sp.granted_at DESC
|
||||
LIMIT 8;
|
||||
|
||||
@@ -31,6 +31,7 @@ type WorkspacePlan struct {
|
||||
type WorkspaceKolCard struct {
|
||||
SubscriptionPromptID int64
|
||||
GrantedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
PromptName string
|
||||
PlatformHint *string
|
||||
PackageName string
|
||||
@@ -127,6 +128,7 @@ func (r *workspaceRepository) ListKolCards(ctx context.Context, tenantID int64)
|
||||
cards = append(cards, WorkspaceKolCard{
|
||||
SubscriptionPromptID: row.SubscriptionPromptID,
|
||||
GrantedAt: timeFromTimestamp(row.GrantedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
PromptName: row.PromptName,
|
||||
PlatformHint: nullableText(row.PlatformHint),
|
||||
PackageName: row.PackageName,
|
||||
|
||||
Reference in New Issue
Block a user