feat(questions): make brand questions the primary monitoring & template axis
- add /questions/combination-fill endpoint with AI-driven, IP-region-aware matrix fill - extract ip2region resolver from ops/app into shared/ipregion for cross-service use - thread question_id filter through dashboard composite, citation summary, and collect-now - switch template wizard from keyword inputs to brand-question selection (primary + supplemental) - pass brand_question and supplemental_questions through assist/title/outline prompts - add AiWaitingModal + 45s client timeout and request_timeout error mapping for long AI flows Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,8 @@ import type {
|
||||
PublishRecord,
|
||||
Question,
|
||||
QuestionCandidateResult,
|
||||
QuestionCombinationFillRequest,
|
||||
QuestionCombinationFillResult,
|
||||
QuestionCombinationRequest,
|
||||
QuestionDistillRequest,
|
||||
QuestionRequest,
|
||||
@@ -150,6 +152,7 @@ const rawBaseURL = import.meta.env.VITE_API_BASE_URL ?? ''
|
||||
const baseURL = normalizeApiBaseURL(rawBaseURL)
|
||||
const generationStreamEnabled = import.meta.env.VITE_GENERATION_STREAM_ENABLED === 'true'
|
||||
const accessRefreshSkewMs = 2 * 60 * 1000
|
||||
const aiDistillRequestTimeoutMs = 45_000
|
||||
|
||||
const publicClient = createApiClient({ baseURL })
|
||||
let storedRefreshPromise: Promise<AuthTokens> | null = null
|
||||
@@ -1049,10 +1052,18 @@ export const brandsApi = {
|
||||
payload,
|
||||
)
|
||||
},
|
||||
fillQuestionCombination(brandId: number, payload: QuestionCombinationFillRequest) {
|
||||
return apiClient.post<QuestionCombinationFillResult, QuestionCombinationFillRequest>(
|
||||
`/api/tenant/brands/${brandId}/questions/combination-fill`,
|
||||
payload,
|
||||
{ timeout: aiDistillRequestTimeoutMs },
|
||||
)
|
||||
},
|
||||
distillQuestions(brandId: number, payload: QuestionDistillRequest) {
|
||||
return apiClient.post<QuestionCandidateResult, QuestionDistillRequest>(
|
||||
`/api/tenant/brands/${brandId}/questions/ai-distill`,
|
||||
payload,
|
||||
{ timeout: aiDistillRequestTimeoutMs },
|
||||
)
|
||||
},
|
||||
classifyQuestions(brandId: number, payload: ClassifyQuestionsRequest) {
|
||||
@@ -1219,6 +1230,7 @@ export const monitoringApi = {
|
||||
dashboardComposite(params: {
|
||||
brand_id?: number
|
||||
keyword_id?: number | null
|
||||
question_id?: number | null
|
||||
days?: number
|
||||
business_date?: string
|
||||
ai_platform_id?: string
|
||||
@@ -1234,6 +1246,7 @@ export const monitoringApi = {
|
||||
days?: number
|
||||
brand_id?: number
|
||||
keyword_id?: number | null
|
||||
question_id?: number | null
|
||||
business_date?: string
|
||||
ai_platform_id?: string
|
||||
}) {
|
||||
@@ -1259,10 +1272,13 @@ export const monitoringApi = {
|
||||
{ params },
|
||||
)
|
||||
},
|
||||
collectNow(brandId: number, payload?: { keyword_id?: number | null; platform_ids?: string[] }) {
|
||||
collectNow(
|
||||
brandId: number,
|
||||
payload?: { keyword_id?: number | null; question_id?: number | null; platform_ids?: string[] },
|
||||
) {
|
||||
return apiClient.post<
|
||||
MonitoringCollectNowResponse,
|
||||
{ keyword_id?: number | null; platform_ids?: string[] }
|
||||
{ keyword_id?: number | null; question_id?: number | null; platform_ids?: string[] }
|
||||
>(`/api/tenant/monitoring/brands/${brandId}/collect-now`, payload ?? {})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ const errorMessageMap: Record<string, string> = {
|
||||
publisher_plugin_unknown_error: '浏览器插件调用失败,请稍后重试',
|
||||
installation_offline: '桌面客户端未在线,请打开桌面客户端后重试',
|
||||
network_error: '网络连接失败,请稍后重试',
|
||||
request_timeout: '请求超时,AI 生成耗时较长,请稍后重试或稍候再查看结果',
|
||||
unknown_error: '发生未知错误',
|
||||
}
|
||||
|
||||
@@ -136,13 +137,27 @@ export function isHandledAuthError(error: unknown): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
const TIMEOUT_MESSAGE_PATTERN = /^timeout of \d+ms exceeded$/i
|
||||
|
||||
function translateRawErrorMessage(raw: string): string | null {
|
||||
const mapped = errorMessageMap[raw]
|
||||
if (mapped) {
|
||||
return mapped
|
||||
}
|
||||
if (TIMEOUT_MESSAGE_PATTERN.test(raw)) {
|
||||
return errorMessageMap.request_timeout
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function formatError(error: unknown): string {
|
||||
if (isHandledAuthError(error)) {
|
||||
return '登录已过期,请重新登录'
|
||||
}
|
||||
|
||||
if (error instanceof ApiClientError) {
|
||||
const message = errorMessageMap[error.message] ?? error.message.replaceAll('_', ' ')
|
||||
const translated = translateRawErrorMessage(error.message)
|
||||
const message = translated ?? error.message.replaceAll('_', ' ')
|
||||
if (isAiPointsInsufficient(error)) {
|
||||
return message
|
||||
}
|
||||
@@ -150,7 +165,7 @@ export function formatError(error: unknown): string {
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return errorMessageMap[error.message] ?? error.message
|
||||
return translateRawErrorMessage(error.message) ?? error.message
|
||||
}
|
||||
|
||||
return '发生未知错误'
|
||||
|
||||
Reference in New Issue
Block a user