diff --git a/src/core/tools/__tests__/searchFilesTool.spec.ts b/src/core/tools/__tests__/searchFilesTool.spec.ts new file mode 100644 index 0000000000..7d19457208 --- /dev/null +++ b/src/core/tools/__tests__/searchFilesTool.spec.ts @@ -0,0 +1,300 @@ +import path from "path" +import { describe, it, expect, beforeEach, vi, type Mock, type MockedFunction } from "vitest" +import { searchFilesTool } from "../searchFilesTool" +import { Task } from "../../task/Task" +import { SearchFilesToolUse } from "../../../shared/tools" +import { isPathOutsideWorkspace } from "../../../utils/pathUtils" +import { regexSearchFiles } from "../../../services/ripgrep" +import { RooIgnoreController } from "../../ignore/RooIgnoreController" + +// Mock dependencies +vi.mock("../../../utils/pathUtils", () => ({ + isPathOutsideWorkspace: vi.fn(), +})) + +vi.mock("../../../services/ripgrep", () => ({ + regexSearchFiles: vi.fn(), +})) + +vi.mock("../../../utils/path", () => ({ + getReadablePath: vi.fn((cwd: string, relPath: string) => relPath), +})) + +vi.mock("../../ignore/RooIgnoreController") + +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string, params?: any) => { + if (key === "tools:searchFiles.workspaceBoundaryError") { + return `Cannot search outside workspace. Path '${params?.path}' is outside the current workspace.` + } + return key + }), +})) + +const mockedIsPathOutsideWorkspace = isPathOutsideWorkspace as MockedFunction +const mockedRegexSearchFiles = regexSearchFiles as MockedFunction + +describe("searchFilesTool", () => { + let mockTask: Partial + let mockAskApproval: Mock + let mockHandleError: Mock + let mockPushToolResult: Mock + let mockRemoveClosingTag: Mock + + beforeEach(() => { + vi.clearAllMocks() + + mockTask = { + cwd: "/workspace", + consecutiveMistakeCount: 0, + recordToolError: vi.fn(), + sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"), + say: vi.fn().mockResolvedValue(undefined), + rooIgnoreController: new RooIgnoreController("/workspace"), + } + + mockAskApproval = vi.fn().mockResolvedValue(true) + mockHandleError = vi.fn() + mockPushToolResult = vi.fn() + mockRemoveClosingTag = vi.fn((tag: string, value: string | undefined) => value || "") + + mockedRegexSearchFiles.mockResolvedValue("Search results") + }) + + describe("workspace boundary validation", () => { + it("should allow search within workspace", async () => { + const block: SearchFilesToolUse = { + type: "tool_use", + name: "search_files", + params: { + path: "src", + regex: "test", + file_pattern: "*.ts", + }, + partial: false, + } + + mockedIsPathOutsideWorkspace.mockReturnValue(false) + + await searchFilesTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockedIsPathOutsideWorkspace).toHaveBeenCalledWith(path.resolve("/workspace", "src")) + expect(mockedRegexSearchFiles).toHaveBeenCalled() + expect(mockPushToolResult).toHaveBeenCalledWith("Search results") + }) + + it("should block search outside workspace", async () => { + const block: SearchFilesToolUse = { + type: "tool_use", + name: "search_files", + params: { + path: "../external", + regex: "test", + file_pattern: "*.ts", + }, + partial: false, + } + + mockedIsPathOutsideWorkspace.mockReturnValue(true) + + await searchFilesTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockedIsPathOutsideWorkspace).toHaveBeenCalledWith(path.resolve("/workspace", "../external")) + expect(mockedRegexSearchFiles).not.toHaveBeenCalled() + expect(mockTask.say).toHaveBeenCalledWith( + "error", + "Cannot search outside workspace. Path '../external' is outside the current workspace.", + ) + expect(mockPushToolResult).toHaveBeenCalledWith( + "Cannot search outside workspace. Path '../external' is outside the current workspace.", + ) + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("search_files") + }) + + it("should block search with absolute path outside workspace", async () => { + const block: SearchFilesToolUse = { + type: "tool_use", + name: "search_files", + params: { + path: "/etc/passwd", + regex: "root", + }, + partial: false, + } + + mockedIsPathOutsideWorkspace.mockReturnValue(true) + + await searchFilesTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockedIsPathOutsideWorkspace).toHaveBeenCalledWith(path.resolve("/workspace", "/etc/passwd")) + expect(mockedRegexSearchFiles).not.toHaveBeenCalled() + expect(mockTask.say).toHaveBeenCalledWith( + "error", + "Cannot search outside workspace. Path '/etc/passwd' is outside the current workspace.", + ) + expect(mockPushToolResult).toHaveBeenCalledWith( + "Cannot search outside workspace. Path '/etc/passwd' is outside the current workspace.", + ) + }) + + it("should handle relative paths that resolve outside workspace", async () => { + const block: SearchFilesToolUse = { + type: "tool_use", + name: "search_files", + params: { + path: "../../..", + regex: "sensitive", + }, + partial: false, + } + + mockedIsPathOutsideWorkspace.mockReturnValue(true) + + await searchFilesTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockedIsPathOutsideWorkspace).toHaveBeenCalledWith(path.resolve("/workspace", "../../..")) + expect(mockedRegexSearchFiles).not.toHaveBeenCalled() + expect(mockTask.say).toHaveBeenCalledWith( + "error", + "Cannot search outside workspace. Path '../../..' is outside the current workspace.", + ) + expect(mockPushToolResult).toHaveBeenCalledWith( + "Cannot search outside workspace. Path '../../..' is outside the current workspace.", + ) + }) + }) + + describe("existing functionality", () => { + beforeEach(() => { + mockedIsPathOutsideWorkspace.mockReturnValue(false) + }) + + it("should handle missing path parameter", async () => { + const block: SearchFilesToolUse = { + type: "tool_use", + name: "search_files", + params: { + regex: "test", + }, + partial: false, + } + + await searchFilesTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("search_files", "path") + expect(mockedRegexSearchFiles).not.toHaveBeenCalled() + }) + + it("should handle missing regex parameter", async () => { + const block: SearchFilesToolUse = { + type: "tool_use", + name: "search_files", + params: { + path: "src", + }, + partial: false, + } + + await searchFilesTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("search_files", "regex") + expect(mockedRegexSearchFiles).not.toHaveBeenCalled() + }) + + it("should handle partial blocks", async () => { + const block: SearchFilesToolUse = { + type: "tool_use", + name: "search_files", + params: { + path: "src", + regex: "test", + }, + partial: true, + } + + const mockAsk = vi.fn() + mockTask.ask = mockAsk + + await searchFilesTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockAsk).toHaveBeenCalled() + expect(mockedRegexSearchFiles).not.toHaveBeenCalled() + }) + + it("should handle user rejection", async () => { + const block: SearchFilesToolUse = { + type: "tool_use", + name: "search_files", + params: { + path: "src", + regex: "test", + }, + partial: false, + } + + mockAskApproval.mockResolvedValue(false) + + await searchFilesTool( + mockTask as Task, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockedRegexSearchFiles).toHaveBeenCalled() + expect(mockPushToolResult).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/core/tools/searchFilesTool.ts b/src/core/tools/searchFilesTool.ts index 6528f20d54..c006b3b3a9 100644 --- a/src/core/tools/searchFilesTool.ts +++ b/src/core/tools/searchFilesTool.ts @@ -4,7 +4,9 @@ import { Task } from "../task/Task" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" +import { isPathOutsideWorkspace } from "../../utils/pathUtils" import { regexSearchFiles } from "../../services/ripgrep" +import { t } from "../../i18n" export async function searchFilesTool( cline: Task, @@ -49,6 +51,17 @@ export async function searchFilesTool( const absolutePath = path.resolve(cline.cwd, relDirPath) + // Check if path is outside workspace + if (isPathOutsideWorkspace(absolutePath)) { + const userErrorMessage = t("tools:searchFiles.workspaceBoundaryError", { path: relDirPath }) + const llmErrorMessage = `Cannot search outside workspace. Path '${relDirPath}' is outside the current workspace.` + cline.consecutiveMistakeCount++ + cline.recordToolError("search_files") + await cline.say("error", userErrorMessage) + pushToolResult(llmErrorMessage) + return + } + const results = await regexSearchFiles( cline.cwd, absolutePath, diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 0fe673310f..ba42ca420d 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent.", "codebaseSearch": { "approval": "Cercant '{{query}}' a la base de codi..." + }, + "searchFiles": { + "workspaceBoundaryError": "No es pot cercar fora de l'espai de treball. El camí '{{path}}' està fora de l'espai de treball actual." } } diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index 03c491c115..64dd24b53d 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Roo zu einem anderen Ansatz zu führen.", "codebaseSearch": { "approval": "Suche nach '{{query}}' im Codebase..." + }, + "searchFiles": { + "workspaceBoundaryError": "Kann nicht außerhalb des Arbeitsbereichs suchen. Pfad '{{path}}' liegt außerhalb des aktuellen Arbeitsbereichs." } } diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 9932fc4d06..bb298a32e6 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo appears to be stuck in a loop, attempting the same action ({{toolName}}) repeatedly. This might indicate a problem with its current strategy. Consider rephrasing the task, providing more specific instructions, or guiding it towards a different approach.", "codebaseSearch": { "approval": "Searching for '{{query}}' in codebase..." + }, + "searchFiles": { + "workspaceBoundaryError": "Cannot search outside workspace. Path '{{path}}' is outside the current workspace." } } diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 0dbba751b7..14683a3a1e 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente.", "codebaseSearch": { "approval": "Buscando '{{query}}' en la base de código..." + }, + "searchFiles": { + "workspaceBoundaryError": "No se puede buscar fuera del espacio de trabajo. La ruta '{{path}}' está fuera del espacio de trabajo actual." } } diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index bdf26fb3cb..d700abbf5e 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente.", "codebaseSearch": { "approval": "Recherche de '{{query}}' dans la base de code..." + }, + "searchFiles": { + "workspaceBoundaryError": "Impossible de rechercher en dehors de l'espace de travail. Le chemin '{{path}}' est en dehors de l'espace de travail actuel." } } diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 257fc8a531..8985451365 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।", "codebaseSearch": { "approval": "कोडबेस में '{{query}}' खोज रहा है..." + }, + "searchFiles": { + "workspaceBoundaryError": "वर्कस्पेस के बाहर खोज नहीं की जा सकती। पथ '{{path}}' वर्तमान वर्कस्पेस के बाहर है।" } } diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index 0dc14f94a5..8ccca383e5 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso.", "codebaseSearch": { "approval": "Ricerca di '{{query}}' nella base di codice..." + }, + "searchFiles": { + "workspaceBoundaryError": "Impossibile cercare al di fuori dell'area di lavoro. Il percorso '{{path}}' è al di fuori dell'area di lavoro corrente." } } diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index ad6b7019c8..2cce55b41c 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Rooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。", "codebaseSearch": { "approval": "コードベースで '{{query}}' を検索中..." + }, + "searchFiles": { + "workspaceBoundaryError": "ワークスペース外では検索できません。パス '{{path}}' は現在のワークスペース外にあります。" } } diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index c8c8deebec..80a3974fbc 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.", "codebaseSearch": { "approval": "코드베이스에서 '{{query}}' 검색 중..." + }, + "searchFiles": { + "workspaceBoundaryError": "워크스페이스 외부에서는 검색할 수 없습니다. 경로 '{{path}}'는 현재 워크스페이스 외부에 있습니다." } } diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index 8779caaf38..ec8bc50db4 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Roo naar een andere aanpak te leiden.", "codebaseSearch": { "approval": "Zoeken naar '{{query}}' in codebase..." + }, + "searchFiles": { + "workspaceBoundaryError": "Kan niet zoeken buiten de werkruimte. Pad '{{path}}' ligt buiten de huidige werkruimte." } } diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index 1cfb8d59de..37b2767498 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Wygląda na to, że Roo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście.", "codebaseSearch": { "approval": "Wyszukiwanie '{{query}}' w bazie kodu..." + }, + "searchFiles": { + "workspaceBoundaryError": "Nie można wyszukiwać poza obszarem roboczym. Ścieżka '{{path}}' znajduje się poza bieżącym obszarem roboczym." } } diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 9c03e6082f..0ac2728690 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente.", "codebaseSearch": { "approval": "Pesquisando '{{query}}' na base de código..." + }, + "searchFiles": { + "workspaceBoundaryError": "Não é possível pesquisar fora do espaço de trabalho. O caminho '{{path}}' está fora do espaço de trabalho atual." } } diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index 42705f5ec3..d7471d331f 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.", "codebaseSearch": { "approval": "Поиск '{{query}}' в кодовой базе..." + }, + "searchFiles": { + "workspaceBoundaryError": "Невозможно выполнить поиск за пределами рабочего пространства. Путь '{{path}}' находится за пределами текущего рабочего пространства." } } diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index 4dff83eac4..7f90877e11 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün.", "codebaseSearch": { "approval": "Kod tabanında '{{query}}' aranıyor..." + }, + "searchFiles": { + "workspaceBoundaryError": "Çalışma alanı dışında arama yapılamaz. '{{path}}' yolu mevcut çalışma alanının dışında." } } diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index 67d83f90fc..2cdd610ced 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Roo theo một cách tiếp cận khác.", "codebaseSearch": { "approval": "Đang tìm kiếm '{{query}}' trong cơ sở mã..." + }, + "searchFiles": { + "workspaceBoundaryError": "Không thể tìm kiếm bên ngoài không gian làm việc. Đường dẫn '{{path}}' nằm ngoài không gian làm việc hiện tại." } } diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index 9328251d05..301812d919 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。", "codebaseSearch": { "approval": "正在搜索代码库中的 '{{query}}'..." + }, + "searchFiles": { + "workspaceBoundaryError": "无法在工作区外搜索。路径 '{{path}}' 位于当前工作区外。" } } diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 04b16c2bc7..10d5b32c1e 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -7,5 +7,8 @@ "toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。", "codebaseSearch": { "approval": "正在搜尋程式碼庫中的「{{query}}」..." + }, + "searchFiles": { + "workspaceBoundaryError": "無法在工作區外搜尋。路徑「{{path}}」位於目前工作區外。" } }