diff --git a/webui/src/components/Header.tsx b/webui/src/components/Header.tsx
index 2735660..0066378 100644
--- a/webui/src/components/Header.tsx
+++ b/webui/src/components/Header.tsx
@@ -47,7 +47,7 @@ const Header: React.FC = () => {
className="flex items-center gap-2 text-sm font-medium text-zinc-400 hover:text-zinc-200 transition-colors bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 px-3 py-1.5 rounded-lg"
>
- {i18n.language === 'en' ? '中文' : 'English'}
+ {i18n.language === 'en' ? t('languageZh') : t('languageEn')}
diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts
index de4ee38..52bd753 100644
--- a/webui/src/i18n/index.ts
+++ b/webui/src/i18n/index.ts
@@ -223,6 +223,10 @@ const resources = {
actionFailed: 'Action failed',
cronExpressionPlaceholder: '*/5 * * * *',
recipientId: 'recipient id',
+ languageZh: '中文',
+ languageEn: 'English',
+ configRoot: '(root)',
+ configCommaSeparatedHint: ', a, b',
configLabels: {
gateway: 'Gateway',
host: 'Host',
@@ -302,7 +306,114 @@ const resources = {
factor: 'Factor',
min_delay: 'Min Delay',
max_delay: 'Max Delay',
- jitter: 'Jitter'
+ jitter: 'Jitter',
+ channels: 'Channels',
+ cron: 'Cron',
+ workspace: 'Workspace',
+ proxy_fallbacks: 'Proxy Fallbacks',
+ heartbeat: 'Heartbeat',
+ every_sec: 'Interval (Seconds)',
+ ack_max_chars: 'Ack Max Chars',
+ prompt_template: 'Prompt Template',
+ autonomy: 'Autonomy',
+ tick_interval_sec: 'Tick Interval (Seconds)',
+ min_run_interval_sec: 'Min Run Interval (Seconds)',
+ max_pending_duration_sec: 'Max Pending Duration (Seconds)',
+ max_consecutive_stalls: 'Max Consecutive Stalls',
+ max_dispatch_per_tick: 'Max Dispatch Per Tick',
+ notify_cooldown_sec: 'Notify Cooldown (Seconds)',
+ notify_same_reason_cooldown_sec: 'Same-reason Notify Cooldown (Seconds)',
+ quiet_hours: 'Quiet Hours',
+ user_idle_resume_sec: 'User Idle Resume (Seconds)',
+ max_rounds_without_user: 'Max Rounds Without User',
+ task_history_retention_days: 'Task History Retention (Days)',
+ waiting_resume_debounce_sec: 'Waiting Resume Debounce (Seconds)',
+ idle_round_budget_release_sec: 'Idle Round Budget Release (Seconds)',
+ allowed_task_keywords: 'Allowed Task Keywords',
+ ekg_consecutive_error_threshold: 'EKG Consecutive Error Threshold',
+ texts: 'Text Templates',
+ no_response_fallback: 'No-response Fallback',
+ think_only_fallback: 'Think-only Fallback',
+ memory_recall_keywords: 'Memory Recall Keywords',
+ lang_usage: 'Language Usage Hint',
+ lang_invalid: 'Invalid Language Message',
+ lang_updated_template: 'Language Updated Template',
+ subagents_none: 'No-subagents Message',
+ sessions_none: 'No-sessions Message',
+ unsupported_action: 'Unsupported Action Message',
+ system_rewrite_template: 'System Rewrite Template',
+ runtime_compaction_note: 'Runtime Compaction Note',
+ startup_compaction_note: 'Startup Compaction Note',
+ autonomy_important_keywords: 'Autonomy Important Keywords',
+ autonomy_completion_template: 'Autonomy Completion Template',
+ autonomy_blocked_template: 'Autonomy Blocked Template',
+ context_compaction: 'Context Compaction',
+ mode: 'Mode',
+ trigger_messages: 'Trigger Messages',
+ keep_recent_messages: 'Keep Recent Messages',
+ max_summary_chars: 'Max Summary Chars',
+ max_transcript_chars: 'Max Transcript Chars',
+ runtime_control: 'Runtime Control',
+ intent_max_input_chars: 'Intent Max Input Chars',
+ autonomy_tick_interval_sec: 'Autonomy Tick Interval (Seconds)',
+ autonomy_min_run_interval_sec: 'Autonomy Min Run Interval (Seconds)',
+ autonomy_idle_threshold_sec: 'Autonomy Idle Threshold (Seconds)',
+ autonomy_max_rounds_without_user: 'Autonomy Max Rounds Without User',
+ autonomy_max_pending_duration_sec: 'Autonomy Max Pending Duration (Seconds)',
+ autonomy_max_consecutive_stalls: 'Autonomy Max Consecutive Stalls',
+ autolearn_max_rounds_without_user: 'Autolearn Max Rounds Without User',
+ run_state_ttl_seconds: 'Run State TTL (Seconds)',
+ run_state_max: 'Run State Max',
+ tool_parallel_safe_names: 'Tool Parallel Safe Names',
+ tool_max_parallel_calls: 'Tool Max Parallel Calls',
+ system_summary: 'System Summary',
+ marker: 'Summary Marker',
+ completed_prefix: 'Completed Prefix',
+ changes_prefix: 'Changes Prefix',
+ outcome_prefix: 'Outcome Prefix',
+ completed_title: 'Completed Title',
+ changes_title: 'Changes Title',
+ outcomes_title: 'Outcomes Title',
+ inbound_message_id_dedupe_ttl_seconds: 'Inbound Message Dedupe TTL (Seconds)',
+ inbound_content_dedupe_window_seconds: 'Inbound Content Dedupe Window (Seconds)',
+ outbound_dedupe_window_seconds: 'Outbound Dedupe Window (Seconds)',
+ telegram: 'Telegram',
+ allow_from: 'Allowed Senders',
+ allow_chats: 'Allowed Chats',
+ enable_groups: 'Enable Groups',
+ require_mention_in_groups: 'Require Mention In Groups',
+ discord: 'Discord',
+ maixcam: 'MaixCam',
+ whatsapp: 'WhatsApp',
+ bridge_url: 'Bridge URL',
+ feishu: 'Feishu',
+ app_id: 'App ID',
+ app_secret: 'App Secret',
+ encrypt_key: 'Encrypt Key',
+ verification_token: 'Verification Token',
+ dingtalk: 'DingTalk',
+ filesystem: 'Filesystem',
+ working_dir: 'Working Directory',
+ timeout: 'Timeout',
+ auto_install_missing: 'Auto-install Missing',
+ sandbox: 'Sandbox',
+ image: 'Image',
+ web: 'Web',
+ search: 'Search',
+ max_results: 'Max Results',
+ proxies: 'Proxies',
+ cross_session_call_id: 'Cross-session Call ID',
+ supports_responses_compact: 'Supports Responses Compact',
+ min_sleep_sec: 'Min Sleep (Seconds)',
+ max_sleep_sec: 'Max Sleep (Seconds)',
+ retry_backoff_base_sec: 'Retry Backoff Base (Seconds)',
+ retry_backoff_max_sec: 'Retry Backoff Max (Seconds)',
+ max_consecutive_failure_retries: 'Max Consecutive Failure Retries',
+ max_workers: 'Max Workers',
+ dir: 'Directory',
+ filename: 'Filename',
+ max_size_mb: 'Max Size (MB)',
+ retention_days: 'Retention Days'
}
}
},
@@ -526,6 +637,10 @@ const resources = {
actionFailed: '操作失败',
cronExpressionPlaceholder: '*/5 * * * *',
recipientId: '接收者 ID',
+ languageZh: '中文',
+ languageEn: 'English',
+ configRoot: '(根)',
+ configCommaSeparatedHint: ',例如 a,b',
configLabels: {
gateway: '网关',
host: '主机',
@@ -605,7 +720,114 @@ const resources = {
factor: '因子',
min_delay: '最小延迟',
max_delay: '最大延迟',
- jitter: '抖动'
+ jitter: '抖动',
+ channels: '通道',
+ cron: '定时任务',
+ workspace: '工作目录',
+ proxy_fallbacks: '代理回退链',
+ heartbeat: '心跳',
+ every_sec: '间隔(秒)',
+ ack_max_chars: '确认最大字符数',
+ prompt_template: '提示模板',
+ autonomy: '自治',
+ tick_interval_sec: '轮询间隔(秒)',
+ min_run_interval_sec: '最小运行间隔(秒)',
+ max_pending_duration_sec: '最大挂起时长(秒)',
+ max_consecutive_stalls: '最大连续停滞次数',
+ max_dispatch_per_tick: '每次轮询最大派发数',
+ notify_cooldown_sec: '通知冷却(秒)',
+ notify_same_reason_cooldown_sec: '同原因通知冷却(秒)',
+ quiet_hours: '静默时段',
+ user_idle_resume_sec: '用户空闲恢复(秒)',
+ max_rounds_without_user: '无用户最大轮数',
+ task_history_retention_days: '任务历史保留天数',
+ waiting_resume_debounce_sec: '等待恢复防抖(秒)',
+ idle_round_budget_release_sec: '空闲轮次预算释放(秒)',
+ allowed_task_keywords: '允许任务关键词',
+ ekg_consecutive_error_threshold: 'EKG 连续错误阈值',
+ texts: '文本模板',
+ no_response_fallback: '无响应兜底文案',
+ think_only_fallback: '仅思考兜底文案',
+ memory_recall_keywords: '记忆召回关键词',
+ lang_usage: '语言用法提示',
+ lang_invalid: '语言非法提示',
+ lang_updated_template: '语言更新模板',
+ subagents_none: '无子代理提示',
+ sessions_none: '无会话提示',
+ unsupported_action: '不支持操作提示',
+ system_rewrite_template: '系统改写模板',
+ runtime_compaction_note: '运行期压缩说明',
+ startup_compaction_note: '启动期压缩说明',
+ autonomy_important_keywords: '自治重要关键词',
+ autonomy_completion_template: '自治完成模板',
+ autonomy_blocked_template: '自治阻塞模板',
+ context_compaction: '上下文压缩',
+ mode: '模式',
+ trigger_messages: '触发消息数',
+ keep_recent_messages: '保留最近消息数',
+ max_summary_chars: '摘要最大字符数',
+ max_transcript_chars: '转录最大字符数',
+ runtime_control: '运行时控制',
+ intent_max_input_chars: '意图输入最大字符数',
+ autonomy_tick_interval_sec: '自治轮询间隔(秒)',
+ autonomy_min_run_interval_sec: '自治最小运行间隔(秒)',
+ autonomy_idle_threshold_sec: '自治空闲阈值(秒)',
+ autonomy_max_rounds_without_user: '自治无用户最大轮数',
+ autonomy_max_pending_duration_sec: '自治最大挂起时长(秒)',
+ autonomy_max_consecutive_stalls: '自治最大连续停滞次数',
+ autolearn_max_rounds_without_user: '自学习无用户最大轮数',
+ run_state_ttl_seconds: '运行状态 TTL(秒)',
+ run_state_max: '运行状态上限',
+ tool_parallel_safe_names: '工具并行安全名单',
+ tool_max_parallel_calls: '工具最大并行调用数',
+ system_summary: '系统摘要',
+ marker: '摘要标记',
+ completed_prefix: '完成前缀',
+ changes_prefix: '变更前缀',
+ outcome_prefix: '结果前缀',
+ completed_title: '完成标题',
+ changes_title: '变更标题',
+ outcomes_title: '结果标题',
+ inbound_message_id_dedupe_ttl_seconds: '入站消息去重 TTL(秒)',
+ inbound_content_dedupe_window_seconds: '入站内容去重窗口(秒)',
+ outbound_dedupe_window_seconds: '出站去重窗口(秒)',
+ telegram: 'Telegram',
+ allow_from: '允许发送者',
+ allow_chats: '允许会话',
+ enable_groups: '启用群组',
+ require_mention_in_groups: '群组需 @ 提及',
+ discord: 'Discord',
+ maixcam: 'MaixCam',
+ whatsapp: 'WhatsApp',
+ bridge_url: '桥接地址',
+ feishu: '飞书',
+ app_id: '应用 ID',
+ app_secret: '应用密钥',
+ encrypt_key: '加密 Key',
+ verification_token: '校验 Token',
+ dingtalk: '钉钉',
+ filesystem: '文件系统',
+ working_dir: '工作目录',
+ timeout: '超时',
+ auto_install_missing: '自动安装缺失依赖',
+ sandbox: '沙箱',
+ image: '镜像',
+ web: 'Web',
+ search: '搜索',
+ max_results: '最大结果数',
+ proxies: '代理集合',
+ cross_session_call_id: '跨会话调用 ID',
+ supports_responses_compact: '支持紧凑 responses',
+ min_sleep_sec: '最小休眠(秒)',
+ max_sleep_sec: '最大休眠(秒)',
+ retry_backoff_base_sec: '重试退避基准(秒)',
+ retry_backoff_max_sec: '重试退避上限(秒)',
+ max_consecutive_failure_retries: '最大连续失败重试次数',
+ max_workers: '最大 worker 数',
+ dir: '目录',
+ filename: '文件名',
+ max_size_mb: '最大大小(MB)',
+ retention_days: '保留天数'
}
}
}
diff --git a/webui/src/pages/Config.tsx b/webui/src/pages/Config.tsx
index 7266386..d923249 100644
--- a/webui/src/pages/Config.tsx
+++ b/webui/src/pages/Config.tsx
@@ -26,6 +26,11 @@ const Config: React.FC = () => {
const [search, setSearch] = useState('');
const [newProxyName, setNewProxyName] = useState('');
+ const configLabels = useMemo(
+ () => t('configLabels', { returnObjects: true }) as Record,
+ [t]
+ );
+
const hotPrefixes = useMemo(() => hotReloadFieldDetails.map((x) => String(x.path || '').replace(/\.\*$/, '')).filter(Boolean), [hotReloadFieldDetails]);
const allTopKeys = useMemo(() => Object.keys(cfg || {}).filter(k => typeof (cfg as any)?.[k] === 'object' && (cfg as any)?.[k] !== null), [cfg]);
@@ -63,7 +68,7 @@ const Config: React.FC = () => {
const walk = (a: any, b: any, p: string) => {
const keys = new Set([...(a && typeof a === 'object' ? Object.keys(a) : []), ...(b && typeof b === 'object' ? Object.keys(b) : [])]);
if (keys.size === 0) {
- if (JSON.stringify(a) !== JSON.stringify(b)) out.push({ path: p || '(root)', before: a, after: b });
+ if (JSON.stringify(a) !== JSON.stringify(b)) out.push({ path: p || t('configRoot'), before: a, after: b });
return;
}
keys.forEach((k) => {
@@ -193,7 +198,7 @@ const Config: React.FC = () => {
onClick={() => setSelectedTop(k)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${activeTop === k ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'text-zinc-300 hover:bg-zinc-800/60'}`}
>
- {k}
+ {configLabels[k] || k}
))}
@@ -216,7 +221,7 @@ const Config: React.FC = () => {
updateProxyField(name, 'api_base', e.target.value)} placeholder={t('configLabels.api_base')} className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
updateProxyField(name, 'api_key', e.target.value)} placeholder={t('configLabels.api_key')} className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
updateProxyField(name, 'protocol', e.target.value)} placeholder={t('configLabels.protocol')} className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
- updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.models')},a,b`} className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
+ updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.models')}${t('configCommaSeparatedHint')}`} className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
))}
@@ -229,7 +234,7 @@ const Config: React.FC = () => {
{activeTop ? (
}
+ labels={configLabels}
path={activeTop}
hotPaths={hotReloadFieldDetails.map((x) => x.path)}
onlyHot={hotOnly}