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

@@ -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 {

View File

@@ -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>