Version: 0.9.25.dev.260417

后端:
1. AIHub 模型分级从 Worker/Strategist 两级重构为 Lite/Pro/Max 三级
- AIHub 结构体从 Worker + Strategist 改为 Lite + Pro + Max,分别对应轻量(标题生成)、标准(Chat 路由/闲聊/交付总结)、高能力(Plan 规划/Execute ReAct)三个能力层级
- config.example.yaml 新增 liteModel / proModel / maxModel 三个模型配置项,替代原 workerModel / strategistModel
- 启动层 InitEino 改为创建三个独立模型实例,抽取公共 baseURL 和 apiKey 减少重复
- pickChatModel 统一返回 Pro 模型,旧 strategist 参数不再生效;pickTitleModel 从 Worker 切到 Lite
- runNewAgentGraph 按 Plan/Execute→Max、Chat/Deliver→Pro 分级注入;Graph 出错回退也切到 Pro
- Memory 模块初始化从 Worker 改为 Pro
2. Plan 节点从"两阶段评估"简化为"单轮深度规划",thinking 开关改为全配置化
- 移除 Phase 1(快速评估 1600 token)+ Phase 2(深度规划 3200 token)的两轮调用逻辑,改为单轮不限 token 深度规划
- PlanDecision 移除 need_thinking 字段,prompt 规则和 JSON contract 同步删除该字段
- 各节点(Plan / Execute / Deliver)thinking 开关从硬编码改为从 AgentGraphDeps 读取,由 config.yaml 的 agent.thinking 段按节点注入
- 新增 agent.thinking 配置段(plan / execute / deliver / memory 四个独立布尔开关),config.example.yaml 补齐默认值
- 新增 resolveThinkingMode 公共函数,plan / execute / deliver 和 memory 决策/抽取链路统一使用
3. Memory 模块 LLM 调用支持 thinking 开关
- Config 新增 LLMThinking 字段,config_loader 从 agent.thinking.memory 读取
- LLMDecisionOrchestrator.Compare 和 LLMWriteOrchestrator.ExtractFacts 的 thinking 模式从硬编码 Disabled 改为读取配置
前端:
1. 移除助手输入区模型选择器及全部偏好持久化逻辑
- 删除 ModelType 类型、selectedModel ref、MODEL_PREFERENCE_STORAGE_KEY 常量
- 删除 isModelType / loadModelPreferenceMap / persistModelPreferenceMap / savePreferredModel / resolvePreferredModel / applyPreferredModelForConversation 六个函数及 modelPreferenceMap ref
- 删除 selectedModel watch 监听、发送消息时的 savePreferredModel 调用、切会话时的 applyPreferredModelForConversation 调用、会话迁移时的模型偏好迁移
- fetchChatStream 的 model 参数硬编码为 'worker'
- 删除模板中"模型"下拉选择器(标准/策略)及对应的全局样式 .assistant-model-select-panel
2. 上下文窗口指示器简化为仅显示总占用
- ContextWindowMeter 移除 msg0~msg3 四段彩色分段逻辑(ContextSegment 接口、segments computed、v-for 渲染)
- 进度条改为单一蓝色条,按 total/budget 比例填充;超预算时变红
- Tooltip 简化为仅显示"总计 X / 预算 Y(Z%)"

仓库:无
This commit is contained in:
Losita
2026-04-17 12:27:04 +08:00
parent dd6638f8db
commit d47a8bcabd
19 changed files with 147 additions and 306 deletions

View File

