Version: 0.9.46.dev.260427

后端:
1. taskclass 执行闭环继续收紧——Plan / Execute 全面切到“最小工具闭环”视角,明确学习目标/总节数/禁排时段/排除星期默认停留 taskclass 域;未给日期范围时禁止擅自补 start_date/end_date,upsert_task_class 重试前先做写前检查并区分“内部表示修正”与“必须追问用户”的关键时间事实
2. QuickTask / TaskQuery 轻量链路继续收敛——新增 model/taskquery_contract.go 统一查询协议,QuickTaskDeps / start.go 改用 model 层参数;删除 query_tasks / quick_note_create 旧工具实现,避免任务查询与随口记再回流 execute 工具链
3. schedule 微调工具继续瘦身——下线 spread_even / min_context_switch 及其复合规划逻辑,清理 analyze_load / analyze_subjects / analyze_context / analyze_tolerance 等历史能力;execute 顺序策略收敛为局部 move / swap,提示词与工具目录仅暴露当前真实可用工具
4. 执行与时间线体验补齐——execute 为流式 speak 补发归一化尾部,避免 deliver 文案黏连;前端时间线新增 interrupt / status 协议识别、工具事件归并与状态过滤,减少 ToolTrace 重复和会话重建误判
前端:
5. AssistantPanel 适配新版 timeline extra 事件——schedule_agent.ts 补齐 interrupt / status kind,工具调用与结果按摘要/参数/工具名合并,恢复历史时不再把协议事件误判成用户消息
This commit is contained in:
LoveLosita
2026-04-27 12:20:17 +08:00
parent 66c06eed0a
commit 736ba0cff3
25 changed files with 425 additions and 2173 deletions

View File

