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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, TypeVar, Generic, Optional
|
|
5
|
+
|
|
6
|
+
import cv2
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Query, Body
|
|
8
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from pydantic.generics import GenericModel
|
|
11
|
+
|
|
12
|
+
from kotonebot.devtools.project.project import Project
|
|
13
|
+
from kotonebot.devtools.project.scanner import scan_prefabs
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ResponseModel(GenericModel, Generic[T]):
|
|
20
|
+
success: bool
|
|
21
|
+
message: Optional[str] = None
|
|
22
|
+
data: Optional[T] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WriteTextRequest(BaseModel):
|
|
26
|
+
content: str
|
|
27
|
+
|
|
28
|
+
def create_rest_router(project: Project) -> APIRouter:
|
|
29
|
+
router = APIRouter(prefix="/api")
|
|
30
|
+
_prefabs_cache = None
|
|
31
|
+
|
|
32
|
+
project_root = Path(project.conf_path).parent.resolve()
|
|
33
|
+
thumbnail_cache_root = project_root / ".kotonebot" / "cache" / "thumbnails"
|
|
34
|
+
image_suffixes = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
|
|
35
|
+
|
|
36
|
+
def _is_image_file(path: Path) -> bool:
|
|
37
|
+
return path.suffix.lower() in image_suffixes
|
|
38
|
+
|
|
39
|
+
def _get_thumbnail_path(source: Path, size: int) -> Path:
|
|
40
|
+
if size <= 0:
|
|
41
|
+
raise ValueError("size must be positive")
|
|
42
|
+
try:
|
|
43
|
+
rel = source.resolve().relative_to(project_root)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
raise ValueError(str(e))
|
|
46
|
+
size_dir = thumbnail_cache_root / str(size)
|
|
47
|
+
target_dir = size_dir / rel.parent
|
|
48
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
return target_dir / rel.name
|
|
50
|
+
|
|
51
|
+
def _ensure_thumbnail(source: Path, size: int) -> Path:
|
|
52
|
+
cache_path = _get_thumbnail_path(source, size)
|
|
53
|
+
regenerate = True
|
|
54
|
+
if cache_path.exists():
|
|
55
|
+
src_stat = source.stat()
|
|
56
|
+
cache_stat = cache_path.stat()
|
|
57
|
+
if cache_stat.st_mtime >= src_stat.st_mtime and cache_stat.st_size > 0:
|
|
58
|
+
regenerate = False
|
|
59
|
+
if regenerate:
|
|
60
|
+
img = cv2.imread(str(source))
|
|
61
|
+
if img is None:
|
|
62
|
+
raise ValueError(f"Could not read image: {source}")
|
|
63
|
+
height, width = img.shape[:2]
|
|
64
|
+
longest = max(width, height)
|
|
65
|
+
if longest <= 0:
|
|
66
|
+
raise ValueError("invalid image size")
|
|
67
|
+
scale = size / float(longest)
|
|
68
|
+
new_width = max(1, int(round(width * scale)))
|
|
69
|
+
new_height = max(1, int(round(height * scale)))
|
|
70
|
+
resized = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_AREA)
|
|
71
|
+
cv2.imwrite(str(cache_path), resized)
|
|
72
|
+
return cache_path
|
|
73
|
+
|
|
74
|
+
def _get_safe_path(path_str: str) -> Path:
|
|
75
|
+
p = Path(path_str)
|
|
76
|
+
if not p.is_absolute():
|
|
77
|
+
p = project_root / p
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
p = p.resolve()
|
|
81
|
+
if not str(p).startswith(str(project_root)):
|
|
82
|
+
raise ValueError(f"Access denied: Path {p} is outside project root {project_root}")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise ValueError(f"Invalid path: {e}")
|
|
85
|
+
|
|
86
|
+
return p
|
|
87
|
+
|
|
88
|
+
def _ok(data: Any = None, message: Optional[str] = None) -> JSONResponse:
|
|
89
|
+
return JSONResponse(ResponseModel[Any](success=True, message=message, data=data).dict())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _err(message: str) -> JSONResponse:
|
|
93
|
+
return JSONResponse(ResponseModel[Any](success=False, message=message, data=None).dict())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@router.get("/project/root")
|
|
97
|
+
async def get_project_root():
|
|
98
|
+
try:
|
|
99
|
+
data: dict = {"resource_root": str(project_root)}
|
|
100
|
+
# include editor configuration if available (prefabs_module, resource_path)
|
|
101
|
+
try:
|
|
102
|
+
if project.conf and project.conf.editor:
|
|
103
|
+
data["editor"] = project.conf.editor.model_dump()
|
|
104
|
+
except Exception:
|
|
105
|
+
logging.exception("Failed to include editor config in /project/root response")
|
|
106
|
+
|
|
107
|
+
return _ok(data)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logging.exception("Error while handling /project/root")
|
|
110
|
+
return _err(str(e))
|
|
111
|
+
|
|
112
|
+
@router.get("/fs/list_dir")
|
|
113
|
+
async def list_dir(path: str = Query(..., description="Path relative to project root or absolute path")):
|
|
114
|
+
try:
|
|
115
|
+
safe_path = _get_safe_path(path)
|
|
116
|
+
if not safe_path.exists():
|
|
117
|
+
return _err("Path not found")
|
|
118
|
+
if not safe_path.is_dir():
|
|
119
|
+
return _err("Not a directory")
|
|
120
|
+
|
|
121
|
+
items = []
|
|
122
|
+
entries = sorted(list(safe_path.iterdir()), key=lambda x: (not x.is_dir(), x.name.lower()))
|
|
123
|
+
for item in entries:
|
|
124
|
+
is_image = _is_image_file(item) if item.is_file() else False
|
|
125
|
+
thumbnail_url: Optional[str]
|
|
126
|
+
if is_image:
|
|
127
|
+
thumbnail_url = f"/api/image/thumbnail?path={item}&size=128"
|
|
128
|
+
else:
|
|
129
|
+
thumbnail_url = None
|
|
130
|
+
items.append({
|
|
131
|
+
"name": item.name,
|
|
132
|
+
"isDirectory": item.is_dir(),
|
|
133
|
+
"path": str(item),
|
|
134
|
+
"isImage": is_image,
|
|
135
|
+
"thumbnailUrl": thumbnail_url,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return _ok({"items": items})
|
|
139
|
+
except PermissionError:
|
|
140
|
+
return _err("Permission denied")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
return _err(str(e))
|
|
143
|
+
|
|
144
|
+
@router.get("/fs/read_text")
|
|
145
|
+
async def read_text(path: str = Query(...)):
|
|
146
|
+
try:
|
|
147
|
+
safe_path = _get_safe_path(path)
|
|
148
|
+
if not safe_path.exists():
|
|
149
|
+
return _err("File not found")
|
|
150
|
+
if not safe_path.is_file():
|
|
151
|
+
return _err("Not a file")
|
|
152
|
+
|
|
153
|
+
content = safe_path.read_text(encoding="utf-8")
|
|
154
|
+
return _ok({"content": content})
|
|
155
|
+
except Exception as e:
|
|
156
|
+
return _err(str(e))
|
|
157
|
+
|
|
158
|
+
@router.put("/fs/write_text")
|
|
159
|
+
async def write_text(path: str = Query(...), body: WriteTextRequest = Body(...)):
|
|
160
|
+
try:
|
|
161
|
+
safe_path = _get_safe_path(path)
|
|
162
|
+
if not safe_path.parent.exists():
|
|
163
|
+
return _err("Parent directory does not exist")
|
|
164
|
+
|
|
165
|
+
temp_path = safe_path.with_suffix(safe_path.suffix + ".tmp")
|
|
166
|
+
temp_path.write_text(body.content, encoding="utf-8")
|
|
167
|
+
os.replace(temp_path, safe_path)
|
|
168
|
+
return _ok({"status": "ok"})
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return _err(str(e))
|
|
171
|
+
|
|
172
|
+
@router.get("/image")
|
|
173
|
+
async def get_image(path: str = Query(...)):
|
|
174
|
+
safe_path = _get_safe_path(path)
|
|
175
|
+
if not safe_path.exists():
|
|
176
|
+
raise HTTPException(status_code=404, detail="Image not found")
|
|
177
|
+
|
|
178
|
+
if not _is_image_file(safe_path):
|
|
179
|
+
raise HTTPException(status_code=400, detail="Not an image file")
|
|
180
|
+
|
|
181
|
+
return FileResponse(safe_path)
|
|
182
|
+
|
|
183
|
+
@router.get("/image/thumbnail")
|
|
184
|
+
async def get_image_thumbnail(path: str = Query(...), size: int = Query(128, ge=1, le=2048)):
|
|
185
|
+
safe_path = _get_safe_path(path)
|
|
186
|
+
if not safe_path.exists():
|
|
187
|
+
raise HTTPException(status_code=404, detail="Image not found")
|
|
188
|
+
if not _is_image_file(safe_path):
|
|
189
|
+
raise HTTPException(status_code=400, detail="Not an image file")
|
|
190
|
+
try:
|
|
191
|
+
cache_path = _ensure_thumbnail(safe_path, size)
|
|
192
|
+
except ValueError as e:
|
|
193
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
194
|
+
return FileResponse(cache_path)
|
|
195
|
+
|
|
196
|
+
@router.get("/prefabs/schema")
|
|
197
|
+
async def get_prefabs_schema():
|
|
198
|
+
nonlocal _prefabs_cache
|
|
199
|
+
try:
|
|
200
|
+
if _prefabs_cache is not None:
|
|
201
|
+
return _ok(_prefabs_cache)
|
|
202
|
+
|
|
203
|
+
if not project.conf or not project.conf.editor or not project.conf.editor.prefabs_module:
|
|
204
|
+
return _ok({"version": 1, "prefabs": {}})
|
|
205
|
+
|
|
206
|
+
schema = scan_prefabs(project.conf.editor.prefabs_module)
|
|
207
|
+
_prefabs_cache = schema
|
|
208
|
+
return _ok(schema)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
return _err(str(e))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@router.get("/health")
|
|
214
|
+
async def health_check():
|
|
215
|
+
return _ok({"status": "ok", "service": "kotonebot-devtools"})
|
|
216
|
+
|
|
217
|
+
return router
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi.staticfiles import StaticFiles
|
|
6
|
+
from fastapi.responses import JSONResponse, HTMLResponse
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
from kotonebot.devtools.project.project import Project
|
|
10
|
+
from .rest_api import create_rest_router
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_app():
|
|
14
|
+
"""Create and configure the FastAPI application."""
|
|
15
|
+
app = FastAPI(title="KotoneBot DevTools")
|
|
16
|
+
|
|
17
|
+
project = Project()
|
|
18
|
+
|
|
19
|
+
# REST API for DevTools2 (file IO, images, prefab schema)
|
|
20
|
+
app.include_router(create_rest_router(project))
|
|
21
|
+
|
|
22
|
+
# Get the dist directory path
|
|
23
|
+
dist_dir = Path(__file__).parent.parent / "web" / "dist"
|
|
24
|
+
|
|
25
|
+
# Mount static files if dist directory exists
|
|
26
|
+
if dist_dir.exists():
|
|
27
|
+
# 优先将打包好的静态资源挂载到 /assets(如果构建将资源放在 dist/assets)
|
|
28
|
+
assets_dir = dist_dir / "assets"
|
|
29
|
+
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
|
30
|
+
|
|
31
|
+
# SPA: 直接将除 /api/* 之外的所有路径映射到 index.html
|
|
32
|
+
@app.get("/{_path:path}")
|
|
33
|
+
async def spa_catchall(_path: str):
|
|
34
|
+
# 保留 API 路由的行为
|
|
35
|
+
if _path.startswith("api/"):
|
|
36
|
+
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
|
37
|
+
|
|
38
|
+
index_file = dist_dir / "index.html"
|
|
39
|
+
if index_file.exists():
|
|
40
|
+
return HTMLResponse(index_file.read_text(encoding="utf-8"))
|
|
41
|
+
|
|
42
|
+
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
|
43
|
+
else:
|
|
44
|
+
# If dist doesn't exist, provide a helpful message
|
|
45
|
+
@app.get("/")
|
|
46
|
+
async def missing_dist():
|
|
47
|
+
return JSONResponse({
|
|
48
|
+
"error": "DevTools frontend not found",
|
|
49
|
+
"message": f"Expected frontend dist at {dist_dir}",
|
|
50
|
+
"info": "Build the frontend using: npm run build in kotonebot-devtool directory"
|
|
51
|
+
}, status_code=503)
|
|
52
|
+
|
|
53
|
+
return app
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def start_devtools(
|
|
57
|
+
host: str = "127.0.0.1",
|
|
58
|
+
port: int = 1178,
|
|
59
|
+
open_browser: bool = False
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Start the DevTools web server.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
host: Host to listen on (default: 127.0.0.1)
|
|
65
|
+
port: Port to listen on (default: 1178)
|
|
66
|
+
open_browser: Automatically open browser (default: False)
|
|
67
|
+
"""
|
|
68
|
+
app = create_app()
|
|
69
|
+
|
|
70
|
+
# Open browser before starting server
|
|
71
|
+
if open_browser:
|
|
72
|
+
url = f"http://{host}:{port}"
|
|
73
|
+
webbrowser.open(url)
|
|
74
|
+
|
|
75
|
+
# Start server
|
|
76
|
+
uvicorn.run(
|
|
77
|
+
app,
|
|
78
|
+
host=host,
|
|
79
|
+
port=port,
|
|
80
|
+
log_level="info"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
start_devtools()
|
kotonebot/errors.py
CHANGED
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
from typing import Callable
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class KotonebotError(Exception):
|
|
5
|
-
pass
|
|
6
|
-
|
|
7
|
-
class KotonebotWarning(Warning):
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
class MissingDependencyError(KotonebotError, ImportError):
|
|
11
|
-
def __init__(self, e: ImportError, group_name: str) -> None:
|
|
12
|
-
self.original_error = e
|
|
13
|
-
super().__init__(f'Cannot import module "{e.name}". Did you forget to run "pip install kotonebot[{group_name}]"?')
|
|
14
|
-
|
|
15
|
-
class UserFriendlyError(KotonebotError):
|
|
16
|
-
def __init__(
|
|
17
|
-
self,
|
|
18
|
-
message: str,
|
|
19
|
-
actions: list[tuple[int, str, Callable[[], None]]] = [],
|
|
20
|
-
*args, **kwargs
|
|
21
|
-
) -> None:
|
|
22
|
-
super().__init__(*args, **kwargs)
|
|
23
|
-
self.message = message
|
|
24
|
-
self.actions = actions or []
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
def action_buttons(self) -> list[tuple[int, str]]:
|
|
28
|
-
"""
|
|
29
|
-
以 (id: int, btn_text: str) 的形式返回所有按钮定义。
|
|
30
|
-
"""
|
|
31
|
-
return [(id, text) for id, text, _ in self.actions]
|
|
32
|
-
|
|
33
|
-
def invoke(self, action_id: int):
|
|
34
|
-
"""
|
|
35
|
-
执行指定 ID 的 action。
|
|
36
|
-
"""
|
|
37
|
-
for id, _, func in self.actions:
|
|
38
|
-
if id == action_id:
|
|
39
|
-
func()
|
|
40
|
-
break
|
|
41
|
-
else:
|
|
42
|
-
raise ValueError(f'Action with id {action_id} not found.')
|
|
43
|
-
|
|
44
|
-
class UnrecoverableError(KotonebotError):
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
class GameUpdateNeededError(UnrecoverableError):
|
|
48
|
-
def __init__(self):
|
|
49
|
-
super().__init__(
|
|
50
|
-
'Game update required. '
|
|
51
|
-
'Please go to Play Store and update the game manually.'
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
class ResourceFileMissingError(KotonebotError):
|
|
55
|
-
def __init__(self, file_path: str, description: str):
|
|
56
|
-
self.file_path = file_path
|
|
57
|
-
self.description = description
|
|
58
|
-
super().__init__(f'Resource file ({description}) "{file_path}" is missing.')
|
|
59
|
-
|
|
60
|
-
class TaskNotFoundError(KotonebotError):
|
|
61
|
-
def __init__(self, task_id: str):
|
|
62
|
-
self.task_id = task_id
|
|
63
|
-
super().__init__(f'Task "{task_id}" not found.')
|
|
64
|
-
|
|
65
|
-
class UnscalableResolutionError(KotonebotError):
|
|
66
|
-
def __init__(self, target_resolution:
|
|
67
|
-
self.target_resolution = target_resolution
|
|
68
|
-
self.screen_size = screen_size
|
|
69
|
-
super().__init__(f'Cannot scale to target resolution {target_resolution}. '
|
|
70
|
-
f'Screen size: {screen_size}')
|
|
71
|
-
|
|
72
|
-
class ContextNotInitializedError(KotonebotError):
|
|
73
|
-
def __init__(self, msg: str = 'Context not initialized'):
|
|
74
|
-
super().__init__(msg)
|
|
75
|
-
|
|
76
|
-
class StopCurrentTask(KotonebotError):
|
|
1
|
+
from typing import Callable, Sequence
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KotonebotError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
class KotonebotWarning(Warning):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
class MissingDependencyError(KotonebotError, ImportError):
|
|
11
|
+
def __init__(self, e: ImportError, group_name: str) -> None:
|
|
12
|
+
self.original_error = e
|
|
13
|
+
super().__init__(f'Cannot import module "{e.name}". Did you forget to run "pip install kotonebot[{group_name}]"?')
|
|
14
|
+
|
|
15
|
+
class UserFriendlyError(KotonebotError):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
actions: list[tuple[int, str, Callable[[], None]]] = [],
|
|
20
|
+
*args, **kwargs
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
self.message = message
|
|
24
|
+
self.actions = actions or []
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def action_buttons(self) -> list[tuple[int, str]]:
|
|
28
|
+
"""
|
|
29
|
+
以 (id: int, btn_text: str) 的形式返回所有按钮定义。
|
|
30
|
+
"""
|
|
31
|
+
return [(id, text) for id, text, _ in self.actions]
|
|
32
|
+
|
|
33
|
+
def invoke(self, action_id: int):
|
|
34
|
+
"""
|
|
35
|
+
执行指定 ID 的 action。
|
|
36
|
+
"""
|
|
37
|
+
for id, _, func in self.actions:
|
|
38
|
+
if id == action_id:
|
|
39
|
+
func()
|
|
40
|
+
break
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f'Action with id {action_id} not found.')
|
|
43
|
+
|
|
44
|
+
class UnrecoverableError(KotonebotError):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
class GameUpdateNeededError(UnrecoverableError):
|
|
48
|
+
def __init__(self):
|
|
49
|
+
super().__init__(
|
|
50
|
+
'Game update required. '
|
|
51
|
+
'Please go to Play Store and update the game manually.'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
class ResourceFileMissingError(KotonebotError):
|
|
55
|
+
def __init__(self, file_path: str, description: str):
|
|
56
|
+
self.file_path = file_path
|
|
57
|
+
self.description = description
|
|
58
|
+
super().__init__(f'Resource file ({description}) "{file_path}" is missing.')
|
|
59
|
+
|
|
60
|
+
class TaskNotFoundError(KotonebotError):
|
|
61
|
+
def __init__(self, task_id: str):
|
|
62
|
+
self.task_id = task_id
|
|
63
|
+
super().__init__(f'Task "{task_id}" not found.')
|
|
64
|
+
|
|
65
|
+
class UnscalableResolutionError(KotonebotError):
|
|
66
|
+
def __init__(self, target_resolution: Sequence[int], screen_size: Sequence[int]):
|
|
67
|
+
self.target_resolution = target_resolution
|
|
68
|
+
self.screen_size = screen_size
|
|
69
|
+
super().__init__(f'Cannot scale to target resolution {target_resolution}. '
|
|
70
|
+
f'Screen size: {screen_size}')
|
|
71
|
+
|
|
72
|
+
class ContextNotInitializedError(KotonebotError):
|
|
73
|
+
def __init__(self, msg: str = 'Context not initialized'):
|
|
74
|
+
super().__init__(msg)
|
|
75
|
+
|
|
76
|
+
class StopCurrentTask(KotonebotError):
|
|
77
77
|
pass
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
# ruff: noqa: E402
|
|
2
|
-
from kotonebot.util import require_windows
|
|
3
|
-
require_windows('kotonebot.interop.win module')
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from . import _mouse as mouse
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
# ruff: noqa: E402
|
|
2
|
+
from kotonebot.util import require_windows
|
|
3
|
+
require_windows('kotonebot.interop.win module')
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from . import _mouse as mouse
|
|
7
|
+
from .shake_mouse import ShakeMouse
|
|
8
|
+
from .window import Win32Window, FindWindowMethod
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'mouse',
|
|
12
|
+
'ShakeMouse',
|
|
13
|
+
'Win32Window', 'FindWindowMethod'
|
|
10
14
|
]
|