feat(cloud-agent): 云端 Agent - 完善运行时与资源持久化
This commit is contained in:
@@ -15,12 +15,16 @@ export type AgentFeedRecord = {
|
||||
role: string;
|
||||
title?: string;
|
||||
text: string;
|
||||
reasoning?: boolean;
|
||||
detail?: unknown;
|
||||
planItems?: readonly unknown[];
|
||||
question?: unknown;
|
||||
};
|
||||
|
||||
export type AgentFeedSegment<T> = { kind: "operations"; key: string; items: T[] } | { kind: "message"; key: string; item: T };
|
||||
export type AgentFeedSegment<T> =
|
||||
| { kind: "operations"; key: string; items: T[] }
|
||||
| { kind: "reasoning"; key: string; items: T[] }
|
||||
| { kind: "message"; key: string; item: T };
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
||||
@@ -41,6 +45,11 @@ export function isAgentOperationRecord(item: AgentFeedRecord): boolean {
|
||||
return item.role === "tool" && !isAgentCarrierRecord(item) && record(item.detail).status !== "pending";
|
||||
}
|
||||
|
||||
/** 连续推理事件共用一条可展开记录,避免每个 SSE chunk/摘要都变成一行。 */
|
||||
export function isAgentReasoningRecord(item: AgentFeedRecord): boolean {
|
||||
return item.role === "assistant" && item.reasoning === true;
|
||||
}
|
||||
|
||||
/** 折叠行上报的那一步:最新一条操作记录的友好摘要(等待执行时用"准备…"口径)。 */
|
||||
export function agentOperationLabel(item: AgentFeedRecord): string {
|
||||
const name = agentToolName(item.title || "工具执行", item.detail);
|
||||
@@ -103,6 +112,12 @@ export function buildAgentFeedSegments<T extends AgentFeedRecord>(messages: read
|
||||
const segments: AgentFeedSegment<T>[] = [];
|
||||
for (const item of messages) {
|
||||
if (isAgentCarrierRecord(item)) continue;
|
||||
if (isAgentReasoningRecord(item)) {
|
||||
const last = segments[segments.length - 1];
|
||||
if (last?.kind === "reasoning") last.items.push(item);
|
||||
else segments.push({ kind: "reasoning", key: item.id, items: [item] });
|
||||
continue;
|
||||
}
|
||||
if (isAgentOperationRecord(item)) {
|
||||
const last = segments[segments.length - 1];
|
||||
if (last?.kind === "operations") last.items.push(item);
|
||||
|
||||
@@ -135,7 +135,9 @@ function field(value: unknown, key: string): unknown {
|
||||
|
||||
export function agentToolStatus(title: string, text: string, detail?: unknown): ToolStatus {
|
||||
const event = field(detail, "eventType");
|
||||
if (event === "tool_failed" && agentToolRetry(detail)?.status === "retrying") return "retrying";
|
||||
const retry = agentToolRetry(detail);
|
||||
if (retry?.status === "retrying") return "retrying";
|
||||
if (retry?.status === "recovered") return "completed";
|
||||
if (event === "tool_completed" || event === "generation_task_created" || event === "canvas_updated") return "completed";
|
||||
if (event === "tool_failed") return "failed";
|
||||
const raw = `${title} ${text} ${field(detail, "error") || ""}`;
|
||||
@@ -149,8 +151,14 @@ export function agentToolStatus(title: string, text: string, detail?: unknown):
|
||||
export function friendlyAgentToolSummary(toolName: string, text: string, detail?: unknown, pending = false) {
|
||||
const status = agentToolStatus(toolName, text, detail);
|
||||
const failed = status === "failed" || status === "rejected";
|
||||
if (toolName === "generate_media" && failed && field(field(detail, "result"), "phase") === "admission") return "生成请求未提交";
|
||||
if (toolName === "generate_media" && failed && field(field(detail, "result"), "status") === "succeeded") return "生成成功,画布回写未完成";
|
||||
const mediaTool = toolName === "generate_media" || toolName === "image_layer_split";
|
||||
const result = record(field(detail, "result"));
|
||||
const phase = result.phase;
|
||||
if (mediaTool && failed && phase === "admission") return "参数未通过校验,未提交生成任务";
|
||||
if (mediaTool && failed && phase === "completion" && result.taskSubmitted === true) {
|
||||
if (result.writebackReason || result.status === "succeeded") return "生成成功,画布回写未完成";
|
||||
return "任务已提交,但生成结果未完成";
|
||||
}
|
||||
if (toolName === "skills_load") {
|
||||
const count = /(?:加载|启用)\s*(\d+)\s*个/u.exec(text)?.[1];
|
||||
return failed ? "Skills 技能加载失败" : count ? `已加载 ${count} 个 Skills 技能` : "已加载 Skills 技能";
|
||||
|
||||
@@ -67,7 +67,7 @@ export function uninsertedCanvasAssetHandoffPayloads(nodes: Iterable<{ metadata?
|
||||
return payloads.filter((payload) => !payload.assetId || !insertedAssetIds.has(payload.assetId));
|
||||
}
|
||||
|
||||
export function creationResultAssetIds(assets: Asset[], input: { messageId: string; taskIds: string[]; resultUrls: string[] }) {
|
||||
export function creationResultAssetIds(assets: Asset[], input: { messageId: string; taskIds: string[]; resultUrls: string[]; resultStorageKeys?: string[] }) {
|
||||
const taskOrder = new Map(input.taskIds.map((taskId, index) => [taskId, index]));
|
||||
const candidates = assets
|
||||
.filter((asset) => asset.kind === "image" || asset.kind === "video")
|
||||
@@ -82,13 +82,24 @@ export function creationResultAssetIds(assets: Asset[], input: { messageId: stri
|
||||
return leftResult - rightResult;
|
||||
});
|
||||
const unused = new Set(candidates.map((asset) => asset.id));
|
||||
const allowOrderedFallback = candidates.length === input.resultUrls.length;
|
||||
return input.resultUrls.flatMap((url) => {
|
||||
const exact = candidates.find((asset) => unused.has(asset.id) && (asset.coverUrl === url || (asset.kind === "image" ? asset.data.dataUrl === url : asset.data.url === url)));
|
||||
const stableKeys = input.resultStorageKeys || [];
|
||||
const allowOrderedFallback = candidates.length === Math.max(input.resultUrls.length, stableKeys.length);
|
||||
return Array.from({ length: Math.max(input.resultUrls.length, stableKeys.length) }, (_, index) => {
|
||||
const storageKey = stableKeys[index];
|
||||
const url = input.resultUrls[index] || "";
|
||||
const exact = candidates.find((asset) => unused.has(asset.id) && Boolean(storageKey) && asset.data.storageKey === storageKey)
|
||||
|| candidates.find((asset) => unused.has(asset.id) && (asset.coverUrl === url || (asset.kind === "image" ? asset.data.dataUrl === url : asset.data.url === url)));
|
||||
const fallback = exact || (allowOrderedFallback ? candidates.find((asset) => unused.has(asset.id)) : undefined);
|
||||
if (!fallback) return [];
|
||||
if (!fallback) return undefined;
|
||||
unused.delete(fallback.id);
|
||||
return [fallback.id];
|
||||
return fallback.id;
|
||||
}).filter((id): id is string => Boolean(id));
|
||||
}
|
||||
|
||||
export function creationResultStorageKeys(assets: Asset[], input: { messageId: string; taskIds: string[]; resultUrls: string[]; resultStorageKeys?: string[] }) {
|
||||
return creationResultAssetIds(assets, input).flatMap((assetId) => {
|
||||
const asset = assets.find((candidate) => candidate.id === assetId);
|
||||
return asset && (asset.kind === "image" || asset.kind === "video") && asset.data.storageKey ? [asset.data.storageKey] : [];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user