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:
@@ -3,14 +3,6 @@ import { computed } from 'vue'
|
||||
|
||||
import type { ConversationContextStats } from '@/types/dashboard'
|
||||
|
||||
interface ContextSegment {
|
||||
key: 'msg0' | 'msg1' | 'msg2' | 'msg3'
|
||||
label: string
|
||||
value: number
|
||||
widthPercent: number
|
||||
color: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
stats?: ConversationContextStats | null
|
||||
@@ -33,6 +25,14 @@ const usagePercent = computed(() => {
|
||||
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(() => {
|
||||
if (!safeStats.value) {
|
||||
return false
|
||||
@@ -40,31 +40,6 @@ const isOverBudget = computed(() => {
|
||||
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(() => {
|
||||
if (props.loading) {
|
||||
return '...'
|
||||
@@ -86,9 +61,7 @@ const tooltipText = computed(() => {
|
||||
return props.disabled ? '新会话发送首条消息后展示上下文窗口统计' : '当前会话暂无上下文窗口统计'
|
||||
}
|
||||
|
||||
const segmentText = segments.value.map((segment) => `${segment.label} ${segment.value}`).join(' / ')
|
||||
const usageSummary = `总计 ${safeStats.value.total} / 预算 ${safeStats.value.budget}(${usagePercent.value}%)`
|
||||
return segmentText ? `${usageSummary};${segmentText}` : usageSummary
|
||||
return `总计 ${safeStats.value.total} / 预算 ${safeStats.value.budget}(${usagePercent.value}%)`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -106,18 +79,7 @@ const tooltipText = computed(() => {
|
||||
|
||||
<div class="assistant-context-meter__track" aria-hidden="true">
|
||||
<div v-if="loading" class="assistant-context-meter__loading-bar" />
|
||||
|
||||
<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 v-else-if="barWidthPercent > 0" class="assistant-context-meter__bar" :style="{ width: `${barWidthPercent}%` }" />
|
||||
</div>
|
||||
|
||||
<span class="assistant-context-meter__value">{{ usageText }}</span>
|
||||
@@ -195,7 +157,6 @@ const tooltipText = computed(() => {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(232, 238, 246, 0.95), rgba(243, 247, 251, 0.95)),
|
||||
#edf2f7;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.assistant-context-meter--disabled .assistant-context-meter__track {
|
||||
@@ -204,9 +165,15 @@ const tooltipText = computed(() => {
|
||||
#eef2f7;
|
||||
}
|
||||
|
||||
.assistant-context-meter__segment {
|
||||
.assistant-context-meter__bar {
|
||||
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 {
|
||||
|
||||
@@ -48,7 +48,6 @@ interface StreamEventPayload {
|
||||
error?: StreamErrorPayload
|
||||
}
|
||||
|
||||
type ModelType = 'worker' | 'strategist'
|
||||
|
||||
interface ConversationGroup {
|
||||
key: string
|
||||
@@ -86,7 +85,7 @@ const conversationLoadingMore = ref(false)
|
||||
const chatLoading = ref(false)
|
||||
const historyExpanded = ref(true)
|
||||
const selectedConversationId = ref('')
|
||||
const selectedModel = ref<ModelType>('worker')
|
||||
|
||||
const selectedThinkingMode = ref<ThinkingModeType>('auto')
|
||||
const messageInput = ref('')
|
||||
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 = '请基于这些任务类帮我做一版智能编排。'
|
||||
|
||||
let messageScrollRaf = 0
|
||||
@@ -336,85 +335,6 @@ const contextStatsDisabled = computed(() => {
|
||||
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) {
|
||||
if (!conversationMessagesMap[conversationId]) {
|
||||
@@ -476,16 +396,6 @@ function migrateConversationState(fromConversationId: string, toConversationId:
|
||||
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 deduplicated: ConversationListItem[] = []
|
||||
const seen = new Set<string>()
|
||||
@@ -1299,7 +1209,6 @@ async function loadConversationContextStats(conversationId: string, forceReload
|
||||
async function selectConversation(conversationId: string) {
|
||||
cancelEditUserMessage()
|
||||
selectedConversationId.value = conversationId
|
||||
applyPreferredModelForConversation(conversationId)
|
||||
await Promise.allSettled([
|
||||
loadConversationMessages(conversationId),
|
||||
ensureConversationMeta(conversationId),
|
||||
@@ -1502,7 +1411,7 @@ async function streamAssistantReply(
|
||||
const response = await fetchChatStream({
|
||||
conversation_id: isDraftConversationId(draftConversationId) ? undefined : draftConversationId,
|
||||
message: text,
|
||||
model: selectedModel.value,
|
||||
model: 'worker',
|
||||
thinking: selectedThinkingMode.value,
|
||||
extra: requestExtra,
|
||||
})
|
||||
@@ -1577,8 +1486,6 @@ async function sendMessage(preset?: string) {
|
||||
if (!selectedConversationId.value || shouldStartFreshPlanningConversation) {
|
||||
selectedConversationId.value = draftConversationId
|
||||
}
|
||||
savePreferredModel(draftConversationId, selectedModel.value)
|
||||
|
||||
ensureConversationBucket(draftConversationId)
|
||||
unavailableHistoryMap[draftConversationId] = false
|
||||
|
||||
@@ -1734,16 +1641,6 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
selectedModel,
|
||||
(nextModel) => {
|
||||
const conversationId = selectedConversationId.value
|
||||
if (!conversationId) {
|
||||
return
|
||||
}
|
||||
savePreferredModel(conversationId, nextModel)
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
reasoningTicker = window.setInterval(() => {
|
||||
@@ -2126,20 +2023,6 @@ onBeforeUnmount(() => {
|
||||
</el-select>
|
||||
</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
|
||||
class="assistant-toolbar__context-meter"
|
||||
@@ -3183,7 +3066,6 @@ onBeforeUnmount(() => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-toolbar__pill--ds-model,
|
||||
.assistant-toolbar__pill--ds-thinking {
|
||||
height: 32px;
|
||||
padding: 0 8px 0 10px;
|
||||
@@ -3200,10 +3082,6 @@ onBeforeUnmount(() => {
|
||||
min-width: 138px;
|
||||
}
|
||||
|
||||
.assistant-toolbar__pill--ds-model {
|
||||
min-width: 144px;
|
||||
}
|
||||
|
||||
.assistant-toolbar__context-meter {
|
||||
width: 144px;
|
||||
min-width: 144px;
|
||||
@@ -3435,30 +3313,5 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user