@@ -83,7 +83,7 @@ func Start() {
memoryMetrics := memoryobserve.NewMetricsRegistry() memoryMetrics := memoryobserve.NewMetricsRegistry()
memoryModule := memory.NewModuleWithObserve( memoryModule := memory.NewModuleWithObserve(
db, db,
infrallm.WrapArkClient(aiHub.Worker), infrallm.WrapArkClient(aiHub.Pro),
ragRuntime, ragRuntime,
memoryCfg, memoryCfg,
memory.ObserveDeps{ memory.ObserveDeps{

View File

@@ -67,16 +67,27 @@ time:
# 智能体模型与规划参数。 # 智能体模型与规划参数。
agent: agent:
# 日常执行链路使用的主模型 # 轻量模型:标题生成等低复杂度、低延迟场景
workerModel: "doubao-seed-2-0-code-preview-260215" liteModel: "doubao-seed-2-0-code-preview-260215"
# 规划、拆解、策略推导使用的模型 # 标准模型Chat 路由/闲聊/深度回答/Deliver 总结
strategistModel: "doubao-seed-2-0-code-preview-260215" proModel: "doubao-seed-2-0-code-preview-260215"
# 高能力模型Plan 规划 + Execute ReAct 等深度推理场景。
maxModel: "doubao-seed-2-0-code-preview-260215"
# 模型服务根路径。 # 模型服务根路径。
baseURL: "https://ark.cn-beijing.volces.com/api/v3" baseURL: "https://ark.cn-beijing.volces.com/api/v3"
# 日内并发优化并发度,建议按模型配额调整。 # 日内并发优化并发度,建议按模型配额调整。
dailyRefineConcurrency: 7 dailyRefineConcurrency: 7
# 周级跨天配平额度上限,防止过度调整。 # 周级跨天配平额度上限,防止过度调整。
weeklyAdjustBudget: 5 weeklyAdjustBudget: 5
thinking:
# plan 节点(单轮深度规划),默认开 thinking。
plan: true
# execute 节点ReAct 深度推理),默认开 thinking。
execute: true
# deliver 节点(交付总结),默认关 thinking。
deliver: false
# 记忆模块(决策比对 + 抽取),默认关 thinking。
memory: false
# 通用 RAG 配置。 # 通用 RAG 配置。
rag: rag:

View File

@@ -8,32 +8,53 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// AIHub 存储不同能力的模型实例 // AIHub 存储三级模型实例,按能力分级调度。
//
// 分级策略:
// 1. Lite轻量模型用于标题生成等低复杂度、低延迟场景
// 2. Pro标准模型用于 Chat 路由/闲聊/深度回答/Deliver 总结;
// 3. Max高能力模型用于 Plan 规划和 Execute ReAct 等需要深度推理的场景。
type AIHub struct { type AIHub struct {
Strategist *ark.ChatModel // 智力担当:处理复杂排程逻辑 Lite *ark.ChatModel // 轻量模型:标题生成等低复杂度任务
Worker *ark.ChatModel // 效率担当:处理简单任务、总结 Pro *ark.ChatModel // 标准模型Chat 路由、闲聊、交付总结
Max *ark.ChatModel // 高能力模型Plan 规划、Execute ReAct
} }
func InitEino() (*AIHub, error) { func InitEino() (*AIHub, error) {
ctx := context.Background() ctx := context.Background()
worker, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{ baseURL := viper.GetString("agent.baseURL")
Model: viper.GetString("agent.workerModel"), // 使用的模型版本 apiKey := os.Getenv("ARK_API_KEY")
BaseURL: viper.GetString("agent.baseURL"), // Eino API 的基础 URL
APIKey: os.Getenv("ARK_API_KEY"), // API 密钥 // 1. Lite 模型:标题生成等低复杂度场景,优先控制成本和延迟。
lite, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{
Model: viper.GetString("agent.liteModel"),
BaseURL: baseURL,
APIKey: apiKey,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
strategist, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{ // 2. Pro 模型Chat 路由/闲聊/交付总结等标准复杂度场景。
Model: viper.GetString("agent.strategistModel"), // 使用的模型版本 pro, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{
BaseURL: viper.GetString("agent.baseURL"), // Eino API 的基础 URL Model: viper.GetString("agent.proModel"),
APIKey: os.Getenv("ARK_API_KEY"), // API 密钥 BaseURL: baseURL,
APIKey: apiKey,
})
if err != nil {
return nil, err
}
// 3. Max 模型Plan 规划和 Execute ReAct 等需要深度推理的场景。
maxModel, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{
Model: viper.GetString("agent.maxModel"),
BaseURL: baseURL,
APIKey: apiKey,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &AIHub{ return &AIHub{
Strategist: strategist, Lite: lite,
Worker: worker, Pro: pro,
Max: maxModel,
}, nil }, nil
} }

View File

@@ -72,6 +72,9 @@ type Config struct {
// 2. 默认 0.5,与"守门员"prompt 的 confidence>=0.5 输出规则配合; // 2. 默认 0.5,与"守门员"prompt 的 confidence>=0.5 输出规则配合;
// 3. fallback 路径 confidence 设为 0.45低于默认阈值LLM 不可用时不写入。 // 3. fallback 路径 confidence 设为 0.45低于默认阈值LLM 不可用时不写入。
WriteMinConfidence float64 WriteMinConfidence float64
// 记忆模块 LLM 调用是否开启 thinking由 config.yaml 的 agent.thinking.memory 注入。
LLMThinking bool
} }
// NormalizeReadMode 统一读取模式字符串。 // NormalizeReadMode 统一读取模式字符串。

View File

@@ -62,10 +62,7 @@ func (o *LLMDecisionOrchestrator) Compare(
infrallm.GenerateOptions{ infrallm.GenerateOptions{
Temperature: 0.1, Temperature: 0.1,
MaxTokens: defaultDecisionCompareMaxTokens, MaxTokens: defaultDecisionCompareMaxTokens,
Thinking: infrallm.ThinkingModeDisabled, Thinking: resolveMemoryThinkingMode(o.cfg.LLMThinking),
Metadata: map[string]any{
"stage": "memory_decision_compare",
},
}, },
) )
if err != nil { if err != nil {
@@ -128,3 +125,11 @@ func buildDecisionCompareUserPrompt(fact memorymodel.NormalizedFact, candidate m
candidate.MemoryType, candidate.Content, candidate.MemoryType, candidate.Content,
) )
} }
// resolveMemoryThinkingMode 根据配置布尔值返回对应的 ThinkingMode。
func resolveMemoryThinkingMode(enabled bool) infrallm.ThinkingMode {
if enabled {
return infrallm.ThinkingModeEnabled
}
return infrallm.ThinkingModeDisabled
}

View File

@@ -67,7 +67,7 @@ func (o *LLMWriteOrchestrator) ExtractFacts(ctx context.Context, payload memorym
infrallm.GenerateOptions{ infrallm.GenerateOptions{
Temperature: clampTemperature(o.cfg.LLMTemperature), Temperature: clampTemperature(o.cfg.LLMTemperature),
MaxTokens: defaultMemoryExtractMaxTokens, MaxTokens: defaultMemoryExtractMaxTokens,
Thinking: infrallm.ThinkingModeDisabled, Thinking: resolveMemoryThinkingMode(o.cfg.LLMThinking),
Metadata: map[string]any{ Metadata: map[string]any{
"stage": "memory_extract", "stage": "memory_extract",
"user_id": payload.UserID, "user_id": payload.UserID,

View File

@@ -40,6 +40,7 @@ func LoadConfigFromViper() memorymodel.Config {
DecisionFallbackMode: viper.GetString("memory.decision.fallbackMode"), DecisionFallbackMode: viper.GetString("memory.decision.fallbackMode"),
WriteMode: viper.GetString("memory.write.mode"), WriteMode: viper.GetString("memory.write.mode"),
WriteMinConfidence: viper.GetFloat64("memory.write.minConfidence"), WriteMinConfidence: viper.GetFloat64("memory.write.minConfidence"),
LLMThinking: viper.GetBool("agent.thinking.memory"),
} }
if cfg.Threshold <= 0 { if cfg.Threshold <= 0 {

View File

@@ -72,6 +72,11 @@ type AgentGraphDeps struct {
RoughBuildFunc RoughBuildFunc // 按 Service 注入,粗排算法入口 RoughBuildFunc RoughBuildFunc // 按 Service 注入,粗排算法入口
WriteSchedulePreview WriteSchedulePreviewFunc // 按 Service 注入,排程预览写入入口 WriteSchedulePreview WriteSchedulePreviewFunc // 按 Service 注入,排程预览写入入口
// thinking 开关:由 config.yaml 的 agent.thinking 段注入,各节点按需读取。
ThinkingPlan bool
ThinkingExecute bool
ThinkingDeliver bool
// 记忆预取管线:由 service 层启动的后台检索 goroutine 写入。 // 记忆预取管线:由 service 层启动的后台检索 goroutine 写入。
// channel 携带已渲染的文本内容(非原始 ItemDTO节点直接写入 pinned block。 // channel 携带已渲染的文本内容(非原始 ItemDTO节点直接写入 pinned block。
MemoryFuture chan string // buffered(1),携带 renderMemoryPinnedContentByMode 的输出 MemoryFuture chan string // buffered(1),携带 renderMemoryPinnedContentByMode 的输出

View File

@@ -52,7 +52,6 @@ type PlanDecision struct {
Action PlanAction `json:"action"` Action PlanAction `json:"action"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
Complexity PlanComplexity `json:"complexity"` Complexity PlanComplexity `json:"complexity"`
NeedThinking bool `json:"need_thinking"`
PlanSteps []PlanStep `json:"plan_steps,omitempty"` PlanSteps []PlanStep `json:"plan_steps,omitempty"`
NeedsRoughBuild bool `json:"needs_rough_build,omitempty"` NeedsRoughBuild bool `json:"needs_rough_build,omitempty"`
TaskClassIDs []int `json:"task_class_ids,omitempty"` TaskClassIDs []int `json:"task_class_ids,omitempty"`

View File

@@ -120,6 +120,7 @@ func (n *AgentNodes) Plan(ctx context.Context, st *newagentmodel.AgentGraphState
ChunkEmitter: st.EnsureChunkEmitter(), ChunkEmitter: st.EnsureChunkEmitter(),
ResumeNode: "plan", ResumeNode: "plan",
AlwaysExecute: st.Request.AlwaysExecute, AlwaysExecute: st.Request.AlwaysExecute,
ThinkingEnabled: st.Deps.ThinkingPlan,
}, },
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -230,6 +231,7 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt
WriteSchedulePreview: st.Deps.WriteSchedulePreview, WriteSchedulePreview: st.Deps.WriteSchedulePreview,
OriginalScheduleState: st.OriginalScheduleState, OriginalScheduleState: st.OriginalScheduleState,
AlwaysExecute: st.Request.AlwaysExecute, AlwaysExecute: st.Request.AlwaysExecute,
ThinkingEnabled: st.Deps.ThinkingExecute,
}, },
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -277,6 +279,7 @@ func (n *AgentNodes) Deliver(ctx context.Context, st *newagentmodel.AgentGraphSt
ConversationContext: st.EnsureConversationContext(), ConversationContext: st.EnsureConversationContext(),
Client: st.Deps.ResolveDeliverClient(), Client: st.Deps.ResolveDeliverClient(),
ChunkEmitter: st.EnsureChunkEmitter(), ChunkEmitter: st.EnsureChunkEmitter(),
ThinkingEnabled: st.Deps.ThinkingDeliver,
}, },
); err != nil { ); err != nil {
return nil, err return nil, err

View File

@@ -32,6 +32,7 @@ type DeliverNodeInput struct {
ConversationContext *newagentmodel.ConversationContext ConversationContext *newagentmodel.ConversationContext
Client *infrallm.Client Client *infrallm.Client
ChunkEmitter *newagentstream.ChunkEmitter ChunkEmitter *newagentstream.ChunkEmitter
ThinkingEnabled bool // 是否开启 thinking由 config.yaml 的 agent.thinking.deliver 注入
} }
// RunDeliverNode 执行一轮交付节点逻辑。 // RunDeliverNode 执行一轮交付节点逻辑。
@@ -64,7 +65,7 @@ func RunDeliverNode(ctx context.Context, input DeliverNodeInput) error {
} }
// 2. 调 LLM 生成交付总结。 // 2. 调 LLM 生成交付总结。
summary := generateDeliverSummary(ctx, input.Client, flowState, conversationContext) summary := generateDeliverSummary(ctx, input.Client, flowState, conversationContext, input.ThinkingEnabled)
// 3. 伪流式推送总结。 // 3. 伪流式推送总结。
if strings.TrimSpace(summary) != "" { if strings.TrimSpace(summary) != "" {
@@ -98,6 +99,7 @@ func generateDeliverSummary(
client *infrallm.Client, client *infrallm.Client,
flowState *newagentmodel.CommonState, flowState *newagentmodel.CommonState,
conversationContext *newagentmodel.ConversationContext, conversationContext *newagentmodel.ConversationContext,
thinkingEnabled bool,
) string { ) string {
if flowState != nil { if flowState != nil {
switch { switch {
@@ -119,7 +121,7 @@ func generateDeliverSummary(
infrallm.GenerateOptions{ infrallm.GenerateOptions{
Temperature: 0.5, Temperature: 0.5,
MaxTokens: 800, MaxTokens: 800,
Thinking: infrallm.ThinkingModeDisabled, Thinking: resolveThinkingMode(thinkingEnabled),
Metadata: map[string]any{ Metadata: map[string]any{
"stage": deliverStageName, "stage": deliverStageName,
}, },

View File

@@ -59,6 +59,7 @@ type ExecuteNodeInput struct {
WriteSchedulePreview newagentmodel.WriteSchedulePreviewFunc WriteSchedulePreview newagentmodel.WriteSchedulePreviewFunc
OriginalScheduleState *schedule.ScheduleState OriginalScheduleState *schedule.ScheduleState
AlwaysExecute bool // true 时写工具跳过确认闸门直接执行 AlwaysExecute bool // true 时写工具跳过确认闸门直接执行
ThinkingEnabled bool // 是否开启 thinking由 config.yaml 的 agent.thinking.execute 注入
} }
// ExecuteRoundObservation 记录执行阶段每轮的关键观察。 // ExecuteRoundObservation 记录执行阶段每轮的关键观察。
@@ -203,7 +204,7 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
infrallm.GenerateOptions{ infrallm.GenerateOptions{
Temperature: 1.0, // thinking 模式强制要求 temperature=1 Temperature: 1.0, // thinking 模式强制要求 temperature=1
MaxTokens: 16000, // 需为 thinking chain 留出足够预算 MaxTokens: 16000, // 需为 thinking chain 留出足够预算
Thinking: infrallm.ThinkingModeEnabled, Thinking: resolveThinkingMode(input.ThinkingEnabled),
Metadata: map[string]any{ Metadata: map[string]any{
"stage": executeStageName, "stage": executeStageName,
"step_index": flowState.CurrentStep, "step_index": flowState.CurrentStep,

View File

@@ -35,19 +35,19 @@ type PlanNodeInput struct {
ChunkEmitter *newagentstream.ChunkEmitter ChunkEmitter *newagentstream.ChunkEmitter
ResumeNode string ResumeNode string
AlwaysExecute bool // true 时计划生成后自动确认,不进入 confirm 节点 AlwaysExecute bool // true 时计划生成后自动确认,不进入 confirm 节点
ThinkingEnabled bool // 是否开启 thinking由 config.yaml 的 agent.thinking.plan 注入
} }
// RunPlanNode 执行一轮规划节点逻辑。 // RunPlanNode 执行一轮规划节点逻辑。
// //
// 步骤说明: // 步骤说明:
// 1. 先校验最小依赖,并推送一条正在规划的状态,避免用户空等; // 1. 先校验最小依赖,并推送一条"正在规划"的状态,避免用户空等;
// 2. Phase 1快速评估不开 thinking让 LLM 同时产出复杂度评估和规划结果 // 2. 单轮深度规划:开 thinking、无 token 上限,让 LLM 一步到位产出完整计划
// 3. Phase 2深度规划若 LLM 自评需要深度思考且规划已完成,开 thinking 重跑 // 3. 若模型先对用户说了话,则先把 speak 伪流式推给前端,并写回 history
// 4. 若模型先对用户说了话,则先把 speak 伪流式推给前端,并写回 history // 4. 最后按 action 推进流程:
// 5. 最后按 action 推进流程: // 4.1 continue继续停留在 planning
// 5.1 continue继续停留在 planning // 4.2 ask_user打开 pending interaction后续交给 interrupt 收口
// 5.2 ask_user打开 pending interaction后续交给 interrupt 收口; // 4.3 plan_done固化完整计划刷新 pinned context并进入 waiting_confirm。
// 5.3 plan_done固化完整计划刷新 pinned context并进入 waiting_confirm。
func RunPlanNode(ctx context.Context, input PlanNodeInput) error { func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
runtimeState, conversationContext, emitter, err := preparePlanNodeInput(input) runtimeState, conversationContext, emitter, err := preparePlanNodeInput(input)
if err != nil { if err != nil {
@@ -69,68 +69,31 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
// 2. 构造本轮规划输入。 // 2. 构造本轮规划输入。
messages := newagentprompt.BuildPlanMessages(flowState, conversationContext, input.UserInput) messages := newagentprompt.BuildPlanMessages(flowState, conversationContext, input.UserInput)
// 3. Phase 1快速评估开 thinking让 LLM 同时产出复杂度评估和规划结果 // 3. 单轮深度规划:由配置决定是否开启 thinking不做 token 上限约束
decision, rawResult, err := infrallm.GenerateJSON[newagentmodel.PlanDecision]( decision, rawResult, err := infrallm.GenerateJSON[newagentmodel.PlanDecision](
ctx, ctx,
input.Client, input.Client,
messages, messages,
infrallm.GenerateOptions{ infrallm.GenerateOptions{
Temperature: 0.2, Temperature: 0.2,
MaxTokens: 1600, Thinking: resolveThinkingMode(input.ThinkingEnabled),
Thinking: infrallm.ThinkingModeEnabled,
Metadata: map[string]any{ Metadata: map[string]any{
"stage": planStageName, "stage": planStageName,
"phase": "assessment", "phase": "planning",
}, },
}, },
) )
if err != nil { if err != nil {
if rawResult != nil && strings.TrimSpace(rawResult.Text) != "" { if rawResult != nil && strings.TrimSpace(rawResult.Text) != "" {
return fmt.Errorf("规划评估解析失败,原始输出=%s错误=%w", strings.TrimSpace(rawResult.Text), err) return fmt.Errorf("规划解析失败,原始输出=%s错误=%w", strings.TrimSpace(rawResult.Text), err)
} }
return fmt.Errorf("规划评估阶段模型调用失败: %w", err) return fmt.Errorf("规划阶段模型调用失败: %w", err)
} }
if err := decision.Validate(); err != nil { if err := decision.Validate(); err != nil {
return fmt.Errorf("规划评估决策不合法: %w", err) return fmt.Errorf("规划决策不合法: %w", err)
} }
// 4. Phase 2若 LLM 自评需要深度思考且本轮规划已完成,则开启 thinking 重跑 // 4. 若模型先对用户说了话,且不是 ask_userask_user 交给 interrupt 收口),则先以伪流式推送,再写回 history
// 条件NeedThinking=true + Action=plan_done → 说明 LLM 认为当前无 thinking 的计划质量不够。
// 其他 actioncontinue / ask_user不需要 thinking直接用 Phase 1 结果。
if decision.NeedThinking && decision.Action == newagentmodel.PlanActionDone {
if err := emitter.EmitStatus(
planStatusBlockID,
planStageName,
"deep_planning",
"正在深入思考,生成更完善的计划。",
false,
); err != nil {
return fmt.Errorf("深度规划状态推送失败: %w", err)
}
deepDecision, _, deepErr := infrallm.GenerateJSON[newagentmodel.PlanDecision](
ctx,
input.Client,
messages,
infrallm.GenerateOptions{
Temperature: 0.2,
MaxTokens: 3200,
Thinking: infrallm.ThinkingModeEnabled,
Metadata: map[string]any{
"stage": planStageName,
"phase": "deep_planning",
},
},
)
if deepErr == nil && deepDecision != nil {
if validateErr := deepDecision.Validate(); validateErr == nil {
decision = deepDecision
}
}
// 深度规划失败时静默降级到 Phase 1 结果,不中断流程。
}
// 5. 若模型先对用户说了话,且不是 ask_userask_user 交给 interrupt 收口),则先以伪流式推送,再写回 history。
if strings.TrimSpace(decision.Speak) != "" && decision.Action != newagentmodel.PlanActionAskUser { if strings.TrimSpace(decision.Speak) != "" && decision.Action != newagentmodel.PlanActionAskUser {
if err := emitter.EmitPseudoAssistantText( if err := emitter.EmitPseudoAssistantText(
ctx, ctx,
@@ -144,7 +107,7 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
conversationContext.AppendHistory(schema.AssistantMessage(decision.Speak, nil)) conversationContext.AppendHistory(schema.AssistantMessage(decision.Speak, nil))
} }
// 6. 按规划动作推进流程状态。 // 5. 按规划动作推进流程状态。
switch decision.Action { switch decision.Action {
case newagentmodel.PlanActionContinue: case newagentmodel.PlanActionContinue:
flowState.Phase = newagentmodel.PhasePlanning flowState.Phase = newagentmodel.PhasePlanning
@@ -169,10 +132,10 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
} }
} }
// always_execute 开启时,计划层跳过确认闸门,直接进入执行阶段。 // always_execute 开启时,计划层跳过确认闸门,直接进入执行阶段。
// 这样可以与 Execute 节点的写工具跳过确认语义保持一致。 // 这样可以与 Execute 节点的"写工具跳过确认"语义保持一致。
if input.AlwaysExecute { if input.AlwaysExecute {
// 1. 自动执行模式不会经过 Confirm 卡片,因此这里先把完整计划明确展示给用户。 // 1. 自动执行模式不会经过 Confirm 卡片,因此这里先把完整计划明确展示给用户。
// 2. 摘要格式复用 Confirm 节点,保证手动确认”和“自动执行两条链路文案一致。 // 2. 摘要格式复用 Confirm 节点,保证"手动确认"和"自动执行"两条链路文案一致。
// 3. 推流后同步写入历史,确保后续 Execute 阶段的上下文也能看到这份计划。 // 3. 推流后同步写入历史,确保后续 Execute 阶段的上下文也能看到这份计划。
summary := strings.TrimSpace(buildPlanSummary(decision.PlanSteps)) summary := strings.TrimSpace(buildPlanSummary(decision.PlanSteps))
if summary != "" { if summary != "" {
@@ -296,3 +259,12 @@ func buildPinnedPlanText(steps []newagentmodel.PlanStep) string {
} }
return strings.TrimSpace(strings.Join(lines, "\n\n")) return strings.TrimSpace(strings.Join(lines, "\n\n"))
} }
// resolveThinkingMode 根据配置布尔值返回对应的 ThinkingMode。
// 供 plan / execute / deliver 节点统一使用。
func resolveThinkingMode(enabled bool) infrallm.ThinkingMode {
if enabled {
return infrallm.ThinkingModeEnabled
}
return infrallm.ThinkingModeDisabled
}

View File

@@ -21,8 +21,7 @@ const planSystemPrompt = `
5. plan_steps 必须使用自然语言,便于后端将完整 plan 重新注入到后续上下文顶部。 5. plan_steps 必须使用自然语言,便于后端将完整 plan 重新注入到后续上下文顶部。
6. 只输出 JSON不要输出 markdown不要输出额外解释不要在 JSON 外再补文字。 6. 只输出 JSON不要输出 markdown不要输出额外解释不要在 JSON 外再补文字。
7. 每次输出前先评估任务复杂度simple简单明确无复杂依赖、moderate多步操作需要一定推理、complex需要深度推理、多方案比较或复杂依赖关系 7. 每次输出前先评估任务复杂度simple简单明确无复杂依赖、moderate多步操作需要一定推理、complex需要深度推理、多方案比较或复杂依赖关系
8. 根据复杂度判断 need_thinking你是否需要深度思考才能生成高质量计划当不确定时倾向于 false。 8. 粗排识别规则:若满足以下两个条件,在 action=plan_done 时附加 needs_rough_build=true 和 task_class_ids
9. 粗排识别规则:若满足以下两个条件,在 action=plan_done 时附加 needs_rough_build=true 和 task_class_ids
条件1用户输入中存在"任务类 ID"字段(见上下文"任务类 ID"部分); 条件1用户输入中存在"任务类 ID"字段(见上下文"任务类 ID"部分);
条件2用户意图明确是"批量安排/帮我排课/把任务类排进日程"等批量调度需求。 条件2用户意图明确是"批量安排/帮我排课/把任务类排进日程"等批量调度需求。
满足时:后端会在用户确认计划后自动运行粗排算法(硬性约束已由算法保证,无需 LLM 校验)。 满足时:后端会在用户确认计划后自动运行粗排算法(硬性约束已由算法保证,无需 LLM 校验)。
@@ -99,7 +98,6 @@ func BuildPlanDecisionContractText() string {
- action只能是 %s / %s / %s - action只能是 %s / %s / %s
- reason给后端和日志看的简短说明 - reason给后端和日志看的简短说明
- complexity任务复杂度只能是 simple / moderate / complex - complexity任务复杂度只能是 simple / moderate / complex
- need_thinking是否需要深度思考才能生成高质量计划只能是 true / false
- plan_steps仅当 action=%s 时允许返回;返回时必须是完整计划,不是增量 - plan_steps仅当 action=%s 时允许返回;返回时必须是完整计划,不是增量
- plan_steps[].content步骤正文必填 - plan_steps[].content步骤正文必填
- plan_steps[].done_when可选建议写"什么情况下算这一步做完" - plan_steps[].done_when可选建议写"什么情况下算这一步做完"
@@ -112,7 +110,6 @@ func BuildPlanDecisionContractText() string {
"action": "%s", "action": "%s",
"reason": "当前信息已足够继续规划", "reason": "当前信息已足够继续规划",
"complexity": "moderate", "complexity": "moderate",
"need_thinking": false
} }
{ {
@@ -120,7 +117,6 @@ func BuildPlanDecisionContractText() string {
"action": "%s", "action": "%s",
"reason": "当前时间范围仍不明确", "reason": "当前时间范围仍不明确",
"complexity": "simple", "complexity": "simple",
"need_thinking": false
} }
{ {
@@ -128,7 +124,7 @@ func BuildPlanDecisionContractText() string {
"action": "%s", "action": "%s",
"reason": "当前计划已具备执行条件", "reason": "当前计划已具备执行条件",
"complexity": "simple", "complexity": "simple",
"need_thinking": false,
"plan_steps": [ "plan_steps": [
{ {
"content": "先确认本周可用时间范围", "content": "先确认本周可用时间范围",

View File

@@ -104,14 +104,10 @@ func thinkingModeToBool(mode string) bool {
// pickChatModel 根据请求选择模型。 // pickChatModel 根据请求选择模型。
// 当前约定: // 当前约定:
// - strategist策略模型 // - 旧链路已全面切到 newAgent graph这里仅作为 runNormalChatFlow 回退时的模型选择入口
// - 其余值默认 worker包含空字符串场景 // - 统一返回 Pro 模型,旧 strategist 参数不再生效
func (s *AgentService) pickChatModel(requestModel string) (*ark.ChatModel, string) { func (s *AgentService) pickChatModel(requestModel string) (*ark.ChatModel, string) {
modelName := strings.TrimSpace(requestModel) return s.AIHub.Pro, "pro"
if strings.EqualFold(modelName, "strategist") {
return s.AIHub.Strategist, "strategist"
}
return s.AIHub.Worker, "worker"
} }
// PersistChatHistory 是 Agent 聊天链路唯一的“消息持久化入口”。 // PersistChatHistory 是 Agent 聊天链路唯一的“消息持久化入口”。

View File

@@ -278,15 +278,15 @@ func (s *AgentService) generateConversationTitle(ctx context.Context, history []
} }
// pickTitleModel 选择用于标题生成的模型。 // pickTitleModel 选择用于标题生成的模型。
// 优先 worker(成本低、速度快);worker 不可用时回退 strategist // 优先 Lite(成本低、速度快);Lite 不可用时回退 Pro
func (s *AgentService) pickTitleModel() *ark.ChatModel { func (s *AgentService) pickTitleModel() *ark.ChatModel {
if s.AIHub == nil { if s.AIHub == nil {
return nil return nil
} }
if s.AIHub.Worker != nil { if s.AIHub.Lite != nil {
return s.AIHub.Worker return s.AIHub.Lite
} }
return s.AIHub.Strategist return s.AIHub.Pro
} }
// buildConversationTitleUserPrompt 把消息历史拼成可读文本供模型总结。 // buildConversationTitleUserPrompt 把消息历史拼成可读文本供模型总结。

View File

@@ -15,6 +15,7 @@ import (
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
"github.com/cloudwego/eino/schema" "github.com/cloudwego/eino/schema"
"github.com/spf13/viper"
agentchat "github.com/LoveLosita/smartflow/backend/agent/chat" agentchat "github.com/LoveLosita/smartflow/backend/agent/chat"
"github.com/LoveLosita/smartflow/backend/conv" "github.com/LoveLosita/smartflow/backend/conv"
@@ -149,10 +150,12 @@ func (s *AgentService) runNewAgentGraph(
graphRequest.Normalize() graphRequest.Normalize()
// 7. 适配 LLM clients从 AIHub 的 ark.ChatModel 转换为 newAgent LLM Client // 7. 适配 LLM clients从 AIHub 的 ark.ChatModel 转换为 newAgent LLM Client
chatClient := infrallm.WrapArkClient(s.AIHub.Worker) // 7.1 Chat/Deliver 使用 Pro 模型:路由分流、闲聊、交付总结属于标准复杂度。
planClient := infrallm.WrapArkClient(s.AIHub.Worker) // 7.2 Plan/Execute 使用 Max 模型:规划和 ReAct 循环需要深度推理能力。
executeClient := infrallm.WrapArkClient(s.AIHub.Worker) chatClient := infrallm.WrapArkClient(s.AIHub.Pro)
deliverClient := infrallm.WrapArkClient(s.AIHub.Worker) planClient := infrallm.WrapArkClient(s.AIHub.Max)
executeClient := infrallm.WrapArkClient(s.AIHub.Max)
deliverClient := infrallm.WrapArkClient(s.AIHub.Pro)
// 8. 适配 SSE emitter。 // 8. 适配 SSE emitter。
sseEmitter := newagentstream.NewSSEPayloadEmitter(outChan) sseEmitter := newagentstream.NewSSEPayloadEmitter(outChan)
@@ -173,6 +176,9 @@ func (s *AgentService) runNewAgentGraph(
RoughBuildFunc: s.makeRoughBuildFunc(), RoughBuildFunc: s.makeRoughBuildFunc(),
WriteSchedulePreview: s.makeWriteSchedulePreviewFunc(), WriteSchedulePreview: s.makeWriteSchedulePreviewFunc(),
MemoryFuture: memoryFuture, MemoryFuture: memoryFuture,
ThinkingPlan: viper.GetBool("agent.thinking.plan"),
ThinkingExecute: viper.GetBool("agent.thinking.execute"),
ThinkingDeliver: viper.GetBool("agent.thinking.deliver"),
} }
// 10. 构造 AgentGraphRunInput 并运行 graph。 // 10. 构造 AgentGraphRunInput 并运行 graph。
@@ -190,8 +196,8 @@ func (s *AgentService) runNewAgentGraph(
log.Printf("[ERROR] newAgent graph 执行失败 trace=%s chat=%s: %v", traceID, chatID, graphErr) log.Printf("[ERROR] newAgent graph 执行失败 trace=%s chat=%s: %v", traceID, chatID, graphErr)
pushErrNonBlocking(errChan, fmt.Errorf("graph 执行失败: %w", graphErr)) pushErrNonBlocking(errChan, fmt.Errorf("graph 执行失败: %w", graphErr))
// Graph 出错时回退普通聊天,保证可用性。 // Graph 出错时回退普通聊天,保证可用性。回退使用 Pro 模型。
s.runNormalChatFlow(requestCtx, s.AIHub.Worker, resolvedModelName, userMessage, "", nil, retryMeta, thinkingModeToBool(thinkingMode), userID, chatID, traceID, requestStart, outChan, errChan) s.runNormalChatFlow(requestCtx, s.AIHub.Pro, resolvedModelName, userMessage, "", nil, retryMeta, thinkingModeToBool(thinkingMode), userID, chatID, traceID, requestStart, outChan, errChan)
return return
} }

View File

@@ -3,14 +3,6 @@ import { computed } from 'vue'
import type { ConversationContextStats } from '@/types/dashboard' import type { ConversationContextStats } from '@/types/dashboard'
interface ContextSegment {
key: 'msg0' | 'msg1' | 'msg2' | 'msg3'
label: string
value: number
widthPercent: number
color: string
}
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
stats?: ConversationContextStats | null stats?: ConversationContextStats | null
@@ -33,6 +25,14 @@ const usagePercent = computed(() => {
return Math.round((safeStats.value.total / safeStats.value.budget) * 100) return Math.round((safeStats.value.total / safeStats.value.budget) * 100)
}) })
const barWidthPercent = computed(() => {
if (!safeStats.value || safeStats.value.budget <= 0) {
return 0
}
// 1. 按 total / budget 计算宽度,上限 100%(超预算时撑满进度条)。
return Math.min(100, (safeStats.value.total / safeStats.value.budget) * 100)
})
const isOverBudget = computed(() => { const isOverBudget = computed(() => {
if (!safeStats.value) { if (!safeStats.value) {
return false return false
@@ -40,31 +40,6 @@ const isOverBudget = computed(() => {
return safeStats.value.total > safeStats.value.budget return safeStats.value.total > safeStats.value.budget
}) })
const segments = computed<ContextSegment[]>(() => {
const stats = safeStats.value
if (!stats) {
return []
}
// 1. 进度条固定做成紧凑胶囊,因此按 max(total, budget) 计算比例,既保留预算留白,也兼容超预算占满。
// 2. 四段颜色继续对应后端 msg0~msg3 的真实语义,避免前端为了视觉压缩而打乱统计含义。
// 3. 零值段不渲染,减少窄尺寸下的噪点,让小组件也能保留基本可读性。
const base = Math.max(stats.total, stats.budget, 1)
const rawSegments = [
{ key: 'msg0', label: '规则', value: stats.msg0, color: 'linear-gradient(90deg, #2556c7, #3b82f6)' },
{ key: 'msg1', label: '历史', value: stats.msg1, color: 'linear-gradient(90deg, #0f766e, #14b8a6)' },
{ key: 'msg2', label: '执行', value: stats.msg2, color: 'linear-gradient(90deg, #b45309, #f59e0b)' },
{ key: 'msg3', label: '当前', value: stats.msg3, color: 'linear-gradient(90deg, #15803d, #22c55e)' },
] as const
return rawSegments
.filter((segment) => segment.value > 0)
.map((segment) => ({
...segment,
widthPercent: Math.max(0, Math.min(100, (segment.value / base) * 100)),
}))
})
const usageText = computed(() => { const usageText = computed(() => {
if (props.loading) { if (props.loading) {
return '...' return '...'
@@ -86,9 +61,7 @@ const tooltipText = computed(() => {
return props.disabled ? '新会话发送首条消息后展示上下文窗口统计' : '当前会话暂无上下文窗口统计' return props.disabled ? '新会话发送首条消息后展示上下文窗口统计' : '当前会话暂无上下文窗口统计'
} }
const segmentText = segments.value.map((segment) => `${segment.label} ${segment.value}`).join(' / ') return `总计 ${safeStats.value.total} / 预算 ${safeStats.value.budget}${usagePercent.value}%`
const usageSummary = `总计 ${safeStats.value.total} / 预算 ${safeStats.value.budget}${usagePercent.value}%`
return segmentText ? `${usageSummary}${segmentText}` : usageSummary
}) })
</script> </script>
@@ -106,18 +79,7 @@ const tooltipText = computed(() => {
<div class="assistant-context-meter__track" aria-hidden="true"> <div class="assistant-context-meter__track" aria-hidden="true">
<div v-if="loading" class="assistant-context-meter__loading-bar" /> <div v-if="loading" class="assistant-context-meter__loading-bar" />
<div v-else-if="barWidthPercent > 0" class="assistant-context-meter__bar" :style="{ width: `${barWidthPercent}%` }" />
<template v-else>
<div
v-for="segment in segments"
:key="segment.key"
class="assistant-context-meter__segment"
:style="{
width: `${segment.widthPercent}%`,
background: segment.color,
}"
/>
</template>
</div> </div>
<span class="assistant-context-meter__value">{{ usageText }}</span> <span class="assistant-context-meter__value">{{ usageText }}</span>
@@ -195,7 +157,6 @@ const tooltipText = computed(() => {
background: background:
linear-gradient(180deg, rgba(232, 238, 246, 0.95), rgba(243, 247, 251, 0.95)), linear-gradient(180deg, rgba(232, 238, 246, 0.95), rgba(243, 247, 251, 0.95)),
#edf2f7; #edf2f7;
display: flex;
} }
.assistant-context-meter--disabled .assistant-context-meter__track { .assistant-context-meter--disabled .assistant-context-meter__track {
@@ -204,9 +165,15 @@ const tooltipText = computed(() => {
#eef2f7; #eef2f7;
} }
.assistant-context-meter__segment { .assistant-context-meter__bar {
height: 100%; height: 100%;
flex: 0 0 auto; border-radius: inherit;
background: linear-gradient(90deg, #2556c7, #3b82f6);
transition: width 0.3s ease;
}
.assistant-context-meter--danger .assistant-context-meter__bar {
background: linear-gradient(90deg, #b42318, #ef4444);
} }
.assistant-context-meter__loading-bar { .assistant-context-meter__loading-bar {

View File

@@ -48,7 +48,6 @@ interface StreamEventPayload {
error?: StreamErrorPayload error?: StreamErrorPayload
} }
type ModelType = 'worker' | 'strategist'
interface ConversationGroup { interface ConversationGroup {
key: string key: string
@@ -86,7 +85,7 @@ const conversationLoadingMore = ref(false)
const chatLoading = ref(false) const chatLoading = ref(false)
const historyExpanded = ref(true) const historyExpanded = ref(true)
const selectedConversationId = ref('') const selectedConversationId = ref('')
const selectedModel = ref<ModelType>('worker')
const selectedThinkingMode = ref<ThinkingModeType>('auto') const selectedThinkingMode = ref<ThinkingModeType>('auto')
const messageInput = ref('') const messageInput = ref('')
const historyPanelWidth = ref(props.initialHistoryWidth) const historyPanelWidth = ref(props.initialHistoryWidth)
@@ -120,7 +119,7 @@ const quickActions = [
'给我一个更稳妥的推进方案', '给我一个更稳妥的推进方案',
] ]
const MODEL_PREFERENCE_STORAGE_KEY = 'smartflow.assistant.model.byConversation.v1'
const DEFAULT_PLANNING_PROMPT = '请基于这些任务类帮我做一版智能编排。' const DEFAULT_PLANNING_PROMPT = '请基于这些任务类帮我做一版智能编排。'
let messageScrollRaf = 0 let messageScrollRaf = 0
@@ -336,85 +335,6 @@ const contextStatsDisabled = computed(() => {
return !selectedConversationId.value || isDraftConversationId(selectedConversationId.value) return !selectedConversationId.value || isDraftConversationId(selectedConversationId.value)
}) })
function isModelType(value: unknown): value is ModelType {
return value === 'worker' || value === 'strategist'
}
function loadModelPreferenceMap() {
if (typeof window === 'undefined') {
return {} as Record<string, ModelType>
}
try {
const raw = window.localStorage.getItem(MODEL_PREFERENCE_STORAGE_KEY)
if (!raw) {
return {} as Record<string, ModelType>
}
const parsed = JSON.parse(raw) as unknown
const normalized: Record<string, ModelType> = {}
const entries = typeof parsed === 'object' && parsed ? Object.entries(parsed) : []
// 1. 只接收结构合法且值在白名单内的记录,避免脏数据把模型值污染为非法字符串。
// 2. 键为空字符串的记录直接丢弃,防止“新建会话未落库”场景写入无效索引。
// 3. 解析失败时回退为空对象,不阻塞聊天主流程。
for (const [conversationId, model] of entries) {
if (!conversationId || !isModelType(model)) {
continue
}
normalized[conversationId] = model
}
return normalized
} catch {
return {} as Record<string, ModelType>
}
}
const modelPreferenceMap = ref<Record<string, ModelType>>(loadModelPreferenceMap())
function persistModelPreferenceMap() {
if (typeof window === 'undefined') {
return
}
try {
window.localStorage.setItem(MODEL_PREFERENCE_STORAGE_KEY, JSON.stringify(modelPreferenceMap.value))
} catch {
// 1. 本地存储失败只影响“记忆体验”,不影响消息收发主链路。
// 2. 这里静默处理,避免用户每次切模型都被错误提示打断。
// 3. 若用户清理缓存或隐私模式限制写入,后续会自动退化为会话内临时选择。
}
}
function savePreferredModel(conversationId: string, model: ModelType) {
if (!conversationId || modelPreferenceMap.value[conversationId] === model) {
return
}
modelPreferenceMap.value = {
...modelPreferenceMap.value,
[conversationId]: model,
}
persistModelPreferenceMap()
}
function resolvePreferredModel(conversationId: string) {
if (!conversationId) {
return null
}
return modelPreferenceMap.value[conversationId] ?? null
}
function applyPreferredModelForConversation(conversationId: string) {
const preferredModel = resolvePreferredModel(conversationId)
if (!preferredModel || preferredModel === selectedModel.value) {
return
}
selectedModel.value = preferredModel
}
function ensureConversationBucket(conversationId: string) { function ensureConversationBucket(conversationId: string) {
if (!conversationMessagesMap[conversationId]) { if (!conversationMessagesMap[conversationId]) {
@@ -476,16 +396,6 @@ function migrateConversationState(fromConversationId: string, toConversationId:
delete conversationMetaMap[fromConversationId] delete conversationMetaMap[fromConversationId]
} }
if (modelPreferenceMap.value[fromConversationId]) {
const migratedModelMap = { ...modelPreferenceMap.value }
if (!migratedModelMap[toConversationId]) {
migratedModelMap[toConversationId] = migratedModelMap[fromConversationId]!
}
delete migratedModelMap[fromConversationId]
modelPreferenceMap.value = migratedModelMap
persistModelPreferenceMap()
}
const latestMap = new Map<string, ConversationListItem>() const latestMap = new Map<string, ConversationListItem>()
const deduplicated: ConversationListItem[] = [] const deduplicated: ConversationListItem[] = []
const seen = new Set<string>() const seen = new Set<string>()
@@ -1299,7 +1209,6 @@ async function loadConversationContextStats(conversationId: string, forceReload
async function selectConversation(conversationId: string) { async function selectConversation(conversationId: string) {
cancelEditUserMessage() cancelEditUserMessage()
selectedConversationId.value = conversationId selectedConversationId.value = conversationId
applyPreferredModelForConversation(conversationId)
await Promise.allSettled([ await Promise.allSettled([
loadConversationMessages(conversationId), loadConversationMessages(conversationId),
ensureConversationMeta(conversationId), ensureConversationMeta(conversationId),
@@ -1502,7 +1411,7 @@ async function streamAssistantReply(
const response = await fetchChatStream({ const response = await fetchChatStream({
conversation_id: isDraftConversationId(draftConversationId) ? undefined : draftConversationId, conversation_id: isDraftConversationId(draftConversationId) ? undefined : draftConversationId,
message: text, message: text,
model: selectedModel.value, model: 'worker',
thinking: selectedThinkingMode.value, thinking: selectedThinkingMode.value,
extra: requestExtra, extra: requestExtra,
}) })
@@ -1577,8 +1486,6 @@ async function sendMessage(preset?: string) {
if (!selectedConversationId.value || shouldStartFreshPlanningConversation) { if (!selectedConversationId.value || shouldStartFreshPlanningConversation) {
selectedConversationId.value = draftConversationId selectedConversationId.value = draftConversationId
} }
savePreferredModel(draftConversationId, selectedModel.value)
ensureConversationBucket(draftConversationId) ensureConversationBucket(draftConversationId)
unavailableHistoryMap[draftConversationId] = false unavailableHistoryMap[draftConversationId] = false
@@ -1734,16 +1641,6 @@ watch(
}, },
) )
watch(
selectedModel,
(nextModel) => {
const conversationId = selectedConversationId.value
if (!conversationId) {
return
}
savePreferredModel(conversationId, nextModel)
},
)
onMounted(async () => { onMounted(async () => {
reasoningTicker = window.setInterval(() => { reasoningTicker = window.setInterval(() => {
@@ -2126,20 +2023,6 @@ onBeforeUnmount(() => {
</el-select> </el-select>
</div> </div>
<div class="assistant-toolbar__pill assistant-toolbar__pill--select assistant-toolbar__pill--ds-model">
<span class="assistant-toolbar__select-label">模型</span>
<el-select
v-model="selectedModel"
class="assistant-toolbar__select-box"
size="small"
popper-class="assistant-model-select-panel"
placement="top-start"
:teleported="true"
>
<el-option value="worker" label="标准" />
<el-option value="strategist" label="策略" />
</el-select>
</div>
<ContextWindowMeter <ContextWindowMeter
class="assistant-toolbar__context-meter" class="assistant-toolbar__context-meter"
@@ -3183,7 +3066,6 @@ onBeforeUnmount(() => {
font-weight: 600; font-weight: 600;
} }
.assistant-toolbar__pill--ds-model,
.assistant-toolbar__pill--ds-thinking { .assistant-toolbar__pill--ds-thinking {
height: 32px; height: 32px;
padding: 0 8px 0 10px; padding: 0 8px 0 10px;
@@ -3200,10 +3082,6 @@ onBeforeUnmount(() => {
min-width: 138px; min-width: 138px;
} }
.assistant-toolbar__pill--ds-model {
min-width: 144px;
}
.assistant-toolbar__context-meter { .assistant-toolbar__context-meter {
width: 144px; width: 144px;
min-width: 144px; min-width: 144px;
@@ -3435,30 +3313,5 @@ onBeforeUnmount(() => {
} }
</style> </style>
<style> <style>
.assistant-model-select-panel.el-popper {
border-radius: 12px;
border: 1px solid rgba(15, 23, 42, 0.1);
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.14);
padding: 6px;
}
.assistant-model-select-panel .el-select-dropdown__item {
height: 36px;
line-height: 36px;
border-radius: 8px;
padding: 0 12px;
color: #4d5d73;
font-size: 14px;
font-weight: 600;
}
.assistant-model-select-panel .el-select-dropdown__item.hover,
.assistant-model-select-panel .el-select-dropdown__item:hover {
background: rgba(51, 95, 194, 0.1);
}
.assistant-model-select-panel .el-select-dropdown__item.is-selected {
color: #2f56b0;
background: rgba(51, 95, 194, 0.16);
}
</style> </style>