11import type { ToolDefinition } from "@App/app/service/agent/types" ;
22import type { ToolExecutor } from "@App/app/service/agent/tool_registry" ;
3+ import {
4+ sanitizePath ,
5+ getDirectory ,
6+ getWorkspaceRoot ,
7+ splitPath ,
8+ writeWorkspaceFile ,
9+ } from "@App/app/service/agent/opfs_helpers" ;
310
4- const WORKSPACE_ROOT = "agents/workspace" ;
5-
6- /** Strip leading `/`, reject `..` segments */
7- export function sanitizePath ( raw : string ) : string {
8- const stripped = raw . replace ( / ^ \/ + / , "" ) ;
9- const segments = stripped . split ( "/" ) . filter ( Boolean ) ;
10- for ( const seg of segments ) {
11- if ( seg === ".." ) {
12- throw new Error ( `Invalid path: ".." is not allowed` ) ;
13- }
14- }
15- return segments . join ( "/" ) ;
16- }
17-
18- /** Navigate into nested directories, creating them as needed */
19- async function getDirectory (
20- root : FileSystemDirectoryHandle ,
21- path : string ,
22- create = false
23- ) : Promise < FileSystemDirectoryHandle > {
24- const segments = path . split ( "/" ) . filter ( Boolean ) ;
25- let dir = root ;
26- for ( const seg of segments ) {
27- dir = await dir . getDirectoryHandle ( seg , { create } ) ;
28- }
29- return dir ;
30- }
31-
32- /** Get the workspace root directory handle */
33- async function getWorkspaceRoot ( create = false ) : Promise < FileSystemDirectoryHandle > {
34- const opfsRoot = await navigator . storage . getDirectory ( ) ;
35- return getDirectory ( opfsRoot , WORKSPACE_ROOT , create ) ;
36- }
37-
38- /** Split a sanitized path into parent directory path and filename */
39- function splitPath ( sanitized : string ) : { dirPath : string ; fileName : string } {
40- const lastSlash = sanitized . lastIndexOf ( "/" ) ;
41- if ( lastSlash === - 1 ) {
42- return { dirPath : "" , fileName : sanitized } ;
43- }
44- return {
45- dirPath : sanitized . substring ( 0 , lastSlash ) ,
46- fileName : sanitized . substring ( lastSlash + 1 ) ,
47- } ;
48- }
11+ // re-export sanitizePath 供外部使用
12+ export { sanitizePath } ;
4913
5014// ---- Tool Definitions ----
5115
@@ -64,11 +28,18 @@ const OPFS_WRITE_DEFINITION: ToolDefinition = {
6428
6529const OPFS_READ_DEFINITION : ToolDefinition = {
6630 name : "opfs_read" ,
67- description : "Read text content from a file in the workspace." ,
31+ description :
32+ "Read a file from the workspace. For text files returns content. For binary files use format='bloburl' to get a blob URL usable in executeScript (ISOLATED world)." ,
6833 parameters : {
6934 type : "object" ,
7035 properties : {
7136 path : { type : "string" , description : "File path relative to workspace root" } ,
37+ format : {
38+ type : "string" ,
39+ enum : [ "text" , "bloburl" ] ,
40+ description :
41+ "Output format: 'text' (default) returns file content as string; 'bloburl' returns a blob:chrome-extension:// URL for binary files (usable in executeScript ISOLATED world)" ,
42+ } ,
7243 } ,
7344 required : [ "path" ] ,
7445 } ,
@@ -97,41 +68,76 @@ const OPFS_DELETE_DEFINITION: ToolDefinition = {
9768 } ,
9869} ;
9970
71+ // ---- blob URL 创建(通过 Offscreen) ----
72+
73+ // 创建 blob URL 的回调,由外部注入(Offscreen 通道)
74+ type CreateBlobUrlFn = ( data : ArrayBuffer , mimeType : string ) => Promise < string > ;
75+ let createBlobUrlFn : CreateBlobUrlFn | null = null ;
76+
77+ /** 注入 Offscreen blob URL 创建函数 */
78+ export function setCreateBlobUrlFn ( fn : CreateBlobUrlFn ) : void {
79+ createBlobUrlFn = fn ;
80+ }
81+
82+ /** 根据文件扩展名推断 MIME 类型 */
83+ function guessMimeType ( path : string ) : string {
84+ const ext = path . split ( "." ) . pop ( ) ?. toLowerCase ( ) || "" ;
85+ const map : Record < string , string > = {
86+ jpg : "image/jpeg" ,
87+ jpeg : "image/jpeg" ,
88+ png : "image/png" ,
89+ gif : "image/gif" ,
90+ webp : "image/webp" ,
91+ svg : "image/svg+xml" ,
92+ mp3 : "audio/mpeg" ,
93+ wav : "audio/wav" ,
94+ mp4 : "video/mp4" ,
95+ pdf : "application/pdf" ,
96+ json : "application/json" ,
97+ txt : "text/plain" ,
98+ html : "text/html" ,
99+ css : "text/css" ,
100+ js : "application/javascript" ,
101+ } ;
102+ return map [ ext ] || "application/octet-stream" ;
103+ }
104+
100105// ---- Factory ----
101106
102107export function createOPFSTools ( ) : {
103108 tools : Array < { definition : ToolDefinition ; executor : ToolExecutor } > ;
104109} {
105110 const writeExecutor : ToolExecutor = {
106111 execute : async ( args : Record < string , unknown > ) => {
107- const safePath = sanitizePath ( args . path as string ) ;
108- const content = args . content as string ;
109- if ( ! safePath ) throw new Error ( "path is required" ) ;
110-
111- const workspace = await getWorkspaceRoot ( true ) ;
112- const { dirPath, fileName } = splitPath ( safePath ) ;
113- const dir = dirPath ? await getDirectory ( workspace , dirPath , true ) : workspace ;
114- const fileHandle = await dir . getFileHandle ( fileName , { create : true } ) ;
115- const writable = await fileHandle . createWritable ( ) ;
116- await writable . write ( content ) ;
117- await writable . close ( ) ;
118-
119- return JSON . stringify ( { path : safePath , size : new Blob ( [ content ] ) . size } ) ;
112+ const result = await writeWorkspaceFile ( args . path as string , args . content as string ) ;
113+ return JSON . stringify ( result ) ;
120114 } ,
121115 } ;
122116
123117 const readExecutor : ToolExecutor = {
124118 execute : async ( args : Record < string , unknown > ) => {
125119 const safePath = sanitizePath ( args . path as string ) ;
126120 if ( ! safePath ) throw new Error ( "path is required" ) ;
121+ const format = ( args . format as string ) || "text" ;
127122
128123 const workspace = await getWorkspaceRoot ( ) ;
129124 const { dirPath, fileName } = splitPath ( safePath ) ;
130125 const dir = dirPath ? await getDirectory ( workspace , dirPath ) : workspace ;
131126 const fileHandle = await dir . getFileHandle ( fileName ) ;
132127 const file = await fileHandle . getFile ( ) ;
133- const content = await file . text ( ) ;
134128
129+ if ( format === "bloburl" ) {
130+ if ( ! createBlobUrlFn ) {
131+ throw new Error ( "Blob URL creation not available (Offscreen not initialized)" ) ;
132+ }
133+ const arrayBuffer = await file . arrayBuffer ( ) ;
134+ const mimeType = guessMimeType ( safePath ) ;
135+ const blobUrl = await createBlobUrlFn ( arrayBuffer , mimeType ) ;
136+ return JSON . stringify ( { path : safePath , blobUrl, size : file . size , mimeType } ) ;
137+ }
138+
139+ // 默认 text 模式
140+ const content = await file . text ( ) ;
135141 return JSON . stringify ( { path : safePath , content, size : file . size } ) ;
136142 } ,
137143 } ;
@@ -147,8 +153,8 @@ export function createOPFSTools(): {
147153 const entries : Array < { name : string ; type : "file" | "directory" ; size ?: number } > = [ ] ;
148154 for await ( const [ name , handle ] of dir as unknown as AsyncIterable < [ string , FileSystemHandle ] > ) {
149155 if ( handle . kind === "file" ) {
150- const file = await ( handle as FileSystemFileHandle ) . getFile ( ) ;
151- entries . push ( { name, type : "file" , size : file . size } ) ;
156+ const f = await ( handle as FileSystemFileHandle ) . getFile ( ) ;
157+ entries . push ( { name, type : "file" , size : f . size } ) ;
152158 } else {
153159 entries . push ( { name, type : "directory" } ) ;
154160 }
0 commit comments