@@ -13,6 +13,7 @@ import {
1313 getProviderTemplate ,
1414 isKnownProviderTemplate ,
1515} from './llmProviderTemplates' ;
16+ import { SettingsHelpButton } from './SettingsHelpButton' ;
1617
1718const PROTOCOL_OPTIONS : Array < { value : ChannelProtocol ; label : string } > = [
1819 { value : 'openai' , label : 'OpenAI Compatible' } ,
@@ -128,6 +129,51 @@ interface ChannelRowProps {
128129 onCheckCapabilities : ( channel : ChannelConfig ) => void ;
129130}
130131
132+ const LLM_CHANNEL_HELP_DOCS = [
133+ {
134+ label : 'LLM 配置指南' ,
135+ href : 'https://github.com/ZhuLinsen/daily_stock_analysis/blob/main/docs/LLM_CONFIG_GUIDE.md' ,
136+ } ,
137+ {
138+ label : 'LLM 服务商配置速查' ,
139+ href : 'https://github.com/ZhuLinsen/daily_stock_analysis/blob/main/docs/llm-providers.md' ,
140+ } ,
141+ ] ;
142+
143+ function HelpLabel ( {
144+ htmlFor,
145+ label,
146+ fieldKey,
147+ helpKey,
148+ examples,
149+ compact = false ,
150+ } : {
151+ htmlFor ?: string ;
152+ label : string ;
153+ fieldKey : string ;
154+ helpKey : string ;
155+ examples ?: string [ ] ;
156+ compact ?: boolean ;
157+ } ) {
158+ return (
159+ < div className = { compact ? 'mb-1 flex items-center gap-1.5' : 'mb-2 flex items-center gap-1.5' } >
160+ < label
161+ htmlFor = { htmlFor }
162+ className = { compact ? 'text-xs text-muted-text' : 'text-sm font-medium text-foreground' }
163+ >
164+ { label }
165+ </ label >
166+ < SettingsHelpButton
167+ fieldKey = { fieldKey }
168+ title = { label }
169+ helpKey = { helpKey }
170+ examples = { examples }
171+ docs = { LLM_CHANNEL_HELP_DOCS }
172+ />
173+ </ div >
174+ ) ;
175+ }
176+
131177const ChannelRow : React . FC < ChannelRowProps > = ( {
132178 channel,
133179 index,
@@ -169,6 +215,11 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
169215 const selectedCapabilities = capabilityState ?. selected || [ ] ;
170216 const capabilityResults = capabilityState ?. results || { } ;
171217 const capabilityBusy = capabilityState ?. status === 'loading' ;
218+ const channelNameInputId = `llm-channel-${ channel . id } -name` ;
219+ const protocolInputId = `llm-channel-${ channel . id } -protocol` ;
220+ const baseUrlInputId = `llm-channel-${ channel . id } -base-url` ;
221+ const apiKeyInputId = `llm-channel-${ channel . id } -api-key` ;
222+ const modelsInputId = `llm-channel-${ channel . id } -models` ;
172223
173224 return (
174225 < div className = "mb-2 overflow-hidden rounded-xl border border-[var(--settings-border)] bg-[var(--settings-surface)] shadow-soft-card transition-[background-color,border-color,box-shadow] duration-200 hover:border-[var(--settings-border-strong)] hover:bg-[var(--settings-surface-hover)]" >
@@ -259,16 +310,32 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
259310 { expanded ? (
260311 < div className = "settings-surface-overlay-soft space-y-4 px-4 py-4" >
261312 < div className = "grid gap-2 sm:grid-cols-2" >
313+ < div >
314+ < HelpLabel
315+ htmlFor = { channelNameInputId }
316+ label = "渠道名称"
317+ fieldKey = "LLM_CHANNEL_NAME"
318+ helpKey = "settings.llm_channel.channel_name"
319+ examples = { [ 'LLM_CHANNELS=deepseek,aihubmix' , 'LLM_DEEPSEEK_MODELS=deepseek-v4-flash,deepseek-v4-pro' ] }
320+ />
262321 < Input
263- label = "渠道名称"
322+ id = { channelNameInputId }
264323 value = { channel . name }
265324 disabled = { busy }
266325 onChange = { ( e ) => onUpdate ( index , 'name' , e . target . value . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 _ ] / g, '' ) ) }
267326 placeholder = "primary"
268327 />
328+ </ div >
269329 < div className = "space-y-2" >
270- < label className = "block text-sm font-medium text-foreground" > 协议</ label >
330+ < HelpLabel
331+ htmlFor = { protocolInputId }
332+ label = "协议"
333+ fieldKey = "LLM_CHANNEL_PROTOCOL"
334+ helpKey = "settings.llm_channel.protocol"
335+ examples = { [ 'LLM_DEEPSEEK_PROTOCOL=deepseek' , 'LLM_OPENROUTER_PROTOCOL=openai' ] }
336+ />
271337 < Select
338+ id = { protocolInputId }
272339 value = { channel . protocol }
273340 onChange = { ( v ) => onUpdate ( index , 'protocol' , normalizeProtocol ( v ) ) }
274341 options = { PROTOCOL_OPTIONS }
@@ -278,8 +345,16 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
278345 </ div >
279346 </ div >
280347
348+ < div >
349+ < HelpLabel
350+ htmlFor = { baseUrlInputId }
351+ label = "Base URL"
352+ fieldKey = "LLM_CHANNEL_BASE_URL"
353+ helpKey = "settings.llm_channel.base_url"
354+ examples = { [ 'LLM_DEEPSEEK_BASE_URL=https://api.deepseek.com' , 'LLM_OPENROUTER_BASE_URL=https://openrouter.ai/api/v1' ] }
355+ />
281356 < Input
282- label = "Base URL"
357+ id = { baseUrlInputId }
283358 value = { channel . baseUrl }
284359 disabled = { busy }
285360 onChange = { ( e ) => onUpdate ( index , 'baseUrl' , e . target . value ) }
@@ -289,6 +364,7 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
289364 : preset ?. baseUrl || 'https://api.example.com/v1'
290365 }
291366 />
367+ </ div >
292368
293369 { showProviderTemplateDetails ? (
294370 < div className = "space-y-2 rounded-xl border border-[var(--settings-border)] bg-[var(--settings-surface-hover)] p-3" >
@@ -332,8 +408,16 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
332408 </ div >
333409 ) : null }
334410
411+ < div >
412+ < HelpLabel
413+ htmlFor = { apiKeyInputId }
414+ label = "API Key"
415+ fieldKey = "LLM_CHANNEL_API_KEY"
416+ helpKey = "settings.llm_channel.api_key"
417+ examples = { [ 'LLM_DEEPSEEK_API_KEY=sk-xxxx' , 'LLM_OPENAI_API_KEYS=sk-key-1,sk-key-2' ] }
418+ />
335419 < Input
336- label = "API Key"
420+ id = { apiKeyInputId }
337421 type = "password"
338422 allowTogglePassword
339423 iconType = "key"
@@ -344,6 +428,7 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
344428 onChange = { ( e ) => onUpdate ( index , 'apiKey' , e . target . value ) }
345429 placeholder = { channel . protocol === 'ollama' ? '本地 Ollama 可留空' : '支持多个 Key 逗号分隔' }
346430 />
431+ </ div >
347432
348433 < div className = "space-y-3 rounded-xl border border-[var(--settings-border)] bg-[var(--settings-surface-hover)] p-3" >
349434 < div className = "flex flex-wrap items-center gap-2" >
@@ -376,7 +461,12 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
376461
377462 { discoveredModels . length > 0 ? (
378463 < div >
379- < label className = "mb-2 block text-sm font-medium text-foreground" > 可选模型(可多选)</ label >
464+ < HelpLabel
465+ label = "可选模型(可多选)"
466+ fieldKey = "LLM_CHANNEL_DISCOVERED_MODELS"
467+ helpKey = "settings.llm_channel.models"
468+ examples = { [ 'LLM_DEEPSEEK_MODELS=deepseek-v4-flash,deepseek-v4-pro' ] }
469+ />
380470 < div className = "max-h-48 space-y-2 overflow-y-auto rounded-xl border border-[var(--settings-border)] bg-[var(--settings-surface)] p-3" >
381471 { discoveredModels . map ( ( model ) => (
382472 < label key = { model } className = "flex items-center gap-2 text-sm text-secondary-text" >
@@ -396,8 +486,16 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
396486 </ div >
397487 ) : null }
398488
489+ < div >
490+ < HelpLabel
491+ htmlFor = { modelsInputId }
492+ label = { discoveredModels . length > 0 ? '手动模型(逗号分隔)' : '模型(逗号分隔)' }
493+ fieldKey = "LLM_CHANNEL_MODELS"
494+ helpKey = "settings.llm_channel.models"
495+ examples = { [ 'LLM_DEEPSEEK_MODELS=deepseek-v4-flash,deepseek-v4-pro' , 'LLM_OLLAMA_MODELS=qwen3:8b,llama3.1:8b' ] }
496+ />
399497 < Input
400- label = { discoveredModels . length > 0 ? '手动模型(逗号分隔)' : '模型(逗号分隔)' }
498+ id = { modelsInputId }
401499 value = { channel . models }
402500 disabled = { busy }
403501 onChange = { ( e ) => onUpdate ( index , 'models' , e . target . value ) }
@@ -408,6 +506,7 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
408506 : '若渠道不支持自动发现或请求失败,可直接手动填写模型列表。'
409507 }
410508 />
509+ </ div >
411510
412511 { manualOnlyModels . length > 0 ? (
413512 < p className = "text-[11px] text-secondary-text" >
@@ -456,7 +555,16 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
456555 < div className = "space-y-3 rounded-xl border border-[var(--settings-border)] bg-[var(--settings-surface-hover)] p-3" >
457556 < div className = "flex flex-wrap items-center justify-between gap-2" >
458557 < div >
459- < p className = "text-[11px] font-medium text-muted-text" > 运行时能力检测(可选)</ p >
558+ < div className = "flex items-center gap-1.5" >
559+ < p className = "text-[11px] font-medium text-muted-text" > 运行时能力检测(可选)</ p >
560+ < SettingsHelpButton
561+ fieldKey = "LLM_CHANNEL_CAPABILITY_CHECKS"
562+ title = "运行时能力检测"
563+ helpKey = "settings.llm_channel.capability_checks"
564+ examples = { [ 'JSON / Tools / Stream / Vision' ] }
565+ docs = { LLM_CHANNEL_HELP_DOCS }
566+ />
567+ </ div >
460568 < p className = "mt-0.5 text-[11px] text-secondary-text" >
461569 仅在手动触发时发起真实 LLM 请求;多选可能需要 20-40 秒。
462570 </ p >
@@ -1649,7 +1757,13 @@ export const LLMChannelEditor: React.FC<LLMChannelEditorProps> = ({
16491757 < Badge variant = "default" className = "border-[var(--settings-border)] bg-[var(--settings-surface-hover)] text-muted-text" > Runtime</ Badge >
16501758 </ div >
16511759 < div className = "mb-4" >
1652- < label className = "mb-1 block text-xs text-muted-text" > Temperature</ label >
1760+ < HelpLabel
1761+ label = "Temperature"
1762+ fieldKey = "LLM_TEMPERATURE"
1763+ helpKey = "settings.llm_channel.temperature"
1764+ examples = { [ 'LLM_TEMPERATURE=0.2' , 'LLM_TEMPERATURE=0.7' ] }
1765+ compact
1766+ />
16531767 < div className = "flex items-center gap-3" >
16541768 < input
16551769 type = "range"
@@ -1675,7 +1789,14 @@ export const LLMChannelEditor: React.FC<LLMChannelEditorProps> = ({
16751789 ) : (
16761790 < div className = "space-y-4" >
16771791 < div >
1678- < label htmlFor = "runtime-primary-model" className = "mb-1 block text-xs text-muted-text" > 主模型</ label >
1792+ < HelpLabel
1793+ htmlFor = "runtime-primary-model"
1794+ label = "主模型"
1795+ fieldKey = "LITELLM_MODEL"
1796+ helpKey = "settings.llm_channel.primary_model"
1797+ examples = { [ 'LITELLM_MODEL=deepseek/deepseek-v4-flash' ] }
1798+ compact
1799+ />
16791800 < Select
16801801 id = "runtime-primary-model"
16811802 value = { runtimeConfig . primaryModel }
@@ -1687,7 +1808,14 @@ export const LLMChannelEditor: React.FC<LLMChannelEditorProps> = ({
16871808 </ div >
16881809
16891810 < div >
1690- < label htmlFor = "runtime-agent-primary-model" className = "mb-1 block text-xs text-muted-text" > Agent 主模型</ label >
1811+ < HelpLabel
1812+ htmlFor = "runtime-agent-primary-model"
1813+ label = "Agent 主模型"
1814+ fieldKey = "AGENT_LITELLM_MODEL"
1815+ helpKey = "settings.llm_channel.agent_primary_model"
1816+ examples = { [ 'AGENT_LITELLM_MODEL=deepseek/deepseek-v4-pro' ] }
1817+ compact
1818+ />
16911819 < Select
16921820 id = "runtime-agent-primary-model"
16931821 value = { runtimeConfig . agentPrimaryModel }
@@ -1702,7 +1830,13 @@ export const LLMChannelEditor: React.FC<LLMChannelEditorProps> = ({
17021830 </ div >
17031831
17041832 < div >
1705- < label className = "mb-2 block text-xs text-muted-text" > 备选模型</ label >
1833+ < HelpLabel
1834+ label = "备选模型"
1835+ fieldKey = "LITELLM_FALLBACK_MODELS"
1836+ helpKey = "settings.llm_channel.fallback_models"
1837+ examples = { [ 'LITELLM_FALLBACK_MODELS=deepseek/deepseek-v4-pro,gemini/gemini-3-flash-preview' ] }
1838+ compact
1839+ />
17061840 < div className = "space-y-2 rounded-xl border settings-border-strong settings-surface-overlay-soft p-3" >
17071841 { availableModels . map ( ( model ) => (
17081842 < label key = { model } className = "flex items-center gap-2 text-sm text-secondary-text" >
@@ -1723,7 +1857,14 @@ export const LLMChannelEditor: React.FC<LLMChannelEditorProps> = ({
17231857 </ div >
17241858
17251859 < div >
1726- < label htmlFor = "runtime-vision-model" className = "mb-1 block text-xs text-muted-text" > Vision 模型</ label >
1860+ < HelpLabel
1861+ htmlFor = "runtime-vision-model"
1862+ label = "Vision 模型"
1863+ fieldKey = "VISION_MODEL"
1864+ helpKey = "settings.llm_channel.vision_model"
1865+ examples = { [ 'VISION_MODEL=gemini/gemini-3.1-pro-preview' ] }
1866+ compact
1867+ />
17271868 < Select
17281869 id = "runtime-vision-model"
17291870 value = { runtimeConfig . visionModel }
0 commit comments