1b010fb1f8
聊天历史按有效消息分页并返回原生游标,保留默认原始切片接口 工具和推理记录不占聊天页名额,逐行过滤避免传回大段工具输出 隔离重新加载和项目切换的迟到响应,补齐单飞、重试与消息去重 补充原生和真实App回归测试,增加人工只读日志重放入口 更新B05状态、规范与PR验收记录,不提交用户原始日志
122 lines
3.5 KiB
TypeScript
122 lines
3.5 KiB
TypeScript
import type {
|
|
ChatMessage,
|
|
LocalConversationMessageRecord,
|
|
} from '../../app/types';
|
|
|
|
export type DirectThreadRawEvent = {
|
|
seq: number;
|
|
type: string;
|
|
turnId: string;
|
|
itemId?: string;
|
|
payload: Record<string, unknown>;
|
|
};
|
|
|
|
export type DirectThreadSubscriptionBootstrap = {
|
|
subscriptionId: string;
|
|
lastCompletedItemId: string | null;
|
|
events: DirectThreadRawEvent[];
|
|
};
|
|
|
|
export type DirectThreadConsumeResult = {
|
|
events: DirectThreadRawEvent[];
|
|
};
|
|
|
|
export type DirectThreadHistorySlice = {
|
|
items: unknown[];
|
|
hasMore: boolean;
|
|
itemTimestamps?: Record<string, number>;
|
|
oldestItemId?: string | null;
|
|
};
|
|
|
|
/** 游标取原始响应,而非过滤后的聊天消息;拒绝不能前进的页,避免静默反复回读。 */
|
|
export function directThreadHistoryPage(
|
|
slice: DirectThreadHistorySlice,
|
|
previousCursor: string | null = null,
|
|
) {
|
|
const first = slice.items[0];
|
|
const firstId =
|
|
first && typeof first === 'object' && 'id' in first
|
|
? (first as { id?: unknown }).id
|
|
: null;
|
|
const cursor =
|
|
slice.oldestItemId ??
|
|
(typeof firstId === 'string' && firstId ? firstId : null);
|
|
if (slice.hasMore && (!cursor || cursor === previousCursor)) {
|
|
throw new Error('对话历史分页游标未前进,请重新读取项目历史');
|
|
}
|
|
return {
|
|
messages: directThreadHistoryItemsToMessages(
|
|
slice.items,
|
|
slice.itemTimestamps,
|
|
),
|
|
hasMore: slice.hasMore,
|
|
cursor,
|
|
};
|
|
}
|
|
|
|
/** 保留当前实时/已显示版本;原始身份相同的回读消息不能插入第二次。 */
|
|
export function prependDirectHistoryMessages(
|
|
current: readonly ChatMessage[],
|
|
older: readonly ChatMessage[],
|
|
): ChatMessage[] {
|
|
const ids = new Set(
|
|
current.flatMap((message) =>
|
|
message.messageId ? [message.messageId] : [],
|
|
),
|
|
);
|
|
const additions = older.filter((message) => {
|
|
if (!message.messageId) return true;
|
|
if (ids.has(message.messageId)) return false;
|
|
ids.add(message.messageId);
|
|
return true;
|
|
});
|
|
return [...additions, ...current];
|
|
}
|
|
|
|
export function directThreadHistoryItemsToMessages(
|
|
items: unknown[],
|
|
itemTimestamps: Readonly<Record<string, number>> = {},
|
|
): LocalConversationMessageRecord[] {
|
|
return items.flatMap((raw) => {
|
|
if (!raw || typeof raw !== 'object') return [];
|
|
const item = raw as Record<string, unknown>;
|
|
const role = item.role;
|
|
if (role !== 'user' && role !== 'assistant') return [];
|
|
const messageRole = role as 'user' | 'assistant';
|
|
const content = Array.isArray(item.content)
|
|
? item.content
|
|
.map((part) =>
|
|
part && typeof part === 'object' && 'text' in part
|
|
? (part as { text?: unknown }).text
|
|
: null,
|
|
)
|
|
.filter((text): text is string => typeof text === 'string')
|
|
.join('')
|
|
: '';
|
|
if (!content) return [];
|
|
const messageId = typeof item.id === 'string' ? item.id : undefined;
|
|
return [
|
|
{
|
|
schemaVersion: 'agc-direct-project-context.v1',
|
|
role: messageRole,
|
|
content,
|
|
agentId: null,
|
|
messageId,
|
|
updatedAt: messageId ? (itemTimestamps[messageId] ?? 0) : 0,
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
/** 只有这些状态表示仍持有活动回合;Provider 回放的终态不是活动快照。 */
|
|
export function isDirectTurnInProgress(
|
|
status: string | null | undefined,
|
|
): status is 'accepted' | 'running' | 'streaming' | 'finalizing' {
|
|
return (
|
|
status === 'accepted' ||
|
|
status === 'running' ||
|
|
status === 'streaming' ||
|
|
status === 'finalizing'
|
|
);
|
|
}
|