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
+21 -12
View File
@@ -33,12 +33,11 @@ import type {
KolAssistTask,
KolPackageSummary,
KolPackageDetail,
PublishKolPromptRequest,
KolSubscription,
KolSubscriptionPromptCard,
KolProfile,
KolPromptDetail,
KolPromptRevision,
KolSaveDraftRequest,
KolSubscriptionPromptSchema,
KolGenerateRequest,
KolGenerateResponse,
@@ -323,15 +322,27 @@ export const kolManageApi = {
deletePrompt(id: number) {
return apiClient.remove<null>(`/api/tenant/kol/manage/prompts/${id}`);
},
saveDraft(promptId: number, body: KolSaveDraftRequest) {
return apiClient.post<KolPromptRevision, KolSaveDraftRequest>(
`/api/tenant/kol/manage/prompts/${promptId}/draft`,
savePrompt(promptId: number, body: PublishKolPromptRequest) {
return apiClient.post<KolPromptDetail, PublishKolPromptRequest>(
`/api/tenant/kol/manage/prompts/${promptId}/save`,
body,
);
},
publishPrompt(promptId: number) {
return apiClient.post<{ ok: boolean }, Record<string, never>>(
publishPrompt(promptId: number, body: PublishKolPromptRequest) {
return apiClient.post<KolPromptDetail, PublishKolPromptRequest>(
`/api/tenant/kol/manage/prompts/${promptId}/publish`,
body,
);
},
activatePrompt(promptId: number) {
return apiClient.put<KolPromptDetail, Record<string, never>>(
`/api/tenant/kol/manage/prompts/${promptId}/activate`,
{},
);
},
archivePrompt(promptId: number) {
return apiClient.put<KolPromptDetail, Record<string, never>>(
`/api/tenant/kol/manage/prompts/${promptId}/archive`,
{},
);
},
@@ -348,7 +359,7 @@ export const kolManageApi = {
export const kolMarketplaceApi = {
list(params: { industry?: string; keyword?: string; limit?: number; offset?: number }) {
return apiClient.get<KolPackageSummary[]>("/api/tenant/kol/marketplace", { params });
return apiClient.get<KolPackageSummary[]>("/api/tenant/kol/marketplace/packages", { params });
},
detail(id: number) {
return apiClient.get<KolPackageDetail>(`/api/tenant/kol/marketplace/packages/${id}`);
@@ -360,12 +371,10 @@ export const kolMarketplaceApi = {
);
},
mySubscriptions() {
return apiClient.get<KolSubscription[]>("/api/tenant/kol/marketplace/my-subscriptions");
return apiClient.get<KolSubscription[]>("/api/tenant/kol/subscriptions");
},
mySubscriptionPrompts() {
return apiClient.get<KolSubscriptionPromptCard[]>(
"/api/tenant/kol/marketplace/my-subscription-prompts",
);
return apiClient.get<KolSubscriptionPromptCard[]>("/api/tenant/kol/subscription-prompts");
},
};
+113
View File
@@ -0,0 +1,113 @@
import type { KolVariableDefinition, KolVariableType } from "@geo/shared-types";
import { newVariableId } from "./kol-variable-id";
const KOL_PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
function normalizePlaceholderKey(value: string): string {
return value.trim();
}
function normalizeVariableType(type?: KolVariableType): KolVariableType {
switch (type) {
case "textarea":
case "select":
case "number":
case "checkbox":
return type;
case "input":
default:
return "input";
}
}
export function buildKolPlaceholderToken(key: string): string {
return `{{${normalizePlaceholderKey(key)}}}`;
}
export function extractKolPlaceholderKeys(content: string): string[] {
const text = String(content ?? "");
const seen = new Set<string>();
const keys: string[] = [];
for (const match of text.matchAll(KOL_PLACEHOLDER_RE)) {
const key = normalizePlaceholderKey(match[1] ?? "");
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
keys.push(key);
}
return keys;
}
export function findKolPlaceholderKeyAtPosition(content: string, position: number): string | null {
const text = String(content ?? "");
if (!text || position < 0) {
return null;
}
for (const match of text.matchAll(KOL_PLACEHOLDER_RE)) {
const token = match[0] ?? "";
const start = match.index ?? -1;
const end = start + token.length;
if (start < 0) {
continue;
}
if (position >= start && position <= end) {
return normalizePlaceholderKey(match[1] ?? "");
}
}
return null;
}
export function createKolVariableDefinition(
key: string,
overrides: Partial<KolVariableDefinition> = {},
): KolVariableDefinition {
const normalizedKey = normalizePlaceholderKey(key);
return {
id: overrides.id?.trim() || newVariableId(),
key: normalizedKey,
label: overrides.label?.trim() || normalizedKey,
type: normalizeVariableType(overrides.type),
required: overrides.required ?? true,
placeholder: overrides.placeholder,
options: overrides.options,
};
}
export function syncKolVariablesWithContent(
content: string,
variables: KolVariableDefinition[],
): KolVariableDefinition[] {
const keys = extractKolPlaceholderKeys(content);
const byKey = new Map<string, KolVariableDefinition>();
for (const variable of variables) {
const key = normalizePlaceholderKey(variable.key || variable.label || "");
if (!key || byKey.has(key)) {
continue;
}
byKey.set(
key,
createKolVariableDefinition(key, {
...variable,
key,
label: variable.label?.trim() || key,
}),
);
}
return keys.map((key) => {
const existing = byKey.get(key);
if (existing) {
return existing;
}
return createKolVariableDefinition(key);
});
}
@@ -0,0 +1,15 @@
export interface KolPlatformOption {
label: string;
value: string;
}
export function buildKolPlatformOptions(defaultLabel: string): KolPlatformOption[] {
return [
{ label: defaultLabel, value: "通用" },
{ label: "小红书", value: "xiaohongshu" },
{ label: "微信", value: "wechat" },
{ label: "豆包", value: "doubao" },
{ label: "DeepSeek", value: "deepseek" },
{ label: "ChatGPT", value: "chatgpt" },
];
}