kotonebot 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotonebot/__init__.py +39 -39
- kotonebot/backend/bot.py +312 -312
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +1002 -1002
- kotonebot/backend/context/task_action.py +183 -183
- kotonebot/backend/core.py +86 -129
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +535 -535
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +369 -529
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -43
- kotonebot/client/host/adb_common.py +101 -107
- kotonebot/client/host/custom.py +118 -118
- kotonebot/client/host/leidian_host.py +196 -196
- kotonebot/client/host/mumu12_host.py +353 -353
- kotonebot/client/host/protocol.py +214 -214
- kotonebot/client/host/windows_common.py +73 -58
- kotonebot/client/implements/__init__.py +65 -70
- kotonebot/client/implements/adb.py +89 -89
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -188
- kotonebot/client/implements/uiautomator2.py +85 -85
- kotonebot/client/implements/windows/__init__.py +1 -0
- kotonebot/client/implements/windows/print_window.py +133 -0
- kotonebot/client/implements/windows/send_message.py +324 -0
- kotonebot/client/implements/{windows.py → windows/windows.py} +175 -176
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +103 -96
- kotonebot/config/config.py +61 -0
- kotonebot/config/manager.py +36 -36
- kotonebot/core/__init__.py +13 -0
- kotonebot/core/entities/base.py +182 -0
- kotonebot/core/entities/compound.py +75 -0
- kotonebot/core/entities/ocr.py +117 -0
- kotonebot/core/entities/template_match.py +198 -0
- kotonebot/devtools/__init__.py +42 -0
- kotonebot/devtools/cli/__init__.py +6 -0
- kotonebot/devtools/cli/main.py +53 -0
- kotonebot/{tools → devtools}/mirror.py +354 -354
- kotonebot/devtools/project/project.py +41 -0
- kotonebot/devtools/project/scanner.py +202 -0
- kotonebot/devtools/project/schema.py +99 -0
- kotonebot/devtools/resgen/__init__.py +42 -0
- kotonebot/devtools/resgen/codegen.py +331 -0
- kotonebot/devtools/resgen/core.py +94 -0
- kotonebot/devtools/resgen/parsers.py +360 -0
- kotonebot/devtools/resgen/utils.py +158 -0
- kotonebot/devtools/resgen/validation.py +115 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
- kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
- kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
- kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
- kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
- kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
- kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
- kotonebot/devtools/web/dist/index.html +25 -0
- kotonebot/devtools/web/server/__init__.py +0 -0
- kotonebot/devtools/web/server/rest_api.py +217 -0
- kotonebot/devtools/web/server/server.py +85 -0
- kotonebot/errors.py +76 -76
- kotonebot/interop/win/__init__.py +13 -9
- kotonebot/interop/win/_mouse.py +310 -310
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -513
- kotonebot/interop/win/window.py +89 -0
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +19 -17
- kotonebot/primitives/geometry.py +1067 -862
- kotonebot/primitives/visual.py +143 -63
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -88
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -54
- kotonebot/ui/user.py +148 -148
- kotonebot/util.py +436 -436
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/METADATA +84 -82
- kotonebot-0.7.0.dist-info/RECORD +109 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/WHEEL +1 -1
- kotonebot-0.7.0.dist-info/entry_points.txt +2 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot/client/implements/adb_raw.py +0 -163
- kotonebot-0.5.0.dist-info/RECORD +0 -71
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -1,223 +1,223 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import asyncio
|
|
3
|
-
import inspect
|
|
4
|
-
import threading
|
|
5
|
-
import traceback
|
|
6
|
-
import subprocess
|
|
7
|
-
from io import StringIO
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Literal
|
|
10
|
-
from collections import deque
|
|
11
|
-
from contextlib import redirect_stdout
|
|
12
|
-
|
|
13
|
-
import cv2
|
|
14
|
-
import uvicorn
|
|
15
|
-
from thefuzz import fuzz
|
|
16
|
-
from pydantic import BaseModel
|
|
17
|
-
from fastapi.responses import FileResponse, Response
|
|
18
|
-
from fastapi import FastAPI, WebSocket, HTTPException
|
|
19
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
20
|
-
|
|
21
|
-
import kotonebot
|
|
22
|
-
import kotonebot.backend
|
|
23
|
-
import kotonebot.backend.context
|
|
24
|
-
from kotonebot.backend.core import HintBox, Image
|
|
25
|
-
from ..context import manual_context
|
|
26
|
-
from . import vars as debug_vars
|
|
27
|
-
from .vars import WSImage, WSMessageData, WSMessage, WSCallstack
|
|
28
|
-
|
|
29
|
-
app = FastAPI()
|
|
30
|
-
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
|
31
|
-
|
|
32
|
-
# 获取当前文件夹路径
|
|
33
|
-
CURRENT_DIR = Path(__file__).parent
|
|
34
|
-
|
|
35
|
-
APP_DIR = Path.cwd()
|
|
36
|
-
|
|
37
|
-
class File(BaseModel):
|
|
38
|
-
name: str
|
|
39
|
-
full_path: str
|
|
40
|
-
type: Literal["file", "dir"]
|
|
41
|
-
|
|
42
|
-
@app.get("/api/read_file")
|
|
43
|
-
async def read_file(path: str):
|
|
44
|
-
"""读取文件内容"""
|
|
45
|
-
try:
|
|
46
|
-
# 确保路径在当前目录下
|
|
47
|
-
full_path = (APP_DIR / path).resolve()
|
|
48
|
-
if not Path(full_path).is_relative_to(APP_DIR):
|
|
49
|
-
raise HTTPException(status_code=403, detail="Access denied")
|
|
50
|
-
|
|
51
|
-
if not full_path.exists():
|
|
52
|
-
raise HTTPException(status_code=404, detail="File not found")
|
|
53
|
-
# 添加缓存控制头
|
|
54
|
-
headers = {
|
|
55
|
-
"Cache-Control": "public, max-age=3600", # 缓存1小时
|
|
56
|
-
"ETag": f'"{hash(full_path)}"' # 使用full_path的哈希值作为ETag
|
|
57
|
-
}
|
|
58
|
-
return FileResponse(full_path, headers=headers)
|
|
59
|
-
except Exception as e:
|
|
60
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
61
|
-
|
|
62
|
-
@app.get("/api/read_memory")
|
|
63
|
-
async def read_memory(key: str):
|
|
64
|
-
"""读取内存中的数据"""
|
|
65
|
-
try:
|
|
66
|
-
image = None
|
|
67
|
-
if (image := debug_vars._read_image(key)) is not None:
|
|
68
|
-
pass
|
|
69
|
-
else:
|
|
70
|
-
raise HTTPException(status_code=404, detail="Key not found")
|
|
71
|
-
|
|
72
|
-
# 编码图片
|
|
73
|
-
encode_params = [cv2.IMWRITE_PNG_COMPRESSION, 4]
|
|
74
|
-
_, buffer = cv2.imencode('.png', image, encode_params)
|
|
75
|
-
# 添加缓存控制头
|
|
76
|
-
headers = {
|
|
77
|
-
"Cache-Control": "public, max-age=3600", # 缓存1小时
|
|
78
|
-
"ETag": f'"{hash(key)}"' # 使用key的哈希值作为ETag
|
|
79
|
-
}
|
|
80
|
-
return Response(
|
|
81
|
-
buffer.tobytes(),
|
|
82
|
-
media_type="image/jpeg",
|
|
83
|
-
headers=headers
|
|
84
|
-
)
|
|
85
|
-
except Exception as e:
|
|
86
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
87
|
-
|
|
88
|
-
@app.get("/api/screenshot")
|
|
89
|
-
def screenshot():
|
|
90
|
-
from ..context import device
|
|
91
|
-
img = device.screenshot()
|
|
92
|
-
buff = cv2.imencode('.png', img)[1].tobytes()
|
|
93
|
-
return Response(buff, media_type="image/png")
|
|
94
|
-
|
|
95
|
-
class RunCodeRequest(BaseModel):
|
|
96
|
-
code: str
|
|
97
|
-
|
|
98
|
-
@app.post("/api/code/run")
|
|
99
|
-
async def run_code(request: RunCodeRequest):
|
|
100
|
-
event = asyncio.Event()
|
|
101
|
-
stdout = StringIO()
|
|
102
|
-
code = f"from kotonebot import *\n" + request.code
|
|
103
|
-
result = {}
|
|
104
|
-
def _runner():
|
|
105
|
-
nonlocal result
|
|
106
|
-
from kotonebot.backend.context import vars as context_vars
|
|
107
|
-
try:
|
|
108
|
-
with manual_context():
|
|
109
|
-
global_vars = dict(vars(kotonebot.backend.context))
|
|
110
|
-
with redirect_stdout(stdout):
|
|
111
|
-
exec(code, global_vars)
|
|
112
|
-
result = {"status": "ok", "result": stdout.getvalue()}
|
|
113
|
-
except (Exception) as e:
|
|
114
|
-
result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
|
|
115
|
-
except KeyboardInterrupt as e:
|
|
116
|
-
result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
|
|
117
|
-
finally:
|
|
118
|
-
context_vars.flow.clear_interrupt()
|
|
119
|
-
event.set()
|
|
120
|
-
threading.Thread(target=_runner, daemon=True).start()
|
|
121
|
-
await event.wait()
|
|
122
|
-
return result
|
|
123
|
-
|
|
124
|
-
@app.get("/api/code/stop")
|
|
125
|
-
async def stop_code():
|
|
126
|
-
from kotonebot.backend.context import vars
|
|
127
|
-
vars.flow.request_interrupt()
|
|
128
|
-
while vars.flow.is_interrupted:
|
|
129
|
-
await asyncio.sleep(0.1)
|
|
130
|
-
return {"status": "ok"}
|
|
131
|
-
|
|
132
|
-
@app.get("/api/fs/list_dir")
|
|
133
|
-
def list_dir(path: str) -> list[File]:
|
|
134
|
-
result = []
|
|
135
|
-
for item in Path(path).iterdir():
|
|
136
|
-
result.append(File(
|
|
137
|
-
name=item.name,
|
|
138
|
-
full_path=str(item),
|
|
139
|
-
type="file" if item.is_file() else "dir"
|
|
140
|
-
))
|
|
141
|
-
return result
|
|
142
|
-
|
|
143
|
-
@app.get("/api/resources/autocomplete")
|
|
144
|
-
def autocomplete(class_path: str) -> list[str]:
|
|
145
|
-
from kotonebot.kaa.tasks import R
|
|
146
|
-
class_names = class_path.split(".")[:-1]
|
|
147
|
-
target_class = R
|
|
148
|
-
# 定位到目标类
|
|
149
|
-
for name in class_names:
|
|
150
|
-
target_class = getattr(target_class, name, None)
|
|
151
|
-
if target_class is None:
|
|
152
|
-
return []
|
|
153
|
-
# 获取目标类的所有属性
|
|
154
|
-
attrs = [attr for attr in dir(target_class) if not attr.startswith("_")]
|
|
155
|
-
filtered_attrs = []
|
|
156
|
-
for attr in attrs:
|
|
157
|
-
if inspect.isclass(getattr(target_class, attr)):
|
|
158
|
-
filtered_attrs.append(attr)
|
|
159
|
-
elif isinstance(getattr(target_class, attr), (Image, HintBox)):
|
|
160
|
-
filtered_attrs.append(attr)
|
|
161
|
-
attrs = filtered_attrs
|
|
162
|
-
# 排序
|
|
163
|
-
attrs.sort(key=lambda x: fuzz.ratio(x, class_path), reverse=True)
|
|
164
|
-
return attrs
|
|
165
|
-
|
|
166
|
-
@app.get("/api/ping")
|
|
167
|
-
async def ping():
|
|
168
|
-
return {"status": "ok"}
|
|
169
|
-
|
|
170
|
-
message_queue = deque()
|
|
171
|
-
@app.websocket("/ws")
|
|
172
|
-
async def websocket_endpoint(websocket: WebSocket):
|
|
173
|
-
await websocket.accept()
|
|
174
|
-
try:
|
|
175
|
-
while True:
|
|
176
|
-
if len(message_queue) > 0:
|
|
177
|
-
message = message_queue.popleft()
|
|
178
|
-
await websocket.send_json(message)
|
|
179
|
-
await asyncio.sleep(0.1)
|
|
180
|
-
except:
|
|
181
|
-
await websocket.close()
|
|
182
|
-
|
|
183
|
-
def send_ws_message(title: str, image: list[str], text: str = '', callstack: list[WSCallstack] = [], wait: bool = False):
|
|
184
|
-
"""发送 WebSocket 消息"""
|
|
185
|
-
message = WSMessage(
|
|
186
|
-
type="visual",
|
|
187
|
-
data=WSMessageData(
|
|
188
|
-
image=WSImage(type="memory", value=image),
|
|
189
|
-
name=title,
|
|
190
|
-
details=text,
|
|
191
|
-
timestamp=int(time.time() * 1000),
|
|
192
|
-
callstack=callstack
|
|
193
|
-
)
|
|
194
|
-
)
|
|
195
|
-
message_queue.append(message.dict())
|
|
196
|
-
if wait:
|
|
197
|
-
while len(message_queue) > 0:
|
|
198
|
-
time.sleep(0.3)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
thread = None
|
|
202
|
-
def start_server():
|
|
203
|
-
global thread
|
|
204
|
-
def run_server():
|
|
205
|
-
uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug_vars.debug.hide_server_log else None)
|
|
206
|
-
if thread is None:
|
|
207
|
-
thread = threading.Thread(target=run_server, daemon=True)
|
|
208
|
-
thread.start()
|
|
209
|
-
|
|
210
|
-
def wait_message_all_done():
|
|
211
|
-
global thread
|
|
212
|
-
def _wait():
|
|
213
|
-
while len(message_queue) > 0:
|
|
214
|
-
time.sleep(0.1)
|
|
215
|
-
if thread is not None:
|
|
216
|
-
threading.Thread(target=_wait, daemon=True).start()
|
|
217
|
-
|
|
218
|
-
if __name__ == "__main__":
|
|
219
|
-
debug_vars.debug.hide_server_log = False
|
|
220
|
-
process = subprocess.Popen(["pylsp", "--port", "5479", "--ws"])
|
|
221
|
-
print("LSP started. PID=", process.pid)
|
|
222
|
-
uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug_vars.debug.hide_server_log else None)
|
|
1
|
+
import time
|
|
2
|
+
import asyncio
|
|
3
|
+
import inspect
|
|
4
|
+
import threading
|
|
5
|
+
import traceback
|
|
6
|
+
import subprocess
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal
|
|
10
|
+
from collections import deque
|
|
11
|
+
from contextlib import redirect_stdout
|
|
12
|
+
|
|
13
|
+
import cv2
|
|
14
|
+
import uvicorn
|
|
15
|
+
from thefuzz import fuzz
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
from fastapi.responses import FileResponse, Response
|
|
18
|
+
from fastapi import FastAPI, WebSocket, HTTPException
|
|
19
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
20
|
+
|
|
21
|
+
import kotonebot
|
|
22
|
+
import kotonebot.backend
|
|
23
|
+
import kotonebot.backend.context
|
|
24
|
+
from kotonebot.backend.core import HintBox, Image
|
|
25
|
+
from ..context import manual_context
|
|
26
|
+
from . import vars as debug_vars
|
|
27
|
+
from .vars import WSImage, WSMessageData, WSMessage, WSCallstack
|
|
28
|
+
|
|
29
|
+
app = FastAPI()
|
|
30
|
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
|
31
|
+
|
|
32
|
+
# 获取当前文件夹路径
|
|
33
|
+
CURRENT_DIR = Path(__file__).parent
|
|
34
|
+
|
|
35
|
+
APP_DIR = Path.cwd()
|
|
36
|
+
|
|
37
|
+
class File(BaseModel):
|
|
38
|
+
name: str
|
|
39
|
+
full_path: str
|
|
40
|
+
type: Literal["file", "dir"]
|
|
41
|
+
|
|
42
|
+
@app.get("/api/read_file")
|
|
43
|
+
async def read_file(path: str):
|
|
44
|
+
"""读取文件内容"""
|
|
45
|
+
try:
|
|
46
|
+
# 确保路径在当前目录下
|
|
47
|
+
full_path = (APP_DIR / path).resolve()
|
|
48
|
+
if not Path(full_path).is_relative_to(APP_DIR):
|
|
49
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
50
|
+
|
|
51
|
+
if not full_path.exists():
|
|
52
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
53
|
+
# 添加缓存控制头
|
|
54
|
+
headers = {
|
|
55
|
+
"Cache-Control": "public, max-age=3600", # 缓存1小时
|
|
56
|
+
"ETag": f'"{hash(full_path)}"' # 使用full_path的哈希值作为ETag
|
|
57
|
+
}
|
|
58
|
+
return FileResponse(full_path, headers=headers)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
61
|
+
|
|
62
|
+
@app.get("/api/read_memory")
|
|
63
|
+
async def read_memory(key: str):
|
|
64
|
+
"""读取内存中的数据"""
|
|
65
|
+
try:
|
|
66
|
+
image = None
|
|
67
|
+
if (image := debug_vars._read_image(key)) is not None:
|
|
68
|
+
pass
|
|
69
|
+
else:
|
|
70
|
+
raise HTTPException(status_code=404, detail="Key not found")
|
|
71
|
+
|
|
72
|
+
# 编码图片
|
|
73
|
+
encode_params = [cv2.IMWRITE_PNG_COMPRESSION, 4]
|
|
74
|
+
_, buffer = cv2.imencode('.png', image, encode_params)
|
|
75
|
+
# 添加缓存控制头
|
|
76
|
+
headers = {
|
|
77
|
+
"Cache-Control": "public, max-age=3600", # 缓存1小时
|
|
78
|
+
"ETag": f'"{hash(key)}"' # 使用key的哈希值作为ETag
|
|
79
|
+
}
|
|
80
|
+
return Response(
|
|
81
|
+
buffer.tobytes(),
|
|
82
|
+
media_type="image/jpeg",
|
|
83
|
+
headers=headers
|
|
84
|
+
)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
87
|
+
|
|
88
|
+
@app.get("/api/screenshot")
|
|
89
|
+
def screenshot():
|
|
90
|
+
from ..context import device
|
|
91
|
+
img = device.screenshot()
|
|
92
|
+
buff = cv2.imencode('.png', img)[1].tobytes()
|
|
93
|
+
return Response(buff, media_type="image/png")
|
|
94
|
+
|
|
95
|
+
class RunCodeRequest(BaseModel):
|
|
96
|
+
code: str
|
|
97
|
+
|
|
98
|
+
@app.post("/api/code/run")
|
|
99
|
+
async def run_code(request: RunCodeRequest):
|
|
100
|
+
event = asyncio.Event()
|
|
101
|
+
stdout = StringIO()
|
|
102
|
+
code = f"from kotonebot import *\n" + request.code
|
|
103
|
+
result = {}
|
|
104
|
+
def _runner():
|
|
105
|
+
nonlocal result
|
|
106
|
+
from kotonebot.backend.context import vars as context_vars
|
|
107
|
+
try:
|
|
108
|
+
with manual_context():
|
|
109
|
+
global_vars = dict(vars(kotonebot.backend.context))
|
|
110
|
+
with redirect_stdout(stdout):
|
|
111
|
+
exec(code, global_vars)
|
|
112
|
+
result = {"status": "ok", "result": stdout.getvalue()}
|
|
113
|
+
except (Exception) as e:
|
|
114
|
+
result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
|
|
115
|
+
except KeyboardInterrupt as e:
|
|
116
|
+
result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
|
|
117
|
+
finally:
|
|
118
|
+
context_vars.flow.clear_interrupt()
|
|
119
|
+
event.set()
|
|
120
|
+
threading.Thread(target=_runner, daemon=True).start()
|
|
121
|
+
await event.wait()
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
@app.get("/api/code/stop")
|
|
125
|
+
async def stop_code():
|
|
126
|
+
from kotonebot.backend.context import vars
|
|
127
|
+
vars.flow.request_interrupt()
|
|
128
|
+
while vars.flow.is_interrupted:
|
|
129
|
+
await asyncio.sleep(0.1)
|
|
130
|
+
return {"status": "ok"}
|
|
131
|
+
|
|
132
|
+
@app.get("/api/fs/list_dir")
|
|
133
|
+
def list_dir(path: str) -> list[File]:
|
|
134
|
+
result = []
|
|
135
|
+
for item in Path(path).iterdir():
|
|
136
|
+
result.append(File(
|
|
137
|
+
name=item.name,
|
|
138
|
+
full_path=str(item),
|
|
139
|
+
type="file" if item.is_file() else "dir"
|
|
140
|
+
))
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
@app.get("/api/resources/autocomplete")
|
|
144
|
+
def autocomplete(class_path: str) -> list[str]:
|
|
145
|
+
from kotonebot.kaa.tasks import R
|
|
146
|
+
class_names = class_path.split(".")[:-1]
|
|
147
|
+
target_class = R
|
|
148
|
+
# 定位到目标类
|
|
149
|
+
for name in class_names:
|
|
150
|
+
target_class = getattr(target_class, name, None)
|
|
151
|
+
if target_class is None:
|
|
152
|
+
return []
|
|
153
|
+
# 获取目标类的所有属性
|
|
154
|
+
attrs = [attr for attr in dir(target_class) if not attr.startswith("_")]
|
|
155
|
+
filtered_attrs = []
|
|
156
|
+
for attr in attrs:
|
|
157
|
+
if inspect.isclass(getattr(target_class, attr)):
|
|
158
|
+
filtered_attrs.append(attr)
|
|
159
|
+
elif isinstance(getattr(target_class, attr), (Image, HintBox)):
|
|
160
|
+
filtered_attrs.append(attr)
|
|
161
|
+
attrs = filtered_attrs
|
|
162
|
+
# 排序
|
|
163
|
+
attrs.sort(key=lambda x: fuzz.ratio(x, class_path), reverse=True)
|
|
164
|
+
return attrs
|
|
165
|
+
|
|
166
|
+
@app.get("/api/ping")
|
|
167
|
+
async def ping():
|
|
168
|
+
return {"status": "ok"}
|
|
169
|
+
|
|
170
|
+
message_queue = deque()
|
|
171
|
+
@app.websocket("/ws")
|
|
172
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
173
|
+
await websocket.accept()
|
|
174
|
+
try:
|
|
175
|
+
while True:
|
|
176
|
+
if len(message_queue) > 0:
|
|
177
|
+
message = message_queue.popleft()
|
|
178
|
+
await websocket.send_json(message)
|
|
179
|
+
await asyncio.sleep(0.1)
|
|
180
|
+
except:
|
|
181
|
+
await websocket.close()
|
|
182
|
+
|
|
183
|
+
def send_ws_message(title: str, image: list[str], text: str = '', callstack: list[WSCallstack] = [], wait: bool = False):
|
|
184
|
+
"""发送 WebSocket 消息"""
|
|
185
|
+
message = WSMessage(
|
|
186
|
+
type="visual",
|
|
187
|
+
data=WSMessageData(
|
|
188
|
+
image=WSImage(type="memory", value=image),
|
|
189
|
+
name=title,
|
|
190
|
+
details=text,
|
|
191
|
+
timestamp=int(time.time() * 1000),
|
|
192
|
+
callstack=callstack
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
message_queue.append(message.dict())
|
|
196
|
+
if wait:
|
|
197
|
+
while len(message_queue) > 0:
|
|
198
|
+
time.sleep(0.3)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
thread = None
|
|
202
|
+
def start_server():
|
|
203
|
+
global thread
|
|
204
|
+
def run_server():
|
|
205
|
+
uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug_vars.debug.hide_server_log else None)
|
|
206
|
+
if thread is None:
|
|
207
|
+
thread = threading.Thread(target=run_server, daemon=True)
|
|
208
|
+
thread.start()
|
|
209
|
+
|
|
210
|
+
def wait_message_all_done():
|
|
211
|
+
global thread
|
|
212
|
+
def _wait():
|
|
213
|
+
while len(message_queue) > 0:
|
|
214
|
+
time.sleep(0.1)
|
|
215
|
+
if thread is not None:
|
|
216
|
+
threading.Thread(target=_wait, daemon=True).start()
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
debug_vars.debug.hide_server_log = False
|
|
220
|
+
process = subprocess.Popen(["pylsp", "--port", "5479", "--ws"])
|
|
221
|
+
print("LSP started. PID=", process.pid)
|
|
222
|
+
uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug_vars.debug.hide_server_log else None)
|
|
223
223
|
process.kill()
|