| 131 | // ---- Factory ---- |
| 132 | |
| 133 | export function createOPFSTools(): { |
| 134 | tools: Array<{ definition: ToolDefinition; executor: ToolExecutor }>; |
| 135 | } { |
| 136 | const writeExecutor: ToolExecutor = { |
| 137 | execute: async (args: Record<string, unknown>) => { |
| 138 | const path = requireString(args, "path"); |
| 139 | const result = await writeWorkspaceFile(path, args.content as string | Blob); |
| 140 | return JSON.stringify(result); |
| 141 | }, |
| 142 | }; |
| 143 | |
| 144 | const readExecutor: ToolExecutor = { |
| 145 | execute: async (args: Record<string, unknown>) => { |
| 146 | const safePath = sanitizePath(requireString(args, "path")); |
| 147 | if (!safePath) throw new Error("path is required"); |
| 148 | |
| 149 | const workspace = await getWorkspaceRoot(); |
| 150 | const { dirPath, fileName } = splitPath(safePath); |
| 151 | const dir = dirPath ? await getDirectory(workspace, dirPath) : workspace; |
| 152 | const fileHandle = await dir.getFileHandle(fileName); |
| 153 | const file = await fileHandle.getFile(); |
| 154 | const mimeType = guessMimeType(safePath); |
| 155 | const arrayBuffer = await file.arrayBuffer(); |
| 156 | |
| 157 | // 确定返回模式:auto 通过内容字节检测文本/二进制 |
| 158 | const mode = (args.mode as string) || "auto"; |
| 159 | const useText = mode === "text" || (mode === "auto" && isText(new Uint8Array(arrayBuffer))); |
| 160 | |
| 161 | // blob 模式:返回 blob URL |
| 162 | if (!useText) { |
| 163 | if (!createBlobUrlFn) { |
| 164 | throw new Error("Blob URL creation not available (Offscreen not initialized)"); |
| 165 | } |
| 166 | const blobUrl = await createBlobUrlFn(arrayBuffer, mimeType); |
| 167 | return JSON.stringify({ path: safePath, blobUrl, size: file.size, mimeType, type: "binary" }); |
| 168 | } |
| 169 | |
| 170 | // text 模式:返回文本内容 |
| 171 | const text = new TextDecoder().decode(arrayBuffer); |
| 172 | const lines = text.split("\n"); |
| 173 | const totalLines = lines.length; |
| 174 | |
| 175 | const offset = typeof args.offset === "number" ? args.offset : undefined; |
| 176 | const limit = typeof args.limit === "number" ? args.limit : undefined; |
| 177 | |
| 178 | // 超过行数限制且未指定分页参数,报错要求分段读取 |
| 179 | if (offset == null && limit == null && totalLines > MAX_TEXT_LINES) { |
| 180 | throw new Error( |
| 181 | `文件共 ${totalLines} 行,超过单次读取上限(${MAX_TEXT_LINES} 行)。` + |
| 182 | `请使用 offset 和 limit 参数分段读取,例如:offset=1, limit=${MAX_TEXT_LINES}` |
| 183 | ); |
| 184 | } |
| 185 | |
| 186 | const startLine = offset != null ? Math.max(1, offset) : 1; |
| 187 | const endLine = limit != null ? Math.min(totalLines, startLine + limit - 1) : totalLines; |
| 188 | const selectedLines = lines.slice(startLine - 1, endLine); |
| 189 | const content = selectedLines.join("\n"); |
| 190 | |