feat(*): 实现脚本录制器运行代码功能
This commit is contained in:
parent
0364169f6e
commit
7d32c43051
|
@ -1,18 +1,13 @@
|
|||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import VSToolBar from '../../components/VSToolBar';
|
||||
import ImageEditor, { AnnotationChangedEvent } from '../../components/ImageEditor/ImageEditor';
|
||||
import { MdDevices, MdPlayArrow, MdSearch, MdTouchApp, MdTextFields, MdTextSnippet, MdCropFree, MdRefresh, MdPause, MdBackHand, MdCheck, MdClose, MdEdit, MdContentCopy, MdContentCut, MdFolder } from 'react-icons/md';
|
||||
import { AiOutlineLoading3Quarters } from 'react-icons/ai';
|
||||
import AceEditor from 'react-ace';
|
||||
import { Splitable } from '../../components/Splitable';
|
||||
import { Tool, Annotation } from '../../components/ImageEditor/types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
// 引入 ace 编辑器的主题和语言模式
|
||||
import 'ace-builds/src-noconflict/mode-python';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-chrome';
|
||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||
import { css } from '@emotion/react';
|
||||
import useImageMetaData, { Definition, DefinitionType, ImageMetaData, TemplateDefinition } from '../../hooks/useImageMetaData';
|
||||
import { useDarkMode } from '../../hooks/useDarkMode';
|
||||
|
@ -21,6 +16,13 @@ import useLatestCallback from '../../hooks/useLatestCallback';
|
|||
import useHotkey from '../../hooks/useHotkey';
|
||||
import { useFormModal } from '../../hooks/useFormModal';
|
||||
import { openDirectory } from '../../utils/fileUtils';
|
||||
import { useToast } from '../../components/ToastMessage';
|
||||
|
||||
// 引入 ace 编辑器的主题和语言模式
|
||||
import 'ace-builds/src-noconflict/mode-python';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-chrome';
|
||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
|
@ -49,6 +51,7 @@ interface ScriptRecorderState {
|
|||
imageUrl: string;
|
||||
inEditMode: boolean;
|
||||
directoryHandle: FileSystemDirectoryHandle | null;
|
||||
isRunning: boolean;
|
||||
|
||||
imageMetaDataObject: ReturnType<typeof useImageMetaData> | null;
|
||||
setImageMetaDataObject: (imageMetaData: ReturnType<typeof useImageMetaData>) => void;
|
||||
|
@ -58,21 +61,33 @@ interface ScriptRecorderState {
|
|||
setAutoScreenshot: (auto: boolean) => void;
|
||||
setConnected: (connected: boolean) => void;
|
||||
setImageUrl: (url: string) => void;
|
||||
setIsRunning: (isRunning: boolean) => void;
|
||||
|
||||
setDirectoryHandle: (handle: FileSystemDirectoryHandle | null) => void;
|
||||
enterEditMode: () => void;
|
||||
exitEditMode: () => void;
|
||||
}
|
||||
|
||||
// HACK: hard coded
|
||||
const DEFAULT_CODE = `from kotonebot import *
|
||||
from kotonebot.tasks import R
|
||||
from kotonebot.backend.context import ContextStackVars
|
||||
|
||||
ContextStackVars.screenshot_mode = 'manual'
|
||||
|
||||
device.screenshot()
|
||||
`
|
||||
|
||||
const useScriptRecorderStore = create<ScriptRecorderState>((set) => ({
|
||||
code: '',
|
||||
code: DEFAULT_CODE,
|
||||
tool: 'drag',
|
||||
autoScreenshot: true,
|
||||
connected: false,
|
||||
imageUrl: '',
|
||||
inEditMode: false,
|
||||
|
||||
directoryHandle: null,
|
||||
isRunning: false,
|
||||
|
||||
imageMetaDataObject: null,
|
||||
setImageMetaDataObject: (imageMetaData) => set({ imageMetaDataObject: imageMetaData }),
|
||||
|
@ -82,12 +97,12 @@ const useScriptRecorderStore = create<ScriptRecorderState>((set) => ({
|
|||
setAutoScreenshot: (auto) => set({ autoScreenshot: auto }),
|
||||
setConnected: (connected) => set({ connected }),
|
||||
setImageUrl: (url) => set({ imageUrl: url }),
|
||||
setIsRunning: (isRunning) => set({ isRunning }),
|
||||
setDirectoryHandle: (handle) => set({ directoryHandle: handle }),
|
||||
enterEditMode: () => set({ inEditMode: true, autoScreenshot: false }),
|
||||
exitEditMode: () => set({ inEditMode: false }),
|
||||
}));
|
||||
|
||||
|
||||
interface ToolConfigItem {
|
||||
code?: (d: Definition, a: Annotation) => string;
|
||||
}
|
||||
|
@ -114,8 +129,6 @@ const ToolConfig: Record<ScriptRecorderTool, ToolConfigItem> = {
|
|||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface ViewToolBarProps {
|
||||
onOpenDirectory: () => void;
|
||||
}
|
||||
|
@ -296,34 +309,80 @@ const EditToolBar: React.FC<EditToolBarProps> = ({
|
|||
interface CodeEditorToolBarProps {
|
||||
onCopyAll: () => void;
|
||||
onCutAll: () => void;
|
||||
code: string;
|
||||
client: ReturnType<typeof useDebugClient>;
|
||||
}
|
||||
|
||||
const CodeEditorToolBar: React.FC<CodeEditorToolBarProps> = ({
|
||||
onCopyAll,
|
||||
onCutAll,
|
||||
code,
|
||||
client
|
||||
}) => {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const { showToast, ToastComponent } = useToast();
|
||||
|
||||
const spinnerCss = useMemo(() => css`
|
||||
animation: spin 1s linear infinite;
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`, []);
|
||||
|
||||
const handleRunCode = async () => {
|
||||
if (!code.trim()) {
|
||||
showToast('warning', '警告', '请先输入代码');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRunning(true);
|
||||
try {
|
||||
const result = await client.runCode(code);
|
||||
if (result.status === 'error') {
|
||||
showToast('danger', '运行错误', result.message);
|
||||
console.error('运行错误:', result.traceback);
|
||||
} else {
|
||||
if (result.result !== undefined) {
|
||||
showToast('success', '运行成功', `执行结果: ${JSON.stringify(result.result)}`);
|
||||
} else {
|
||||
showToast('success', '运行成功', '代码执行完成');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('danger', '运行错误', '执行代码时发生错误');
|
||||
console.error('执行错误:', error);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VSToolBar align='center'>
|
||||
<VSToolBar.Button
|
||||
id="run"
|
||||
icon={<MdPlayArrow />}
|
||||
label="运行"
|
||||
disabled={true}
|
||||
/>
|
||||
<VSToolBar.Separator />
|
||||
<VSToolBar.Button
|
||||
id="copy-all"
|
||||
icon={<MdContentCopy />}
|
||||
label="复制全部"
|
||||
onClick={onCopyAll}
|
||||
/>
|
||||
<VSToolBar.Button
|
||||
id="cut-all"
|
||||
icon={<MdContentCut />}
|
||||
label="剪切全部"
|
||||
onClick={onCutAll}
|
||||
/>
|
||||
</VSToolBar>
|
||||
<>
|
||||
{ToastComponent}
|
||||
<VSToolBar align='center'>
|
||||
<VSToolBar.Button
|
||||
id="run"
|
||||
icon={isRunning ? <AiOutlineLoading3Quarters css={spinnerCss} /> : <MdPlayArrow />}
|
||||
label="运行"
|
||||
onClick={handleRunCode}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
<VSToolBar.Separator />
|
||||
<VSToolBar.Button
|
||||
id="copy-all"
|
||||
icon={<MdContentCopy />}
|
||||
label="复制全部"
|
||||
onClick={onCopyAll}
|
||||
/>
|
||||
<VSToolBar.Button
|
||||
id="cut-all"
|
||||
icon={<MdContentCut />}
|
||||
label="剪切全部"
|
||||
onClick={onCutAll}
|
||||
/>
|
||||
</VSToolBar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -336,6 +395,7 @@ function useStoreImageMetaData() {
|
|||
|
||||
const ScriptRecorder: React.FC = () => {
|
||||
const client = useDebugClient();
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
const { imageMetaData, Definitions, Annotations, clear } = useStoreImageMetaData();
|
||||
const code = useScriptRecorderStore((s) => s.code);
|
||||
|
@ -349,8 +409,6 @@ const ScriptRecorder: React.FC = () => {
|
|||
const setImageUrl = useScriptRecorderStore((s) => s.setImageUrl);
|
||||
const setDirectoryHandle = useScriptRecorderStore((s) => s.setDirectoryHandle);
|
||||
|
||||
|
||||
|
||||
const { theme: editorTheme } = useDarkMode({
|
||||
whenDark: 'monokai',
|
||||
whenLight: 'chrome'
|
||||
|
@ -552,6 +610,8 @@ const ScriptRecorder: React.FC = () => {
|
|||
<CodeEditorToolBar
|
||||
onCopyAll={handleCopyAll}
|
||||
onCutAll={handleCutAll}
|
||||
code={code}
|
||||
client={client}
|
||||
/>
|
||||
<AceEditor
|
||||
ref={editorRef}
|
||||
|
|
|
@ -52,6 +52,18 @@ type EventListenerMap = {
|
|||
connectionStatus: Array<(data: ConnectionStatusEvent) => void>;
|
||||
};
|
||||
|
||||
export type RunCodeResultSuccess = {
|
||||
status: 'ok';
|
||||
result: any;
|
||||
};
|
||||
|
||||
export type RunCodeResultError = {
|
||||
status: 'error';
|
||||
message: string;
|
||||
traceback: string;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Kotone 调试客户端类
|
||||
* 用于处理与调试服务器的 WebSocket 通信
|
||||
|
@ -194,4 +206,18 @@ export class KotoneDebugClient {
|
|||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async runCode(code: string): Promise<RunCodeResultSuccess | RunCodeResultError> {
|
||||
const response = await fetch(`http://${this.host}/api/code/run`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
return response.json();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -668,6 +668,7 @@ debug: ContextDebug = cast(ContextDebug, Forwarded(name="debug"))
|
|||
config: ContextConfig = cast(ContextConfig, Forwarded(name="config"))
|
||||
"""配置数据。"""
|
||||
|
||||
|
||||
def init_context(
|
||||
*,
|
||||
config_type: Type[T] = dict[str, Any],
|
||||
|
@ -693,4 +694,18 @@ def init_context(
|
|||
color._FORWARD_getter = lambda: _c.color # type: ignore
|
||||
vars._FORWARD_getter = lambda: _c.vars # type: ignore
|
||||
debug._FORWARD_getter = lambda: _c.debug # type: ignore
|
||||
config._FORWARD_getter = lambda: _c.config # type: ignore
|
||||
config._FORWARD_getter = lambda: _c.config # type: ignore
|
||||
|
||||
class ManualContextManager:
|
||||
def __enter__(self):
|
||||
ContextStackVars.push()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
ContextStackVars.pop()
|
||||
|
||||
def manual_context() -> ManualContextManager:
|
||||
"""
|
||||
默认情况下,Context* 类仅允许在 @task/@action 函数中使用。
|
||||
如果想要在其他地方使用,使用此函数手动创建一个上下文。
|
||||
"""
|
||||
return ManualContextManager()
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import time
|
||||
import asyncio
|
||||
import threading
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from collections import deque
|
||||
|
||||
import cv2
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, Response
|
||||
|
||||
from fastapi import FastAPI, WebSocket, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from ..context import manual_context
|
||||
|
||||
from . import vars
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"])
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
# 获取当前文件夹路径
|
||||
CURRENT_DIR = Path(__file__).parent
|
||||
|
@ -83,8 +88,22 @@ def screenshot():
|
|||
buff = cv2.imencode('.png', img)[1].tobytes()
|
||||
return Response(buff, media_type="image/png")
|
||||
|
||||
class RunCodeRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
@app.post("/api/code/run")
|
||||
async def run_code(request: RunCodeRequest):
|
||||
code = f"from kotonebot import *\n" + request.code
|
||||
try:
|
||||
with manual_context():
|
||||
ret = exec(code)
|
||||
return {"status": "ok", "result": ret}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
@app.get("/api/ping")
|
||||
async def ping():
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
message_queue = deque()
|
||||
|
|
Loading…
Reference in New Issue