feat(devtool): 脚本录制器中允许停止执行脚本

This commit is contained in:
XcantloadX 2025-02-05 14:40:03 +08:00
parent b5b53eed2c
commit 0d90ffd014
4 changed files with 62 additions and 26 deletions

View File

@ -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

View File

@ -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();
}
}

View File

@ -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):

View File

@ -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()