返回 DeepSeek-Reasonix
fileResource.ts
根目录 / desktop / frontend / src / lib / fileResource.ts
1 import { app } from "./bridge";
2 import { pathExtension } from "./filePaths";
3
4 /** Where a file reference came from: an agent presentation, the workspace, or verified answer text. */
5 export type FileResourceSource = "presented" | "workspace" | "reference";
6
7 type ResourceBase = { hostId: string; tabId: string; path: string };
8
9 /** What a caller knows about a file: host, session, path, origin and tool call. */
10 export type FileResourceRef =
11 | (ResourceBase & { source: "presented"; toolCallId: string })
12 | (ResourceBase & { source: "workspace"; toolCallId?: string })
13 // An answer-named path has no standing authorization. Every read and action
14 // goes through the reference endpoints, which resolve it again for this tab.
15 | (ResourceBase & { source: "reference" });
16
17 /**
18 * The credentials a single read must present. Captured from the command that
19 * asks for the read and never inherited by a later entry point: a workspace
20 * reference carries no presented tool scope even when the same path was
21 * presented earlier, so a path alone cannot re-grant an earlier presentation.
22 */
23 export type FileAccessContext = Readonly<{
24 source: FileResourceSource;
25 /** Session tab whose scope authorizes the read. */
26 tabId: string;
27 /** Presented tool call; absent for workspace reads. */
28 toolCallId?: string;
29 }>;
30
31 /** A file the backend entry point confirmed, with the context that reads it. */
32 export type ResolvedFileResource = Readonly<{
33 hostId: string;
34 /** Path the read entry points accept. */
35 path: string;
36 /** Stable backend-resolved coordinate used only for resource identity. */
37 identityPath: string;
38 /** Path as the caller supplied it, for display and tree reveal. */
39 requestedPath: string;
40 access: FileAccessContext;
41 }>;
42
43 export function fileAccessContext(ref: FileResourceRef): FileAccessContext {
44 if (ref.source === "presented") return { source: "presented", tabId: ref.tabId, toolCallId: ref.toolCallId };
45 return { source: ref.source, tabId: ref.tabId };
46 }
47
48 export const sameAccessContext = (left: FileAccessContext, right: FileAccessContext): boolean =>
49 left.source === right.source && left.tabId === right.tabId && left.toolCallId === right.toolCallId;
50
51 /**
52 * Resolve a caller spelling into the stable coordinate that identifies the
53 * file. The original local spelling remains the read path because it may be an
54 * external-folder token or a presented path whose access must be revalidated by
55 * its scoped read entry point. Remote reads accept the resolved host coordinate.
56 */
57 export async function resolveFileResource(ref: FileResourceRef): Promise<ResolvedFileResource> {
58 const access = fileAccessContext(ref);
59 const identityPath = await resolveFileResourcePath(ref);
60 return {
61 hostId: ref.hostId,
62 path: ref.hostId === "local" ? ref.path : identityPath,
63 identityPath,
64 requestedPath: ref.path,
65 access,
66 };
67 }
68
69 /** Absolute path for copy-to-clipboard and save-a-copy, where display needs one. */
70 export function resolveFileResourcePath(ref: FileResourceRef): Promise<string> {
71 if (ref.hostId !== "local") {
72 if (ref.source === "reference") return Promise.resolve(ref.path);
73 return ref.source === "presented"
74 ? app.ResolveRemotePresentedPathForTab(ref.tabId, ref.hostId, ref.toolCallId, ref.path)
75 : app.ResolveRemoteWorkspacePathForTab(ref.tabId, ref.hostId, ref.toolCallId ?? "", ref.path);
76 }
77 if (ref.source === "reference") return app.ResolveReferencePathForTab(ref.tabId, ref.path);
78 return ref.source === "presented"
79 ? app.ResolvePresentedPathForTab(ref.tabId, ref.toolCallId, ref.path)
80 : app.ResolveWorkspacePathForTab(ref.tabId, ref.path);
81 }
82
83 const MEDIA = new Set(["html", "htm", "pdf", "png", "jpg", "jpeg", "gif", "webp", "bmp", "ico", "svg", "mp3", "wav", "ogg", "m4a", "aac", "mp4", "webm", "mov", "m4v", "ogv"]);
84 // SVG is an image to the previewer and a document to the editor, so both views
85 // are legitimate.
86 const BINARY = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "ico", "pdf", "mp3", "wav", "ogg", "m4a", "aac", "flac", "mp4", "webm", "mov", "m4v", "ogv", "zip", "tar", "gz", "7z", "rar", "doc", "docx", "xls", "xlsx", "ppt", "pptx"]);
87
88 export interface FileResourceCapabilities {
89 preview: boolean;
90 source: boolean;
91 browser: boolean;
92 revealTree: boolean;
93 copyPath: boolean;
94 openNative: boolean;
95 revealNative: boolean;
96 saveCopy: boolean;
97 }
98
99 /** Identity-only view of a file: capabilities depend on the host and the name. */
100 export type FileResourceIdentity = Readonly<{ hostId: string; path: string }>;
101
102 export const fileResourceIdentity = (resource: FileResourceIdentity): FileResourceIdentity =>
103 ({ hostId: resource.hostId, path: resource.path });
104
105 /** Maps the host's verified action list onto the menu capability shape. */
106 export function capabilityActions(actions: readonly string[]): FileResourceCapabilities {
107 const has = (action: string) => actions.includes(action);
108 return {
109 preview: has("preview"),
110 source: has("source"),
111 browser: has("browser"),
112 revealTree: has("reveal-tree"),
113 copyPath: has("copy-path"),
114 openNative: has("open-native"),
115 revealNative: has("reveal-native"),
116 saveCopy: has("save-copy"),
117 };
118 }
119
120 /** The host still revalidates every action; this snapshot only controls honest UI affordances. */
121 export function fileResourceCapabilities(resource: FileResourceIdentity, verified?: readonly string[]): FileResourceCapabilities {
122 if (verified) return capabilityActions(verified);
123 const remote = resource.hostId !== "local";
124 const ext = pathExtension(resource.path);
125 return {
126 preview: true,
127 source: !BINARY.has(ext),
128 browser: !remote && MEDIA.has(ext),
129 revealTree: true,
130 copyPath: true,
131 openNative: !remote,
132 revealNative: !remote,
133 saveCopy: true,
134 };
135 }
136
136 lines TYPESCRIPT