feat(cloud-agent): 云端 Agent - 完善运行时与资源持久化

This commit is contained in:
ddcat666
2026-09-26 17:05:36 +08:00
parent 4613cca38c
commit f4480b129b
40 changed files with 1739 additions and 440 deletions

View File

@@ -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);

View File

@@ -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 技能";

View File

@@ -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] : [];
});
}