Compare commits
13 Commits
feat/backu
...
feat/mcp-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cd7f91fff | ||
|
|
ad7a043fb3 | ||
|
|
7db628702d | ||
|
|
44d2cb345f | ||
|
|
49eec68434 | ||
|
|
d5ae3e6edc | ||
|
|
6eedcc29ba | ||
|
|
71d35eddf7 | ||
|
|
9f1c8f2c17 | ||
|
|
651e9a529e | ||
|
|
f68f6e9896 | ||
|
|
2dcc68da87 | ||
|
|
1db259cd3e |
@@ -96,6 +96,9 @@ export enum IpcChannel {
|
|||||||
AgentMessage_PersistExchange = 'agent-message:persist-exchange',
|
AgentMessage_PersistExchange = 'agent-message:persist-exchange',
|
||||||
AgentMessage_GetHistory = 'agent-message:get-history',
|
AgentMessage_GetHistory = 'agent-message:get-history',
|
||||||
|
|
||||||
|
// JavaScript
|
||||||
|
Js_Execute = 'js:execute',
|
||||||
|
|
||||||
//copilot
|
//copilot
|
||||||
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
||||||
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
||||||
|
|||||||
BIN
resources/wasm/qjs-wasi.wasm
Normal file
BIN
resources/wasm/qjs-wasi.wasm
Normal file
Binary file not shown.
@@ -37,6 +37,7 @@ import DxtService from './services/DxtService'
|
|||||||
import { ExportService } from './services/ExportService'
|
import { ExportService } from './services/ExportService'
|
||||||
import { fileStorage as fileManager } from './services/FileStorage'
|
import { fileStorage as fileManager } from './services/FileStorage'
|
||||||
import FileService from './services/FileSystemService'
|
import FileService from './services/FileSystemService'
|
||||||
|
import { jsService } from './services/JsService'
|
||||||
import KnowledgeService from './services/KnowledgeService'
|
import KnowledgeService from './services/KnowledgeService'
|
||||||
import mcpService from './services/MCPService'
|
import mcpService from './services/MCPService'
|
||||||
import MemoryService from './services/memory/MemoryService'
|
import MemoryService from './services/memory/MemoryService'
|
||||||
@@ -741,6 +742,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_IsBinaryExist, (_, name: string) => isBinaryExists(name))
|
||||||
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
|
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
|
||||||
ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js'))
|
ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js'))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import BraveSearchServer from './brave-search'
|
|||||||
import DifyKnowledgeServer from './dify-knowledge'
|
import DifyKnowledgeServer from './dify-knowledge'
|
||||||
import FetchServer from './fetch'
|
import FetchServer from './fetch'
|
||||||
import FileSystemServer from './filesystem'
|
import FileSystemServer from './filesystem'
|
||||||
|
import JsServer from './js'
|
||||||
import MemoryServer from './memory'
|
import MemoryServer from './memory'
|
||||||
import PythonServer from './python'
|
import PythonServer from './python'
|
||||||
import ThinkingServer from './sequentialthinking'
|
import ThinkingServer from './sequentialthinking'
|
||||||
@@ -42,6 +43,9 @@ export function createInMemoryMCPServer(
|
|||||||
case BuiltinMCPServerNames.python: {
|
case BuiltinMCPServerNames.python: {
|
||||||
return new PythonServer().server
|
return new PythonServer().server
|
||||||
}
|
}
|
||||||
|
case BuiltinMCPServerNames.js: {
|
||||||
|
return new JsServer().server
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
||||||
}
|
}
|
||||||
|
|||||||
139
src/main/mcpServers/js.ts
Normal file
139
src/main/mcpServers/js.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// port from https://github.com/jlucaso1/mcp-javascript-sandbox
|
||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { jsService } from '@main/services/JsService'
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
const TOOL_NAME = 'run_javascript_code'
|
||||||
|
const DEFAULT_TIMEOUT = 60_000
|
||||||
|
|
||||||
|
export const RequestPayloadSchema = z.object({
|
||||||
|
javascript_code: z.string().min(1).describe('The JavaScript code to execute in the sandbox.'),
|
||||||
|
timeout: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.max(5 * 60_000)
|
||||||
|
.optional()
|
||||||
|
.describe('Execution timeout in milliseconds (default 60000, max 300000).')
|
||||||
|
})
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('MCPServer:JavaScript')
|
||||||
|
|
||||||
|
function formatExecutionResult(result: {
|
||||||
|
stdout: string
|
||||||
|
stderr: string
|
||||||
|
error?: string | undefined
|
||||||
|
exitCode: number
|
||||||
|
}) {
|
||||||
|
let combinedOutput = ''
|
||||||
|
if (result.stdout) {
|
||||||
|
combinedOutput += result.stdout
|
||||||
|
}
|
||||||
|
if (result.stderr) {
|
||||||
|
combinedOutput += `--- stderr ---\n${result.stderr}\n--- stderr ---\n`
|
||||||
|
}
|
||||||
|
if (result.error) {
|
||||||
|
combinedOutput += `--- Execution Error ---\n${result.error}\n--- Execution Error ---\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
const isError = Boolean(result.error) || Boolean(result.stderr?.trim()) || result.exitCode !== 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
combinedOutput: combinedOutput.trim(),
|
||||||
|
isError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsServer {
|
||||||
|
public server: Server
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: 'MCP QuickJS Runner',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'An MCP server that provides a tool to execute JavaScript code in a QuickJS WASM sandbox.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
resources: {},
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.setupHandlers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers() {
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: TOOL_NAME,
|
||||||
|
description:
|
||||||
|
'Executes the provided JavaScript code in a secure WASM sandbox (QuickJS). Returns stdout and stderr. Non-zero exit code indicates an error.',
|
||||||
|
inputSchema: z.toJSONSchema(RequestPayloadSchema)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params
|
||||||
|
|
||||||
|
if (name !== TOOL_NAME) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Tool not found: ${name}` }],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseResult = RequestPayloadSchema.safeParse(args)
|
||||||
|
if (!parseResult.success) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Invalid arguments: ${parseResult.error.message}` }],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { javascript_code, timeout } = parseResult.data
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug('Executing JavaScript code via JsService')
|
||||||
|
const result = await jsService.executeScript(javascript_code, {
|
||||||
|
timeout: timeout ?? DEFAULT_TIMEOUT
|
||||||
|
})
|
||||||
|
|
||||||
|
const { combinedOutput, isError } = formatExecutionResult(result)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: combinedOutput
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
logger.error(`JavaScript execution failed: ${message}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Server error during tool execution: ${message}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JsServer
|
||||||
115
src/main/services/JsService.ts
Normal file
115
src/main/services/JsService.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
|
||||||
|
import type { JsExecutionResult } from './workers/JsWorker'
|
||||||
|
// oxlint-disable-next-line default
|
||||||
|
import createJsWorker from './workers/JsWorker?nodeWorker'
|
||||||
|
|
||||||
|
interface ExecuteScriptOptions {
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkerResponse =
|
||||||
|
| {
|
||||||
|
success: true
|
||||||
|
result: JsExecutionResult
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT = 60_000
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('JsService')
|
||||||
|
|
||||||
|
export class JsService {
|
||||||
|
private static instance: JsService | null = null
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): JsService {
|
||||||
|
if (!JsService.instance) {
|
||||||
|
JsService.instance = new JsService()
|
||||||
|
}
|
||||||
|
return JsService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
public async executeScript(code: string, options: ExecuteScriptOptions = {}): Promise<JsExecutionResult> {
|
||||||
|
const { timeout = DEFAULT_TIMEOUT } = options
|
||||||
|
|
||||||
|
if (!code || typeof code !== 'string') {
|
||||||
|
throw new Error('JavaScript code must be a non-empty string')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit code size to 1MB to prevent memory issues
|
||||||
|
const MAX_CODE_SIZE = 1_000_000
|
||||||
|
if (code.length > MAX_CODE_SIZE) {
|
||||||
|
throw new Error(`JavaScript code exceeds maximum size of ${MAX_CODE_SIZE / 1_000_000}MB`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<JsExecutionResult>((resolve, reject) => {
|
||||||
|
const worker = createJsWorker({
|
||||||
|
workerData: { code },
|
||||||
|
argv: [],
|
||||||
|
trackUnmanagedFds: false
|
||||||
|
})
|
||||||
|
|
||||||
|
let settled = false
|
||||||
|
let timeoutId: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
timeoutId = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await worker.terminate()
|
||||||
|
} catch {
|
||||||
|
// ignore termination errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settleSuccess = async (result: JsExecutionResult) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
await cleanup()
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
const settleError = async (error: Error) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
await cleanup()
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.once('message', async (message: WorkerResponse) => {
|
||||||
|
if (message.success) {
|
||||||
|
await settleSuccess(message.result)
|
||||||
|
} else {
|
||||||
|
await settleError(new Error(message.error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
worker.once('error', async (error) => {
|
||||||
|
logger.error(`JsWorker error: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
await settleError(error instanceof Error ? error : new Error(String(error)))
|
||||||
|
})
|
||||||
|
|
||||||
|
worker.once('exit', async (exitCode) => {
|
||||||
|
if (!settled && exitCode !== 0) {
|
||||||
|
await settleError(new Error(`JsWorker exited with code ${exitCode}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
logger.warn(`JavaScript execution timed out after ${timeout}ms`)
|
||||||
|
settleError(new Error('JavaScript execution timed out')).catch((err) => {
|
||||||
|
logger.error('Error during timeout cleanup:', err instanceof Error ? err : new Error(String(err)))
|
||||||
|
})
|
||||||
|
}, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jsService = JsService.getInstance()
|
||||||
118
src/main/services/workers/JsWorker.ts
Normal file
118
src/main/services/workers/JsWorker.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { mkdtemp, open, readFile, rm } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { WASI } from 'node:wasi'
|
||||||
|
import { parentPort, workerData } from 'node:worker_threads'
|
||||||
|
|
||||||
|
import loadWasm from '../../../../resources/wasm/qjs-wasi.wasm?loader'
|
||||||
|
|
||||||
|
interface WorkerPayload {
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JsExecutionResult {
|
||||||
|
stdout: string
|
||||||
|
stderr: string
|
||||||
|
error?: string
|
||||||
|
exitCode: number
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentPort) {
|
||||||
|
throw new Error('JsWorker requires a parent port')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runQuickJsInSandbox(jsCode: string): Promise<JsExecutionResult> {
|
||||||
|
let tempDir: string | undefined
|
||||||
|
let stdoutHandle: Awaited<ReturnType<typeof open>> | undefined
|
||||||
|
let stderrHandle: Awaited<ReturnType<typeof open>> | undefined
|
||||||
|
let stdoutPath: string | undefined
|
||||||
|
let stderrPath: string | undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
tempDir = await mkdtemp(join(tmpdir(), 'quickjs-wasi-'))
|
||||||
|
stdoutPath = join(tempDir, 'stdout.log')
|
||||||
|
stderrPath = join(tempDir, 'stderr.log')
|
||||||
|
|
||||||
|
stdoutHandle = await open(stdoutPath, 'w')
|
||||||
|
stderrHandle = await open(stderrPath, 'w')
|
||||||
|
|
||||||
|
const wasi = new WASI({
|
||||||
|
version: 'preview1',
|
||||||
|
args: ['qjs', '-e', jsCode],
|
||||||
|
env: {}, // Empty environment for security - don't expose host env vars
|
||||||
|
stdin: 0,
|
||||||
|
stdout: stdoutHandle.fd,
|
||||||
|
stderr: stderrHandle.fd,
|
||||||
|
returnOnExit: true
|
||||||
|
})
|
||||||
|
const instance = await loadWasm(wasi.getImportObject() as WebAssembly.Imports)
|
||||||
|
|
||||||
|
let exitCode = 0
|
||||||
|
try {
|
||||||
|
exitCode = wasi.start(instance)
|
||||||
|
} catch (wasiError: any) {
|
||||||
|
return {
|
||||||
|
stdout: '',
|
||||||
|
stderr: `WASI start error: ${wasiError?.message ?? String(wasiError)}`,
|
||||||
|
error: `Sandbox execution failed during start: ${wasiError?.message ?? String(wasiError)}`,
|
||||||
|
exitCode: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close handles before reading files to prevent descriptor leak
|
||||||
|
const _stdoutHandle = stdoutHandle
|
||||||
|
stdoutHandle = undefined
|
||||||
|
await _stdoutHandle.close()
|
||||||
|
|
||||||
|
const _stderrHandle = stderrHandle
|
||||||
|
stderrHandle = undefined
|
||||||
|
await _stderrHandle.close()
|
||||||
|
|
||||||
|
const capturedStdout = await readFile(stdoutPath, 'utf8')
|
||||||
|
const capturedStderr = await readFile(stderrPath, 'utf8')
|
||||||
|
|
||||||
|
let executionError: string | undefined
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
executionError = `QuickJS process exited with code ${exitCode}. Check stderr for details.`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stdout: capturedStdout,
|
||||||
|
stderr: capturedStderr,
|
||||||
|
error: executionError,
|
||||||
|
exitCode
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
stdout: '',
|
||||||
|
stderr: '',
|
||||||
|
error: `Sandbox setup or execution failed: ${error?.message ?? String(error)}`,
|
||||||
|
exitCode: -1
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (stdoutHandle) await stdoutHandle.close()
|
||||||
|
if (stderrHandle) await stderrHandle.close()
|
||||||
|
if (tempDir) {
|
||||||
|
await rm(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execute(code: string) {
|
||||||
|
return runQuickJsInSandbox(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = workerData as WorkerPayload | undefined
|
||||||
|
|
||||||
|
if (!payload?.code || typeof payload.code !== 'string') {
|
||||||
|
parentPort.postMessage({ success: false, error: 'JavaScript code must be provided to the worker' })
|
||||||
|
} else {
|
||||||
|
execute(payload.code)
|
||||||
|
.then((result) => {
|
||||||
|
parentPort?.postMessage({ success: true, result })
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
parentPort?.postMessage({ success: false, error: errorMessage })
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -345,6 +345,9 @@ const api = {
|
|||||||
execute: (script: string, context?: Record<string, any>, timeout?: number) =>
|
execute: (script: string, context?: Record<string, any>, timeout?: number) =>
|
||||||
ipcRenderer.invoke(IpcChannel.Python_Execute, script, context, timeout)
|
ipcRenderer.invoke(IpcChannel.Python_Execute, script, context, timeout)
|
||||||
},
|
},
|
||||||
|
js: {
|
||||||
|
execute: (code: string, timeout?: number) => ipcRenderer.invoke(IpcChannel.Js_Execute, code, timeout)
|
||||||
|
},
|
||||||
shell: {
|
shell: {
|
||||||
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
|
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
|
|||||||
const [tools, setTools] = useState<ActionTool[]>([])
|
const [tools, setTools] = useState<ActionTool[]>([])
|
||||||
|
|
||||||
const isExecutable = useMemo(() => {
|
const isExecutable = useMemo(() => {
|
||||||
return codeExecution.enabled && language === 'python'
|
const executableLanguages = ['python', 'py', 'javascript', 'js']
|
||||||
|
return codeExecution.enabled && executableLanguages.includes(language.toLowerCase())
|
||||||
}, [codeExecution.enabled, language])
|
}, [codeExecution.enabled, language])
|
||||||
|
|
||||||
const sourceViewRef = useRef<CodeEditorHandles>(null)
|
const sourceViewRef = useRef<CodeEditorHandles>(null)
|
||||||
@@ -152,21 +153,49 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
|
|||||||
setIsRunning(true)
|
setIsRunning(true)
|
||||||
setExecutionResult(null)
|
setExecutionResult(null)
|
||||||
|
|
||||||
pyodideService
|
const isPython = ['python', 'py'].includes(language.toLowerCase())
|
||||||
.runScript(children, {}, codeExecution.timeoutMinutes * 60000)
|
const isJavaScript = ['javascript', 'js'].includes(language.toLowerCase())
|
||||||
.then((result) => {
|
|
||||||
setExecutionResult(result)
|
if (isPython) {
|
||||||
})
|
pyodideService
|
||||||
.catch((error) => {
|
.runScript(children, {}, codeExecution.timeoutMinutes * 60000)
|
||||||
logger.error('Unexpected error:', error)
|
.then((result) => {
|
||||||
setExecutionResult({
|
setExecutionResult(result)
|
||||||
text: `Unexpected error: ${error.message || 'Unknown error'}`
|
|
||||||
})
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
.finally(() => {
|
logger.error('Unexpected error:', error)
|
||||||
setIsRunning(false)
|
setExecutionResult({
|
||||||
})
|
text: `Unexpected error: ${error.message || 'Unknown error'}`
|
||||||
}, [children, codeExecution.timeoutMinutes])
|
})
|
||||||
|
})
|
||||||
|
.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(() => {
|
const showPreviewTools = useMemo(() => {
|
||||||
return viewMode !== 'source' && hasSpecialView
|
return viewMode !== 'source' && hasSpecialView
|
||||||
|
|||||||
@@ -329,7 +329,8 @@ const builtInMcpDescriptionKeyMap: Record<BuiltinMCPServerName, string> = {
|
|||||||
[BuiltinMCPServerNames.fetch]: 'settings.mcp.builtinServersDescriptions.fetch',
|
[BuiltinMCPServerNames.fetch]: 'settings.mcp.builtinServersDescriptions.fetch',
|
||||||
[BuiltinMCPServerNames.filesystem]: 'settings.mcp.builtinServersDescriptions.filesystem',
|
[BuiltinMCPServerNames.filesystem]: 'settings.mcp.builtinServersDescriptions.filesystem',
|
||||||
[BuiltinMCPServerNames.difyKnowledge]: 'settings.mcp.builtinServersDescriptions.dify_knowledge',
|
[BuiltinMCPServerNames.difyKnowledge]: 'settings.mcp.builtinServersDescriptions.dify_knowledge',
|
||||||
[BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python'
|
[BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python',
|
||||||
|
[BuiltinMCPServerNames.js]: 'settings.mcp.builtinServersDescriptions.js'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const getBuiltInMcpServerDescriptionLabel = (key: string): string => {
|
export const getBuiltInMcpServerDescriptionLabel = (key: string): string => {
|
||||||
|
|||||||
@@ -3571,6 +3571,7 @@
|
|||||||
"dify_knowledge": "Dify's MCP server implementation provides a simple API to interact with Dify. Requires configuring the Dify Key",
|
"dify_knowledge": "Dify's MCP server implementation provides a simple API to interact with Dify. Requires configuring the Dify Key",
|
||||||
"fetch": "MCP server for retrieving URL web content",
|
"fetch": "MCP server for retrieving URL web content",
|
||||||
"filesystem": "A Node.js server implementing the Model Context Protocol (MCP) for file system operations. Requires configuration of directories allowed for access.",
|
"filesystem": "A Node.js server implementing the Model Context Protocol (MCP) for file system operations. Requires configuration of directories allowed for access.",
|
||||||
|
"js": "Execute JavaScript code in a secure sandbox environment. Run JavaScript using QuickJS, supporting most standard libraries and popular third-party libraries.",
|
||||||
"mcp_auto_install": "Automatically install MCP service (beta)",
|
"mcp_auto_install": "Automatically install MCP service (beta)",
|
||||||
"memory": "Persistent memory implementation based on a local knowledge graph. This enables the model to remember user-related information across different conversations. Requires configuring the MEMORY_FILE_PATH environment variable.",
|
"memory": "Persistent memory implementation based on a local knowledge graph. This enables the model to remember user-related information across different conversations. Requires configuring the MEMORY_FILE_PATH environment variable.",
|
||||||
"no": "No description",
|
"no": "No description",
|
||||||
|
|||||||
@@ -3571,6 +3571,7 @@
|
|||||||
"dify_knowledge": "Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互。需要配置 Dify Key",
|
"dify_knowledge": "Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互。需要配置 Dify Key",
|
||||||
"fetch": "用于获取 URL 网页内容的 MCP 服务器",
|
"fetch": "用于获取 URL 网页内容的 MCP 服务器",
|
||||||
"filesystem": "实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器。需要配置允许访问的目录",
|
"filesystem": "实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器。需要配置允许访问的目录",
|
||||||
|
"js": "在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript,支持大多数标准库和流行的第三方库",
|
||||||
"mcp_auto_install": "自动安装 MCP 服务(测试版)",
|
"mcp_auto_install": "自动安装 MCP 服务(测试版)",
|
||||||
"memory": "基于本地知识图谱的持久性记忆基础实现。这使得模型能够在不同对话间记住用户的相关信息。需要配置 MEMORY_FILE_PATH 环境变量。",
|
"memory": "基于本地知识图谱的持久性记忆基础实现。这使得模型能够在不同对话间记住用户的相关信息。需要配置 MEMORY_FILE_PATH 环境变量。",
|
||||||
"no": "无描述",
|
"no": "无描述",
|
||||||
|
|||||||
@@ -3571,6 +3571,7 @@
|
|||||||
"dify_knowledge": "Dify 的 MCP 伺服器實現,提供了一個簡單的 API 來與 Dify 進行互動。需要配置 Dify Key",
|
"dify_knowledge": "Dify 的 MCP 伺服器實現,提供了一個簡單的 API 來與 Dify 進行互動。需要配置 Dify Key",
|
||||||
"fetch": "用於獲取 URL 網頁內容的 MCP 伺服器",
|
"fetch": "用於獲取 URL 網頁內容的 MCP 伺服器",
|
||||||
"filesystem": "實現文件系統操作的模型上下文協議(MCP)的 Node.js 伺服器。需要配置允許訪問的目錄",
|
"filesystem": "實現文件系統操作的模型上下文協議(MCP)的 Node.js 伺服器。需要配置允許訪問的目錄",
|
||||||
|
"js": "在安全的沙盒環境中執行 JavaScript 程式碼。使用 quickJs 執行 JavaScript,支援大多數標準函式庫和流行的第三方函式庫",
|
||||||
"mcp_auto_install": "自動安裝 MCP 服務(測試版)",
|
"mcp_auto_install": "自動安裝 MCP 服務(測試版)",
|
||||||
"memory": "基於本地知識圖譜的持久性記憶基礎實現。這使得模型能夠在不同對話間記住使用者的相關資訊。需要配置 MEMORY_FILE_PATH 環境變數。",
|
"memory": "基於本地知識圖譜的持久性記憶基礎實現。這使得模型能夠在不同對話間記住使用者的相關資訊。需要配置 MEMORY_FILE_PATH 環境變數。",
|
||||||
"no": "無描述",
|
"no": "無描述",
|
||||||
|
|||||||
@@ -535,7 +535,30 @@ const CollapsedContent: FC<{ isExpanded: boolean; resultString: string }> = ({ i
|
|||||||
}
|
}
|
||||||
|
|
||||||
const highlight = async () => {
|
const highlight = async () => {
|
||||||
const result = await highlightCode(resultString, 'json')
|
// 处理转义字符
|
||||||
|
let processedString = resultString
|
||||||
|
try {
|
||||||
|
// 尝试解析字符串以处理可能的转义
|
||||||
|
const parsed = JSON.parse(resultString)
|
||||||
|
if (typeof parsed === 'string') {
|
||||||
|
// 如果解析后是字符串,再次尝试解析(处理双重转义)
|
||||||
|
try {
|
||||||
|
const doubleParsed = JSON.parse(parsed)
|
||||||
|
processedString = JSON.stringify(doubleParsed, null, 2)
|
||||||
|
} catch {
|
||||||
|
// 不是有效的 JSON,使用解析后的字符串
|
||||||
|
processedString = parsed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 重新格式化 JSON
|
||||||
|
processedString = JSON.stringify(parsed, null, 2)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 解析失败,使用原始字符串
|
||||||
|
processedString = resultString
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await highlightCode(processedString, 'json')
|
||||||
setStyledResult(result)
|
setStyledResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ export const builtinMCPServers: BuiltinMCPServer[] = [
|
|||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
isActive: false,
|
isActive: false,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
name: BuiltinMCPServerNames.js,
|
||||||
|
type: 'inMemory',
|
||||||
|
isActive: false,
|
||||||
|
provider: 'CherryAI'
|
||||||
}
|
}
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
|||||||
@@ -688,7 +688,8 @@ export const BuiltinMCPServerNames = {
|
|||||||
fetch: '@cherry/fetch',
|
fetch: '@cherry/fetch',
|
||||||
filesystem: '@cherry/filesystem',
|
filesystem: '@cherry/filesystem',
|
||||||
difyKnowledge: '@cherry/dify-knowledge',
|
difyKnowledge: '@cherry/dify-knowledge',
|
||||||
python: '@cherry/python'
|
python: '@cherry/python',
|
||||||
|
js: '@cherry/js'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type BuiltinMCPServerName = (typeof BuiltinMCPServerNames)[keyof typeof BuiltinMCPServerNames]
|
export type BuiltinMCPServerName = (typeof BuiltinMCPServerNames)[keyof typeof BuiltinMCPServerNames]
|
||||||
|
|||||||
Reference in New Issue
Block a user