Version: 0.9.59.dev.260430

后端:
1. 主动调度预览确认主链路落地——新增主动调度数据模型、DAO 与事件契约;接入 dry-run pipeline 与任务触发的 job upsert/cancel;新增 preview 查询与 confirm API,支持 apply_id 幂等确认并同步写入 task_pool 日程
2. 同步更新主动调度实施文档的阶段状态与验收记录

前端:
3. AssistantPanel 脚本层继续解耦——私有类型迁移到独立类型文件,并抽离会话、工具轨迹、思考摘要、任务表单等纯函数辅助逻辑;保持助手面板模板与样式不变,降低表现层回归风险
This commit is contained in:
LoveLosita
2026-04-30 12:05:15 +08:00
parent 1555042e80
commit e945578fbf
38 changed files with 10267 additions and 580 deletions

View File

@@ -36,12 +36,36 @@ import type {
ConversationMeta,
ThinkingModeType,
SchedulePreviewData,
ThinkingSummaryPayload,
} from '@/types/dashboard'
import ScheduleResultCard from '@/components/assistant/ScheduleResultCard.vue'
import ScheduleFineTuneModal from '@/components/assistant/ScheduleFineTuneModal.vue'
import { formatConversationTime, formatMessageTime } from '@/utils/date'
import { renderMarkdown } from '@/utils/markdown'
import {
buildAssistantChatRequestExtra,
buildConversationPreviewItem,
buildThinkingSummarySignature,
buildStatusSummary,
buildTaskUpdatePayload,
buildToolDetail,
createDraftConversationId,
createMessageId,
formatTaskDeadlineForStatus,
getThinkingBackendKey,
isAssistantTimelineKind,
isDraftConversationId,
isLocalEphemeralMessageId,
isManualThinkingEnabled,
isLegacyToolStatusCode,
mapLegacyToolStatusToState,
mapToolEventState,
mergeConversationListItems,
migrateConversationListIds,
normalizeStatusCode,
normalizeToolSummary,
resolveConversationGroupLabel,
shouldSkipStatusEvent,
} from '@/utils/assistantPanelTrace'
import BusinessCardRenderer from '@/components/assistant/cards/BusinessCardRenderer.vue'
import ToolCardRenderer from '@/components/dashboard/ToolCardRenderer.vue'
import type {
@@ -49,150 +73,24 @@ import type {
TaskQueryCardData,
TaskRecordCardData
} from '@/api/schedule_agent'
interface StreamDeltaPayload {
content?: string
}
interface StreamChoicePayload {
delta?: StreamDeltaPayload
finish_reason?: string | null
}
interface StreamErrorPayload {
message?: string
}
interface StreamConfirmPayload {
interaction_id?: string
title?: string
summary?: string
}
interface StreamStatusExtraPayload {
code?: string
summary?: string
}
interface StreamToolExtraPayload {
name?: string
status?: string
summary?: string
arguments_preview?: string
argument_view?: ToolView
result_view?: ToolView
}
interface StreamExtraPayload {
kind?: string
block_id?: string
stage?: string
status?: StreamStatusExtraPayload
tool?: StreamToolExtraPayload
confirm?: StreamConfirmPayload
business_card?: TimelineBusinessCardPayload
thinking_summary?: ThinkingSummaryPayload
}
interface StreamEventPayload {
choices?: StreamChoicePayload[]
delta?: StreamDeltaPayload
content?: string
finish_reason?: string | null
error?: StreamErrorPayload
extra?: StreamExtraPayload
}
type ToolTraceState = 'called' | 'completed' | 'create' | 'blocked'
interface ToolTraceEvent {
id: string
seq: number
state: ToolTraceState
summary: string
detail?: string
toolName?: string
argumentView?: ToolView
resultView?: ToolView
}
interface StatusTraceEvent {
id: string
seq: number
code: string
stage: string
summary: string
}
interface ConversationGroup {
key: string
label: string
items: ConversationListItem[]
}
interface ConfirmOverlayState {
visible: boolean
manuallyClosed: boolean
interactionId: string
title: string
summary: string
}
interface ConversationListItemRevealOptions {
animate?: boolean
}
interface EnsureConversationMetaOptions {
forceReload?: boolean
syncListItem?: boolean
listItemReveal?: ConversationListItemRevealOptions
}
// 展示用消息:合并连续 assistant 消息后的视图模型
interface DisplayMessage {
/** 第一条源消息的 id用作 Vue key */
id: string
role: 'user' | 'assistant' | 'system'
/** 合并后的正文内容 */
content: string
/** 最后一条源消息的时间 */
createdAt: string
/** 合并后的推理内容 */
reasoning?: string
/** 原始消息引用列表 */
sources: AssistantMessage[]
/** 是否为多条合并 */
merged: boolean
}
interface DisplayAssistantBlock {
id: string
type: 'tool' | 'status' | 'reasoning' | 'content' | 'content_indicator' | 'schedule_card' | 'business_card'
seq: number
text?: string
event?: ToolTraceEvent
statusEvent?: StatusTraceEvent
schedulePreview?: SchedulePreviewData
businessCard?: TimelineBusinessCardPayload
/** 所属的源消息 ID用于状态查询 */
sourceId?: string
/** 所属的源消息引用,用于渲染辅助信息 */
source?: AssistantMessage
}
interface AssistantContentBlock {
id: string
seq: number
text: string
}
interface ThinkingSummaryBlockState {
blockId: string
lastSummarySeq: number
lastSignature: string
finished: boolean
}
import type {
ApplyThinkingSummaryOptions,
AssistantContentBlock,
ConfirmOverlayState,
ConversationGroup,
ConversationListItemRevealOptions,
DisplayAssistantBlock,
DisplayMessage,
EnsureConversationMetaOptions,
StatusTraceEvent,
StreamConfirmPayload,
StreamEventPayload,
StreamExtraPayload,
StreamToolExtraPayload,
ThinkingSummaryBlockState,
ToolTraceEvent,
ToolTraceState,
} from '@/types/assistant-panel'
const props = withDefaults(
defineProps<{
@@ -589,34 +487,6 @@ const displayMessages = computed<DisplayMessage[]>(() => {
return result
})
function resolveConversationGroupLabel(timeText?: string | null) {
if (!timeText) {
return '更早'
}
const messageDate = new Date(timeText)
if (Number.isNaN(messageDate.getTime())) {
return '更早'
}
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const targetDay = new Date(messageDate.getFullYear(), messageDate.getMonth(), messageDate.getDate())
const diffDays = Math.floor((today.getTime() - targetDay.getTime()) / (24 * 60 * 60 * 1000))
if (diffDays <= 0) {
return '今天'
}
if (diffDays < 7) {
return '7 天内'
}
if (diffDays < 30) {
return '30 天内'
}
return `${messageDate.getFullYear()}-${String(messageDate.getMonth() + 1).padStart(2, '0')}`
}
const groupedConversationList = computed<ConversationGroup[]>(() => {
const orderedGroups: ConversationGroup[] = []
const groupMap = new Map<string, ConversationGroup>()
@@ -1080,31 +950,6 @@ function enqueueThinkingDetail(messageId: string, blockId: string, detail: strin
startThinkingStreamTicker(messageId, blockId)
}
interface ApplyThinkingSummaryOptions {
backendBlockId?: string
stage?: string
summary: ThinkingSummaryPayload
/** 历史回放时使用 timeline 事件自身顺序;实时流未传时按真实到达顺序分配 */
eventSeq?: number
/** true 表示来自 timeline 历史恢复:不写短摘要、不更新思考态、长摘要一次性写入 */
fromHistory?: boolean
}
function getThinkingBackendKey(opts: Pick<ApplyThinkingSummaryOptions, 'backendBlockId' | 'stage'>) {
const backendKeyRaw = (opts.backendBlockId || opts.stage || 'thinking').trim()
return backendKeyRaw || 'thinking'
}
function buildThinkingSummarySignature(summary: ThinkingSummaryPayload) {
return [
typeof summary.summary_seq === 'number' ? summary.summary_seq : '',
(summary.short_summary || '').trim(),
(summary.detail_summary || '').trim(),
typeof summary.duration_seconds === 'number' ? summary.duration_seconds : '',
summary.final === true ? '1' : '0',
].join('\u001f')
}
function ensureThinkingSummaryBlockBucket(messageId: string) {
if (!thinkingSummaryBlockStateMap[messageId]) {
thinkingSummaryBlockStateMap[messageId] = {}
@@ -1261,150 +1106,6 @@ function appendBusinessCardEvent(messageId: string, payload: TimelineBusinessCar
assistantTimelineLastKindMap[messageId] = 'business_card'
}
function mapToolEventState(rawStatus?: string): ToolTraceState {
const normalized = `${rawStatus || ''}`.trim().toLowerCase()
if (normalized === 'start' || normalized === 'calling' || normalized === 'called') {
return 'called'
}
if (normalized === 'create' || normalized === 'created') {
return 'create'
}
if (normalized === 'blocked') {
return 'blocked'
}
if (normalized === 'failed' || normalized === 'error') {
return 'blocked'
}
return 'completed'
}
function normalizeToolSummary(extra: StreamToolExtraPayload): string {
const summary = `${extra.summary || ''}`.trim()
if (summary) {
return summary
}
const toolName = `${extra.name || ''}`.trim()
if (!toolName) {
return '工具事件'
}
return `已调用工具:${toolName}`
}
function buildToolDetail(extra: StreamToolExtraPayload): string {
const argsPreview = `${extra.arguments_preview || ''}`.trim()
if (!argsPreview || argsPreview === '{}') {
return ''
}
return argsPreview
}
function normalizeStatusCode(rawCode?: string) {
const code = `${rawCode || ''}`.trim().toLowerCase()
if (!code) {
return 'status'
}
return code
}
function mapStatusCodeLabel(code: string) {
const labelMap: Record<string, string> = {
accepted: '请求已接收',
planning: '正在规划',
resumed: '继续处理中',
confirmed: '确认后继续执行',
rejected: '已取消并重新规划',
executing: '正在执行',
plan_confirm: '等待计划确认',
tool_confirm: '等待操作确认',
ask_user: '等待补充信息',
confirm: '等待用户确认',
interrupted: '会话已中断',
summarizing: '正在生成总结',
done: '流程已结束',
rough_building: '正在生成初始排课方案',
rough_build_failed: '初始排课失败',
rough_build_done: '初始排课已完成',
rough_build_done_no_refine: '初始排课已完成',
order_guard_initialized: '已记录顺序基线',
order_guard_passed: '顺序校验通过',
order_guard_restored: '顺序已自动恢复',
order_guard_restore_skipped: '顺序恢复已跳过',
context_compact_start: '正在压缩上下文',
context_compact_done: '上下文压缩完成',
plan_auto_confirmed: '计划已自动确认',
}
return labelMap[code] || '状态已更新'
}
function buildStatusSummary(extra: StreamExtraPayload): string {
const summary = `${extra.status?.summary || ''}`.trim()
if (summary) {
return summary
}
return mapStatusCodeLabel(normalizeStatusCode(extra.status?.code))
}
function isLegacyToolStatusCode(code: string) {
return code === 'tool_call' || code === 'tool_result' || code === 'tool_blocked'
}
function mapLegacyToolStatusToState(code: string): ToolTraceState {
if (code === 'tool_call') {
return 'called'
}
if (code === 'tool_blocked') {
return 'blocked'
}
return 'completed'
}
function shouldSkipStatusEvent(code: string, stage = '') {
// confirm_request 已有专属卡片,避免重复显示同语义状态行。
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',
'business_card',
'thinking_summary',
])
return assistantKinds.has(kind)
}
function isToolTraceExpanded(eventId: string) {
return toolTraceExpandedMap[eventId] === true
}
@@ -1450,18 +1151,6 @@ function clearConfirmStreamFlags(messageId: string) {
delete confirmVisiblePrefixMap[messageId]
}
function createDraftConversationId() {
return `draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
function createMessageId(role: AssistantMessage['role']) {
return `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
function isDraftConversationId(conversationId: string) {
return conversationId.startsWith('draft-')
}
function upsertConversationMeta(meta: ConversationMeta) {
conversationMetaMap[meta.conversation_id] = meta
}
@@ -1495,26 +1184,11 @@ function migrateConversationState(fromConversationId: string, toConversationId:
delete conversationMetaMap[fromConversationId]
}
const latestMap = new Map<string, ConversationListItem>()
const deduplicated: ConversationListItem[] = []
const seen = new Set<string>()
for (const item of conversationList.value) {
const nextItem =
item.conversation_id === fromConversationId ? { ...item, conversation_id: toConversationId } : item
latestMap.set(nextItem.conversation_id, nextItem)
}
for (const item of conversationList.value) {
const nextId = item.conversation_id === fromConversationId ? toConversationId : item.conversation_id
if (seen.has(nextId)) {
continue
}
seen.add(nextId)
deduplicated.push(latestMap.get(nextId)!)
}
conversationList.value = deduplicated
conversationList.value = migrateConversationListIds(
conversationList.value,
fromConversationId,
toConversationId,
)
if (selectedConversationId.value === fromConversationId) {
selectedConversationId.value = toConversationId
@@ -1530,37 +1204,18 @@ function migrateConversationState(fromConversationId: string, toConversationId:
// 2. 不负责决定选中哪个会话,选中逻辑交给 ensureSelectedConversationAfterListLoad。
// 3. 本地尚未完成 round-trip 的 draft 会话会原样保留,避免用户发出首条消息后列表瞬间丢失。
function mergeConversationList(items: ConversationListItem[]) {
const merged = [...conversationList.value, ...items]
const latestMap = new Map<string, ConversationListItem>()
const deduplicated: ConversationListItem[] = []
const seen = new Set<string>()
for (const item of merged) {
latestMap.set(item.conversation_id, item)
}
for (const item of merged) {
if (seen.has(item.conversation_id)) {
continue
}
seen.add(item.conversation_id)
deduplicated.push(latestMap.get(item.conversation_id) ?? item)
}
conversationList.value = deduplicated
conversationList.value = mergeConversationListItems(conversationList.value, items)
}
function prependConversationPreview(conversationId: string, previewText: string, createdAt: string) {
const current = conversationList.value.find((item) => item.conversation_id === conversationId)
const nextItem: ConversationListItem = {
conversation_id: conversationId,
title: current?.title || previewText.slice(0, 24),
has_title: current?.has_title ?? false,
message_count: Math.max(current?.message_count ?? 0, conversationMessagesMap[conversationId]?.length ?? 0),
last_message_at: createdAt,
status: current?.status || 'active',
created_at: current?.created_at || createdAt,
}
const nextItem = buildConversationPreviewItem(
conversationId,
previewText,
createdAt,
current,
conversationMessagesMap[conversationId]?.length ?? 0,
)
conversationList.value = [
nextItem,
@@ -1666,10 +1321,6 @@ function isLatestAssistantMessage(messageId: string) {
return lastAssistant?.id === messageId
}
function isLocalEphemeralMessageId(id: string) {
return /^(user|assistant|system)-\d{13}-[a-z0-9]+$/i.test(id)
}
function resolvePromptBeforeAssistantMessage(messageId: string) {
const index = findMessageIndex(messageId)
if (index <= 0) {
@@ -2766,10 +2417,6 @@ function startNewConversation() {
suppressEmptyStateTransition.value = false
}
function isManualThinkingEnabled(mode: ThinkingModeType) {
return mode === 'true'
}
async function openFineTuneModal(data: SchedulePreviewData) {
// 1. 如果点击的是占位卡片(尚未加载详情),则触发实时拉取。
if ((data as any).is_placeholder) {
@@ -2901,13 +2548,7 @@ async function handleSaveTask() {
try {
if (isEditMode.value && editingTaskId.value) {
const taskId = editingTaskId.value
const updateData = {
task_id: taskId,
title: taskForm.title.trim(),
priority_group: taskForm.priority_group,
deadline_at: taskForm.deadline_at ? (typeof taskForm.deadline_at === 'string' ? taskForm.deadline_at : taskForm.deadline_at.toISOString()) : null,
urgency_threshold_at: taskForm.urgency_threshold_at ? (typeof taskForm.urgency_threshold_at === 'string' ? taskForm.urgency_threshold_at : taskForm.urgency_threshold_at.toISOString()) : null,
}
const updateData = buildTaskUpdatePayload(taskId, taskForm)
await updateTask(updateData)
// 同步更新本地状态映射,让所有历史卡片实时联动
@@ -2915,11 +2556,7 @@ async function handleSaveTask() {
taskStatusMap[taskId].title = updateData.title
taskStatusMap[taskId].priority_group = updateData.priority_group
// 格式化截止时间用于展示
taskStatusMap[taskId].deadline_at = taskForm.deadline_at
? (taskForm.deadline_at instanceof Date
? taskForm.deadline_at.toLocaleDateString('zh-CN').replace(/\//g, '-')
: String(taskForm.deadline_at).split('T')[0])
: null
taskStatusMap[taskId].deadline_at = formatTaskDeadlineForStatus(taskForm.deadline_at)
}
ElMessage.success('任务详情已更新')
@@ -2961,19 +2598,7 @@ function applyConfirmOverlay(confirmPayload?: StreamConfirmPayload) {
function buildChatRequestExtra(
planningTaskClassIds: number[] = [],
): ChatRequestExtra | undefined {
const extra: ChatRequestExtra = {}
// 1. 任务类别过滤:将智能编排所需的 task_class_ids 透传给后端。
if (planningTaskClassIds.length > 0) {
extra.task_class_ids = [...planningTaskClassIds]
}
// 2. 执行模式控制:若开启“自动执行”,则透传 always_execute 标志,跳过工具调用确认逻辑。
if (selectedExecutionMode.value === 'always') {
extra.always_execute = true
}
return Object.keys(extra).length > 0 ? extra : undefined
return buildAssistantChatRequestExtra(planningTaskClassIds, selectedExecutionMode.value)
}
function handlePlanningSelectionApplied(taskClassIds: number[]) {

View File

@@ -0,0 +1,160 @@
import type { TimelineBusinessCardPayload, ToolView } from '@/api/schedule_agent'
import type {
AssistantMessage,
ConversationListItem,
SchedulePreviewData,
ThinkingSummaryPayload,
} from '@/types/dashboard'
export interface StreamDeltaPayload {
content?: string
}
export interface StreamChoicePayload {
delta?: StreamDeltaPayload
finish_reason?: string | null
}
export interface StreamErrorPayload {
message?: string
}
export interface StreamConfirmPayload {
interaction_id?: string
title?: string
summary?: string
}
export interface StreamStatusExtraPayload {
code?: string
summary?: string
}
export interface StreamToolExtraPayload {
name?: string
status?: string
summary?: string
arguments_preview?: string
argument_view?: ToolView
result_view?: ToolView
}
export interface StreamExtraPayload {
kind?: string
block_id?: string
stage?: string
status?: StreamStatusExtraPayload
tool?: StreamToolExtraPayload
confirm?: StreamConfirmPayload
business_card?: TimelineBusinessCardPayload
thinking_summary?: ThinkingSummaryPayload
}
export interface StreamEventPayload {
choices?: StreamChoicePayload[]
delta?: StreamDeltaPayload
content?: string
finish_reason?: string | null
error?: StreamErrorPayload
extra?: StreamExtraPayload
}
export type ToolTraceState = 'called' | 'completed' | 'create' | 'blocked'
export interface ToolTraceEvent {
id: string
seq: number
state: ToolTraceState
summary: string
detail?: string
toolName?: string
argumentView?: ToolView
resultView?: ToolView
}
export interface StatusTraceEvent {
id: string
seq: number
code: string
stage: string
summary: string
}
export interface ConversationGroup {
key: string
label: string
items: ConversationListItem[]
}
export interface ConfirmOverlayState {
visible: boolean
manuallyClosed: boolean
interactionId: string
title: string
summary: string
}
export interface ConversationListItemRevealOptions {
animate?: boolean
}
export interface EnsureConversationMetaOptions {
forceReload?: boolean
syncListItem?: boolean
listItemReveal?: ConversationListItemRevealOptions
}
// 展示用消息:合并连续 assistant 消息后的视图模型。
export interface DisplayMessage {
/** 第一条源消息的 id用作 Vue key。 */
id: string
role: 'user' | 'assistant' | 'system'
/** 合并后的正文内容。 */
content: string
/** 最后一条源消息的时间。 */
createdAt: string
/** 合并后的推理内容。 */
reasoning?: string
/** 原始消息引用列表。 */
sources: AssistantMessage[]
/** 是否为多条合并。 */
merged: boolean
}
export interface DisplayAssistantBlock {
id: string
type: 'tool' | 'status' | 'reasoning' | 'content' | 'content_indicator' | 'schedule_card' | 'business_card'
seq: number
text?: string
event?: ToolTraceEvent
statusEvent?: StatusTraceEvent
schedulePreview?: SchedulePreviewData
businessCard?: TimelineBusinessCardPayload
/** 所属的源消息 ID用于状态查询。 */
sourceId?: string
/** 所属的源消息引用,用于渲染辅助信息。 */
source?: AssistantMessage
}
export interface AssistantContentBlock {
id: string
seq: number
text: string
}
export interface ThinkingSummaryBlockState {
blockId: string
lastSummarySeq: number
lastSignature: string
finished: boolean
}
export interface ApplyThinkingSummaryOptions {
backendBlockId?: string
stage?: string
summary: ThinkingSummaryPayload
/** 历史回放时使用 timeline 事件自身顺序;实时流未传时按真实到达顺序分配。 */
eventSeq?: number
/** true 表示来自 timeline 历史恢复:不写短摘要、不更新思考态、长摘要一次性写入。 */
fromHistory?: boolean
}

View File

@@ -0,0 +1,336 @@
import type { ApplyThinkingSummaryOptions, StreamExtraPayload, StreamToolExtraPayload, ToolTraceState } from '@/types/assistant-panel'
import type {
AssistantMessage,
ChatRequestExtra,
ConversationListItem,
ThinkingModeType,
ThinkingSummaryPayload,
} from '@/types/dashboard'
interface TaskFormLike {
title: string
priority_group: number
deadline_at: Date | string | null
urgency_threshold_at: Date | string | null
}
export function createDraftConversationId() {
return `draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
export function createMessageId(role: AssistantMessage['role']) {
return `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
export function isDraftConversationId(conversationId: string) {
return conversationId.startsWith('draft-')
}
export function isLocalEphemeralMessageId(id: string) {
return /^(user|assistant|system)-\d{13}-[a-z0-9]+$/i.test(id)
}
export function resolveConversationGroupLabel(timeText?: string | null) {
if (!timeText) {
return '更早'
}
const messageDate = new Date(timeText)
if (Number.isNaN(messageDate.getTime())) {
return '更早'
}
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const targetDay = new Date(messageDate.getFullYear(), messageDate.getMonth(), messageDate.getDate())
const diffDays = Math.floor((today.getTime() - targetDay.getTime()) / (24 * 60 * 60 * 1000))
if (diffDays <= 0) {
return '今天'
}
if (diffDays < 7) {
return '7 天内'
}
if (diffDays < 30) {
return '30 天内'
}
return `${messageDate.getFullYear()}-${String(messageDate.getMonth() + 1).padStart(2, '0')}`
}
export function migrateConversationListIds(
items: ConversationListItem[],
fromConversationId: string,
toConversationId: string,
) {
const latestMap = new Map<string, ConversationListItem>()
const deduplicated: ConversationListItem[] = []
const seen = new Set<string>()
for (const item of items) {
const nextItem =
item.conversation_id === fromConversationId ? { ...item, conversation_id: toConversationId } : item
latestMap.set(nextItem.conversation_id, nextItem)
}
for (const item of items) {
const nextId = item.conversation_id === fromConversationId ? toConversationId : item.conversation_id
if (seen.has(nextId)) {
continue
}
seen.add(nextId)
deduplicated.push(latestMap.get(nextId)!)
}
return deduplicated
}
export function mergeConversationListItems(
currentItems: ConversationListItem[],
nextItems: ConversationListItem[],
) {
const merged = [...currentItems, ...nextItems]
const latestMap = new Map<string, ConversationListItem>()
const deduplicated: ConversationListItem[] = []
const seen = new Set<string>()
for (const item of merged) {
latestMap.set(item.conversation_id, item)
}
for (const item of merged) {
if (seen.has(item.conversation_id)) {
continue
}
seen.add(item.conversation_id)
deduplicated.push(latestMap.get(item.conversation_id) ?? item)
}
return deduplicated
}
export function buildConversationPreviewItem(
conversationId: string,
previewText: string,
createdAt: string,
current: ConversationListItem | undefined,
messageCount: number,
): ConversationListItem {
return {
conversation_id: conversationId,
title: current?.title || previewText.slice(0, 24),
has_title: current?.has_title ?? false,
message_count: Math.max(current?.message_count ?? 0, messageCount),
last_message_at: createdAt,
status: current?.status || 'active',
created_at: current?.created_at || createdAt,
}
}
export function serializeTaskDateForApi(value: Date | string | null) {
if (!value) {
return null
}
return typeof value === 'string' ? value : value.toISOString()
}
export function formatTaskDeadlineForStatus(value: Date | string | null) {
if (!value) {
return null
}
return value instanceof Date
? value.toLocaleDateString('zh-CN').replace(/\//g, '-')
: String(value).split('T')[0]
}
export function buildTaskUpdatePayload(taskId: number, taskForm: TaskFormLike) {
return {
task_id: taskId,
title: taskForm.title.trim(),
priority_group: taskForm.priority_group,
deadline_at: serializeTaskDateForApi(taskForm.deadline_at),
urgency_threshold_at: serializeTaskDateForApi(taskForm.urgency_threshold_at),
}
}
export function buildAssistantChatRequestExtra(
planningTaskClassIds: number[] = [],
executionMode: 'manual' | 'always',
): ChatRequestExtra | undefined {
const extra: ChatRequestExtra = {}
// 1. 任务类别过滤:将智能编排所需的 task_class_ids 透传给后端。
if (planningTaskClassIds.length > 0) {
extra.task_class_ids = [...planningTaskClassIds]
}
// 2. 执行模式控制:若开启“自动执行”,则透传 always_execute 标志,跳过工具调用确认逻辑。
if (executionMode === 'always') {
extra.always_execute = true
}
return Object.keys(extra).length > 0 ? extra : undefined
}
export function isManualThinkingEnabled(mode: ThinkingModeType) {
return mode === 'true'
}
export function getThinkingBackendKey(opts: Pick<ApplyThinkingSummaryOptions, 'backendBlockId' | 'stage'>) {
const backendKeyRaw = (opts.backendBlockId || opts.stage || 'thinking').trim()
return backendKeyRaw || 'thinking'
}
export function buildThinkingSummarySignature(summary: ThinkingSummaryPayload) {
return [
typeof summary.summary_seq === 'number' ? summary.summary_seq : '',
(summary.short_summary || '').trim(),
(summary.detail_summary || '').trim(),
typeof summary.duration_seconds === 'number' ? summary.duration_seconds : '',
summary.final === true ? '1' : '0',
].join('\u001f')
}
export function mapToolEventState(rawStatus?: string): ToolTraceState {
const normalized = `${rawStatus || ''}`.trim().toLowerCase()
if (normalized === 'start' || normalized === 'calling' || normalized === 'called') {
return 'called'
}
if (normalized === 'create' || normalized === 'created') {
return 'create'
}
if (normalized === 'blocked') {
return 'blocked'
}
if (normalized === 'failed' || normalized === 'error') {
return 'blocked'
}
return 'completed'
}
export function normalizeToolSummary(extra: StreamToolExtraPayload): string {
const summary = `${extra.summary || ''}`.trim()
if (summary) {
return summary
}
const toolName = `${extra.name || ''}`.trim()
if (!toolName) {
return '工具事件'
}
return `已调用工具:${toolName}`
}
export function buildToolDetail(extra: StreamToolExtraPayload): string {
const argsPreview = `${extra.arguments_preview || ''}`.trim()
if (!argsPreview || argsPreview === '{}') {
return ''
}
return argsPreview
}
export function normalizeStatusCode(rawCode?: string) {
const code = `${rawCode || ''}`.trim().toLowerCase()
if (!code) {
return 'status'
}
return code
}
export function mapStatusCodeLabel(code: string) {
const labelMap: Record<string, string> = {
accepted: '请求已接收',
planning: '正在规划',
resumed: '继续处理中',
confirmed: '确认后继续执行',
rejected: '已取消并重新规划',
executing: '正在执行',
plan_confirm: '等待计划确认',
tool_confirm: '等待操作确认',
ask_user: '等待补充信息',
confirm: '等待用户确认',
interrupted: '会话已中断',
summarizing: '正在生成总结',
done: '流程已结束',
rough_building: '正在生成初始排课方案',
rough_build_failed: '初始排课失败',
rough_build_done: '初始排课已完成',
rough_build_done_no_refine: '初始排课已完成',
order_guard_initialized: '已记录顺序基线',
order_guard_passed: '顺序校验通过',
order_guard_restored: '顺序已自动恢复',
order_guard_restore_skipped: '顺序恢复已跳过',
context_compact_start: '正在压缩上下文',
context_compact_done: '上下文压缩完成',
plan_auto_confirmed: '计划已自动确认',
}
return labelMap[code] || '状态已更新'
}
export function buildStatusSummary(extra: StreamExtraPayload): string {
const summary = `${extra.status?.summary || ''}`.trim()
if (summary) {
return summary
}
return mapStatusCodeLabel(normalizeStatusCode(extra.status?.code))
}
export function isLegacyToolStatusCode(code: string) {
return code === 'tool_call' || code === 'tool_result' || code === 'tool_blocked'
}
export function mapLegacyToolStatusToState(code: string): ToolTraceState {
if (code === 'tool_call') {
return 'called'
}
if (code === 'tool_blocked') {
return 'blocked'
}
return 'completed'
}
export function shouldSkipStatusEvent(code: string, stage = '') {
// confirm_request 已有专属卡片,避免重复显示同语义状态行。
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
}
export function isAssistantTimelineKind(kind: string) {
const assistantKinds = new Set([
'assistant_text',
'tool_call',
'tool_result',
'confirm_request',
'schedule_completed',
'interrupt',
'status',
'business_card',
'thinking_summary',
])
return assistantKinds.has(kind)
}