From 7b459b8ca1de04511725c6728fe39e363138e2b1 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Thu, 6 Aug 2026 11:03:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E5=8E=8B=E7=BC=A9=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E5=85=B3=E9=94=AE=E5=8F=82=E6=95=B0=E6=9A=B4=E9=9C=B2?= =?UTF-8?q?=E5=88=B0=E8=AE=BE=E7=BD=AE=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 4 个压缩参数(threshold_ratio、llm_compaction_threshold_ratio、 truncate_max_tokens、preserve_count)从硬编码提取到 config.json 顶层 compaction 节,前端新增 CompactionTab 供用户调整。 后端: - config: 新增 CompactionConfig 结构体,Config 新增 compaction 字段 - context_compressor: 4 个参数内聚到 ContextCompressor 实例字段; 新增 with_compaction_config() 构造函数和 truncate_tool_results() 方法; 对用户配置做防御性 clamp(ratio ∈ [0.1,1.0],整数 ≥ 1) - agent_loop: 调用 compressor.truncate_tool_results() 实现零参数耦合 - agent_factory: 新增 build_compressor() 方法,in-loop 与 sync 兜底 两条压缩路径共用同一套用户配置 - session: with_factories 改用 agent_factory.build_compressor(),修复 sync 压缩路径忽略用户配置的 P0 问题 前端: - types: 新增 CompactionConfig 接口,TabId 新增 'compaction' - 新建 CompactionTab.tsx,4 个 number input 分两组 SectionCard - constants + ConfigPage: 注册"上下文压缩" Tab(Archive 图标) 清理:删除无调用方的 from_provider_config/from_runtime_config/with_config 方法及 DEFAULT_THRESHOLD_RATIO/LLM_COMPACTION_THRESHOLD_RATIO 常量。 测试:context_compressor 18 + session 31 + agent_loop 42 全通过。 --- src/agent/agent_loop.rs | 10 +- src/agent/context_compressor.rs | 101 ++++++++++-------- src/cli/init.rs | 2 + src/config/mod.rs | 50 +++++++++ src/gateway/agent_factory.rs | 26 +++-- src/gateway/mod.rs | 1 + src/gateway/runtime.rs | 7 +- src/gateway/session.rs | 10 +- web/src/components/Settings/ConfigPage.tsx | 3 + web/src/components/Settings/constants.ts | 2 + .../Settings/tabs/CompactionTab.tsx | 87 +++++++++++++++ web/src/components/Settings/types.ts | 11 +- 12 files changed, 252 insertions(+), 58 deletions(-) create mode 100644 web/src/components/Settings/tabs/CompactionTab.tsx diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs index 7940e4e..315fa2d 100644 --- a/src/agent/agent_loop.rs +++ b/src/agent/agent_loop.rs @@ -1443,17 +1443,15 @@ impl AgentLoop { if let Some(prompt_tokens) = last_prompt_tokens { if compressor.should_compress_by_usage(prompt_tokens) { - // 阶段 1a:工程化压缩(截断非子代理 tool 结果到 100 token,仅改内存) - crate::agent::context_compressor::truncate_tool_results_in_place( - &mut messages, - 100, - ); + // 阶段 1a:工程化压缩(截断非子代理 tool 结果,仅改内存) + // 参数内聚到 ContextCompressor,AgentLoop 不持有截断 token 数 + compressor.truncate_tool_results(&mut messages); engineering_compaction_applied = true; tracing::info!( iteration, prompt_tokens, threshold = compressor.threshold(), - "Engineering compaction applied (tool results truncated to 100 tokens)" + "Engineering compaction applied (tool results truncated)" ); // 阶段 1b:重新估算,判断是否需要 LLM 压缩(50% 阈值) diff --git a/src/agent/context_compressor.rs b/src/agent/context_compressor.rs index 52506d5..f3bc6a4 100644 --- a/src/agent/context_compressor.rs +++ b/src/agent/context_compressor.rs @@ -3,7 +3,7 @@ use crate::bus::{ ChatMessage, SYSTEM_CONTEXT_AGENT_PROMPT, SYSTEM_CONTEXT_HISTORY_COMPACTION, SYSTEM_CONTEXT_SCHEDULED_PROMPT, }; -use crate::config::LLMProviderConfig; +use crate::config::{CompactionConfig, LLMProviderConfig}; use crate::providers::{ChatCompletionRequest, LLMProvider, Message, create_provider}; use crate::text::{char_count, take_prefix_chars}; @@ -17,12 +17,6 @@ pub const SYSTEM_CONTEXT_HISTORY_COMPACTION_OLDER: &str = "history_compaction_ol /// System context marker for the light compression (newer segment) summary. pub const SYSTEM_CONTEXT_HISTORY_COMPACTION_NEWER: &str = "history_compaction_newer"; -/// Default threshold ratio: compress when estimated tokens exceed 70% of context window. -const DEFAULT_THRESHOLD_RATIO: f64 = 0.7; - -/// LLM 压缩阈值比例:工程化压缩后仍超过此比例才调 LLM -const LLM_COMPACTION_THRESHOLD_RATIO: f64 = 0.5; - // ============================================================================ // HistoryUnit — atomic message units for compression // ============================================================================ @@ -275,6 +269,12 @@ pub struct ContextCompressor { context_window: usize, /// Threshold ratio to trigger compression (70% of context window). threshold_ratio: f64, + /// LLM 压缩阈值比例(工程化压缩后仍超此比例才调 LLM) + llm_compaction_threshold_ratio: f64, + /// 三段压缩保留 unit 数(最旧 N + 最新 N) + preserve_count: usize, + /// 工程化压缩时 tool 结果截断 token 数 + truncate_max_tokens: usize, } impl ContextCompressor { @@ -655,13 +655,13 @@ OLDER SEGMENT (events from earlier in the session): self.compress_two_segment_inner(history, provider).await } - /// 三段压缩核心逻辑:保留最旧5 + 最新5 unit,中间段用 LLM 压缩。 + /// 三段压缩核心逻辑:保留最旧 N + 最新 N unit,中间段用 LLM 压缩。 /// /// 策略: - /// - SystemGuard 永远保留在头部(不计入 5 条配额) + /// - SystemGuard 永远保留在头部(不计入 N 条配额) /// - 可压缩单元(UserMessage / AssistantText / ToolRound)按时间顺序: - /// - 最旧 PRESERVE_COUNT 个 unit 原样保留 - /// - 最新 PRESERVE_COUNT 个 unit 原样保留 + /// - 最旧 preserve_count 个 unit 原样保留 + /// - 最新 preserve_count 个 unit 原样保留 /// - 中间段用 LLM 生成摘要(system 消息,无 tool_calls) /// - ToolRound 原子性由 parse_to_units 保证,切分在 unit 边界 /// - 中间段摘要为纯文本 system 消息,符合 API 提交要求 @@ -670,14 +670,14 @@ OLDER SEGMENT (events from earlier in the session): history: &[ChatMessage], provider: &dyn LLMProvider, ) -> Result, AgentError> { - const PRESERVE_COUNT: usize = 5; + let preserve_count = self.preserve_count; let tokens = estimate_tokens(history); tracing::info!( tokens = tokens, threshold = self.threshold(), msg_count = history.len(), - preserve_count = PRESERVE_COUNT, - "Starting three-segment compression (preserve oldest 5 + newest 5)" + preserve_count = preserve_count, + "Starting three-segment compression" ); let units = parse_to_units(history); @@ -693,10 +693,10 @@ OLDER SEGMENT (events from earlier in the session): } // Step 2: If compressible units are too few, skip LLM compression - if compressible.len() <= PRESERVE_COUNT * 2 { + if compressible.len() <= preserve_count * 2 { tracing::info!( compressible_count = compressible.len(), - preserve_threshold = PRESERVE_COUNT * 2, + preserve_threshold = preserve_count * 2, "Too few compressible units, skipping LLM compaction" ); let mut result = system_guards; @@ -707,10 +707,10 @@ OLDER SEGMENT (events from earlier in the session): } // Step 3: Three-segment split - let split = compressible.len() - PRESERVE_COUNT; - let oldest_units = &compressible[..PRESERVE_COUNT]; + let split = compressible.len() - preserve_count; + let oldest_units = &compressible[..preserve_count]; let newest_units = &compressible[split..]; - let middle_units = &compressible[PRESERVE_COUNT..split]; + let middle_units = &compressible[preserve_count..split]; // Step 4: Build middle segment messages and transcript let middle_messages: Vec = middle_units @@ -743,7 +743,7 @@ OLDER SEGMENT (events from earlier in the session): // System guards first compressed.extend(system_guards); - // Oldest PRESERVE_COUNT units (raw) + // Oldest preserve_count units (raw) for unit in oldest_units { compressed.extend(unit_to_messages(unit)); } @@ -756,7 +756,7 @@ OLDER SEGMENT (events from earlier in the session): )); } - // Newest PRESERVE_COUNT units (raw) + // Newest preserve_count units (raw) for unit in newest_units { compressed.extend(unit_to_messages(unit)); } @@ -766,8 +766,8 @@ OLDER SEGMENT (events from earlier in the session): original_msg_count = history.len(), final_tokens = estimate_tokens(&compressed), final_msg_count = compressed.len(), - oldest_units = PRESERVE_COUNT, - newest_units = PRESERVE_COUNT, + oldest_units = preserve_count, + newest_units = preserve_count, middle_units = middle_units.len(), "Three-segment compression completed" ); @@ -780,34 +780,45 @@ OLDER SEGMENT (events from earlier in the session): // ========================================================================= /// Create a new compressor with the given context window size. + /// 测试 fallback 路径:所有压缩参数用 CompactionConfig::default()。 pub fn new(context_window: usize) -> Self { + let default = CompactionConfig::default(); Self { config: ContextCompressionConfig::default(), context_window, - threshold_ratio: DEFAULT_THRESHOLD_RATIO, + threshold_ratio: default.threshold_ratio, + llm_compaction_threshold_ratio: default.llm_compaction_threshold_ratio, + preserve_count: default.preserve_count, + truncate_max_tokens: default.truncate_max_tokens, } } - pub fn from_provider_config(provider_config: &LLMProviderConfig) -> Self { - Self::from_runtime_config(&AgentRuntimeConfig::from(provider_config.clone())) - } - - pub fn from_runtime_config(config: &AgentRuntimeConfig) -> Self { - Self::with_config( - config.context_window_tokens, - ContextCompressionConfig { - summary_max_chars: config.context_summary_char_budget, + /// 从 runtime config + compaction config 构造(AgentFactory 生产路径调用)。 + /// 所有压缩参数内聚到 ContextCompressor,AgentLoop 零参数耦合。 + /// 对用户配置做防御性 clamp,避免极端值破坏压缩逻辑: + /// - ratio 限制在 [0.1, 1.0]:过低导致每轮压缩,过高导致永不压缩 + /// - truncate_max_tokens 限制 >= 1:0 会把所有 tool 结果截断成空串 + /// - preserve_count 限制 >= 1:0 会丢失全部原始上下文 + pub fn with_compaction_config( + context_window: usize, + summary_max_chars: usize, + compaction: &CompactionConfig, + ) -> Self { + let clamp_ratio = |r: f64| r.clamp(0.1, 1.0); + let threshold_ratio = clamp_ratio(compaction.threshold_ratio); + let llm_compaction_threshold_ratio = clamp_ratio(compaction.llm_compaction_threshold_ratio); + let preserve_count = compaction.preserve_count.max(1); + let truncate_max_tokens = compaction.truncate_max_tokens.max(1); + Self { + config: ContextCompressionConfig { + summary_max_chars, ..ContextCompressionConfig::default() }, - ) - } - - /// Create with custom configuration. - pub fn with_config(context_window: usize, config: ContextCompressionConfig) -> Self { - Self { - config, context_window, - threshold_ratio: DEFAULT_THRESHOLD_RATIO, + threshold_ratio, + llm_compaction_threshold_ratio, + preserve_count, + truncate_max_tokens, } } @@ -827,7 +838,13 @@ OLDER SEGMENT (events from earlier in the session): /// LLM 压缩阈值(50%):工程化压缩后用 estimate_tokens 判断是否需要 LLM 压缩。 pub fn llm_compaction_threshold(&self) -> usize { - (self.context_window as f64 * LLM_COMPACTION_THRESHOLD_RATIO) as usize + (self.context_window as f64 * self.llm_compaction_threshold_ratio) as usize + } + + /// 工程化压缩:截断非子代理 tool 结果到 self.truncate_max_tokens。 + /// AgentLoop 调用此方法,不需要知道截断参数细节(参数内聚)。 + pub fn truncate_tool_results(&self, messages: &mut [ChatMessage]) { + truncate_tool_results_in_place(messages, self.truncate_max_tokens); } fn user_turn_ranges(&self, history: &[ChatMessage]) -> Vec { diff --git a/src/cli/init.rs b/src/cli/init.rs index 536579c..ed85ec2 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -81,6 +81,7 @@ impl InitWizard { image_context: crate::config::ImageContextConfig::default(), subagents: crate::config::SubagentsConfig::default(), experts: crate::config::ExpertsConfig::default(), + compaction: crate::config::CompactionConfig::default(), } } @@ -845,6 +846,7 @@ impl InitWizard { image_context: existing.image_context.clone(), subagents: existing.subagents.clone(), experts: existing.experts.clone(), + compaction: existing.compaction.clone(), } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 6d8773a..d260834 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -40,6 +40,8 @@ pub struct Config { pub subagents: SubagentsConfig, #[serde(default)] pub experts: ExpertsConfig, + #[serde(default)] + pub compaction: CompactionConfig, } /// 图片上下文限制配置 @@ -72,6 +74,54 @@ impl Default for ImageContextConfig { } } +/// 上下文压缩算法配置(全局,所有 agent 共享) +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CompactionConfig { + /// 工程化压缩触发阈值(占 context_window 的比例,0.0-1.0) + /// 当 LLM 返回的 prompt_tokens 超过 context_window × 此比例时,触发压缩流程 + #[serde(default = "default_threshold_ratio")] + pub threshold_ratio: f64, + + /// LLM 压缩触发阈值(占 context_window 的比例,0.0-1.0) + /// 工程化压缩(截断 tool 结果)后,若估算 token 仍超过此比例才调 LLM 压缩 + #[serde(default = "default_llm_compaction_threshold_ratio")] + pub llm_compaction_threshold_ratio: f64, + + /// 工程化压缩时 tool 结果截断到的 token 数 + /// 子代理返回(tool_name="task")不受此限制 + #[serde(default = "default_truncate_max_tokens")] + pub truncate_max_tokens: usize, + + /// LLM 三段压缩保留的最旧/最新 unit 数 + /// 压缩后保留最旧 N 个 unit + 中间段摘要 + 最新 N 个 unit + #[serde(default = "default_preserve_count")] + pub preserve_count: usize, +} + +fn default_threshold_ratio() -> f64 { + 0.7 +} +fn default_llm_compaction_threshold_ratio() -> f64 { + 0.5 +} +fn default_truncate_max_tokens() -> usize { + 100 +} +fn default_preserve_count() -> usize { + 5 +} + +impl Default for CompactionConfig { + fn default() -> Self { + Self { + threshold_ratio: default_threshold_ratio(), + llm_compaction_threshold_ratio: default_llm_compaction_threshold_ratio(), + truncate_max_tokens: default_truncate_max_tokens(), + preserve_count: default_preserve_count(), + } + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TimeConfig { #[serde(default = "default_timezone")] diff --git a/src/gateway/agent_factory.rs b/src/gateway/agent_factory.rs index 56282b3..e3d0c15 100644 --- a/src/gateway/agent_factory.rs +++ b/src/gateway/agent_factory.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::agent::context_compressor::ContextCompressor; -use crate::agent::{AgentError, AgentLoop, CompositeSystemPromptProvider, SystemPromptProvider}; -use crate::config::{LLMProviderConfig, ModelResolver}; +use crate::agent::{AgentError, AgentLoop, AgentRuntimeConfig, CompositeSystemPromptProvider, SystemPromptProvider}; +use crate::config::{CompactionConfig, LLMProviderConfig, ModelResolver}; use crate::domain::CapabilityPolicy; use crate::experts::ExpertPromptProvider; use crate::experts::ExpertRuntime; @@ -54,6 +54,8 @@ pub(crate) struct AgentFactory { model_resolver: Arc, /// per-session 的用户模型选择(最高优先级,覆盖专家配置) model_selections: Arc, + /// 上下文压缩算法配置(所有 agent 共享) + compaction_config: CompactionConfig, /// 实例创建时间戳(用于区分新旧 AgentFactory 实例) instance_id: u64, } @@ -81,6 +83,7 @@ impl AgentFactory { prompt_repository: Arc, model_resolver: Arc, model_selections: Arc, + compaction_config: CompactionConfig, ) -> Self { // 使用 Arc 指针地址作为实例标识符,用于区分新旧 AgentFactory 实例 let instance_id = Arc::as_ptr(&tools) as u64; @@ -98,10 +101,22 @@ impl AgentFactory { prompt_repository, model_resolver, model_selections, + compaction_config, instance_id, } } + /// 构造 ContextCompressor(参数内聚到 ContextCompressor,CompactionConfig 注入)。 + /// AgentLoop(in-loop 压缩)和 Session(sync 兜底压缩)共用此方法, + /// 确保两条压缩路径使用同一套用户配置的压缩参数。 + pub(crate) fn build_compressor(&self, runtime_config: &AgentRuntimeConfig) -> ContextCompressor { + ContextCompressor::with_compaction_config( + runtime_config.context_window_tokens, + runtime_config.context_summary_char_budget, + &self.compaction_config, + ) + } + pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result { let session_id = persistent_session_id(request.channel_name, request.session_chat_id); @@ -211,10 +226,9 @@ impl AgentFactory { let tool_chat_id = request .notification_chat_id .unwrap_or(request.session_chat_id); - // 构建上下文压缩器(基于 effective_provider_config 的 context_window_tokens) - let compressor = Arc::new(ContextCompressor::from_provider_config( - &effective_provider_config, - )); + // 构建上下文压缩器(参数内聚到 ContextCompressor,CompactionConfig 注入) + let runtime_config = AgentRuntimeConfig::from(effective_provider_config.clone()); + let compressor = Arc::new(self.build_compressor(&runtime_config)); let mut agent = agent .with_tool_context(ToolContext { channel_name: Some(request.channel_name.to_string()), diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 5665e20..53b3b86 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -120,6 +120,7 @@ impl GatewayState { mcp_config, Some(bus.clone()), Arc::new(crate::config::ModelResolver::from_config(&config)), + config.compaction.clone(), )?; // 诊断日志:记录新 GatewayState 的创建(用于排查重启后是否使用了新状态) diff --git a/src/gateway/runtime.rs b/src/gateway/runtime.rs index a4dafb2..093100f 100644 --- a/src/gateway/runtime.rs +++ b/src/gateway/runtime.rs @@ -9,7 +9,8 @@ use tokio::sync::RwLock; use crate::agent::AgentError; use crate::bus::MessageBus; use crate::config::{ - LLMProviderConfig, MemoryMaintenanceConfig, ModelResolver, SubagentsConfig, TaskConfig, + CompactionConfig, LLMProviderConfig, MemoryMaintenanceConfig, ModelResolver, SubagentsConfig, + TaskConfig, }; use crate::gateway::model_selection::ModelSelectionStore; use crate::gateway::tool_registry_factory::ToolRegistryFactory; @@ -57,6 +58,7 @@ pub(crate) fn build_session_manager( mcp_config: crate::mcp::McpConfig, bus: Option>, model_resolver: Arc, + compaction_config: CompactionConfig, ) -> Result< ( SessionManager, @@ -84,6 +86,7 @@ pub(crate) fn build_session_manager( mcp_config, bus, model_resolver, + compaction_config, ) } @@ -105,6 +108,7 @@ pub(crate) fn build_session_manager_with_sender( mcp_config: crate::mcp::McpConfig, bus: Option>, model_resolver: Arc, + compaction_config: CompactionConfig, ) -> Result< ( SessionManager, @@ -313,6 +317,7 @@ pub(crate) fn build_session_manager_with_sender( prompt_repository.clone(), model_resolver.clone(), model_selections.clone(), + compaction_config, ); let session_factory = SessionFactory::new( provider_config.clone(), diff --git a/src/gateway/session.rs b/src/gateway/session.rs index d24404f..8ffebb1 100644 --- a/src/gateway/session.rs +++ b/src/gateway/session.rs @@ -1,4 +1,4 @@ -use crate::agent::{AgentError, AgentLoop, ContextCompressor, EmittedMessageHandler}; +use crate::agent::{AgentError, AgentLoop, AgentRuntimeConfig, ContextCompressor, EmittedMessageHandler}; #[cfg(test)] use crate::bus::SYSTEM_CONTEXT_SCHEDULED_PROMPT; use crate::bus::{ChatMessage, MessageBus, OutboundMessage}; @@ -301,6 +301,7 @@ impl Session { prompt_repository.clone(), model_resolver, Arc::new(super::model_selection::ModelSelectionStore::new()), + crate::config::CompactionConfig::default(), ); Self::with_factories( channel_name, @@ -325,6 +326,10 @@ impl Session { skill_events: Arc, store: Arc, ) -> Result { + // Session 的 compressor 用于 sync 兜底压缩路径(compaction.rs), + // 必须与 AgentLoop 的 compressor 共用同一套用户配置的压缩参数。 + let runtime_config = AgentRuntimeConfig::from(provider_config.clone()); + let compressor = agent_factory.build_compressor(&runtime_config); Ok(Self { id: Uuid::new_v4(), channel_name: channel_name.clone(), @@ -332,7 +337,7 @@ impl Session { provider_config: provider_config.clone(), skills, agent_factory, - compressor: ContextCompressor::from_provider_config(&provider_config), + compressor, history: SessionHistory::new(channel_name, conversations, skill_events), store, pending_cancel_tokens: HashMap::new(), @@ -744,6 +749,7 @@ impl SessionManager { mcp_config, None, model_resolver, + crate::config::CompactionConfig::default(), ) .map(|(session_manager, _, _, _, _)| session_manager) } diff --git a/web/src/components/Settings/ConfigPage.tsx b/web/src/components/Settings/ConfigPage.tsx index 6f49576..638260e 100644 --- a/web/src/components/Settings/ConfigPage.tsx +++ b/web/src/components/Settings/ConfigPage.tsx @@ -28,6 +28,7 @@ const ToolsTab = lazy(() => import('./tabs/ToolsTab').then((m) => ({ default: m. const MemoryTab = lazy(() => import('./tabs/MemoryTab').then((m) => ({ default: m.MemoryTab }))); const SchedulerTab = lazy(() => import('./tabs/SchedulerTab').then((m) => ({ default: m.SchedulerTab }))); const ImageTab = lazy(() => import('./tabs/ImageTab').then((m) => ({ default: m.ImageTab }))); +const CompactionTab = lazy(() => import('./tabs/CompactionTab').then((m) => ({ default: m.CompactionTab }))); const TimeTab = lazy(() => import('./tabs/TimeTab').then((m) => ({ default: m.TimeTab }))); const GatewayTab = lazy(() => import('./tabs/GatewayTab').then((m) => ({ default: m.GatewayTab }))); const ConnectionTab = lazy(() => import('./tabs/ConnectionTab').then((m) => ({ default: m.ConnectionTab }))); @@ -212,6 +213,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage return ; case 'image': return ; + case 'compaction': + return ; case 'subagents': return ; case 'experts': diff --git a/web/src/components/Settings/constants.ts b/web/src/components/Settings/constants.ts index a3be499..13472a3 100644 --- a/web/src/components/Settings/constants.ts +++ b/web/src/components/Settings/constants.ts @@ -14,6 +14,7 @@ import { Server, Users, UserCheck, + Archive, } from 'lucide-react'; import type { TabId } from './types'; @@ -54,6 +55,7 @@ export const TAB_GROUPS: TabGroup[] = [ tabs: [ { id: 'gateway', label: '网关', icon: Server }, { id: 'scheduler', label: '调度器', icon: Calendar }, + { id: 'compaction', label: '上下文压缩', icon: Archive }, { id: 'memory', label: '记忆维护', icon: Users }, { id: 'image', label: '图片上下文', icon: Image }, { id: 'time', label: '时间', icon: Clock }, diff --git a/web/src/components/Settings/tabs/CompactionTab.tsx b/web/src/components/Settings/tabs/CompactionTab.tsx new file mode 100644 index 0000000..e3d04a3 --- /dev/null +++ b/web/src/components/Settings/tabs/CompactionTab.tsx @@ -0,0 +1,87 @@ +// CompactionTab - 上下文压缩算法配置 +import { Field, SectionCard } from '../ui'; +import { inputCls } from '../constants'; +import type { TabProps } from '../shared'; + +export function CompactionTab({ config, update }: TabProps) { + return ( + <> + + + + update('compaction', { + ...config.compaction, + threshold_ratio: +e.target.value, + }) + } + className={inputCls} + /> + + + + update('compaction', { + ...config.compaction, + llm_compaction_threshold_ratio: +e.target.value, + }) + } + className={inputCls} + /> + + + + + + update('compaction', { + ...config.compaction, + truncate_max_tokens: +e.target.value, + }) + } + className={inputCls} + /> + + + + update('compaction', { + ...config.compaction, + preserve_count: +e.target.value, + }) + } + className={inputCls} + /> + + + + ); +} diff --git a/web/src/components/Settings/types.ts b/web/src/components/Settings/types.ts index 41dc175..ba24df3 100644 --- a/web/src/components/Settings/types.ts +++ b/web/src/components/Settings/types.ts @@ -233,6 +233,13 @@ export interface SchedulerJobConfig { [key: string]: unknown; } +export interface CompactionConfig { + threshold_ratio: number; + llm_compaction_threshold_ratio: number; + truncate_max_tokens: number; + preserve_count: number; +} + export interface AppConfig { providers: Record; models: Record; @@ -246,6 +253,7 @@ export interface AppConfig { image_context: ImageContextConfig; subagents: SubagentsConfig; experts: ExpertsConfig; + compaction: CompactionConfig; client: ClientConfig; channels: Record; mcpServers: Record; @@ -266,7 +274,8 @@ export type TabId = | 'subagents' | 'experts' | 'mcp' - | 'channels'; + | 'channels' + | 'compaction'; export interface ConfigPageProps { onClose: () => void;