Files
Genarrative/apps/ai-game-creator-shell/src/features/project-workspace/directThreadEvents.ts
T
suzmii 1b010fb1f8 修复DirectProject对话历史分页卡页
聊天历史按有效消息分页并返回原生游标,保留默认原始切片接口
工具和推理记录不占聊天页名额,逐行过滤避免传回大段工具输出
隔离重新加载和项目切换的迟到响应,补齐单飞、重试与消息去重
补充原生和真实App回归测试,增加人工只读日志重放入口
更新B05状态、规范与PR验收记录,不提交用户原始日志
2026-09-16 18:46:47 +08:00

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'
);
}