Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

297 lines
8.9 KiB
TypeScript

import type { AgentPromptContentBlock } from "../../../shared/contracts/dto";
import {
agentComposerFileMentionReferences,
createAgentComposerFileMentionMarkdown,
formatAgentFileMentionMarkdown
} from "../agentRichText/agentMentionMarkdown";
import {
AGENT_PASTED_TEXT_BLOCK_KIND,
type AgentComposerDraftFile
} from "./agentGuiNodeTypes";
interface AgentPreparedExternalPromptFileMetadata {
assetId?: string;
mimeType?: string;
name: string;
sizeBytes?: number;
uploadStatus?: string;
uri?: string;
}
export type AgentPreparedExternalPromptFile =
AgentPreparedExternalPromptFileMetadata &
({ path: string; url?: string } | { path?: string; url: string });
export type AgentExternalPromptFilePreparationErrorCode =
| "file_too_large"
| "folder_unsupported"
| "preparation_failed";
export type AgentExternalPromptFilePreparationResult =
| {
sourceIndex: number;
status: "prepared";
file: AgentPreparedExternalPromptFile;
}
| {
sourceIndex: number;
status: "error";
errorCode: AgentExternalPromptFilePreparationErrorCode;
retryable?: boolean;
};
export type AgentExternalPromptFilePreparer = (
files: readonly File[]
) => Promise<readonly AgentExternalPromptFilePreparationResult[]>;
export interface AgentExternalPromptFilePreparation {
pendingFiles: AgentComposerDraftFile[];
complete(
prepareExternalPromptFiles: AgentExternalPromptFilePreparer
): Promise<AgentComposerDraftFile[]>;
}
export function remainingAgentComposerPromptAssetSlots(input: {
files: number;
images: number;
largeTexts: number;
limit?: number | null;
}): number {
if (
typeof input.limit !== "number" ||
!Number.isFinite(input.limit) ||
input.limit < 0
) {
return Number.POSITIVE_INFINITY;
}
return Math.max(
0,
Math.floor(input.limit) - input.files - input.images - input.largeTexts
);
}
export function createAgentExternalPromptFilePreparation(
files: readonly File[]
): AgentExternalPromptFilePreparation {
const pendingFiles = files.map((file) => ({
id: crypto.randomUUID(),
name: file.name || "file",
...(file.type ? { mimeType: file.type } : {}),
...(Number.isFinite(file.size) ? { sizeBytes: file.size } : {}),
uploading: true
}));
return {
pendingFiles,
async complete(prepareExternalPromptFiles) {
let results: readonly AgentExternalPromptFilePreparationResult[];
try {
results = await prepareExternalPromptFiles(files);
} catch {
return pendingFiles.map((file) =>
failedDraftFile(file, "preparation_failed")
);
}
const resultByIndex = new Map(
results.map((result) => [result.sourceIndex, result])
);
return pendingFiles.map((pendingFile, sourceIndex) => {
const result = resultByIndex.get(sourceIndex);
if (!result) {
return failedDraftFile(pendingFile, "preparation_failed");
}
if (result.status === "error") {
return failedDraftFile(pendingFile, result.errorCode, {
retryable: result.retryable
});
}
const prepared = normalizePreparedFile(result.file);
if (!hasPreparedFileLocator(prepared)) {
return failedDraftFile(pendingFile, "preparation_failed");
}
return {
id: pendingFile.id,
name: prepared.name || pendingFile.name,
...(prepared.mimeType
? { mimeType: prepared.mimeType }
: pendingFile.mimeType
? { mimeType: pendingFile.mimeType }
: {}),
...(prepared.path ? { path: prepared.path } : {}),
...(prepared.url ? { url: prepared.url } : {}),
...(prepared.uri ? { uri: prepared.uri } : {}),
...(prepared.assetId ? { assetId: prepared.assetId } : {}),
...(prepared.uploadStatus
? { uploadStatus: prepared.uploadStatus }
: {}),
...(typeof prepared.sizeBytes === "number"
? { sizeBytes: prepared.sizeBytes }
: typeof pendingFile.sizeBytes === "number"
? { sizeBytes: pendingFile.sizeBytes }
: {}),
uploading: false
};
});
}
};
}
export function agentPreparedPromptFileToDraftFile(
file: AgentPromptContentBlock & { type: "file" },
idPrefix: string,
index: number
): AgentComposerDraftFile {
return {
id: `${idPrefix}:file:${index}`,
name: file.name?.trim() || `file-${index + 1}`,
mimeType: file.mimeType,
path: file.path,
hostPath: file.hostPath,
url: file.url,
uri: file.uri,
assetId: file.assetId,
uploadStatus: file.uploadStatus,
sizeBytes: file.sizeBytes
};
}
export function agentPromptContentToComposerPrompt(
content: readonly AgentPromptContentBlock[],
files: readonly AgentComposerDraftFile[]
): string {
let fileIndex = 0;
const parts: string[] = [];
for (const block of content) {
if (block.type === "text") {
parts.push(block.text?.trim() ?? "");
continue;
}
if (block.type !== "file" || block.kind === AGENT_PASTED_TEXT_BLOCK_KIND) {
continue;
}
const file = files[fileIndex];
fileIndex += 1;
if (!file) continue;
parts.push(
createAgentComposerFileMentionMarkdown({
id: file.id,
name: file.name,
status: "ready"
})
);
}
return parts.join("");
}
export function agentPromptFileBlocks(
content: readonly AgentPromptContentBlock[]
): Array<AgentPromptContentBlock & { type: "file" }> {
return content.filter(
(block): block is AgentPromptContentBlock & { type: "file" } =>
block.type === "file" &&
block.kind !== AGENT_PASTED_TEXT_BLOCK_KIND &&
(typeof block.path === "string" ||
typeof block.hostPath === "string" ||
typeof block.url === "string" ||
typeof block.uri === "string" ||
typeof block.assetId === "string")
);
}
export function agentPromptPastedTextBlocks(
content: readonly AgentPromptContentBlock[]
): Array<AgentPromptContentBlock & { type: "file" }> {
return content.filter(
(block): block is AgentPromptContentBlock & { type: "file" } =>
block.type === "file" &&
block.kind === AGENT_PASTED_TEXT_BLOCK_KIND &&
(typeof block.path === "string" || typeof block.url === "string")
);
}
export function materializeAgentComposerFileMentions(
prompt: string,
files: readonly AgentComposerDraftFile[]
): string {
const references = agentComposerFileMentionReferences(prompt);
if (references.length === 0) return prompt;
const fileById = new Map(files.map((file) => [file.id, file]));
let result = "";
let cursor = 0;
for (const reference of references) {
result += prompt.slice(cursor, reference.start);
const file = fileById.get(reference.id);
if (file && !file.uploading && !file.uploadError) {
const locator = file.path?.trim() || file.url?.trim() || "";
if (locator) {
result += formatAgentFileMentionMarkdown(file.name, locator);
}
}
cursor = reference.end;
}
return result + prompt.slice(cursor);
}
type NormalizedPreparedExternalPromptFile =
AgentPreparedExternalPromptFileMetadata & {
path?: string;
url?: string;
};
function normalizePreparedFile(
file: AgentPreparedExternalPromptFile
): NormalizedPreparedExternalPromptFile {
return {
name: file.name.trim(),
...(file.mimeType?.trim() ? { mimeType: file.mimeType.trim() } : {}),
...(file.path?.trim() ? { path: file.path.trim() } : {}),
...(file.url?.trim() ? { url: file.url.trim() } : {}),
...(file.uri?.trim() ? { uri: file.uri.trim() } : {}),
...(file.assetId?.trim() ? { assetId: file.assetId.trim() } : {}),
...(file.uploadStatus?.trim()
? { uploadStatus: file.uploadStatus.trim() }
: {}),
...(typeof file.sizeBytes === "number" && Number.isFinite(file.sizeBytes)
? { sizeBytes: file.sizeBytes }
: {})
};
}
function hasPreparedFileLocator(
file: NormalizedPreparedExternalPromptFile
): boolean {
return Boolean(file.path || file.url);
}
function failedDraftFile(
file: AgentComposerDraftFile,
errorCode: AgentExternalPromptFilePreparationErrorCode,
options: { retryable?: boolean } = {}
): AgentComposerDraftFile {
return {
...file,
uploading: false,
uploadError: errorCode,
uploadErrorCode: errorCode,
...(typeof options.retryable === "boolean"
? { uploadRetryable: options.retryable }
: {})
};
}
export function agentExternalPromptFileErrorI18nKey(
errorCode: string | null | undefined
):
| "agentHost.agentGui.composerFileFolderUnsupported"
| "agentHost.agentGui.composerFilePreparationFailed"
| "agentHost.agentGui.composerFileTooLarge" {
switch (errorCode) {
case "file_too_large":
return "agentHost.agentGui.composerFileTooLarge";
case "folder_unsupported":
return "agentHost.agentGui.composerFileFolderUnsupported";
default:
return "agentHost.agentGui.composerFilePreparationFailed";
}
}