feat: 完善智能体图片聊天与会话恢复

- 增加私有图片上传、绑定、历史回显与生命周期清理

- 支持输入草稿恢复、图片交互和模型图片能力约束

- 修复旧脏会话幂等删除与前端会话恢复
This commit is contained in:
2026-07-17 19:54:26 +08:00
parent 62d763199f
commit 1e6158be77
62 changed files with 5333 additions and 189 deletions

View File

@@ -1,10 +1,11 @@
import type {
ChatImageAttachment,
ChatTimelineItem,
ChatTimelineKnowledgeHit,
ChatTimelineMessageItem,
ChatTimelineToolStatus,
} from '@easyflow/common-ui';
import {ChatTimelineBuilder} from '@easyflow/common-ui';
import { ChatTimelineBuilder } from '@easyflow/common-ui';
interface AgentTryoutRuntimeEvent {
createdAt: number;
@@ -25,6 +26,7 @@ interface AgentTryoutRawVariant {
interface AgentTryoutRawRound {
createdAt: number;
images?: ChatImageAttachment[];
prompt: string;
roundId: string;
selectedVariantIndex: number;
@@ -110,7 +112,9 @@ function createVariant(variantIndex: number): AgentTryoutRawVariant {
};
}
function normalizeRuntimeEvent(value: any): AgentTryoutRuntimeEvent | undefined {
function normalizeRuntimeEvent(
value: any,
): AgentTryoutRuntimeEvent | undefined {
if (!value || typeof value !== 'object') {
return undefined;
}
@@ -135,8 +139,9 @@ function normalizeVariant(value: any, index: number) {
? value.runtimeEvents
.map((item: any) => normalizeRuntimeEvent(item))
.filter(
(item: AgentTryoutRuntimeEvent | undefined): item is AgentTryoutRuntimeEvent =>
Boolean(item),
(
item: AgentTryoutRuntimeEvent | undefined,
): item is AgentTryoutRuntimeEvent => Boolean(item),
)
: [];
return {
@@ -157,7 +162,8 @@ function normalizeRound(value: any): AgentTryoutRawRound | undefined {
}
const prompt = asText(value.prompt);
const roundId = asText(value.roundId);
if (!prompt || !roundId) {
const images = Array.isArray(value.images) ? value.images.slice(0, 5) : [];
if ((!prompt && images.length === 0) || !roundId) {
return undefined;
}
const variants = Array.isArray(value.variants)
@@ -174,6 +180,7 @@ function normalizeRound(value: any): AgentTryoutRawRound | undefined {
);
return {
createdAt: Number(value.createdAt || Date.now()),
images,
prompt,
roundId,
selectedVariantIndex,
@@ -210,7 +217,10 @@ function restoreSession(mode: string, sessionId: string) {
.map((item) => normalizeRound(item))
.filter((item): item is AgentTryoutRawRound => Boolean(item))
: [];
memorySessions.set(key, rounds.map((item) => clone(item)));
memorySessions.set(
key,
rounds.map((item) => clone(item)),
);
return rounds;
} catch {
return [];
@@ -228,7 +238,10 @@ function persistSession(
sessionId,
version: STORAGE_VERSION,
};
memorySessions.set(key, snapshot.rounds.map((item) => clone(item)));
memorySessions.set(
key,
snapshot.rounds.map((item) => clone(item)),
);
const storage = safeSessionStorage();
if (!storage) {
return;
@@ -269,7 +282,9 @@ function visibleText(item: ChatTimelineMessageItem) {
.join('');
}
function isUserMessage(item: ChatTimelineItem): item is ChatTimelineMessageItem {
function isUserMessage(
item: ChatTimelineItem,
): item is ChatTimelineMessageItem {
return item.type === 'message' && item.role === 'user';
}
@@ -326,7 +341,10 @@ function normalizeAssistantPartIds(
const segment = assistantSegmentIndex(items, roundId);
const latest = [...items]
.reverse()
.find((item): item is ChatTimelineMessageItem => isAssistantMessage(item) && item.roundId === roundId);
.find(
(item): item is ChatTimelineMessageItem =>
isAssistantMessage(item) && item.roundId === roundId,
);
if (!latest) {
return;
}
@@ -468,7 +486,9 @@ function projectEventToTimeline(
metadata: payload.metadata,
requestId: asText(payload.requestId),
resumeToken: asText(payload.resumeToken),
toolCallId: asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id),
toolCallId: asText(
payload.toolCallId ?? payload.tool_call_id ?? payload.id,
),
toolDisplayName: asText(payload.toolDisplayName),
toolName: asText(payload.toolName),
toolType: asText(payload.toolType),
@@ -482,7 +502,9 @@ function projectEventToTimeline(
ChatTimelineBuilder.markToolApproving(items, {
requestId: asText(payload.requestId),
resumeToken: asText(payload.resumeToken),
toolCallId: asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id),
toolCallId: asText(
payload.toolCallId ?? payload.tool_call_id ?? payload.id,
),
});
return;
}
@@ -491,7 +513,9 @@ function projectEventToTimeline(
reason: asText(payload.reason),
requestId: asText(payload.requestId),
resumeToken: asText(payload.resumeToken),
toolCallId: asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id),
toolCallId: asText(
payload.toolCallId ?? payload.tool_call_id ?? payload.id,
),
});
return;
}
@@ -508,8 +532,12 @@ function projectEventToTimeline(
ChatTimelineBuilder.upsertToolCall(items, {
input: payload.input ?? payload.toolInput,
output: asyncTool
? payload.summary ?? payload.label ?? payload.output ?? payload.result ?? payload.text
: payload.output ?? payload.result ?? payload.text,
? (payload.summary ??
payload.label ??
payload.output ??
payload.result ??
payload.text)
: (payload.output ?? payload.result ?? payload.text),
status: asyncTool
? asyncToolTimelineStatus(payload)
: type === 'TOOL_RESULT'
@@ -521,8 +549,17 @@ function projectEventToTimeline(
variantIndex,
'knowledge-retrieval',
),
toolCallId: asText(payload.toolCallId ?? payload.taskId ?? payload.tool_call_id ?? payload.id),
toolName: asyncTool ? displayToolName : isHiddenToolName(rawToolName) ? rawToolName : displayToolName,
toolCallId: asText(
payload.toolCallId ??
payload.taskId ??
payload.tool_call_id ??
payload.id,
),
toolName: asyncTool
? displayToolName
: isHiddenToolName(rawToolName)
? rawToolName
: displayToolName,
});
return;
}
@@ -568,7 +605,9 @@ function projectEventToTimeline(
}
}
function asyncToolTimelineStatus(payload: Record<string, unknown>): ChatTimelineToolStatus {
function asyncToolTimelineStatus(
payload: Record<string, unknown>,
): ChatTimelineToolStatus {
const status = asText(payload.status).toUpperCase();
if (status === 'SUCCEEDED') return 'success';
if (status === 'FAILED' || status === 'TIMEOUT' || status === 'CANCELLED') {
@@ -612,7 +651,10 @@ export function useAgentTryoutRawRounds(options: {
function schedulePersist() {
const key = storageKey(options.mode, options.sessionId);
memorySessions.set(key, [...rounds.values()].map((item) => clone(item)));
memorySessions.set(
key,
[...rounds.values()].map((item) => clone(item)),
);
if (persistTimer) {
return;
}
@@ -630,11 +672,12 @@ export function useAgentTryoutRawRounds(options: {
removeStoredSession(options.mode, options.sessionId);
}
function createRound(prompt: string) {
function createRound(prompt: string, images: ChatImageAttachment[] = []) {
const now = Date.now();
const roundId = createRoundId();
rounds.set(roundId, {
createdAt: now,
images: clone(images),
prompt,
roundId,
selectedVariantIndex: 1,
@@ -738,6 +781,7 @@ export function useAgentTryoutRawRounds(options: {
for (const round of sortedRounds(rounds)) {
ChatTimelineBuilder.appendUserMessage(items, round.prompt, {
id: `user-${round.roundId}`,
images: round.images,
roundId: round.roundId,
});
const variant = selectedVariant(round);
@@ -745,7 +789,12 @@ export function useAgentTryoutRawRounds(options: {
continue;
}
for (const event of variant.runtimeEvents) {
projectEventToTimeline(items, event, round.roundId, variant.variantIndex);
projectEventToTimeline(
items,
event,
round.roundId,
variant.variantIndex,
);
}
if (variant.status === 'completed' || variant.status === 'error') {
ChatTimelineBuilder.finalize(items);
@@ -761,10 +810,7 @@ export function useAgentTryoutRawRounds(options: {
return items;
}
function selectVariant(
roundId: string,
direction: 'next' | 'previous',
) {
function selectVariant(roundId: string, direction: 'next' | 'previous') {
const round = rounds.get(roundId);
if (!round) {
return;