新增: 轮次按发起边界分桶与稳定编号
- 轮号=用户发起消息计数(单一真值来源), 单消息桶不吞号, 编号不再漂移 - 补充注入标 source=inject, 归属当前轮不开新轮(961 轮次语义) - 压缩摘要/错误气泡归属当前轮, 压缩后不再另起新轮次 - reload 消息透传 source 字段, 轮次导航条自动同口径
This commit is contained in:
@@ -1174,7 +1174,15 @@ pub(crate) async fn inject_runtime_message(
|
|||||||
if !cs.is_active() {
|
if !cs.is_active() {
|
||||||
return Err(format!("conv {conv_id} 非活跃态({cs:?}),不走注入"));
|
return Err(format!("conv {conv_id} 非活跃态({cs:?}),不走注入"));
|
||||||
}
|
}
|
||||||
let msg = ChatMessage::user(&format!("[补充指令] {message}"));
|
let mut msg = ChatMessage::user(&format!("[补充指令] {message}"));
|
||||||
|
// 961② 轮次归属标记:注入=对当前轮的补充,不开新轮。前端轮次分桶按此判据
|
||||||
|
// (source.channel=="inject" 不递增轮号),与发起消息(桌面直发/飞书入站)区分
|
||||||
|
msg.source = Some(df_ai::provider::MessageSource {
|
||||||
|
channel: "inject".into(),
|
||||||
|
message_id: msg.id.clone().unwrap_or_default(),
|
||||||
|
chat_id: None,
|
||||||
|
sender: None,
|
||||||
|
});
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
if session.conv_read(conv_id).is_none() {
|
if session.conv_read(conv_id).is_none() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
|||||||
@@ -565,6 +565,11 @@ export interface AiMessage {
|
|||||||
id: MessageId
|
id: MessageId
|
||||||
role: 'user' | 'assistant' | 'tool' | 'system'
|
role: 'user' | 'assistant' | 'tool' | 'system'
|
||||||
content: string
|
content: string
|
||||||
|
/**
|
||||||
|
* 消息来源(958 溯源/961 轮次归属):undefined=null=桌面本端发起;'inject'=运行中
|
||||||
|
* 补充注入(961② 轮次分桶判据,归属当前轮不开新轮);'feishu'=飞书入站发起。
|
||||||
|
*/
|
||||||
|
source?: { channel: string; message_id: string; chat_id?: string; sender?: string } | null
|
||||||
/**
|
/**
|
||||||
* 推理过程文本(DeepSeek reasoning_content / Anthropic thinking 同通道,后端落库
|
* 推理过程文本(DeepSeek reasoning_content / Anthropic thinking 同通道,后端落库
|
||||||
* ai_messages.reasoning_content)。undefined=该轮无思考内容。历史消息 reload 时
|
* ai_messages.reasoning_content)。undefined=该轮无思考内容。历史消息 reload 时
|
||||||
|
|||||||
@@ -1012,7 +1012,7 @@ function traceHoverTitle(startTs: number, endTs: number): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行中实时步骤摘要:「第 N 轮 · ✓ N 次调用完成 · 失败 N · 正在 {工具} · ⏳ N 项待审批」。
|
* 执行中实时步骤摘要:「第 N 轮 · ✓ N 次调用完成 · 失败 N · 正在 {工具} · ⏳ N 项待审批」。
|
||||||
* 轮次号与轨迹卡同源(flushBucket 的 traceCount,执行中=将成为的轨迹序号);
|
* 轮次号与轨迹卡同源(flushBucket 的 currentRound,发起计数口径见 renderItems);
|
||||||
* 失败/拒绝/待审批段仅非零时显示;全部无数据回退「过程记录」。
|
* 失败/拒绝/待审批段仅非零时显示;全部无数据回退「过程记录」。
|
||||||
*/
|
*/
|
||||||
function stepText(s: TurnCallStats, round: number): string {
|
function stepText(s: TurnCallStats, round: number): string {
|
||||||
@@ -1030,7 +1030,7 @@ type RenderItem =
|
|||||||
| { kind: 'msg'; key: string; msg: AiMessage; final?: boolean }
|
| { kind: 'msg'; key: string; msg: AiMessage; final?: boolean }
|
||||||
| { kind: 'sep'; key: string; seg: MessageSegment & { key: string } }
|
| { kind: 'sep'; key: string; seg: MessageSegment & { key: string } }
|
||||||
// A 过程压缩:完成轮次的执行轨迹卡(traceMsgs 为折叠在内的过程消息)与执行中的实时步骤摘要;
|
// A 过程压缩:完成轮次的执行轨迹卡(traceMsgs 为折叠在内的过程消息)与执行中的实时步骤摘要;
|
||||||
// round=轨迹序号(卡头「第 N 轮」,与执行中步骤行、轮次导航条同一计数来源)
|
// round=会话轮号(卡头「第 N 轮」,961③ 发起计数口径,导航条/步骤行同源)
|
||||||
| { kind: 'trace'; key: string; round: number; stats: TurnCallStats; startTs: number; endTs: number; tokens: number; traceMsgs: AiMessage[]; expanded: boolean }
|
| { kind: 'trace'; key: string; round: number; stats: TurnCallStats; startTs: number; endTs: number; tokens: number; traceMsgs: AiMessage[]; expanded: boolean }
|
||||||
| { kind: 'step'; key: string; round: number; stats: TurnCallStats; traceMsgs: AiMessage[]; expanded: boolean }
|
| { kind: 'step'; key: string; round: number; stats: TurnCallStats; traceMsgs: AiMessage[]; expanded: boolean }
|
||||||
// UX 密度改造:相邻消息间隔 ≥30 分钟或跨天的居中时间分隔线(替代 per-block 相对时间)
|
// UX 密度改造:相邻消息间隔 ≥30 分钟或跨天的居中时间分隔线(替代 per-block 相对时间)
|
||||||
@@ -1198,9 +1198,12 @@ const renderItems = computed<RenderItem[]>(() => {
|
|||||||
const items: RenderItem[] = []
|
const items: RenderItem[] = []
|
||||||
let bucket: AiMessage[] = []
|
let bucket: AiMessage[] = []
|
||||||
let lastTs = 0
|
let lastTs = 0
|
||||||
// 轨迹序号计数:轨迹卡头「第 N 轮」与执行中步骤行同一来源(computed 重算从 0 起,
|
// 961②③ 轮次号=「用户发起」计数(单一真值来源,数据派生天然稳定):
|
||||||
// 计数即轨迹卡出现顺序,与轮次导航条序号一致)
|
// 发起=user 且非补充注入(source.channel!='inject',老数据无 source 视为发起);
|
||||||
let traceCount = 0
|
// 补充注入/压缩摘要/错误气泡均归属当前轮不递增——修复旧口径两漂移根源:
|
||||||
|
// ①旧 traceCount=轨迹卡出现顺序,单消息桶不计数致编号随桶形态漂移;
|
||||||
|
// ②压缩摘要切桶即开新号(用户表象「压缩后就是新轮次」)。
|
||||||
|
let currentRound = 0
|
||||||
|
|
||||||
const pushSep = (ts: number) => {
|
const pushSep = (ts: number) => {
|
||||||
if (needsTimeSep(ts, lastTs)) {
|
if (needsTimeSep(ts, lastTs)) {
|
||||||
@@ -1215,9 +1218,8 @@ const renderItems = computed<RenderItem[]>(() => {
|
|||||||
if (msgs.length === 0) return
|
if (msgs.length === 0) return
|
||||||
const lastStoreId = store.state.messages[store.state.messages.length - 1]?.id
|
const lastStoreId = store.state.messages[store.state.messages.length - 1]?.id
|
||||||
const executing = isViewingGenerating.value && msgs[msgs.length - 1].id === lastStoreId
|
const executing = isViewingGenerating.value && msgs[msgs.length - 1].id === lastStoreId
|
||||||
// 轨迹/步骤项产生时轮号自增(仅 msgs.length>1 会产出 step/trace;轮号计入桶签名,
|
// 961③: 轮号=当前发起计数(与轨迹卡是否产出解耦,单消息桶不吞号)
|
||||||
// 前序桶变化致轮号位移时该桶正确重算)
|
const round = currentRound
|
||||||
const round = msgs.length > 1 ? ++traceCount : traceCount
|
|
||||||
const built = buildBucket(msgs, round, executing, lastTs)
|
const built = buildBucket(msgs, round, executing, lastTs)
|
||||||
for (const it of built.items) items.push(it)
|
for (const it of built.items) items.push(it)
|
||||||
lastTs = built.endTs
|
lastTs = built.endTs
|
||||||
@@ -1237,6 +1239,15 @@ const renderItems = computed<RenderItem[]>(() => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const m = seg.msg
|
const m = seg.msg
|
||||||
|
// 961②: 发起边界=user 且非补充注入——flush 当前轮并递增轮号;补充注入归属
|
||||||
|
// 当前轮原样渲染不递增。压缩摘要/错误气泡也不递增(压缩后仍是同轮过程)
|
||||||
|
if (m.role === 'user') {
|
||||||
|
flushBucket()
|
||||||
|
if (m.source?.channel !== 'inject') currentRound++
|
||||||
|
pushSep(m.timestamp)
|
||||||
|
items.push({ kind: 'msg', key: 'm-' + m.id, msg: m })
|
||||||
|
continue
|
||||||
|
}
|
||||||
// assistant 非错误非压缩进轮次桶(compressed 用户诉求不折叠,原样展开)
|
// assistant 非错误非压缩进轮次桶(compressed 用户诉求不折叠,原样展开)
|
||||||
if (m.role === 'assistant' && !m.isError && m.status !== 'compressed') {
|
if (m.role === 'assistant' && !m.isError && m.status !== 'compressed') {
|
||||||
bucket.push(m)
|
bucket.push(m)
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ function parseConvMessages(rawMsgs: any[], fallbackPrefix: string, convGeneratin
|
|||||||
content: m.content || '',
|
content: m.content || '',
|
||||||
// 推理过程透传(DeepSeek/Anthropic thinking 同通道落库字段),undefined 保持缺省
|
// 推理过程透传(DeepSeek/Anthropic thinking 同通道落库字段),undefined 保持缺省
|
||||||
reasoning_content: m.reasoning_content || undefined,
|
reasoning_content: m.reasoning_content || undefined,
|
||||||
|
// 消息来源透传(961② 轮次归属:inject 补充不开新轮);老数据无字段为 undefined
|
||||||
|
source: m.source ?? undefined,
|
||||||
model: m.model,
|
model: m.model,
|
||||||
// 用后端持久化的真实时间;老数据无 timestamp 字段回退 Date.now()
|
// 用后端持久化的真实时间;老数据无 timestamp 字段回退 Date.now()
|
||||||
timestamp: typeof m.timestamp === 'number' ? m.timestamp : Date.now(),
|
timestamp: typeof m.timestamp === 'number' ? m.timestamp : Date.now(),
|
||||||
|
|||||||
Reference in New Issue
Block a user