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 styled from '@emotion/styled';
|
||||||
import VSToolBar from '../../components/VSToolBar';
|
import VSToolBar from '../../components/VSToolBar';
|
||||||
import ImageEditor, { AnnotationChangedEvent } from '../../components/ImageEditor/ImageEditor';
|
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 { 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 AceEditor from 'react-ace';
|
||||||
import { Splitable } from '../../components/Splitable';
|
import { Splitable } from '../../components/Splitable';
|
||||||
import { Tool, Annotation } from '../../components/ImageEditor/types';
|
import { Tool, Annotation } from '../../components/ImageEditor/types';
|
||||||
import { create } from 'zustand';
|
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 { css } from '@emotion/react';
|
||||||
import useImageMetaData, { Definition, DefinitionType, ImageMetaData, TemplateDefinition } from '../../hooks/useImageMetaData';
|
import useImageMetaData, { Definition, DefinitionType, ImageMetaData, TemplateDefinition } from '../../hooks/useImageMetaData';
|
||||||
import { useDarkMode } from '../../hooks/useDarkMode';
|
import { useDarkMode } from '../../hooks/useDarkMode';
|
||||||
|
@ -21,6 +16,13 @@ import useLatestCallback from '../../hooks/useLatestCallback';
|
||||||
import useHotkey from '../../hooks/useHotkey';
|
import useHotkey from '../../hooks/useHotkey';
|
||||||
import { useFormModal } from '../../hooks/useFormModal';
|
import { useFormModal } from '../../hooks/useFormModal';
|
||||||
import { openDirectory } from '../../utils/fileUtils';
|
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`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -49,6 +51,7 @@ interface ScriptRecorderState {
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
inEditMode: boolean;
|
inEditMode: boolean;
|
||||||
directoryHandle: FileSystemDirectoryHandle | null;
|
directoryHandle: FileSystemDirectoryHandle | null;
|
||||||
|
isRunning: boolean;
|
||||||
|
|
||||||
imageMetaDataObject: ReturnType<typeof useImageMetaData> | null;
|
imageMetaDataObject: ReturnType<typeof useImageMetaData> | null;
|
||||||
setImageMetaDataObject: (imageMetaData: ReturnType<typeof useImageMetaData>) => void;
|
setImageMetaDataObject: (imageMetaData: ReturnType<typeof useImageMetaData>) => void;
|
||||||
|
@ -58,21 +61,33 @@ interface ScriptRecorderState {
|
||||||
setAutoScreenshot: (auto: boolean) => void;
|
setAutoScreenshot: (auto: boolean) => void;
|
||||||
setConnected: (connected: boolean) => void;
|
setConnected: (connected: boolean) => void;
|
||||||
setImageUrl: (url: string) => void;
|
setImageUrl: (url: string) => void;
|
||||||
|
setIsRunning: (isRunning: boolean) => void;
|
||||||
|
|
||||||
setDirectoryHandle: (handle: FileSystemDirectoryHandle | null) => void;
|
setDirectoryHandle: (handle: FileSystemDirectoryHandle | null) => void;
|
||||||
enterEditMode: () => void;
|
enterEditMode: () => void;
|
||||||
exitEditMode: () => 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) => ({
|
const useScriptRecorderStore = create<ScriptRecorderState>((set) => ({
|
||||||
code: '',
|
code: DEFAULT_CODE,
|
||||||
tool: 'drag',
|
tool: 'drag',
|
||||||
autoScreenshot: true,
|
autoScreenshot: true,
|
||||||
connected: false,
|
connected: false,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
inEditMode: false,
|
inEditMode: false,
|
||||||
|
|
||||||
directoryHandle: null,
|
directoryHandle: null,
|
||||||
|
isRunning: false,
|
||||||
|
|
||||||
imageMetaDataObject: null,
|
imageMetaDataObject: null,
|
||||||
setImageMetaDataObject: (imageMetaData) => set({ imageMetaDataObject: imageMetaData }),
|
setImageMetaDataObject: (imageMetaData) => set({ imageMetaDataObject: imageMetaData }),
|
||||||
|
@ -82,12 +97,12 @@ const useScriptRecorderStore = create<ScriptRecorderState>((set) => ({
|
||||||
setAutoScreenshot: (auto) => set({ autoScreenshot: auto }),
|
setAutoScreenshot: (auto) => set({ autoScreenshot: auto }),
|
||||||
setConnected: (connected) => set({ connected }),
|
setConnected: (connected) => set({ connected }),
|
||||||
setImageUrl: (url) => set({ imageUrl: url }),
|
setImageUrl: (url) => set({ imageUrl: url }),
|
||||||
|
setIsRunning: (isRunning) => set({ isRunning }),
|
||||||
setDirectoryHandle: (handle) => set({ directoryHandle: handle }),
|
setDirectoryHandle: (handle) => set({ directoryHandle: handle }),
|
||||||
enterEditMode: () => set({ inEditMode: true, autoScreenshot: false }),
|
enterEditMode: () => set({ inEditMode: true, autoScreenshot: false }),
|
||||||
exitEditMode: () => set({ inEditMode: false }),
|
exitEditMode: () => set({ inEditMode: false }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
interface ToolConfigItem {
|
interface ToolConfigItem {
|
||||||
code?: (d: Definition, a: Annotation) => string;
|
code?: (d: Definition, a: Annotation) => string;
|
||||||
}
|
}
|
||||||
|
@ -114,8 +129,6 @@ const ToolConfig: Record<ScriptRecorderTool, ToolConfigItem> = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface ViewToolBarProps {
|
interface ViewToolBarProps {
|
||||||
onOpenDirectory: () => void;
|
onOpenDirectory: () => void;
|
||||||
}
|
}
|
||||||
|
@ -296,34 +309,80 @@ const EditToolBar: React.FC<EditToolBarProps> = ({
|
||||||
interface CodeEditorToolBarProps {
|
interface CodeEditorToolBarProps {
|
||||||
onCopyAll: () => void;
|
onCopyAll: () => void;
|
||||||
onCutAll: () => void;
|
onCutAll: () => void;
|
||||||
|
code: string;
|
||||||
|
client: ReturnType<typeof useDebugClient>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeEditorToolBar: React.FC<CodeEditorToolBarProps> = ({
|
const CodeEditorToolBar: React.FC<CodeEditorToolBarProps> = ({
|
||||||
onCopyAll,
|
onCopyAll,
|
||||||
onCutAll,
|
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 (
|
return (
|
||||||
<VSToolBar align='center'>
|
<>
|
||||||
<VSToolBar.Button
|
{ToastComponent}
|
||||||
id="run"
|
<VSToolBar align='center'>
|
||||||
icon={<MdPlayArrow />}
|
<VSToolBar.Button
|
||||||
label="运行"
|
id="run"
|
||||||
disabled={true}
|
icon={isRunning ? <AiOutlineLoading3Quarters css={spinnerCss} /> : <MdPlayArrow />}
|
||||||
/>
|
label="运行"
|
||||||
<VSToolBar.Separator />
|
onClick={handleRunCode}
|
||||||
<VSToolBar.Button
|
disabled={isRunning}
|
||||||
id="copy-all"
|
/>
|
||||||
icon={<MdContentCopy />}
|
<VSToolBar.Separator />
|
||||||
label="复制全部"
|
<VSToolBar.Button
|
||||||
onClick={onCopyAll}
|
id="copy-all"
|
||||||
/>
|
icon={<MdContentCopy />}
|
||||||
<VSToolBar.Button
|
label="复制全部"
|
||||||
id="cut-all"
|
onClick={onCopyAll}
|
||||||
icon={<MdContentCut />}
|
/>
|
||||||
label="剪切全部"
|
<VSToolBar.Button
|
||||||
onClick={onCutAll}
|
id="cut-all"
|
||||||
/>
|
icon={<MdContentCut />}
|
||||||
</VSToolBar>
|
label="剪切全部"
|
||||||
|
onClick={onCutAll}
|
||||||
|
/>
|
||||||
|
</VSToolBar>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -336,6 +395,7 @@ function useStoreImageMetaData() {
|
||||||
|
|
||||||
const ScriptRecorder: React.FC = () => {
|
const ScriptRecorder: React.FC = () => {
|
||||||
const client = useDebugClient();
|
const client = useDebugClient();
|
||||||
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const { imageMetaData, Definitions, Annotations, clear } = useStoreImageMetaData();
|
const { imageMetaData, Definitions, Annotations, clear } = useStoreImageMetaData();
|
||||||
const code = useScriptRecorderStore((s) => s.code);
|
const code = useScriptRecorderStore((s) => s.code);
|
||||||
|
@ -349,8 +409,6 @@ const ScriptRecorder: React.FC = () => {
|
||||||
const setImageUrl = useScriptRecorderStore((s) => s.setImageUrl);
|
const setImageUrl = useScriptRecorderStore((s) => s.setImageUrl);
|
||||||
const setDirectoryHandle = useScriptRecorderStore((s) => s.setDirectoryHandle);
|
const setDirectoryHandle = useScriptRecorderStore((s) => s.setDirectoryHandle);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const { theme: editorTheme } = useDarkMode({
|
const { theme: editorTheme } = useDarkMode({
|
||||||
whenDark: 'monokai',
|
whenDark: 'monokai',
|
||||||
whenLight: 'chrome'
|
whenLight: 'chrome'
|
||||||
|
@ -552,6 +610,8 @@ const ScriptRecorder: React.FC = () => {
|
||||||
<CodeEditorToolBar
|
<CodeEditorToolBar
|
||||||
onCopyAll={handleCopyAll}
|
onCopyAll={handleCopyAll}
|
||||||
onCutAll={handleCutAll}
|
onCutAll={handleCutAll}
|
||||||
|
code={code}
|
||||||
|
client={client}
|
||||||
/>
|
/>
|
||||||
<AceEditor
|
<AceEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
|
|
@ -52,6 +52,18 @@ type EventListenerMap = {
|
||||||
connectionStatus: Array<(data: ConnectionStatusEvent) => void>;
|
connectionStatus: Array<(data: ConnectionStatusEvent) => void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RunCodeResultSuccess = {
|
||||||
|
status: 'ok';
|
||||||
|
result: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RunCodeResultError = {
|
||||||
|
status: 'error';
|
||||||
|
message: string;
|
||||||
|
traceback: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kotone 调试客户端类
|
* Kotone 调试客户端类
|
||||||
* 用于处理与调试服务器的 WebSocket 通信
|
* 用于处理与调试服务器的 WebSocket 通信
|
||||||
|
@ -194,4 +206,18 @@ export class KotoneDebugClient {
|
||||||
return URL.createObjectURL(blob);
|
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"))
|
config: ContextConfig = cast(ContextConfig, Forwarded(name="config"))
|
||||||
"""配置数据。"""
|
"""配置数据。"""
|
||||||
|
|
||||||
|
|
||||||
def init_context(
|
def init_context(
|
||||||
*,
|
*,
|
||||||
config_type: Type[T] = dict[str, Any],
|
config_type: Type[T] = dict[str, Any],
|
||||||
|
@ -693,4 +694,18 @@ def init_context(
|
||||||
color._FORWARD_getter = lambda: _c.color # type: ignore
|
color._FORWARD_getter = lambda: _c.color # type: ignore
|
||||||
vars._FORWARD_getter = lambda: _c.vars # type: ignore
|
vars._FORWARD_getter = lambda: _c.vars # type: ignore
|
||||||
debug._FORWARD_getter = lambda: _c.debug # 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 time
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
from pydantic import BaseModel
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse, Response
|
from fastapi.responses import FileResponse, Response
|
||||||
|
|
||||||
from fastapi import FastAPI, WebSocket, HTTPException
|
from fastapi import FastAPI, WebSocket, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from ..context import manual_context
|
||||||
|
|
||||||
from . import vars
|
from . import vars
|
||||||
|
|
||||||
app = FastAPI()
|
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
|
CURRENT_DIR = Path(__file__).parent
|
||||||
|
@ -83,8 +88,22 @@ def screenshot():
|
||||||
buff = cv2.imencode('.png', img)[1].tobytes()
|
buff = cv2.imencode('.png', img)[1].tobytes()
|
||||||
return Response(buff, media_type="image/png")
|
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")
|
@app.get("/api/ping")
|
||||||
async def ping():
|
async def ping():
|
||||||
|
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
message_queue = deque()
|
message_queue = deque()
|
||||||
|
|
Loading…
Reference in New Issue