feat(admin-brand): add question expansion service and related functionality

- Implemented QuestionExpansionService for generating and materializing questions based on combinations and AI distillation.
- Added question metadata classification logic to infer intent and layer of questions.
- Created API handlers for question expansion operations including combination preview, AI distillation, metadata classification, and materialization.
- Introduced database migrations to support new question-related fields and constraints in brand_questions and brand_keywords tables.
- Added caching mechanism for AI distillation results to improve performance.
- Defined JSON schemas for question distillation responses to ensure data integrity.
This commit is contained in:
2026-05-12 21:53:36 +08:00
parent 77d542c282
commit 37b0b32327
27 changed files with 4619 additions and 908 deletions
+31
View File
@@ -72,6 +72,8 @@ import type {
ListDesktopPublishTasksParams,
LoginRequest,
LoginResponse,
MaterializeQuestionsRequest,
MaterializeQuestionsResult,
MediaPlatform,
MonitoringCitationSummaryResponse,
MonitoringCollectNowResponse,
@@ -89,6 +91,9 @@ import type {
PublishKolPromptRequest,
PublishRecord,
Question,
QuestionCandidateResult,
QuestionCombinationRequest,
QuestionDistillRequest,
QuestionRequest,
QuotaSummary,
RecentArticle,
@@ -122,6 +127,8 @@ import type {
UpdateQuestionRequest,
UserInfo,
WorkspaceOverview,
ClassifyQuestionsRequest,
ClassifiedQuestion,
} from '@geo/shared-types'
import {
@@ -1036,6 +1043,30 @@ export const brandsApi = {
params: keywordId ? { keyword_id: keywordId } : undefined,
})
},
previewQuestionCombination(brandId: number, payload: QuestionCombinationRequest) {
return apiClient.post<QuestionCandidateResult, QuestionCombinationRequest>(
`/api/tenant/brands/${brandId}/questions/combination-preview`,
payload,
)
},
distillQuestions(brandId: number, payload: QuestionDistillRequest) {
return apiClient.post<QuestionCandidateResult, QuestionDistillRequest>(
`/api/tenant/brands/${brandId}/questions/ai-distill`,
payload,
)
},
classifyQuestions(brandId: number, payload: ClassifyQuestionsRequest) {
return apiClient.post<ClassifiedQuestion[], ClassifyQuestionsRequest>(
`/api/tenant/brands/${brandId}/questions/classify-metadata`,
payload,
)
},
materializeQuestions(brandId: number, payload: MaterializeQuestionsRequest) {
return apiClient.post<MaterializeQuestionsResult, MaterializeQuestionsRequest>(
`/api/tenant/brands/${brandId}/questions/materialize`,
payload,
)
},
createQuestion(brandId: number, payload: QuestionRequest) {
return apiClient.post<Question, QuestionRequest>(
`/api/tenant/brands/${brandId}/questions`,
+7 -1
View File
@@ -67,7 +67,13 @@ const errorMessageMap: Record<string, string> = {
brand_limit_reached: '品牌公司数量已达当前套餐上限',
keyword_exists: '关键词已存在',
keyword_limit_reached: '关键词数量已达当前套餐上限',
question_limit_reached: '当前关键词下的问题数量已达上限',
question_exists: '该品牌下已存在相同问题',
no_valid_questions: '没有可保存的问题,请调整候选后重试',
invalid_enum: '请求枚举值不合法',
llm_timeout: 'AI 扩展超时,AI 点已退还',
llm_invalid_output: 'AI 输出无法解析,AI 点已退还',
materialize_failed: '保存问题失败,请稍后重试',
question_limit_reached: '当前账号问题数量已达上限',
knowledge_text_name_too_long: '文本名称不能超过 20 个字',
brand_not_found: '品牌不存在或已删除',
keyword_not_found: '关键词不存在或已删除',