Version: 0.9.45.dev.260427
后端: 1. execute 主链路重构为“上下文工具域 + 主动优化候选闭环”——移除 order_guard,粗排后默认进入主动微调,先诊断再从后端候选中选择 move/swap,避免 LLM 自由全局乱搜 2. 工具体系升级为动态注入协议——新增 context_tools_add / remove、工具域与二级包映射、主动优化白名单;schedule / taskclass / web 工具按域按包暴露,msg0 规则包与 execute 上下文同步重写 3. analyze_health 升级为主动优化唯一裁判入口——补齐 rhythm / tightness / profile / feasibility 指标、候选扫描与复诊打分、停滞信号、forced imperfection 判定,并把连续优化状态写回运行态 4. 任务类能力并入新 Agent 执行链——新增 upsert_task_class 写工具与启动注入事务写入;任务类模型补充学科画像与整天屏蔽配置,粗排支持 excluded_days_of_week,steady 策略改为基于目标位置/单日负载/分散度/缓冲的候选打分 5. 运行态与路由补齐优化模式语义——新增 active tool domain/packs、pending context hook、active optimize only、taskclass 写入回盘快照;区分 first_full / global_reopt / local_adjust,并完善首次粗排后默认 refine 的判定 前端: 6. 助手时间线渲染细化——推理内容改为独立 reasoning block,支持与工具/状态/正文按时序交错展示,自动收口折叠,修正 confirm reject 恢复动作 仓库: 7. newAgent 文档整体迁入 docs/backend,补充主动优化执行规划与顺序约束拆解文档,删除旧调试日志文件 PS:这次科研了2天,总算是有些进展了——LLM永远只适合做选择题、判断题,不适合做开放创新题。
This commit is contained in:
@@ -153,6 +153,10 @@ interface DisplayAssistantBlock {
|
||||
event?: ToolTraceEvent
|
||||
statusEvent?: StatusTraceEvent
|
||||
schedulePreview?: SchedulePreviewData
|
||||
/** 所属的源消息 ID,用于状态查询 */
|
||||
sourceId?: string
|
||||
/** 所属的源消息引用,用于渲染辅助信息 */
|
||||
source?: AssistantMessage
|
||||
}
|
||||
|
||||
interface AssistantContentBlock {
|
||||
@@ -223,6 +227,7 @@ const statusTraceEventsMap = reactive<Record<string, StatusTraceEvent[]>>({})
|
||||
const toolTraceExpandedMap = reactive<Record<string, boolean>>({})
|
||||
const assistantReasoningSeqMap = reactive<Record<string, number>>({})
|
||||
const assistantContentBlocksMap = reactive<Record<string, AssistantContentBlock[]>>({})
|
||||
const assistantReasoningBlocksMap = reactive<Record<string, AssistantContentBlock[]>>({})
|
||||
const assistantTimelineLastKindMap = reactive<Record<string, 'content' | 'tool' | 'status' | 'reasoning' | 'other'>>({})
|
||||
const conversationContextStatsMap = reactive<Record<string, ConversationContextStats | null>>({})
|
||||
const conversationContextStatsLoadingMap = reactive<Record<string, boolean>>({})
|
||||
@@ -502,6 +507,11 @@ function appendToolTraceEvent(
|
||||
const eventSeq = nextAssistantTimelineSeq()
|
||||
const eventId = `${messageId}:tool:${eventSeq}`
|
||||
|
||||
// 如果上一个阶段是推理,则结束并折叠它
|
||||
if (assistantTimelineLastKindMap[messageId] === 'reasoning') {
|
||||
finishCurrentReasoningBlock(messageId)
|
||||
}
|
||||
|
||||
toolTraceEventsMap[messageId].push({
|
||||
id: eventId,
|
||||
seq: eventSeq,
|
||||
@@ -536,6 +546,11 @@ function appendStatusTraceEvent(
|
||||
}
|
||||
|
||||
const eventSeq = nextAssistantTimelineSeq()
|
||||
// 如果上一个阶段是推理,则结束并折叠它
|
||||
if (assistantTimelineLastKindMap[messageId] === 'reasoning') {
|
||||
finishCurrentReasoningBlock(messageId)
|
||||
}
|
||||
|
||||
statusEvents.push({
|
||||
id: `${messageId}:status:${eventSeq}`,
|
||||
seq: eventSeq,
|
||||
@@ -554,6 +569,11 @@ function appendAssistantContentChunk(messageId: string, chunk: string) {
|
||||
const blocks = assistantContentBlocksMap[messageId]
|
||||
const lastKind = assistantTimelineLastKindMap[messageId]
|
||||
|
||||
// 如果是从推理切换到正文,则结束并折叠推理块
|
||||
if (lastKind === 'reasoning') {
|
||||
finishCurrentReasoningBlock(messageId)
|
||||
}
|
||||
|
||||
if (lastKind === 'content' && blocks.length > 0) {
|
||||
blocks[blocks.length - 1]!.text += chunk
|
||||
return
|
||||
@@ -568,6 +588,41 @@ function appendAssistantContentChunk(messageId: string, chunk: string) {
|
||||
assistantTimelineLastKindMap[messageId] = 'content'
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加助理推理片段到特定消息的块映射中
|
||||
* 1. 采用与正文相同的块化存储逻辑,确保推理片段能按 sequence 与工具等交错排序
|
||||
* 2. 如果当前时间线最后一种类型就是 'reasoning',则追加到最后一个块,避免碎片化
|
||||
*/
|
||||
function appendAssistantReasoningChunk(messageId: string, chunk: string) {
|
||||
if (!chunk) {
|
||||
return
|
||||
}
|
||||
if (!assistantReasoningBlocksMap[messageId]) {
|
||||
assistantReasoningBlocksMap[messageId] = []
|
||||
}
|
||||
const blocks = assistantReasoningBlocksMap[messageId]
|
||||
const lastKind = assistantTimelineLastKindMap[messageId]
|
||||
|
||||
if (lastKind === 'reasoning' && blocks.length > 0) {
|
||||
blocks[blocks.length - 1]!.text += chunk
|
||||
return
|
||||
}
|
||||
|
||||
const seq = nextAssistantTimelineSeq()
|
||||
const blockId = `${messageId}:reasoning:${seq}`
|
||||
blocks.push({
|
||||
id: blockId,
|
||||
seq,
|
||||
text: chunk,
|
||||
})
|
||||
|
||||
// 记录块级别的起始时间和初始折叠状态
|
||||
reasoningStartedAtMap[blockId] = Date.now()
|
||||
reasoningCollapsedMap[blockId] = false
|
||||
|
||||
assistantTimelineLastKindMap[messageId] = 'reasoning'
|
||||
}
|
||||
|
||||
function mapToolEventState(rawStatus?: string): ToolTraceState {
|
||||
const normalized = `${rawStatus || ''}`.trim().toLowerCase()
|
||||
if (normalized === 'start' || normalized === 'calling' || normalized === 'called') {
|
||||
@@ -993,22 +1048,21 @@ function markReasoningStart(message: AssistantMessage) {
|
||||
reasoningStartedAtMap[message.id] = Date.now()
|
||||
}
|
||||
|
||||
function markReasoningFinished(message: AssistantMessage) {
|
||||
const startedAt = reasoningStartedAtMap[message.id]
|
||||
if (startedAt && !reasoningDurationMap[message.id]) {
|
||||
reasoningDurationMap[message.id] = Math.max(1, Math.round((Date.now() - startedAt) / 1000))
|
||||
function markReasoningFinished(blockId: string, messageId: string) {
|
||||
const startedAt = reasoningStartedAtMap[blockId]
|
||||
if (startedAt && !reasoningDurationMap[blockId]) {
|
||||
reasoningDurationMap[blockId] = Math.max(1, Math.round((Date.now() - startedAt) / 1000))
|
||||
}
|
||||
|
||||
thinkingMessageMap[message.id] = false
|
||||
thinkingMessageMap[messageId] = false
|
||||
}
|
||||
|
||||
function getReasoningDurationSeconds(message: AssistantMessage) {
|
||||
const fixedDuration = reasoningDurationMap[message.id]
|
||||
function getReasoningDurationSeconds(blockId: string) {
|
||||
const fixedDuration = reasoningDurationMap[blockId]
|
||||
if (fixedDuration) {
|
||||
return fixedDuration
|
||||
}
|
||||
|
||||
const startedAt = reasoningStartedAtMap[message.id]
|
||||
const startedAt = reasoningStartedAtMap[blockId]
|
||||
if (!startedAt) {
|
||||
return 0
|
||||
}
|
||||
@@ -1016,13 +1070,28 @@ function getReasoningDurationSeconds(message: AssistantMessage) {
|
||||
return Math.max(1, Math.round((reasoningDisplayNow.value - startedAt) / 1000))
|
||||
}
|
||||
|
||||
function getReasoningStatusLabel(message: AssistantMessage) {
|
||||
const durationSeconds = getReasoningDurationSeconds(message)
|
||||
function getReasoningStatusLabel(block: DisplayAssistantBlock) {
|
||||
const durationSeconds = getReasoningDurationSeconds(block.id)
|
||||
if (durationSeconds > 0) {
|
||||
return `已思考(用时 ${durationSeconds} 秒)`
|
||||
}
|
||||
|
||||
return isStreamingMessage(message) && isThinkingMessage(message) ? '思考中' : '已思考'
|
||||
const isThinking = block.sourceId === activeStreamingMessageId.value && thinkingMessageMap[block.sourceId]
|
||||
return isThinking ? '思考中' : '已思考'
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前消息正在进行的推理块
|
||||
* 1. 计算耗时
|
||||
* 2. 自动折叠
|
||||
*/
|
||||
function finishCurrentReasoningBlock(messageId: string) {
|
||||
const blocks = assistantReasoningBlocksMap[messageId] || []
|
||||
if (blocks.length === 0) return
|
||||
const lastBlock = blocks[blocks.length - 1]
|
||||
|
||||
markReasoningFinished(lastBlock.id, messageId)
|
||||
reasoningCollapsedMap[lastBlock.id] = true
|
||||
}
|
||||
|
||||
function isReasoningCollapsed(messageId: string) {
|
||||
@@ -1086,6 +1155,8 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
type: 'tool',
|
||||
seq: event.seq,
|
||||
event,
|
||||
sourceId: source.id,
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1096,6 +1167,32 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
type: 'status',
|
||||
seq: statusEvent.seq,
|
||||
statusEvent,
|
||||
sourceId: source.id,
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
// 从推理块映射中提取所有独立的推理片段
|
||||
const reasoningBlocks = assistantReasoningBlocksMap[source.id] || []
|
||||
if (reasoningBlocks.length > 0) {
|
||||
for (const rb of reasoningBlocks) {
|
||||
blocks.push({
|
||||
id: rb.id,
|
||||
type: 'reasoning',
|
||||
seq: rb.seq,
|
||||
text: rb.text,
|
||||
sourceId: source.id,
|
||||
source,
|
||||
})
|
||||
}
|
||||
} else if (source.id === activeStreamingMessageId.value && thinkingMessageMap[source.id]) {
|
||||
// 流式过程中尚未有实质文本产出时的“思考中”占位块
|
||||
blocks.push({
|
||||
id: `${source.id}:reasoning:streaming`,
|
||||
type: 'reasoning',
|
||||
seq: assistantReasoningSeqMap[source.id] || 10,
|
||||
sourceId: source.id,
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1108,6 +1205,8 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
type: 'content',
|
||||
seq: contentBlock.seq,
|
||||
text: contentBlock.text,
|
||||
sourceId: source.id,
|
||||
source,
|
||||
})
|
||||
}
|
||||
continue
|
||||
@@ -1121,6 +1220,8 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
type: 'content',
|
||||
seq: fallbackSeq,
|
||||
text: source.content,
|
||||
sourceId: source.id,
|
||||
source,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1135,16 +1236,6 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldShowDisplayReasoningBox(dm)) {
|
||||
const reasoningSeq = getDisplayReasoningSeq(dm)
|
||||
blocks.push({
|
||||
id: `${dm.id}:reasoning`,
|
||||
type: 'reasoning',
|
||||
seq: reasoningSeq > 0 ? reasoningSeq : 10,
|
||||
text: dm.reasoning,
|
||||
})
|
||||
}
|
||||
|
||||
if (!hasContentBlock && dm.content) {
|
||||
fallbackSeq += 1
|
||||
blocks.push({
|
||||
@@ -1180,38 +1271,16 @@ function getToolTraceStateLabel(state: ToolTraceState): string {
|
||||
return '已完成'
|
||||
}
|
||||
|
||||
function shouldShowDisplayReasoningBox(dm: DisplayMessage): boolean {
|
||||
if (dm.role !== 'assistant') return false
|
||||
return dm.sources.some(m =>
|
||||
Boolean(m.reasoning?.trim()) ||
|
||||
(m.id === activeStreamingMessageId.value && thinkingMessageMap[m.id] === true),
|
||||
)
|
||||
}
|
||||
|
||||
function shouldShowDisplayAnsweringIndicator(dm: DisplayMessage): boolean {
|
||||
return isDisplayStreaming(dm) &&
|
||||
dm.sources.every(m => thinkingMessageMap[m.id] !== true) &&
|
||||
!dm.content.trim()
|
||||
}
|
||||
|
||||
function isDisplayReasoningCollapsed(dm: DisplayMessage): boolean {
|
||||
return dm.sources.every(m => reasoningCollapsedMap[m.id] === true)
|
||||
}
|
||||
|
||||
function toggleDisplayReasoningCollapse(dm: DisplayMessage): void {
|
||||
const newCollapsed = !isDisplayReasoningCollapsed(dm)
|
||||
dm.sources.forEach(m => { reasoningCollapsedMap[m.id] = newCollapsed })
|
||||
}
|
||||
|
||||
function getDisplayReasoningStatusLabel(dm: DisplayMessage): string {
|
||||
const totalSeconds = dm.sources.reduce(
|
||||
(sum, m) => sum + (reasoningDurationMap[m.id] ?? 0), 0,
|
||||
)
|
||||
if (totalSeconds > 0) return `已思考(用时 ${totalSeconds} 秒)`
|
||||
const hasActiveThinking = dm.sources.some(
|
||||
m => m.id === activeStreamingMessageId.value && thinkingMessageMap[m.id] === true,
|
||||
)
|
||||
return hasActiveThinking ? '思考中' : '已思考'
|
||||
// 此函数已废弃,推理状态现已下沉到各 source 块处理。
|
||||
// 仅保留空实现以防意外调用。
|
||||
return '已思考'
|
||||
}
|
||||
|
||||
function isMessageViewportAtBottom(viewport: HTMLElement) {
|
||||
@@ -1576,7 +1645,8 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
|
||||
|
||||
if (reasoningChunk) {
|
||||
currentAssistantMessage.reasoning = oldReasoning + reasoningChunk
|
||||
// 记录推理块的 seq 环境
|
||||
// 时序化存储推理内容
|
||||
appendAssistantReasoningChunk(mid, reasoningChunk)
|
||||
if (!assistantReasoningSeqMap[mid]) {
|
||||
assistantReasoningSeqMap[mid] = event.seq
|
||||
}
|
||||
@@ -1867,8 +1937,8 @@ async function submitConfirmRejectMessage() {
|
||||
requestExtra: {
|
||||
resume: {
|
||||
interaction_id: interactionId,
|
||||
type: 'ask_user',
|
||||
action: 'reply'
|
||||
type: 'confirm',
|
||||
action: 'reject'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2107,10 +2177,9 @@ function processSseBlock(block: string, assistantMessage: AssistantMessage) {
|
||||
|
||||
if (payload === '[DONE]') {
|
||||
if (isThinkingMessage(assistantMessage)) {
|
||||
markReasoningFinished(assistantMessage)
|
||||
finishCurrentReasoningBlock(assistantMessage.id)
|
||||
}
|
||||
activeStreamingMessageId.value = ''
|
||||
reasoningCollapsedMap[assistantMessage.id] = true
|
||||
// 整个 SSE 流结束信号
|
||||
void loadConversationContextStats(selectedConversationId.value, true)
|
||||
return
|
||||
@@ -2150,27 +2219,23 @@ function processSseBlock(block: string, assistantMessage: AssistantMessage) {
|
||||
if (!assistantReasoningSeqMap[assistantMessage.id]) {
|
||||
assistantReasoningSeqMap[assistantMessage.id] = nextAssistantTimelineSeq()
|
||||
}
|
||||
assistantTimelineLastKindMap[assistantMessage.id] = 'reasoning'
|
||||
appendAssistantReasoningChunk(assistantMessage.id, delta.reasoning_content)
|
||||
assistantMessage.reasoning = `${assistantMessage.reasoning || ''}${delta.reasoning_content}`
|
||||
}
|
||||
|
||||
if (!shouldSuppressVisibleDelta && typeof delta?.content === 'string' && delta.content) {
|
||||
appendAssistantContentChunk(assistantMessage.id, delta.content)
|
||||
if (isThinkingMessage(assistantMessage)) {
|
||||
// 1. 一旦正文开始回流,立刻结束“思考中”阶段,避免两个等待动画同时出现。
|
||||
// 2. 这样视觉上始终保持“先思考,再输出正文”的单阶段感知。
|
||||
// 3. 若后端偶发交错发送 reasoning/content,也以前端阶段机兜底,优先保证阅读一致性。
|
||||
markReasoningFinished(assistantMessage)
|
||||
finishCurrentReasoningBlock(assistantMessage.id)
|
||||
}
|
||||
assistantMessage.content += delta.content
|
||||
}
|
||||
|
||||
if (finishReason) {
|
||||
if (isThinkingMessage(assistantMessage)) {
|
||||
markReasoningFinished(assistantMessage)
|
||||
finishCurrentReasoningBlock(assistantMessage.id)
|
||||
}
|
||||
activeStreamingMessageId.value = ''
|
||||
reasoningCollapsedMap[assistantMessage.id] = true
|
||||
// 单条消息结束标志
|
||||
void loadConversationContextStats(selectedConversationId.value, true)
|
||||
}
|
||||
@@ -2681,18 +2746,18 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="chat-message__reasoning-status">{{ getDisplayReasoningStatusLabel(dm) }}</span>
|
||||
<span class="chat-message__reasoning-status">{{ getReasoningStatusLabel(block) }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="chat-message__reasoning-toggle"
|
||||
:aria-label="isDisplayReasoningCollapsed(dm) ? '展开深度思考' : '折叠深度思考'"
|
||||
@click="toggleDisplayReasoningCollapse(dm)"
|
||||
:aria-label="isReasoningCollapsed(block.id) ? '展开深度思考' : '折叠深度思考'"
|
||||
@click="toggleReasoningCollapse(block.id)"
|
||||
>
|
||||
<span class="chat-message__reasoning-chevron">
|
||||
<svg
|
||||
class="chat-message__reasoning-chevron-icon"
|
||||
:class="{ 'chat-message__reasoning-chevron-icon--expanded': !isDisplayReasoningCollapsed(dm) }"
|
||||
:class="{ 'chat-message__reasoning-chevron-icon--expanded': !isReasoningCollapsed(block.id) }"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
@@ -2709,11 +2774,11 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!isDisplayReasoningCollapsed(dm)" class="chat-message__reasoning-body">
|
||||
<div v-if="isReasoningCollapsed(block.id) === false" class="chat-message__reasoning-body">
|
||||
<div
|
||||
v-if="block.text"
|
||||
class="chat-message__markdown chat-message__markdown--reasoning"
|
||||
v-html="renderMessageMarkdown(block.text)"
|
||||
v-html="renderMessageMarkdown(block.text || '')"
|
||||
/>
|
||||
<div v-else class="chat-message__streaming chat-message__streaming--reasoning">
|
||||
<div class="thinking-indicator">
|
||||
|
||||
Reference in New Issue
Block a user