diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 1481d5acad..6e916b7d09 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -91,6 +91,9 @@ export enum IpcChannel { // Python Python_Execute = 'python:execute', + // JavaScript + Js_Execute = 'js:execute', + //copilot Copilot_GetAuthMessage = 'copilot:get-auth-message', Copilot_GetCopilotToken = 'copilot:get-copilot-token', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 275b3df4f9..37770ce180 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -27,6 +27,7 @@ import DxtService from './services/DxtService' import { ExportService } from './services/ExportService' import { fileStorage as fileManager } from './services/FileStorage' import FileService from './services/FileSystemService' +import { jsService } from './services/JsService' import KnowledgeService from './services/KnowledgeService' import mcpService from './services/MCPService' import MemoryService from './services/memory/MemoryService' @@ -709,6 +710,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { } ) + // Register JavaScript execution handler + ipcMain.handle(IpcChannel.Js_Execute, async (_, code: string, timeout?: number) => { + return await jsService.executeScript(code, { timeout }) + }) + ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name)) ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name)) ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js')) diff --git a/src/preload/index.ts b/src/preload/index.ts index 9244076a9a..1626ba8483 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -343,6 +343,9 @@ const api = { execute: (script: string, context?: Record, timeout?: number) => ipcRenderer.invoke(IpcChannel.Python_Execute, script, context, timeout) }, + js: { + execute: (code: string, timeout?: number) => ipcRenderer.invoke(IpcChannel.Js_Execute, code, timeout) + }, shell: { openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options) }, diff --git a/src/renderer/src/components/CodeBlockView/view.tsx b/src/renderer/src/components/CodeBlockView/view.tsx index a89e545bc9..f9e99044fd 100644 --- a/src/renderer/src/components/CodeBlockView/view.tsx +++ b/src/renderer/src/components/CodeBlockView/view.tsx @@ -87,7 +87,8 @@ export const CodeBlockView: React.FC = memo(({ children, language, onSave const [tools, setTools] = useState([]) const isExecutable = useMemo(() => { - return codeExecution.enabled && language === 'python' + const executableLanguages = ['python', 'javascript', 'js', 'typescript', 'ts'] + return codeExecution.enabled && executableLanguages.includes(language.toLowerCase()) }, [codeExecution.enabled, language]) const sourceViewRef = useRef(null) @@ -152,21 +153,49 @@ export const CodeBlockView: React.FC = memo(({ children, language, onSave setIsRunning(true) setExecutionResult(null) - pyodideService - .runScript(children, {}, codeExecution.timeoutMinutes * 60000) - .then((result) => { - setExecutionResult(result) - }) - .catch((error) => { - logger.error('Unexpected error:', error) - setExecutionResult({ - text: `Unexpected error: ${error.message || 'Unknown error'}` + const isPython = language === 'python' + const isJavaScript = ['javascript', 'js', 'typescript', 'ts'].includes(language.toLowerCase()) + + if (isPython) { + pyodideService + .runScript(children, {}, codeExecution.timeoutMinutes * 60000) + .then((result) => { + setExecutionResult(result) }) - }) - .finally(() => { - setIsRunning(false) - }) - }, [children, codeExecution.timeoutMinutes]) + .catch((error) => { + logger.error('Unexpected error:', error) + setExecutionResult({ + text: `Unexpected error: ${error.message || 'Unknown error'}` + }) + }) + .finally(() => { + setIsRunning(false) + }) + } else if (isJavaScript) { + window.api.js + .execute(children, codeExecution.timeoutMinutes * 60000) + .then((result) => { + if (result.error) { + setExecutionResult({ + text: `Error: ${result.error}\n${result.stderr || ''}` + }) + } else { + setExecutionResult({ + text: result.stdout || (result.stderr ? `stderr: ${result.stderr}` : 'Execution completed') + }) + } + }) + .catch((error) => { + logger.error('Unexpected error:', error) + setExecutionResult({ + text: `Unexpected error: ${error.message || 'Unknown error'}` + }) + }) + .finally(() => { + setIsRunning(false) + }) + } + }, [children, codeExecution.timeoutMinutes, language]) const showPreviewTools = useMemo(() => { return viewMode !== 'source' && hasSpecialView diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index e4f45288ee..6591846e59 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3346,6 +3346,7 @@ "dify_knowledge": "Dify 的 MCP 伺服器實現,提供了一個簡單的 API 來與 Dify 進行互動。需要配置 Dify Key", "fetch": "用於獲取 URL 網頁內容的 MCP 伺服器", "filesystem": "實現文件系統操作的模型上下文協議(MCP)的 Node.js 伺服器。需要配置允許訪問的目錄", + "js": "在安全的沙盒環境中執行 JavaScript 程式碼。使用 quickJs 執行 JavaScript,支援大多數標準函式庫和流行的第三方函式庫", "mcp_auto_install": "自動安裝 MCP 服務(測試版)", "memory": "基於本地知識圖譜的持久性記憶基礎實現。這使得模型能夠在不同對話間記住使用者的相關資訊。需要配置 MEMORY_FILE_PATH 環境變數。", "no": "無描述",