fix: Enhance Kol Variable Rendering and Management

- Updated the regex for placeholder matching to support more flexible key formats.
- Refactored variable storage to allow both ID and key lookups in rendering.
- Improved schema validation to check for duplicate keys and enforce non-empty keys.
- Added new utility functions for variable value lookup and display name generation.
- Enhanced tests to cover new key-based variable scenarios.
- Refactored KolPrompt repository to simplify prompt storage and management, including the removal of obsolete revision handling.
- Introduced new API endpoints for saving, activating, and archiving prompts.
- Added new utility functions for handling Kol placeholders and platform options in the admin web.
- Implemented database migrations to simplify prompt storage structure and ensure data integrity.
This commit is contained in:
2026-04-18 00:47:57 +08:00
parent 614ca4a2ea
commit 3ef0807456
32 changed files with 2414 additions and 3518 deletions
@@ -103,10 +103,10 @@ func (w *ScheduleDispatchWorker) run(ctx context.Context) {
}
func (w *ScheduleDispatchWorker) runOnce(parent context.Context) {
ctx, cancel := context.WithTimeout(parent, w.timeout)
defer cancel()
if count, err := w.hydrateMissingNextRuns(ctx); err != nil {
ctx, cancel := w.stageContext(parent)
count, err := w.hydrateMissingNextRuns(ctx)
cancel()
if err != nil {
if w.logger != nil {
w.logger.Warn("schedule dispatch hydrate missing next_run_at failed", zap.Error(err))
}
@@ -114,7 +114,10 @@ func (w *ScheduleDispatchWorker) runOnce(parent context.Context) {
w.logger.Info("schedule dispatch hydrated next_run_at", zap.Int("task_count", count))
}
if paused, stats, err := w.shouldPauseDispatch(ctx); err != nil {
ctx, cancel = w.stageContext(parent)
paused, stats, err := w.shouldPauseDispatch(ctx)
cancel()
if err != nil {
if w.logger != nil {
w.logger.Warn("schedule dispatch inspect generation queue failed", zap.Error(err))
}
@@ -129,7 +132,9 @@ func (w *ScheduleDispatchWorker) runOnce(parent context.Context) {
return
}
ctx, cancel = w.stageContext(parent)
tasks, err := w.claimDueSchedules(ctx)
cancel()
if err != nil {
if w.logger != nil {
w.logger.Warn("schedule dispatch claim due schedules failed", zap.Error(err))
@@ -477,6 +482,13 @@ func schedulerDispatchConcurrency(cfg config.SchedulerConfig) int {
return 1
}
func (w *ScheduleDispatchWorker) stageContext(parent context.Context) (context.Context, context.CancelFunc) {
if parent == nil {
parent = context.Background()
}
return context.WithTimeout(parent, w.timeout)
}
func (w *ScheduleDispatchWorker) invalidateScheduleTaskCaches(updates []scheduleTaskCacheUpdate) {
if w == nil || w.cache == nil || len(updates) == 0 {
return
@@ -99,16 +99,16 @@ func (s *KolGenerationService) WithCache(c sharedcache.Cache) *KolGenerationServ
}
func (s *KolGenerationService) GetSchema(ctx context.Context, actor auth.Actor, subPromptID int64) (*KolSubscriptionPromptSchemaResponse, error) {
row, revision, err := s.loadAuthorizedRevision(ctx, actor, subPromptID)
row, prompt, err := s.loadAuthorizedPrompt(ctx, actor, subPromptID)
if err != nil {
return nil, err
}
schema, err := decodeKolSchema(revision.SchemaJSON)
schema, err := decodeKolSchema(prompt.SchemaJSON)
if err != nil {
return nil, response.ErrInternal(50096, "kol_generation_schema_decode_failed", err.Error())
}
cardConfig, err := decodeKolCardConfig(revision.CardConfigJSON)
cardConfig, err := decodeKolCardConfig(prompt.CardConfigJSON)
if err != nil {
return nil, response.ErrInternal(50097, "kol_generation_card_config_decode_failed", err.Error())
}
@@ -126,7 +126,7 @@ func (s *KolGenerationService) GetSchema(ctx context.Context, actor auth.Actor,
}
func (s *KolGenerationService) Submit(ctx context.Context, actor auth.Actor, subPromptID int64, variables map[string]any) (*KolGenerationSubmitResponse, error) {
row, revision, err := s.loadAuthorizedRevision(ctx, actor, subPromptID)
row, prompt, err := s.loadAuthorizedPrompt(ctx, actor, subPromptID)
if err != nil {
return nil, err
}
@@ -135,12 +135,16 @@ func (s *KolGenerationService) Submit(ctx context.Context, actor auth.Actor, sub
variables = map[string]any{}
}
schema, err := decodeKolSchema(revision.SchemaJSON)
schema, err := decodeKolSchema(prompt.SchemaJSON)
if err != nil {
return nil, response.ErrInternal(50096, "kol_generation_schema_decode_failed", err.Error())
}
content, err := s.asset.Get(ctx, revision.PromptAssetKey)
if prompt.PromptAssetKey == nil || *prompt.PromptAssetKey == "" {
return nil, response.ErrInternal(50098, "kol_generation_prompt_asset_missing", "published prompt content not found")
}
content, err := s.asset.Get(ctx, *prompt.PromptAssetKey)
if err != nil {
return nil, response.ErrInternal(50098, "kol_generation_prompt_asset_load_failed", err.Error())
}
@@ -150,6 +154,10 @@ func (s *KolGenerationService) Submit(ctx context.Context, actor auth.Actor, sub
return nil, response.ErrBadRequest(40071, "kol_generation_variables_invalid", err.Error())
}
renderedPromptHash := HashPrompt(renderedPrompt)
promptRevisionNo := 1
if row.PublishedRevisionNo != nil && *row.PublishedRevisionNo > 0 {
promptRevisionNo = *row.PublishedRevisionNo
}
balance, err := s.quotaRepo.GetCurrentBalance(ctx, actor.TenantID, "article_generation")
if err != nil || balance < 1 {
@@ -161,10 +169,10 @@ func (s *KolGenerationService) Submit(ctx context.Context, actor auth.Actor, sub
SubscriptionID: row.SubscriptionID,
PackageID: row.PackageID,
PromptID: row.PromptID,
PromptRevisionNo: *row.PublishedRevisionNo,
PromptRevisionNo: promptRevisionNo,
Variables: variables,
SchemaSnapshot: json.RawMessage(revision.SchemaJSON),
CardConfigSnapshot: json.RawMessage(revision.CardConfigJSON),
SchemaSnapshot: json.RawMessage(prompt.SchemaJSON),
CardConfigSnapshot: json.RawMessage(prompt.CardConfigJSON),
RenderedPromptHash: renderedPromptHash,
RenderedPrompt: renderedPrompt,
})
@@ -259,10 +267,10 @@ func (s *KolGenerationService) Submit(ctx context.Context, actor auth.Actor, sub
SubscriptionPromptID: &row.ID,
PackageID: row.PackageID,
PromptID: row.PromptID,
PromptRevisionNo: *row.PublishedRevisionNo,
PromptRevisionNo: promptRevisionNo,
GenerationTaskID: &taskID,
VariablesSnapshot: variablesJSON,
SchemaSnapshot: revision.SchemaJSON,
SchemaSnapshot: prompt.SchemaJSON,
RenderedPromptHash: &renderedPromptHash,
})
if err != nil {
@@ -300,7 +308,7 @@ func (s *KolGenerationService) Submit(ctx context.Context, actor auth.Actor, sub
}, nil
}
func (s *KolGenerationService) loadAuthorizedRevision(ctx context.Context, actor auth.Actor, subPromptID int64) (*repository.KolSubscriptionPrompt, *repository.KolPromptRevision, error) {
func (s *KolGenerationService) loadAuthorizedPrompt(ctx context.Context, actor auth.Actor, subPromptID int64) (*repository.KolSubscriptionPrompt, *repository.KolPrompt, error) {
row, err := s.subRepo.GetSubscriptionPromptByID(ctx, actor.TenantID, subPromptID)
if err != nil {
return nil, nil, response.ErrInternal(50109, "kol_subscription_prompt_query_failed", err.Error())
@@ -319,19 +327,19 @@ func (s *KolGenerationService) loadAuthorizedRevision(ctx context.Context, actor
return nil, nil, ErrKolGenerationForbidden
case row.PackageStatus != "published":
return nil, nil, ErrKolGenerationForbidden
case row.PromptStatus != "active" || row.PublishedRevisionNo == nil:
case row.PromptStatus != "active":
return nil, nil, ErrKolGenerationForbidden
}
revision, err := s.promptRepo.GetRevision(ctx, row.CreatorTenantID, row.PromptID, *row.PublishedRevisionNo)
prompt, err := s.promptRepo.GetByID(ctx, row.CreatorTenantID, row.PromptID)
if err != nil {
return nil, nil, response.ErrInternal(50110, "kol_prompt_revision_query_failed", err.Error())
return nil, nil, response.ErrInternal(50110, "kol_prompt_query_failed", err.Error())
}
if revision == nil {
return nil, nil, response.ErrInternal(50111, "kol_prompt_revision_missing", "published prompt revision not found")
if prompt == nil || prompt.PromptAssetKey == nil || *prompt.PromptAssetKey == "" {
return nil, nil, response.ErrInternal(50111, "kol_prompt_content_missing", "published prompt content not found")
}
return row, revision, nil
return row, prompt, nil
}
func HandleKolGenerationTaskFailure(
+212 -103
View File
@@ -15,11 +15,11 @@ import (
)
var (
errKolPackageNotFound = response.ErrNotFound(40461, "kol_package_not_found", "package not found")
ErrPackageNotOwned = response.ErrForbidden(40361, "kol_package_not_owned", "package is not owned by current KOL")
ErrPromptNotFound = response.ErrNotFound(40462, "kol_prompt_not_found", "prompt not found")
ErrPromptNotOwned = response.ErrForbidden(40362, "kol_prompt_not_owned", "prompt is not owned by current KOL")
errKolPromptDraftEmpty = response.ErrConflict(40961, "kol_prompt_draft_missing", "no draft revision to publish")
errKolPackageNotFound = response.ErrNotFound(40461, "kol_package_not_found", "package not found")
ErrPackageNotOwned = response.ErrForbidden(40361, "kol_package_not_owned", "package is not owned by current KOL")
ErrPromptNotFound = response.ErrNotFound(40462, "kol_prompt_not_found", "prompt not found")
ErrPromptNotOwned = response.ErrForbidden(40362, "kol_prompt_not_owned", "prompt is not owned by current KOL")
errKolPromptContentEmpty = response.ErrConflict(40961, "kol_prompt_content_missing", "prompt content is required before publish or activate")
)
type CreatePromptInput struct {
@@ -34,11 +34,13 @@ type UpdateKolPromptInput struct {
SortOrder *int `json:"sort_order"`
}
type SaveDraftInput struct {
PromptID int64 `json:"-"`
Content string `json:"content" binding:"required"`
Schema KolSchemaJSON `json:"schema" binding:"required"`
CardConfig map[string]interface{} `json:"card_config"`
type PublishPromptInput struct {
Name string `json:"name" binding:"required"`
PlatformHint *string `json:"platform_hint"`
SortOrder *int `json:"sort_order"`
Content string `json:"content" binding:"required"`
Schema KolSchemaJSON `json:"schema" binding:"required"`
CardConfig map[string]interface{} `json:"card_config"`
}
type KolPromptDetailResponse struct {
@@ -57,13 +59,6 @@ type KolPromptDetailResponse struct {
UpdatedAt time.Time `json:"updated_at"`
}
type KolPromptRevisionResponse struct {
ID int64 `json:"id"`
PromptID int64 `json:"prompt_id"`
RevisionNo int `json:"revision_no"`
CreatedAt time.Time `json:"created_at"`
}
type KolPromptService struct {
pool *pgxpool.Pool
profileSvc *KolProfileService
@@ -184,7 +179,16 @@ func (s *KolPromptService) UpdateMetadata(ctx context.Context, actor auth.Actor,
return s.buildPromptDetail(ctx, updated, false)
}
func (s *KolPromptService) SaveDraft(ctx context.Context, actor auth.Actor, input SaveDraftInput) (*KolPromptRevisionResponse, error) {
func (s *KolPromptService) Save(ctx context.Context, actor auth.Actor, promptID int64, input PublishPromptInput) (*KolPromptDetailResponse, error) {
_, prompt, _, err := s.loadOwnedPrompt(ctx, actor, promptID)
if err != nil {
return nil, err
}
name := strings.TrimSpace(input.Name)
if name == "" {
return nil, response.ErrBadRequest(40063, "kol_prompt_name_required", "prompt name is required")
}
if strings.TrimSpace(input.Content) == "" {
return nil, response.ErrBadRequest(40064, "kol_prompt_content_required", "prompt content is required")
}
@@ -192,17 +196,86 @@ func (s *KolPromptService) SaveDraft(ctx context.Context, actor auth.Actor, inpu
return nil, response.ErrBadRequest(40065, "kol_prompt_schema_invalid", err.Error())
}
_, prompt, _, err := s.loadOwnedPrompt(ctx, actor, input.PromptID)
sortOrder := prompt.SortOrder
if input.SortOrder != nil {
sortOrder = *input.SortOrder
}
const currentRevisionNo = 1
assetKey, err := s.asset.Put(ctx, actor.TenantID, promptID, currentRevisionNo, input.Content)
if err != nil {
return nil, response.ErrInternal(50074, "kol_prompt_asset_store_failed", err.Error())
}
schemaJSON, err := JSONEncodeSchema(input.Schema)
if err != nil {
return nil, response.ErrBadRequest(40065, "kol_prompt_schema_invalid", err.Error())
}
cardConfigJSON, err := json.Marshal(normalizeKolCardConfig(input.CardConfig))
if err != nil {
return nil, response.ErrBadRequest(40066, "kol_prompt_card_config_invalid", "card_config must be valid json")
}
publishedRevisionNo := prompt.PublishedRevisionNo
if prompt.Status == "active" {
revNo := currentRevisionNo
publishedRevisionNo = &revNo
}
if err := s.promptRepo.Save(ctx, repository.SaveKolPromptInput{
ID: promptID,
TenantID: actor.TenantID,
Name: name,
PlatformHint: normalizeKolOptionalString(input.PlatformHint),
SortOrder: sortOrder,
PromptAssetKey: assetKey,
SchemaJSON: schemaJSON,
CardConfigJSON: cardConfigJSON,
Status: prompt.Status,
PublishedRevisionNo: publishedRevisionNo,
UpdatedBy: actor.UserID,
}); err != nil {
return nil, response.ErrInternal(50072, "kol_prompt_update_failed", err.Error())
}
updated, err := s.promptRepo.GetByID(ctx, actor.TenantID, promptID)
if err != nil {
return nil, response.ErrInternal(50071, "kol_prompt_query_failed", err.Error())
}
if updated == nil {
return nil, ErrPromptNotFound
}
return s.buildPromptDetail(ctx, updated, true)
}
func (s *KolPromptService) Publish(ctx context.Context, actor auth.Actor, promptID int64, input PublishPromptInput) (*KolPromptDetailResponse, error) {
_, prompt, pkg, err := s.loadOwnedPrompt(ctx, actor, promptID)
if err != nil {
return nil, err
}
nextRevisionNo, err := s.promptRepo.NextRevisionNo(ctx, actor.TenantID, input.PromptID)
if err != nil {
return nil, response.ErrInternal(50073, "kol_prompt_revision_query_failed", err.Error())
name := strings.TrimSpace(input.Name)
if name == "" {
return nil, response.ErrBadRequest(40063, "kol_prompt_name_required", "prompt name is required")
}
if strings.TrimSpace(input.Content) == "" {
return nil, response.ErrBadRequest(40064, "kol_prompt_content_required", "prompt content is required")
}
if err := ValidateSchema(input.Schema); err != nil {
return nil, response.ErrBadRequest(40065, "kol_prompt_schema_invalid", err.Error())
}
assetKey, err := s.asset.Put(ctx, actor.TenantID, input.PromptID, nextRevisionNo, input.Content)
sortOrder := prompt.SortOrder
if input.SortOrder != nil {
sortOrder = *input.SortOrder
}
const publishedRevisionNo = 1
assetKey, err := s.asset.Put(ctx, actor.TenantID, promptID, publishedRevisionNo, input.Content)
if err != nil {
return nil, response.ErrInternal(50074, "kol_prompt_asset_store_failed", err.Error())
}
@@ -219,82 +292,108 @@ func (s *KolPromptService) SaveDraft(ctx context.Context, actor auth.Actor, inpu
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, response.ErrInternal(50075, "kol_prompt_draft_tx_failed", "failed to begin prompt draft transaction")
}
defer tx.Rollback(ctx)
promptTx := repository.NewKolPromptRepository(tx)
revision, err := promptTx.CreateRevision(ctx, repository.CreateKolPromptRevisionInput{
TenantID: actor.TenantID,
PromptID: prompt.ID,
RevisionNo: nextRevisionNo,
PromptAssetKey: assetKey,
SchemaJSON: schemaJSON,
CardConfigJSON: cardConfigJSON,
CreatedBy: actor.UserID,
})
if err != nil {
return nil, response.ErrInternal(50076, "kol_prompt_draft_create_failed", err.Error())
}
if err := promptTx.SetDraftRevision(ctx, actor.TenantID, prompt.ID, nextRevisionNo, actor.UserID); err != nil {
return nil, response.ErrInternal(50077, "kol_prompt_draft_update_failed", err.Error())
}
if err := tx.Commit(ctx); err != nil {
return nil, response.ErrInternal(50078, "kol_prompt_draft_commit_failed", err.Error())
}
return &KolPromptRevisionResponse{
ID: revision.ID,
PromptID: revision.PromptID,
RevisionNo: revision.RevisionNo,
CreatedAt: revision.CreatedAt,
}, nil
}
func (s *KolPromptService) Publish(ctx context.Context, actor auth.Actor, promptID int64) error {
_, prompt, pkg, err := s.loadOwnedPrompt(ctx, actor, promptID)
if err != nil {
return err
}
if prompt.DraftRevisionNo == nil {
return errKolPromptDraftEmpty
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return response.ErrInternal(50079, "kol_prompt_publish_tx_failed", "failed to begin prompt publish transaction")
return nil, response.ErrInternal(50079, "kol_prompt_publish_tx_failed", "failed to begin prompt publish transaction")
}
defer tx.Rollback(ctx)
promptTx := repository.NewKolPromptRepository(tx)
subTx := repository.NewKolSubscriptionRepository(tx)
if err := promptTx.SetPublishedRevision(ctx, actor.TenantID, promptID, *prompt.DraftRevisionNo, actor.UserID); err != nil {
return response.ErrInternal(50080, "kol_prompt_publish_failed", err.Error())
if err := promptTx.Publish(ctx, repository.PublishKolPromptInput{
ID: promptID,
TenantID: actor.TenantID,
Name: name,
PlatformHint: normalizeKolOptionalString(input.PlatformHint),
SortOrder: sortOrder,
PromptAssetKey: assetKey,
SchemaJSON: schemaJSON,
CardConfigJSON: cardConfigJSON,
PublishedRevisionNo: publishedRevisionNo,
UpdatedBy: actor.UserID,
}); err != nil {
return nil, response.ErrInternal(50080, "kol_prompt_publish_failed", err.Error())
}
subscribers, err := subTx.ListActiveSubscribersForPackage(ctx, pkg.ID)
if err != nil {
return response.ErrInternal(50083, "kol_subscription_query_failed", err.Error())
}
for _, sub := range subscribers {
if err := subTx.CreateAccessRow(ctx, repository.CreateAccessRowInput{
TenantID: sub.TenantID,
SubscriptionID: sub.ID,
PackageID: pkg.ID,
PromptID: promptID,
}); err != nil {
return response.ErrInternal(50084, "kol_subscription_access_create_failed", err.Error())
}
if err := s.grantPromptToActiveSubscribers(ctx, subTx, pkg.ID, promptID); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return response.ErrInternal(50081, "kol_prompt_publish_commit_failed", err.Error())
return nil, response.ErrInternal(50081, "kol_prompt_publish_commit_failed", err.Error())
}
return nil
updated, err := s.promptRepo.GetByID(ctx, actor.TenantID, promptID)
if err != nil {
return nil, response.ErrInternal(50071, "kol_prompt_query_failed", err.Error())
}
if updated == nil {
return nil, ErrPromptNotFound
}
return s.buildPromptDetail(ctx, updated, true)
}
func (s *KolPromptService) Activate(ctx context.Context, actor auth.Actor, promptID int64) (*KolPromptDetailResponse, error) {
_, prompt, pkg, err := s.loadOwnedPrompt(ctx, actor, promptID)
if err != nil {
return nil, err
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, response.ErrInternal(50088, "kol_prompt_activate_tx_failed", "failed to begin prompt activate transaction")
}
defer tx.Rollback(ctx)
promptTx := repository.NewKolPromptRepository(tx)
subTx := repository.NewKolSubscriptionRepository(tx)
switch {
case prompt.PromptAssetKey != nil && strings.TrimSpace(*prompt.PromptAssetKey) != "":
if err := promptTx.SetPublishedRevision(ctx, actor.TenantID, promptID, 1, actor.UserID); err != nil {
return nil, response.ErrInternal(50090, "kol_prompt_activate_failed", err.Error())
}
default:
return nil, errKolPromptContentEmpty
}
if err := s.grantPromptToActiveSubscribers(ctx, subTx, pkg.ID, promptID); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, response.ErrInternal(50091, "kol_prompt_activate_commit_failed", "failed to commit prompt activate transaction")
}
updated, err := s.promptRepo.GetByID(ctx, actor.TenantID, promptID)
if err != nil {
return nil, response.ErrInternal(50071, "kol_prompt_query_failed", err.Error())
}
if updated == nil {
return nil, ErrPromptNotFound
}
return s.buildPromptDetail(ctx, updated, false)
}
func (s *KolPromptService) Archive(ctx context.Context, actor auth.Actor, promptID int64) (*KolPromptDetailResponse, error) {
if _, _, _, err := s.loadOwnedPrompt(ctx, actor, promptID); err != nil {
return nil, err
}
if err := s.promptRepo.UpdateStatus(ctx, actor.TenantID, promptID, "archived", actor.UserID); err != nil {
return nil, response.ErrInternal(50092, "kol_prompt_archive_failed", err.Error())
}
updated, err := s.promptRepo.GetByID(ctx, actor.TenantID, promptID)
if err != nil {
return nil, response.ErrInternal(50071, "kol_prompt_query_failed", err.Error())
}
if updated == nil {
return nil, ErrPromptNotFound
}
return s.buildPromptDetail(ctx, updated, false)
}
func (s *KolPromptService) Delete(ctx context.Context, actor auth.Actor, promptID int64) error {
@@ -371,27 +470,15 @@ func (s *KolPromptService) buildPromptDetail(ctx context.Context, prompt *reposi
UpdatedAt: prompt.UpdatedAt,
}
revisionNo := prompt.DraftRevisionNo
if revisionNo == nil {
revisionNo = prompt.PublishedRevisionNo
}
if revisionNo == nil {
if len(prompt.SchemaJSON) == 0 && len(prompt.CardConfigJSON) == 0 && (prompt.PromptAssetKey == nil || strings.TrimSpace(*prompt.PromptAssetKey) == "") {
return detail, nil
}
revision, err := s.promptRepo.GetRevision(ctx, prompt.TenantID, prompt.ID, *revisionNo)
if err != nil {
return nil, response.ErrInternal(50083, "kol_prompt_revision_query_failed", err.Error())
}
if revision == nil {
return nil, response.ErrInternal(50084, "kol_prompt_revision_not_found", "prompt revision not found")
}
schema, err := decodeKolSchema(revision.SchemaJSON)
schema, err := decodeKolSchema(prompt.SchemaJSON)
if err != nil {
return nil, response.ErrInternal(50085, "kol_prompt_schema_decode_failed", err.Error())
}
cardConfig, err := decodeKolCardConfig(revision.CardConfigJSON)
cardConfig, err := decodeKolCardConfig(prompt.CardConfigJSON)
if err != nil {
return nil, response.ErrInternal(50086, "kol_prompt_card_config_decode_failed", err.Error())
}
@@ -399,8 +486,8 @@ func (s *KolPromptService) buildPromptDetail(ctx context.Context, prompt *reposi
detail.LatestSchemaJSON = schema
detail.LatestCardConfigJSON = cardConfig
if includeContent {
content, err := s.asset.Get(ctx, revision.PromptAssetKey)
if includeContent && prompt.PromptAssetKey != nil && strings.TrimSpace(*prompt.PromptAssetKey) != "" {
content, err := s.asset.Get(ctx, *prompt.PromptAssetKey)
if err != nil {
return nil, response.ErrInternal(50087, "kol_prompt_asset_load_failed", err.Error())
}
@@ -410,6 +497,28 @@ func (s *KolPromptService) buildPromptDetail(ctx context.Context, prompt *reposi
return detail, nil
}
func (s *KolPromptService) grantPromptToActiveSubscribers(
ctx context.Context,
subRepo repository.KolSubscriptionRepository,
packageID, promptID int64,
) error {
subscribers, err := subRepo.ListActiveSubscribersForPackage(ctx, packageID)
if err != nil {
return response.ErrInternal(50083, "kol_subscription_query_failed", err.Error())
}
for _, sub := range subscribers {
if err := subRepo.CreateAccessRow(ctx, repository.CreateAccessRowInput{
TenantID: sub.TenantID,
SubscriptionID: sub.ID,
PackageID: packageID,
PromptID: promptID,
}); err != nil {
return response.ErrInternal(50084, "kol_subscription_access_create_failed", err.Error())
}
}
return nil
}
func normalizeKolCardConfig(cardConfig map[string]interface{}) map[string]interface{} {
if cardConfig == nil {
return map[string]interface{}{}
@@ -33,14 +33,21 @@ type KolSchemaJSON struct {
Variables []KolVariableDefinition `json:"variables"`
}
var kolPlaceholderRE = regexp.MustCompile(`\{\{\s*([^}\s]+)\s*\}\}`)
var kolPlaceholderRE = regexp.MustCompile(`\{\{\s*([^{}]+?)\s*\}\}`)
func RenderPrompt(content string, schema KolSchemaJSON, values map[string]any) (string, error) {
schema = normalizeKolSchema(schema)
byID := make(map[string]KolVariableDefinition, len(schema.Variables))
byToken := make(map[string]KolVariableDefinition, len(schema.Variables)*2)
for _, variable := range schema.Variables {
byID[variable.ID] = variable
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{})
@@ -62,19 +69,19 @@ func RenderPrompt(content string, schema KolSchemaJSON, values map[string]any) (
return match
}
id := submatches[1]
variable, exists := byID[id]
token := strings.TrimSpace(submatches[1])
variable, exists := byToken[token]
if !exists {
addMissing(id)
addMissing(token)
return match
}
value, hasValue := values[id]
value, hasValue := lookupKolVariableValue(values, variable, token)
if hasValue {
return fmt.Sprint(value)
}
if variable.Required {
addMissing(id)
addMissing(kolVariableDisplayName(variable))
}
return ""
})
@@ -94,16 +101,26 @@ func HashPrompt(rendered string) string {
func ValidateSchema(schema KolSchemaJSON) error {
schema = normalizeKolSchema(schema)
seen := make(map[string]struct{}, len(schema.Variables))
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 := seen[id]; exists {
if _, exists := seenIDs[id]; exists {
return fmt.Errorf("duplicate variable id: %s", id)
}
seen[id] = struct{}{}
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:
@@ -125,3 +142,38 @@ func normalizeKolSchema(schema KolSchemaJSON) KolSchemaJSON {
}
return schema
}
func lookupKolVariableValue(values map[string]any, variable KolVariableDefinition, token string) (any, bool) {
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 value, true
}
}
return nil, false
}
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)
}
@@ -22,8 +22,8 @@ func TestKolVariableRenderPrompt(t *testing.T) {
content: "Hello {{var_a}} from {{var_b}}",
schema: KolSchemaJSON{
Variables: []KolVariableDefinition{
{ID: "var_a", Type: KolVariableTypeInput, Required: true},
{ID: "var_b", Type: KolVariableTypeSelect},
{ID: "var_a", Key: "brand", Type: KolVariableTypeInput, Required: true},
{ID: "var_b", Key: "industry", Type: KolVariableTypeSelect},
},
},
values: map[string]any{
@@ -32,12 +32,38 @@ func TestKolVariableRenderPrompt(t *testing.T) {
},
want: "Hello 宜家 from 家具",
},
{
name: "supports key placeholders and key values",
content: "围绕 {{目标词}} 写一篇文章,并突出 {{目标词}} 的优势",
schema: KolSchemaJSON{
Variables: []KolVariableDefinition{
{ID: "var_a", Key: "目标词", Label: "目标词", Type: KolVariableTypeInput, Required: true},
},
},
values: map[string]any{
"目标词": "北京装修公司",
},
want: "围绕 北京装修公司 写一篇文章,并突出 北京装修公司 的优势",
},
{
name: "supports key placeholders with id values",
content: "Hello {{brand}}",
schema: KolSchemaJSON{
Variables: []KolVariableDefinition{
{ID: "var_a", Key: "brand", Label: "Brand", Type: KolVariableTypeInput, Required: true},
},
},
values: map[string]any{
"var_a": "宜家",
},
want: "Hello 宜家",
},
{
name: "missing required",
content: "Hello {{var_required}}",
schema: KolSchemaJSON{
Variables: []KolVariableDefinition{
{ID: "var_required", Type: KolVariableTypeInput, Required: true},
{ID: "var_required", Key: "required", Label: "Required", Type: KolVariableTypeInput, Required: true},
},
},
values: map[string]any{},
@@ -75,8 +101,8 @@ func TestKolVariableValidateSchemaRejectsDuplicateID(t *testing.T) {
err := ValidateSchema(KolSchemaJSON{
Variables: []KolVariableDefinition{
{ID: "var_a", Type: KolVariableTypeInput},
{ID: "var_a", Type: KolVariableTypeSelect},
{ID: "var_a", Key: "first", Type: KolVariableTypeInput},
{ID: "var_a", Key: "second", Type: KolVariableTypeSelect},
},
})
@@ -89,10 +115,24 @@ func TestKolVariableValidateSchemaRejectsInvalidID(t *testing.T) {
err := ValidateSchema(KolSchemaJSON{
Variables: []KolVariableDefinition{
{ID: "bad_id", Type: KolVariableTypeInput},
{ID: "bad_id", Key: "bad", Type: KolVariableTypeInput},
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "start with var_")
}
func TestKolVariableValidateSchemaRejectsDuplicateKey(t *testing.T) {
t.Parallel()
err := ValidateSchema(KolSchemaJSON{
Variables: []KolVariableDefinition{
{ID: "var_a", Key: "topic", Type: KolVariableTypeInput},
{ID: "var_b", Key: "topic", Type: KolVariableTypeSelect},
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "duplicate variable key")
}
@@ -21,24 +21,15 @@ type KolPrompt struct {
SortOrder int
PublishedRevisionNo *int
DraftRevisionNo *int
PromptAssetKey *string
SchemaJSON []byte
CardConfigJSON []byte
CreatedBy int64
UpdatedBy int64
CreatedAt time.Time
UpdatedAt time.Time
}
type KolPromptRevision struct {
ID int64
TenantID int64
PromptID int64
RevisionNo int
PromptAssetKey string
SchemaJSON []byte
CardConfigJSON []byte
CreatedBy int64
CreatedAt time.Time
}
type ActiveKolPrompt struct {
ID int64
TenantID int64
@@ -64,14 +55,31 @@ type UpdateKolPromptMetadataInput struct {
UpdatedBy int64
}
type CreateKolPromptRevisionInput struct {
TenantID int64
PromptID int64
RevisionNo int
PromptAssetKey string
SchemaJSON []byte
CardConfigJSON []byte
CreatedBy int64
type SaveKolPromptInput struct {
ID int64
TenantID int64
Name string
PlatformHint *string
SortOrder int
PromptAssetKey string
SchemaJSON []byte
CardConfigJSON []byte
Status string
PublishedRevisionNo *int
UpdatedBy int64
}
type PublishKolPromptInput struct {
ID int64
TenantID int64
Name string
PlatformHint *string
SortOrder int
PromptAssetKey string
SchemaJSON []byte
CardConfigJSON []byte
PublishedRevisionNo int
UpdatedBy int64
}
type KolPromptRepository interface {
@@ -80,41 +88,121 @@ type KolPromptRepository interface {
ListByPackage(ctx context.Context, tenantID, packageID int64) ([]KolPrompt, error)
ListActiveByPackage(ctx context.Context, tenantID, packageID int64) ([]ActiveKolPrompt, error)
UpdateMetadata(ctx context.Context, input UpdateKolPromptMetadataInput) error
SetDraftRevision(ctx context.Context, tenantID, promptID int64, revNo int, updatedBy int64) error
Save(ctx context.Context, input SaveKolPromptInput) error
Publish(ctx context.Context, input PublishKolPromptInput) error
SetPublishedRevision(ctx context.Context, tenantID, promptID int64, revNo int, updatedBy int64) error
UpdateStatus(ctx context.Context, tenantID, id int64, status string, updatedBy int64) error
SoftDelete(ctx context.Context, tenantID, id int64) error
CreateRevision(ctx context.Context, input CreateKolPromptRevisionInput) (*KolPromptRevision, error)
GetRevision(ctx context.Context, tenantID, promptID int64, revNo int) (*KolPromptRevision, error)
NextRevisionNo(ctx context.Context, tenantID, promptID int64) (int, error)
}
type kolPromptRepository struct {
q generated.Querier
db generated.DBTX
q generated.Querier
}
func NewKolPromptRepository(db generated.DBTX) KolPromptRepository {
return &kolPromptRepository{q: newQuerier(db)}
return &kolPromptRepository{db: db, q: newQuerier(db)}
}
func mapKolPrompt(row generated.GetKolPromptByIDRow) *KolPrompt {
func mapKolPromptRow(
id int64,
tenantID int64,
packageID int64,
name string,
platformHint pgtype.Text,
status string,
sortOrder int32,
publishedRevisionNo pgtype.Int4,
draftRevisionNo pgtype.Int4,
promptAssetKey pgtype.Text,
schemaJSON []byte,
cardConfigJSON []byte,
createdBy int64,
updatedBy int64,
createdAt pgtype.Timestamptz,
updatedAt pgtype.Timestamptz,
) *KolPrompt {
return &KolPrompt{
ID: row.ID,
TenantID: row.TenantID,
PackageID: row.PackageID,
Name: row.Name,
PlatformHint: nullableText(row.PlatformHint),
Status: row.Status,
SortOrder: int(row.SortOrder),
PublishedRevisionNo: nullableIntFromInt4(row.PublishedRevisionNo),
DraftRevisionNo: nullableIntFromInt4(row.DraftRevisionNo),
CreatedBy: row.CreatedBy,
UpdatedBy: row.UpdatedBy,
CreatedAt: timeFromTimestamp(row.CreatedAt),
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
ID: id,
TenantID: tenantID,
PackageID: packageID,
Name: name,
PlatformHint: nullableText(platformHint),
Status: status,
SortOrder: int(sortOrder),
PublishedRevisionNo: nullableIntFromInt4(publishedRevisionNo),
DraftRevisionNo: nullableIntFromInt4(draftRevisionNo),
PromptAssetKey: nullableText(promptAssetKey),
SchemaJSON: schemaJSON,
CardConfigJSON: cardConfigJSON,
CreatedBy: createdBy,
UpdatedBy: updatedBy,
CreatedAt: timeFromTimestamp(createdAt),
UpdatedAt: timeFromTimestamp(updatedAt),
}
}
func scanKolPrompt(scanner interface{ Scan(...any) error }) (*KolPrompt, error) {
var (
id int64
tenantID int64
packageID int64
name string
platformHint pgtype.Text
status string
sortOrder int32
publishedRevisionNo pgtype.Int4
draftRevisionNo pgtype.Int4
promptAssetKey pgtype.Text
schemaJSON []byte
cardConfigJSON []byte
createdBy int64
updatedBy int64
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := scanner.Scan(
&id,
&tenantID,
&packageID,
&name,
&platformHint,
&status,
&sortOrder,
&publishedRevisionNo,
&draftRevisionNo,
&promptAssetKey,
&schemaJSON,
&cardConfigJSON,
&createdBy,
&updatedBy,
&createdAt,
&updatedAt,
); err != nil {
return nil, err
}
return mapKolPromptRow(
id,
tenantID,
packageID,
name,
platformHint,
status,
sortOrder,
publishedRevisionNo,
draftRevisionNo,
promptAssetKey,
schemaJSON,
cardConfigJSON,
createdBy,
updatedBy,
createdAt,
updatedAt,
), nil
}
func (r *kolPromptRepository) Create(ctx context.Context, input CreateKolPromptInput) (*KolPrompt, error) {
row, err := r.q.CreateKolPrompt(ctx, generated.CreateKolPromptParams{
TenantID: input.TenantID,
@@ -127,62 +215,56 @@ func (r *kolPromptRepository) Create(ctx context.Context, input CreateKolPromptI
if err != nil {
return nil, err
}
return &KolPrompt{
ID: row.ID,
TenantID: row.TenantID,
PackageID: row.PackageID,
Name: row.Name,
PlatformHint: nullableText(row.PlatformHint),
Status: row.Status,
SortOrder: int(row.SortOrder),
PublishedRevisionNo: nullableIntFromInt4(row.PublishedRevisionNo),
DraftRevisionNo: nullableIntFromInt4(row.DraftRevisionNo),
CreatedBy: row.CreatedBy,
UpdatedBy: row.UpdatedBy,
CreatedAt: timeFromTimestamp(row.CreatedAt),
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
}, nil
return r.GetByID(ctx, input.TenantID, row.ID)
}
func (r *kolPromptRepository) GetByID(ctx context.Context, tenantID, id int64) (*KolPrompt, error) {
row, err := r.q.GetKolPromptByID(ctx, generated.GetKolPromptByIDParams{
ID: id,
TenantID: tenantID,
})
row := r.db.QueryRow(ctx, `
SELECT id, tenant_id, package_id, name, platform_hint, status, sort_order,
published_revision_no, draft_revision_no, prompt_asset_key, schema_json, card_config_json,
created_by, updated_by, created_at, updated_at
FROM kol_prompts
WHERE id = $1
AND tenant_id = $2
AND deleted_at IS NULL
`, id, tenantID)
prompt, err := scanKolPrompt(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return mapKolPrompt(row), nil
return prompt, nil
}
func (r *kolPromptRepository) ListByPackage(ctx context.Context, tenantID, packageID int64) ([]KolPrompt, error) {
rows, err := r.q.ListKolPromptsByPackage(ctx, generated.ListKolPromptsByPackageParams{
PackageID: packageID,
TenantID: tenantID,
})
rows, err := r.db.Query(ctx, `
SELECT id, tenant_id, package_id, name, platform_hint, status, sort_order,
published_revision_no, draft_revision_no, prompt_asset_key, schema_json, card_config_json,
created_by, updated_by, created_at, updated_at
FROM kol_prompts
WHERE package_id = $1
AND tenant_id = $2
AND deleted_at IS NULL
ORDER BY sort_order ASC, created_at ASC
`, packageID, tenantID)
if err != nil {
return nil, err
}
result := make([]KolPrompt, len(rows))
for i, row := range rows {
result[i] = KolPrompt{
ID: row.ID,
TenantID: row.TenantID,
PackageID: row.PackageID,
Name: row.Name,
PlatformHint: nullableText(row.PlatformHint),
Status: row.Status,
SortOrder: int(row.SortOrder),
PublishedRevisionNo: nullableIntFromInt4(row.PublishedRevisionNo),
DraftRevisionNo: nullableIntFromInt4(row.DraftRevisionNo),
CreatedBy: row.CreatedBy,
UpdatedBy: row.UpdatedBy,
CreatedAt: timeFromTimestamp(row.CreatedAt),
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
defer rows.Close()
result := make([]KolPrompt, 0)
for rows.Next() {
prompt, err := scanKolPrompt(rows)
if err != nil {
return nil, err
}
result = append(result, *prompt)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}
@@ -218,13 +300,50 @@ func (r *kolPromptRepository) UpdateMetadata(ctx context.Context, input UpdateKo
})
}
func (r *kolPromptRepository) SetDraftRevision(ctx context.Context, tenantID, promptID int64, revNo int, updatedBy int64) error {
return r.q.UpdateKolPromptDraftRevision(ctx, generated.UpdateKolPromptDraftRevisionParams{
RevisionNo: pgtype.Int4{Int32: int32(revNo), Valid: true},
UpdatedBy: updatedBy,
ID: promptID,
TenantID: tenantID,
})
func (r *kolPromptRepository) Save(ctx context.Context, input SaveKolPromptInput) error {
publishedRevisionNo := pgtype.Int4{}
if input.PublishedRevisionNo != nil {
publishedRevisionNo = pgtype.Int4{Int32: int32(*input.PublishedRevisionNo), Valid: true}
}
_, err := r.db.Exec(ctx, `
UPDATE kol_prompts
SET name = $1,
platform_hint = $2,
sort_order = $3,
prompt_asset_key = $4,
schema_json = $5::jsonb,
card_config_json = $6::jsonb,
status = $7,
published_revision_no = $8,
updated_by = $9,
updated_at = NOW()
WHERE id = $10
AND tenant_id = $11
AND deleted_at IS NULL
`, input.Name, pgText(input.PlatformHint), int32(input.SortOrder), input.PromptAssetKey, input.SchemaJSON, input.CardConfigJSON, input.Status, publishedRevisionNo, input.UpdatedBy, input.ID, input.TenantID)
return err
}
func (r *kolPromptRepository) Publish(ctx context.Context, input PublishKolPromptInput) error {
_, err := r.db.Exec(ctx, `
UPDATE kol_prompts
SET name = $1,
platform_hint = $2,
sort_order = $3,
prompt_asset_key = $4,
schema_json = $5::jsonb,
card_config_json = $6::jsonb,
published_revision_no = $7,
draft_revision_no = NULL,
status = 'active',
updated_by = $8,
updated_at = NOW()
WHERE id = $9
AND tenant_id = $10
AND deleted_at IS NULL
`, input.Name, pgText(input.PlatformHint), int32(input.SortOrder), input.PromptAssetKey, input.SchemaJSON, input.CardConfigJSON, input.PublishedRevisionNo, input.UpdatedBy, input.ID, input.TenantID)
return err
}
func (r *kolPromptRepository) SetPublishedRevision(ctx context.Context, tenantID, promptID int64, revNo int, updatedBy int64) error {
@@ -252,68 +371,6 @@ func (r *kolPromptRepository) SoftDelete(ctx context.Context, tenantID, id int64
})
}
func (r *kolPromptRepository) CreateRevision(ctx context.Context, input CreateKolPromptRevisionInput) (*KolPromptRevision, error) {
row, err := r.q.CreateKolPromptRevision(ctx, generated.CreateKolPromptRevisionParams{
TenantID: input.TenantID,
PromptID: input.PromptID,
RevisionNo: int32(input.RevisionNo),
PromptAssetKey: input.PromptAssetKey,
SchemaJson: input.SchemaJSON,
CardConfigJson: input.CardConfigJSON,
CreatedBy: input.CreatedBy,
})
if err != nil {
return nil, err
}
return &KolPromptRevision{
ID: row.ID,
TenantID: input.TenantID,
PromptID: row.PromptID,
RevisionNo: int(row.RevisionNo),
PromptAssetKey: row.PromptAssetKey,
SchemaJSON: row.SchemaJson,
CardConfigJSON: row.CardConfigJson,
CreatedBy: row.CreatedBy,
CreatedAt: timeFromTimestamp(row.CreatedAt),
}, nil
}
func (r *kolPromptRepository) GetRevision(ctx context.Context, tenantID, promptID int64, revNo int) (*KolPromptRevision, error) {
row, err := r.q.GetKolPromptRevision(ctx, generated.GetKolPromptRevisionParams{
PromptID: promptID,
RevisionNo: int32(revNo),
TenantID: tenantID,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &KolPromptRevision{
ID: row.ID,
TenantID: tenantID,
PromptID: row.PromptID,
RevisionNo: int(row.RevisionNo),
PromptAssetKey: row.PromptAssetKey,
SchemaJSON: row.SchemaJson,
CardConfigJSON: row.CardConfigJson,
CreatedBy: row.CreatedBy,
CreatedAt: timeFromTimestamp(row.CreatedAt),
}, nil
}
func (r *kolPromptRepository) NextRevisionNo(ctx context.Context, tenantID, promptID int64) (int, error) {
next, err := r.q.NextKolPromptRevisionNo(ctx, generated.NextKolPromptRevisionNoParams{
PromptID: promptID,
TenantID: tenantID,
})
if err != nil {
return 0, err
}
return int(next), nil
}
func nullableIntFromInt4(value pgtype.Int4) *int {
if !value.Valid {
return nil
@@ -277,25 +277,24 @@ func (h *KolManageHandler) DeletePrompt(c *gin.Context) {
response.Success(c, nil)
}
func (h *KolManageHandler) SaveDraft(c *gin.Context) {
func (h *KolManageHandler) SavePrompt(c *gin.Context) {
promptID, ok := parseInt64Param(c, "id", "prompt id")
if !ok {
return
}
var req app.SaveDraftInput
var req app.PublishPromptInput
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
return
}
req.PromptID = promptID
actor, ok := actorFromRequest(c)
if !ok {
return
}
data, err := h.promptSvc.SaveDraft(c.Request.Context(), actor, req)
data, err := h.promptSvc.Save(c.Request.Context(), actor, promptID, req)
if err != nil {
response.Error(c, err)
return
@@ -310,17 +309,64 @@ func (h *KolManageHandler) PublishPrompt(c *gin.Context) {
return
}
var req app.PublishPromptInput
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
return
}
actor, ok := actorFromRequest(c)
if !ok {
return
}
if err := h.promptSvc.Publish(c.Request.Context(), actor, promptID); err != nil {
data, err := h.promptSvc.Publish(c.Request.Context(), actor, promptID, req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, gin.H{"ok": true})
response.Success(c, data)
}
func (h *KolManageHandler) ActivatePrompt(c *gin.Context) {
promptID, ok := parseInt64Param(c, "id", "prompt id")
if !ok {
return
}
actor, ok := actorFromRequest(c)
if !ok {
return
}
data, err := h.promptSvc.Activate(c.Request.Context(), actor, promptID)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, data)
}
func (h *KolManageHandler) ArchivePrompt(c *gin.Context) {
promptID, ok := parseInt64Param(c, "id", "prompt id")
if !ok {
return
}
actor, ok := actorFromRequest(c)
if !ok {
return
}
data, err := h.promptSvc.Archive(c.Request.Context(), actor, promptID)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, data)
}
func (h *KolManageHandler) AssistSubmit(c *gin.Context) {
+3 -1
View File
@@ -70,8 +70,10 @@ func RegisterRoutes(app *bootstrap.App) {
kolManage.GET("/prompts/:id", kolManageHandler.GetPromptLatest)
kolManage.PUT("/prompts/:id", kolManageHandler.UpdatePromptMetadata)
kolManage.DELETE("/prompts/:id", kolManageHandler.DeletePrompt)
kolManage.POST("/prompts/:id/draft", kolManageHandler.SaveDraft)
kolManage.POST("/prompts/:id/save", kolManageHandler.SavePrompt)
kolManage.POST("/prompts/:id/publish", kolManageHandler.PublishPrompt)
kolManage.PUT("/prompts/:id/activate", kolManageHandler.ActivatePrompt)
kolManage.PUT("/prompts/:id/archive", kolManageHandler.ArchivePrompt)
kolManage.POST("/assist", kolManageHandler.AssistSubmit)
kolManage.GET("/assist/:id", kolManageHandler.AssistGet)