feat(devtool): 脚本录制器中允许停止执行脚本
This commit is contained in:
parent
b5b53eed2c
commit
0d90ffd014
|
@ -374,6 +374,7 @@ const CodeEditorToolBar: React.FC<CodeEditorToolBarProps> = ({
|
|||
client
|
||||
}) => {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isStoppingDisabled, setIsStoppingDisabled] = useState(false);
|
||||
const { showToast, ToastComponent } = useToast();
|
||||
const setOutputText = useScriptRecorderStore((s) => s.setOutputText);
|
||||
|
||||
|
@ -385,7 +386,25 @@ const CodeEditorToolBar: React.FC<CodeEditorToolBarProps> = ({
|
|||
}
|
||||
`, []);
|
||||
|
||||
const handleStopCode = async () => {
|
||||
setIsStoppingDisabled(true);
|
||||
try {
|
||||
await client.stopCode();
|
||||
} catch (error) {
|
||||
showToast('danger', '停止错误', '停止代码执行时发生错误');
|
||||
console.error('停止错误:', error);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
setIsStoppingDisabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunCode = async () => {
|
||||
if (isRunning) {
|
||||
handleStopCode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!code.trim()) {
|
||||
showToast('warning', '警告', '请先输入代码');
|
||||
return;
|
||||
|
@ -396,21 +415,17 @@ const CodeEditorToolBar: React.FC<CodeEditorToolBarProps> = ({
|
|||
try {
|
||||
const result = await client.runCode(code);
|
||||
if (result.status === 'error') {
|
||||
showToast('danger', '运行错误', result.message);
|
||||
setOutputText(`错误:\n${result.message}\n\n堆栈跟踪:\n${result.traceback}`);
|
||||
setOutputText(`错误:\n${result.message}\n\n堆栈跟踪:\n${result.traceback}\n\n执行结果:\n${result.result}`);
|
||||
console.error('运行错误:', result.traceback);
|
||||
} else {
|
||||
if (result.result !== undefined) {
|
||||
const output = `执行结果:\n${result.result}`;
|
||||
setOutputText(output);
|
||||
showToast('success', '运行成功', '代码执行完成');
|
||||
} else {
|
||||
setOutputText('代码执行完成,无返回值');
|
||||
showToast('success', '运行成功', '代码执行完成');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('danger', '运行错误', '执行代码时发生错误');
|
||||
setOutputText(`执行错误:\n${error}`);
|
||||
console.error('执行错误:', error);
|
||||
} finally {
|
||||
|
@ -425,9 +440,9 @@ const CodeEditorToolBar: React.FC<CodeEditorToolBarProps> = ({
|
|||
<VSToolBar.Button
|
||||
id="run"
|
||||
icon={isRunning ? <AiOutlineLoading3Quarters css={spinnerCss} /> : <MdPlayArrow />}
|
||||
label="运行"
|
||||
label={isRunning ? "停止" : "运行"}
|
||||
onClick={handleRunCode}
|
||||
disabled={isRunning}
|
||||
disabled={isRunning && isStoppingDisabled}
|
||||
/>
|
||||
<VSToolBar.Separator />
|
||||
<VSToolBar.Button
|
||||
|
|
|
@ -59,11 +59,11 @@ export type RunCodeResultSuccess = {
|
|||
|
||||
export type RunCodeResultError = {
|
||||
status: 'error';
|
||||
result: string;
|
||||
message: string;
|
||||
traceback: string;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Kotone 调试客户端类
|
||||
* 用于处理与调试服务器的 WebSocket 通信
|
||||
|
@ -215,9 +215,10 @@ export class KotoneDebugClient {
|
|||
}
|
||||
});
|
||||
return response.json();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async stopCode(): Promise<void> {
|
||||
const response = await fetch(`http://${this.host}/api/code/stop`);
|
||||
return response.json();
|
||||
}
|
||||
}
|
|
@ -326,7 +326,7 @@ class ContextOcr:
|
|||
return result
|
||||
if time.time() - start_time > timeout:
|
||||
raise TimeoutError(f"Timeout waiting for {pattern}")
|
||||
time.sleep(interval)
|
||||
sleep(interval)
|
||||
|
||||
def wait_for(
|
||||
self,
|
||||
|
@ -348,7 +348,7 @@ class ContextOcr:
|
|||
return result
|
||||
if time.time() - start_time > timeout:
|
||||
return None
|
||||
time.sleep(interval)
|
||||
sleep(interval)
|
||||
|
||||
|
||||
@interruptible_class
|
||||
|
@ -384,7 +384,7 @@ class ContextImage:
|
|||
return ret
|
||||
if time.time() - start_time > timeout:
|
||||
return None
|
||||
time.sleep(interval)
|
||||
sleep(interval)
|
||||
|
||||
def wait_for_any(
|
||||
self,
|
||||
|
@ -413,7 +413,7 @@ class ContextImage:
|
|||
return True
|
||||
if time.time() - start_time > timeout:
|
||||
return False
|
||||
time.sleep(interval)
|
||||
sleep(interval)
|
||||
|
||||
def expect_wait(
|
||||
self,
|
||||
|
@ -439,7 +439,7 @@ class ContextImage:
|
|||
return ret
|
||||
if time.time() - start_time > timeout:
|
||||
raise TimeoutError(f"Timeout waiting for {template}")
|
||||
time.sleep(interval)
|
||||
sleep(interval)
|
||||
|
||||
def expect_wait_any(
|
||||
self,
|
||||
|
@ -470,7 +470,7 @@ class ContextImage:
|
|||
return ret
|
||||
if time.time() - start_time > timeout:
|
||||
raise TimeoutError(f"Timeout waiting for any of {templates}")
|
||||
time.sleep(interval)
|
||||
sleep(interval)
|
||||
|
||||
@context(expect)
|
||||
def expect(self, *args, **kwargs):
|
||||
|
|
|
@ -100,20 +100,40 @@ class RunCodeRequest(BaseModel):
|
|||
|
||||
@app.post("/api/code/run")
|
||||
async def run_code(request: RunCodeRequest):
|
||||
event = asyncio.Event()
|
||||
stdout = StringIO()
|
||||
code = f"from kotonebot import *\n" + request.code
|
||||
try:
|
||||
with manual_context():
|
||||
global_vars = dict(vars(kotonebot.backend.context))
|
||||
with redirect_stdout(stdout):
|
||||
exec(code, global_vars)
|
||||
return {"status": "ok", "result": stdout.getvalue()}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e), "traceback": traceback.format_exc()}
|
||||
result = {}
|
||||
def _runner():
|
||||
nonlocal result
|
||||
from kotonebot.backend.context import vars as context_vars
|
||||
try:
|
||||
with manual_context():
|
||||
global_vars = dict(vars(kotonebot.backend.context))
|
||||
with redirect_stdout(stdout):
|
||||
exec(code, global_vars)
|
||||
result = {"status": "ok", "result": stdout.getvalue()}
|
||||
except (Exception) as e:
|
||||
result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
|
||||
except KeyboardInterrupt as e:
|
||||
result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
|
||||
finally:
|
||||
context_vars.interrupted.clear()
|
||||
event.set()
|
||||
threading.Thread(target=_runner, daemon=True).start()
|
||||
await event.wait()
|
||||
return result
|
||||
|
||||
@app.get("/api/code/stop")
|
||||
async def stop_code():
|
||||
from kotonebot.backend.context import vars
|
||||
vars.interrupted.set()
|
||||
while vars.interrupted.is_set():
|
||||
await asyncio.sleep(0.1)
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.get("/api/ping")
|
||||
async def ping():
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
message_queue = deque()
|
||||
|
|
Loading…
Reference in New Issue