@@ -19,7 +19,15 @@ export interface TimelineConfirmPayload {
export interface TimelineEvent {
id: number
seq: number
kind: 'user_text' | 'assistant_text' | 'tool_call' | 'tool_result' | 'confirm_request' | 'schedule_completed'
kind:
| 'user_text'
| 'assistant_text'
| 'tool_call'
| 'tool_result'
| 'confirm_request'
| 'schedule_completed'
| 'interrupt'
| 'status'
role?: 'user' | 'assistant'
content?: string
payload?: {

View File

@@ -504,6 +504,22 @@ function appendToolTraceEvent(
}
ensureToolTraceBucket(messageId)
const normalizedDetail = detail.trim()
const normalizedToolName = toolName.trim()
const matchedPendingEvent = findMergeableToolTraceEvent(
messageId,
state,
normalizedSummary,
normalizedDetail,
normalizedToolName,
)
if (matchedPendingEvent) {
matchedPendingEvent.state = state
matchedPendingEvent.summary = normalizedSummary
matchedPendingEvent.detail = normalizedDetail || matchedPendingEvent.detail
matchedPendingEvent.toolName = normalizedToolName || matchedPendingEvent.toolName
return
}
const eventSeq = nextAssistantTimelineSeq()
const eventId = `${messageId}:tool:${eventSeq}`
@@ -517,12 +533,84 @@ function appendToolTraceEvent(
seq: eventSeq,
state,
summary: normalizedSummary,
detail: detail.trim() || undefined,
toolName: toolName.trim() || undefined,
detail: normalizedDetail || undefined,
toolName: normalizedToolName || undefined,
})
assistantTimelineLastKindMap[messageId] = 'tool'
}
function isPendingToolTraceState(state: ToolTraceState) {
return state === 'called'
}
function findMergeableToolTraceEvent(
messageId: string,
nextState: ToolTraceState,
summary: string,
detail: string,
toolName: string,
): ToolTraceEvent | null {
if (nextState === 'called') {
return null
}
const pendingEvents = (toolTraceEventsMap[messageId] || [])
.slice()
.reverse()
.filter((event) => isPendingToolTraceState(event.state))
if (pendingEvents.length <= 0) {
return null
}
const normalizedToolName = toolName.trim().toLowerCase()
const normalizedDetail = detail.trim()
const normalizedSummary = summary.trim()
if (normalizedToolName && normalizedDetail) {
const exactMatch = pendingEvents.find((event) => {
return (
`${event.toolName || ''}`.trim().toLowerCase() === normalizedToolName &&
`${event.detail || ''}`.trim() === normalizedDetail
)
})
if (exactMatch) {
return exactMatch
}
}
if (normalizedToolName) {
const toolNameMatch = pendingEvents.find((event) => {
return `${event.toolName || ''}`.trim().toLowerCase() === normalizedToolName
})
if (toolNameMatch) {
return toolNameMatch
}
}
if (normalizedDetail) {
const detailMatch = pendingEvents.find((event) => {
return `${event.detail || ''}`.trim() === normalizedDetail
})
if (detailMatch) {
return detailMatch
}
}
if (normalizedSummary) {
const summaryMatch = pendingEvents.find((event) => event.summary === normalizedSummary)
if (summaryMatch) {
return summaryMatch
}
}
if (pendingEvents.length === 1) {
return pendingEvents[0]
}
return null
}
function appendStatusTraceEvent(
messageId: string,
code: string,
@@ -725,9 +813,46 @@ function shouldSkipStatusEvent(code: string, stage = '') {
if (stage === 'confirm' && (code === 'plan_confirm' || code === 'tool_confirm' || code === 'confirm')) {
return true
}
const hiddenStatusCodes = new Set([
'accepted',
'ask_user',
'planning',
'resumed',
'confirmed',
'rejected',
'executing',
'summarizing',
'done',
'rough_building',
'order_guard_initialized',
'order_guard_passed',
'order_guard_restored',
'order_guard_restore_skipped',
'context_compact_start',
'context_compact_done',
'plan_auto_confirmed',
])
if (hiddenStatusCodes.has(code)) {
return true
}
return false
}
function isAssistantTimelineKind(kind: string) {
const assistantKinds = new Set([
'assistant_text',
'tool_call',
'tool_result',
'confirm_request',
'schedule_completed',
'interrupt',
'status',
])
return assistantKinds.has(kind)
}
function isToolTraceExpanded(eventId: string) {
return toolTraceExpandedMap[eventId] === true
}
@@ -1582,12 +1707,12 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
const kind = String(event.kind || '').toLowerCase()
const rawRole = String(event.role || '').toLowerCase()
// 如果 role 已明确为 user或者 kind 包含 user 关键字
// 1. timeline 重建时先识别显式 user 事件,避免把真正的用户输入吞进 assistant 回合。
// 2. interrupt / status 这类 assistant 侧协议事件不能再掉进 user 兜底,否则会把 ask_user 正文切断。
// 3. 这里仍保留 kind.includes('user') 的保守判断,只是把 assistant 白名单补齐到本轮真实协议。
let isUser = rawRole === 'user' || kind.includes('user')
// 终极兜底:只要不是明确的五大助手专属事件,就将其视为用户的消息回合边界
if (!isUser) {
const knownAssistantKinds = ['assistant_text', 'tool_call', 'tool_result', 'confirm_request', 'schedule_completed']
if (!knownAssistantKinds.includes(kind)) {
if (!isAssistantTimelineKind(kind)) {
isUser = true
}
}
@@ -1620,6 +1745,7 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
switch (event.kind) {
case 'assistant_text':
case 'interrupt':
if (event.content) {
const newContent = event.content
const oldContent = currentAssistantMessage.content || ''
@@ -1657,14 +1783,14 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
case 'tool_call':
if (event.payload?.tool) {
const t = event.payload.tool
appendToolTraceEvent(mid, mapToolEventState(t.status), t.summary, t.arguments_preview, t.name)
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name)
}
break
case 'tool_result':
if (event.payload?.tool) {
const t = event.payload.tool
appendToolTraceEvent(mid, mapToolEventState(t.status), t.summary, t.arguments_preview, t.name)
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name)
}
break