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:
@@ -151,6 +151,8 @@ const enUS = {
|
||||
template_outline_generate: 'AI Outline Generation',
|
||||
kol_prompt_generate: 'KOL Prompt Generation',
|
||||
kol_prompt_optimize: 'KOL Prompt Optimize',
|
||||
question_distill: 'AI Candidate Question Generation',
|
||||
question_combination_fill: 'Expansion Tool AI Fill',
|
||||
},
|
||||
aiUsageStatus: {
|
||||
pending: 'Processing',
|
||||
@@ -243,9 +245,20 @@ const enUS = {
|
||||
aiPoints: {
|
||||
balance: 'Available Points',
|
||||
baseChars: 'Base Characters',
|
||||
usageTypeFallback: 'AI Service Call',
|
||||
ledgerTitle: 'Usage Ledger',
|
||||
ledgerLimit: 'Latest {limit} records',
|
||||
ledgerTotal: '{total} records',
|
||||
refundedHelp:
|
||||
'The system deducted points first. After the task failed, the points were returned automatically, so net usage is 0.',
|
||||
amount: {
|
||||
netZero: 'Net 0',
|
||||
zero: '0',
|
||||
pendingFlow: '{points} pts reserved',
|
||||
completedFlow: '{points} pts charged',
|
||||
refundedFlow: '{points} pts deducted, then returned',
|
||||
failedFlow: 'No charge',
|
||||
},
|
||||
table: {
|
||||
usageType: 'Usage',
|
||||
points: 'Points',
|
||||
@@ -431,10 +444,11 @@ const enUS = {
|
||||
},
|
||||
tracking: {
|
||||
brandPlaceholder: 'Select brand',
|
||||
keywordPlaceholder: 'Select keyword',
|
||||
keywordPlaceholder: 'Select question set',
|
||||
allQuestionSets: 'All question sets',
|
||||
collectNow: 'Collect Now',
|
||||
collectBrandRequired: 'Select a brand before collecting.',
|
||||
collectKeywordRequired: 'Select a keyword before collecting the current question set.',
|
||||
collectKeywordRequired: 'Select a question set before collecting.',
|
||||
collectTodayOnly: "Collect Now only supports today's data. Switch back to today to continue.",
|
||||
collectClientChecking: "Checking whether the current account's desktop client is online.",
|
||||
collectClientOnlineRequired:
|
||||
@@ -458,8 +472,9 @@ const enUS = {
|
||||
platformMatrixTitle: 'Platform Comparison Matrix',
|
||||
hotQuestions: 'Hot Questions',
|
||||
hotQuestionsTitle: 'Hot Questions',
|
||||
hotQuestionsHint: 'Questions come from the brand library under the selected keyword.',
|
||||
questionSearchPlaceholder: 'Search questions under the selected keyword',
|
||||
hotQuestionsHint:
|
||||
'Choose all question sets or one specific question. Selecting one question limits display and collection to that question.',
|
||||
questionSearchPlaceholder: 'Search questions in the selected scope',
|
||||
questionDetailEyebrow: 'Question Detail',
|
||||
questionDetailCollectedAt: 'Collected at {time}',
|
||||
questionDetailFallback: 'Question Detail',
|
||||
@@ -813,6 +828,9 @@ const enUS = {
|
||||
website: 'Official website',
|
||||
brandSummary: 'AI brand summary',
|
||||
keywords: 'Keywords',
|
||||
brandQuestions: 'Brand questions',
|
||||
primaryQuestion: 'Primary brand question',
|
||||
supplementalQuestions: 'Supplemental questions',
|
||||
competitors: 'Competitors',
|
||||
chooseTitle: 'Choose a title',
|
||||
customTitle: 'Custom title',
|
||||
@@ -829,11 +847,17 @@ const enUS = {
|
||||
hints: {
|
||||
brandContext: 'Optional. Selecting a brand pulls in keywords and competitor context.',
|
||||
brandFlow:
|
||||
'You can pick a brand from the library or type one manually. AI analysis will expand keywords, competitors, and title options.',
|
||||
'You can pick a brand from the library or type one manually. After you choose a brand question, AI will prepare the summary, competitors, and title options.',
|
||||
brandSummaryPlaceholder:
|
||||
'After analysis, AI will summarize the brand angle, positioning, and content opportunities here.',
|
||||
keywords:
|
||||
'Type keywords directly or reuse brand-library keywords. AI results will be merged into this list.',
|
||||
brandQuestions:
|
||||
'The primary question controls the article spine; supplemental questions broaden nearby search intent.',
|
||||
supplementalQuestions:
|
||||
'Choose up to 3 so the article covers related intent without drifting.',
|
||||
questionEmpty: 'No questions are available for this brand yet.',
|
||||
selectBrandFirst: 'Choose a brand from the library first.',
|
||||
competitors:
|
||||
'Competitors stay editable. You can add rows manually; saving to the competitor library is only available after choosing a library brand.',
|
||||
competitorEmptyEditable:
|
||||
@@ -891,6 +915,8 @@ const enUS = {
|
||||
brandName: 'Enter a brand name, or refine the selected library brand',
|
||||
website: 'Official website, optional',
|
||||
keywords: 'Type keywords and press Enter',
|
||||
primaryQuestion: 'Choose one primary brand question',
|
||||
supplementalQuestions: 'Optional, choose up to 3 supplemental questions',
|
||||
customTitle: 'Override the AI title here if needed',
|
||||
customOutline: 'Add another section, for example “Buying advice”',
|
||||
keyPoints: 'Add any extra instructions, key angles, or constraints for the article',
|
||||
@@ -904,32 +930,33 @@ const enUS = {
|
||||
aiBanner: {
|
||||
title: 'One-click AI analysis',
|
||||
defaultDescription:
|
||||
'AI will complete the brand summary, find core keywords, and extract competitor context.',
|
||||
'AI will use the brand question to complete the brand summary and extract competitor context.',
|
||||
productReviewDescription:
|
||||
'AI will organize the product context, extract review keywords, and suggest comparable alternatives.',
|
||||
'AI will use the brand question to organize the product context and suggest comparable alternatives.',
|
||||
researchReportDescription:
|
||||
'AI will organize the topic context, extract research keywords, and suggest reference targets.',
|
||||
'AI will use the brand question to organize the topic context and suggest reference targets.',
|
||||
},
|
||||
assist: {
|
||||
analyzeTitle: 'AI is analyzing keywords and competitors. Please wait…',
|
||||
analyzeTitleNoCompetitors: 'AI is analyzing keywords and context. Please wait…',
|
||||
analyzeTitle: 'AI is analyzing brand questions and competitors. Please wait…',
|
||||
analyzeTitleNoCompetitors: 'AI is analyzing brand questions and context. Please wait…',
|
||||
titleTitle: 'AI is generating titles. Please wait…',
|
||||
outlineTitle: 'AI is generating the article outline. Please wait…',
|
||||
analyzeStages: {
|
||||
brand: 'Analyzing the brand context…',
|
||||
keywords: 'Extracting keyword opportunities…',
|
||||
questions: 'Organizing brand questions…',
|
||||
competitors: 'Mapping competitors and websites…',
|
||||
context: 'Organizing the content context…',
|
||||
},
|
||||
titleStages: {
|
||||
context: 'Preparing the current brief…',
|
||||
strategy: 'Generating title directions from keywords and competitors…',
|
||||
strategyNoCompetitors: 'Generating title directions from the keywords and brief…',
|
||||
strategy: 'Generating title directions from brand questions and competitors…',
|
||||
strategyNoCompetitors: 'Generating title directions from the brand question and brief…',
|
||||
finalize: 'Returning 5 title options…',
|
||||
},
|
||||
outlineStages: {
|
||||
context: 'Preparing the title, keywords, and competitor context…',
|
||||
contextNoCompetitors: 'Preparing the title, keywords, and content context…',
|
||||
context: 'Preparing the title, brand questions, and competitor context…',
|
||||
contextNoCompetitors: 'Preparing the title, brand questions, and content context…',
|
||||
structure: 'Generating the article outline from the selected structure…',
|
||||
finalize: 'Returning the editable outline result…',
|
||||
},
|
||||
@@ -937,6 +964,8 @@ const enUS = {
|
||||
messages: {
|
||||
requiredField: 'Please fill in {field} first.',
|
||||
missingBrand: 'Choose a brand from the library or enter a brand name first.',
|
||||
selectBrandBeforeQuestion: 'Choose a brand from the library before selecting a question.',
|
||||
missingPrimaryQuestion: 'Choose one primary brand question.',
|
||||
missingTitle: 'Please choose or enter an article title.',
|
||||
missingOutline: 'Please select at least one outline section.',
|
||||
missingGeneratedOutline: 'Please generate and confirm the article outline first.',
|
||||
@@ -1355,10 +1384,27 @@ const enUS = {
|
||||
coreRequired: 'Core is required',
|
||||
industryRequired: 'Industry is required',
|
||||
},
|
||||
aiFill: {
|
||||
button: 'AI fill',
|
||||
success: 'Word columns filled. You can keep editing.',
|
||||
charged: 'Word columns filled. This used {count} AI point.',
|
||||
waitingTitle: 'AI is filling the expansion tool. Please wait...',
|
||||
waitingStages: {
|
||||
region: 'Detecting city, province capital, and macro region...',
|
||||
generate: 'Analyzing brand context and high-frequency intent...',
|
||||
refine: 'Refining short combinable keyword groups...',
|
||||
},
|
||||
},
|
||||
ai: {
|
||||
note: 'AI expansion consumes 1 AI point. Failed generations are refunded automatically.',
|
||||
seedTopic: 'Seed topic',
|
||||
generate: 'Generate with AI',
|
||||
waitingTitle: 'AI is generating candidate questions. Please wait...',
|
||||
waitingStages: {
|
||||
context: 'Organizing brand and topic context...',
|
||||
generate: 'Generating high-intent candidate questions...',
|
||||
refine: 'Deduplicating and filtering savable questions...',
|
||||
},
|
||||
},
|
||||
batch: {
|
||||
label: 'One question per line',
|
||||
@@ -1370,6 +1416,7 @@ const enUS = {
|
||||
messages: {
|
||||
partialSaved: 'Saved {created}; skipped {skipped}.',
|
||||
limitReached: 'This account allows up to {limit} questions shared across all brands.',
|
||||
selectionLimitReached: 'This plan allows up to {limit} questions. Selection limit reached.',
|
||||
truncated: 'Candidates exceeded the limit and were truncated to 500.',
|
||||
cacheHit: 'Used the most recent AI expansion result.',
|
||||
aiCharged: 'This AI expansion consumed {count} AI point.',
|
||||
@@ -1378,8 +1425,6 @@ const enUS = {
|
||||
emptyInput: 'Enter question content first.',
|
||||
noSelection: 'Select at least one candidate.',
|
||||
noValidQuestions: 'No valid questions can be saved. Adjust candidates and try again.',
|
||||
invalidCandidate:
|
||||
'This candidate cannot be selected until it is no longer too short, duplicated, or invalid.',
|
||||
},
|
||||
page: {
|
||||
title: 'New question',
|
||||
|
||||
@@ -147,6 +147,8 @@ const zhCN = {
|
||||
template_outline_generate: "AI 生成大纲",
|
||||
kol_prompt_generate: "KOL 模版生成",
|
||||
kol_prompt_optimize: "KOL 模版优化",
|
||||
question_distill: "AI 生成候选问题",
|
||||
question_combination_fill: "拓词工具 AI 填词",
|
||||
},
|
||||
aiUsageStatus: {
|
||||
pending: "处理中",
|
||||
@@ -232,9 +234,19 @@ const zhCN = {
|
||||
aiPoints: {
|
||||
balance: "可用点数",
|
||||
baseChars: "基准字数",
|
||||
usageTypeFallback: "AI 服务调用",
|
||||
ledgerTitle: "消耗流水",
|
||||
ledgerLimit: "最近 {limit} 条记录",
|
||||
ledgerTotal: "共 {total} 条记录",
|
||||
refundedHelp: "系统已先扣除点数;任务失败后自动退回,本次净消耗为 0。",
|
||||
amount: {
|
||||
netZero: "净消耗 0",
|
||||
zero: "0",
|
||||
pendingFlow: "已预扣 {points} 点",
|
||||
completedFlow: "已扣除 {points} 点",
|
||||
refundedFlow: "已扣除 {points} 点后退回",
|
||||
failedFlow: "未产生扣费",
|
||||
},
|
||||
table: {
|
||||
usageType: "使用场景",
|
||||
points: "点数",
|
||||
@@ -414,10 +426,11 @@ const zhCN = {
|
||||
},
|
||||
tracking: {
|
||||
brandPlaceholder: "选择品牌",
|
||||
keywordPlaceholder: "选择关键词",
|
||||
keywordPlaceholder: "选择问题集",
|
||||
allQuestionSets: "全部问题集",
|
||||
collectNow: "立即采集",
|
||||
collectBrandRequired: "请先选择品牌,再发起采集",
|
||||
collectKeywordRequired: "请先选择关键词,再立即采集当前问题",
|
||||
collectKeywordRequired: "请选择问题集后再发起采集",
|
||||
collectTodayOnly: "立即采集只支持今天的数据,请切回今天后再操作",
|
||||
collectClientChecking: "正在检查当前账号的桌面客户端状态",
|
||||
collectClientOnlineRequired: "当前登录账号的桌面客户端未在线,暂时不能立即采集",
|
||||
@@ -437,8 +450,8 @@ const zhCN = {
|
||||
platformMatrixTitle: "平台对比矩阵",
|
||||
hotQuestions: "Hot Questions",
|
||||
hotQuestionsTitle: "高频问题",
|
||||
hotQuestionsHint: "高频问题来自品牌库当前关键词下的全部问题。",
|
||||
questionSearchPlaceholder: "搜索当前关键词下的问题",
|
||||
hotQuestionsHint: "可选择全部问题集或具体单个问题;选择单个问题时只展示并采集该问题。",
|
||||
questionSearchPlaceholder: "搜索当前范围下的问题",
|
||||
questionDetailEyebrow: "Question Detail",
|
||||
questionDetailCollectedAt: "采集时间 {time}",
|
||||
questionDetailFallback: "问题详情",
|
||||
@@ -781,6 +794,9 @@ const zhCN = {
|
||||
website: "官网地址",
|
||||
brandSummary: "AI 品牌摘要",
|
||||
keywords: "关键词",
|
||||
brandQuestions: "品牌问题",
|
||||
primaryQuestion: "品牌主问题",
|
||||
supplementalQuestions: "补充覆盖问题",
|
||||
competitors: "竞品列表",
|
||||
chooseTitle: "选择文章标题",
|
||||
customTitle: "自定义标题",
|
||||
@@ -797,9 +813,13 @@ const zhCN = {
|
||||
},
|
||||
hints: {
|
||||
brandContext: "可选。选择后会自动带出关键词与竞品信息。",
|
||||
brandFlow: "品牌可直接输入,也可以从品牌库带入。点击分析后,AI 会补充关键词、竞品和标题建议。",
|
||||
brandFlow: "品牌可直接输入,也可以从品牌库带入。选择品牌问题后,AI 会补充品牌摘要、竞品和标题建议。",
|
||||
brandSummaryPlaceholder: "AI 分析后会在这里补充品牌简介、差异点和适合切入的内容角度。",
|
||||
keywords: "支持直接输入,也支持复用品牌词库里的关键词。AI 分析结果会自动补充到这里。",
|
||||
brandQuestions: "主问题决定文章主线,补充覆盖问题只用于扩展相关搜索意图。",
|
||||
supplementalQuestions: "最多选择 3 个,用于覆盖相邻搜索问题,避免文章主题发散。",
|
||||
questionEmpty: "当前品牌还没有可选问题",
|
||||
selectBrandFirst: "请先选择品牌库对象",
|
||||
competitors: "竞品支持手动编辑、追加;只有从品牌库选择品牌后,才可以收藏到品牌词库。AI 分析会尽量补齐候选竞品。",
|
||||
competitorEmptyEditable: "还没有竞品,先手动添加一条或点击分析让 AI 自动补齐。",
|
||||
templateFields: "这里保留当前模板自己的补充字段,用来约束 AI 生成角度。",
|
||||
@@ -852,6 +872,8 @@ const zhCN = {
|
||||
brandName: "输入品牌名,或在选择品牌后继续微调",
|
||||
website: "输入官网地址,选填",
|
||||
keywords: "输入关键词后回车,可多选",
|
||||
primaryQuestion: "选择一个品牌主问题",
|
||||
supplementalQuestions: "可选,最多选择 3 个补充问题",
|
||||
customTitle: "如需覆盖 AI 标题,可在这里直接输入",
|
||||
customOutline: "补充一个额外结构,例如“选购建议”",
|
||||
keyPoints: "告诉 AI 还需要强调哪些要点、风格或限制条件",
|
||||
@@ -864,30 +886,31 @@ const zhCN = {
|
||||
promptSealed: "Prompt 已封装",
|
||||
aiBanner: {
|
||||
title: "一键智能分析",
|
||||
defaultDescription: "AI 将自动补充品牌简介、发掘核心关键词并智能提取竞品信息",
|
||||
productReviewDescription: "AI 将自动梳理产品背景、提炼评测关键词并生成同类替代方案参考",
|
||||
researchReportDescription: "AI 将自动梳理主题背景、提炼研究关键词并生成参照对象参考",
|
||||
defaultDescription: "AI 将基于品牌问题补充品牌简介,并智能提取竞品信息",
|
||||
productReviewDescription: "AI 将基于品牌问题梳理产品背景,并生成同类替代方案参考",
|
||||
researchReportDescription: "AI 将基于品牌问题梳理主题背景,并生成参照对象参考",
|
||||
},
|
||||
assist: {
|
||||
analyzeTitle: "AI 正在分析关键词和竞品,请稍后…",
|
||||
analyzeTitleNoCompetitors: "AI 正在分析关键词和评测上下文,请稍后…",
|
||||
analyzeTitle: "AI 正在分析品牌问题和竞品,请稍后…",
|
||||
analyzeTitleNoCompetitors: "AI 正在分析品牌问题和评测上下文,请稍后…",
|
||||
titleTitle: "AI 正在生成标题,请稍后…",
|
||||
outlineTitle: "AI 正在生成文章大纲,请稍后…",
|
||||
analyzeStages: {
|
||||
brand: "品牌信息分析中…",
|
||||
keywords: "关键词提炼中…",
|
||||
questions: "品牌问题整理中…",
|
||||
competitors: "竞品与官网信息补充中…",
|
||||
context: "评测上下文整理中…",
|
||||
},
|
||||
titleStages: {
|
||||
context: "正在整理当前内容上下文…",
|
||||
strategy: "正在结合关键词和竞品生成标题策略…",
|
||||
strategyNoCompetitors: "正在结合关键词生成标题策略…",
|
||||
strategy: "正在结合品牌问题和竞品生成标题策略…",
|
||||
strategyNoCompetitors: "正在结合品牌问题生成标题策略…",
|
||||
finalize: "正在输出 5 个标题候选…",
|
||||
},
|
||||
outlineStages: {
|
||||
context: "正在整理标题、关键词和竞品上下文…",
|
||||
contextNoCompetitors: "正在整理标题、关键词和评测上下文…",
|
||||
context: "正在整理标题、品牌问题和竞品上下文…",
|
||||
contextNoCompetitors: "正在整理标题、品牌问题和评测上下文…",
|
||||
structure: "正在根据已选结构生成文章大纲…",
|
||||
finalize: "正在输出可编辑的大纲结果…",
|
||||
},
|
||||
@@ -895,6 +918,8 @@ const zhCN = {
|
||||
messages: {
|
||||
requiredField: "请先填写 {field}",
|
||||
missingBrand: "请先选择品牌库对象或输入品牌名称",
|
||||
selectBrandBeforeQuestion: "请先从品牌库选择品牌后再选择品牌问题",
|
||||
missingPrimaryQuestion: "请选择一个品牌主问题",
|
||||
missingTitle: "请先选择或输入文章标题",
|
||||
missingReviewIntroHook: "请先选择一个评测引言钩子",
|
||||
missingOutline: "请至少选择一个文章结构段落",
|
||||
@@ -1287,10 +1312,27 @@ const zhCN = {
|
||||
coreRequired: "核心不能为空",
|
||||
industryRequired: "行业不能为空",
|
||||
},
|
||||
aiFill: {
|
||||
button: "AI 填词",
|
||||
success: "已填入拓词词组,可继续编辑。",
|
||||
charged: "已填入拓词词组,本次消耗 {count} 个 AI 点。",
|
||||
waitingTitle: "AI 正在填充拓词工具,请稍后…",
|
||||
waitingStages: {
|
||||
region: "正在识别所在地域并匹配城市、省会与大区…",
|
||||
generate: "正在分析品牌和高频搜索意图…",
|
||||
refine: "正在整理可组合的核心词组…",
|
||||
},
|
||||
},
|
||||
ai: {
|
||||
note: "AI 扩展会消耗 1 个 AI 点;生成失败时系统会自动退还。",
|
||||
seedTopic: "扩展主题",
|
||||
generate: "AI 生成候选",
|
||||
waitingTitle: "AI 正在生成候选问题,请稍后…",
|
||||
waitingStages: {
|
||||
context: "正在整理品牌与主题上下文…",
|
||||
generate: "正在生成高意图候选问题…",
|
||||
refine: "正在去重并筛选可保存问题…",
|
||||
},
|
||||
},
|
||||
batch: {
|
||||
label: "每行一个问题",
|
||||
@@ -1302,6 +1344,7 @@ const zhCN = {
|
||||
messages: {
|
||||
partialSaved: "已保存 {created} 条,跳过 {skipped} 条",
|
||||
limitReached: "当前账号最多可维护 {limit} 个问题,所有品牌共用该额度",
|
||||
selectionLimitReached: "当前套餐最多可维护 {limit} 个问题,已达到可选上限",
|
||||
truncated: "候选超过上限,已截断为 500 条。",
|
||||
cacheHit: "已使用最近一次 AI 扩展结果。",
|
||||
aiCharged: "本次 AI 扩展已消耗 {count} 个 AI 点。",
|
||||
@@ -1310,7 +1353,6 @@ const zhCN = {
|
||||
emptyInput: "请先输入问题内容",
|
||||
noSelection: "请至少选择 1 条候选",
|
||||
noValidQuestions: "没有可保存的问题,请调整候选后重试",
|
||||
invalidCandidate: "该候选暂不可选择,请先处理过短、重复或非问题文本",
|
||||
},
|
||||
page: {
|
||||
title: "新建问题",
|
||||
|
||||
Reference in New Issue
Block a user