roblox-studio-physical-operation-mcp 0.1.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.
- roblox_studio_physical_operation_mcp/__init__.py +13 -0
- roblox_studio_physical_operation_mcp/__main__.py +8 -0
- roblox_studio_physical_operation_mcp/log_filter.py +99 -0
- roblox_studio_physical_operation_mcp/log_utils.py +467 -0
- roblox_studio_physical_operation_mcp/server.py +602 -0
- roblox_studio_physical_operation_mcp/studio_manager.py +476 -0
- roblox_studio_physical_operation_mcp/toolbar_detector.py +513 -0
- roblox_studio_physical_operation_mcp/windows_utils.py +578 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/METADATA +273 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/RECORD +13 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/WHEEL +4 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP 服务器: 定义所有 MCP 工具 (无状态版本)
|
|
3
|
+
|
|
4
|
+
所有工具都需要传入 place_path 参数
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
|
|
11
|
+
from .studio_manager import open_place, close_place, get_session, get_session_universal
|
|
12
|
+
from .windows_utils import (
|
|
13
|
+
send_key_to_window, send_key_combo_to_window,
|
|
14
|
+
capture_window, capture_window_with_modals, find_all_windows_by_pid,
|
|
15
|
+
get_modal_windows, close_all_modals,
|
|
16
|
+
VK_F5, VK_F12, VK_SHIFT
|
|
17
|
+
)
|
|
18
|
+
from .log_utils import get_recent_logs, search_logs, clean_old_logs
|
|
19
|
+
from .toolbar_detector import detect_toolbar_state, detect_toolbar_state_with_debug
|
|
20
|
+
|
|
21
|
+
mcp = FastMCP("roblox-studio-mcp")
|
|
22
|
+
|
|
23
|
+
# 截图输出目录 (系统临时文件夹)
|
|
24
|
+
import tempfile
|
|
25
|
+
SCREENSHOT_DIR = os.path.join(tempfile.gettempdir(), "roblox_studio_mcp_screenshots")
|
|
26
|
+
README_PATH = os.path.join(os.path.dirname(__file__), "..", "README.md")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_session(place_path: str = None, place_id: int = None):
|
|
30
|
+
"""
|
|
31
|
+
辅助函数:获取会话,支持 place_path 或 place_id
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
(success, message, session)
|
|
35
|
+
"""
|
|
36
|
+
return get_session_universal(place_path=place_path, place_id=place_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ============ 系统工具 ============
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
def studio_help() -> str:
|
|
43
|
+
"""
|
|
44
|
+
获取 Roblox Studio MCP 使用指南。
|
|
45
|
+
|
|
46
|
+
返回 README.md 内容,包含 AI 使用最佳实践和工具列表。
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
with open(README_PATH, 'r', encoding='utf-8') as f:
|
|
50
|
+
return f.read()
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return f"无法读取帮助文档: {e}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@mcp.tool()
|
|
56
|
+
def studio_list() -> list[dict]:
|
|
57
|
+
"""
|
|
58
|
+
列出所有运行中的 Roblox Studio 实例。
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Studio 实例列表,每个包含:
|
|
62
|
+
- pid: 进程 ID
|
|
63
|
+
- hwnd: 窗口句柄
|
|
64
|
+
- type: 类型 (local/cloud)
|
|
65
|
+
- place_path: 本地文件路径(本地类型)
|
|
66
|
+
- place_id: Place ID(云端类型)
|
|
67
|
+
"""
|
|
68
|
+
from .studio_manager import get_all_studio_processes, find_latest_studio_logs, get_all_log_cmdlines
|
|
69
|
+
from .windows_utils import find_window_by_pid
|
|
70
|
+
import re
|
|
71
|
+
|
|
72
|
+
processes = get_all_studio_processes()
|
|
73
|
+
log_files = find_latest_studio_logs(20)
|
|
74
|
+
log_cmdlines = get_all_log_cmdlines(log_files)
|
|
75
|
+
|
|
76
|
+
instances = []
|
|
77
|
+
|
|
78
|
+
for proc in processes:
|
|
79
|
+
pid = proc['pid']
|
|
80
|
+
cmdline = proc['cmdline']
|
|
81
|
+
hwnd = find_window_by_pid(pid)
|
|
82
|
+
|
|
83
|
+
# 判断类型
|
|
84
|
+
place_id_match = re.search(r'-placeId\s+(\d+)', cmdline)
|
|
85
|
+
if place_id_match:
|
|
86
|
+
# 云端 Place
|
|
87
|
+
instances.append({
|
|
88
|
+
"pid": pid,
|
|
89
|
+
"hwnd": hwnd,
|
|
90
|
+
"type": "cloud",
|
|
91
|
+
"place_id": int(place_id_match.group(1))
|
|
92
|
+
})
|
|
93
|
+
else:
|
|
94
|
+
# 本地文件 - 提取 .rbxl 路径(在 exe 路径之后)
|
|
95
|
+
# 命令行格式: "...exe" "path.rbxl" 或 ...exe path.rbxl
|
|
96
|
+
rbxl_match = re.search(r'\.exe["\s]+(.+\.rbxl)', cmdline, re.IGNORECASE)
|
|
97
|
+
place_path = rbxl_match.group(1).strip('"') if rbxl_match else None
|
|
98
|
+
instances.append({
|
|
99
|
+
"pid": pid,
|
|
100
|
+
"hwnd": hwnd,
|
|
101
|
+
"type": "local",
|
|
102
|
+
"place_path": place_path
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return instances
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@mcp.tool()
|
|
109
|
+
def studio_open(place_path: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
打开 Roblox Studio 并加载指定的 Place 文件。
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
place_path: rbxl 文件的完整路径
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
操作结果信息
|
|
118
|
+
"""
|
|
119
|
+
success, message = open_place(place_path)
|
|
120
|
+
return message
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@mcp.tool()
|
|
124
|
+
def studio_close(place_path: str) -> str:
|
|
125
|
+
"""
|
|
126
|
+
关闭指定的 Roblox Studio。
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
place_path: rbxl 文件的完整路径
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
操作结果信息
|
|
133
|
+
"""
|
|
134
|
+
success, message = close_place(place_path)
|
|
135
|
+
return message
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@mcp.tool()
|
|
139
|
+
def studio_status(place_path: str = None, place_id: int = None) -> dict:
|
|
140
|
+
"""
|
|
141
|
+
获取指定 Place 的 Studio 状态。
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
145
|
+
place_id: Roblox Place ID(云端 Place)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
包含状态信息的字典
|
|
149
|
+
"""
|
|
150
|
+
success, message, session = _get_session(place_path, place_id)
|
|
151
|
+
if not success:
|
|
152
|
+
return {"active": False, "error": message}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"active": True,
|
|
156
|
+
"place_path": session.place_path,
|
|
157
|
+
"pid": session.pid,
|
|
158
|
+
"hwnd": session.hwnd,
|
|
159
|
+
"log_path": session.log_path
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@mcp.tool()
|
|
164
|
+
def studio_query(place_path: str = None, place_id: int = None) -> dict:
|
|
165
|
+
"""
|
|
166
|
+
综合查询 Studio 状态,包括运行状态、模态弹窗等信息。
|
|
167
|
+
|
|
168
|
+
建议在启动 Studio 后调用此接口,获取完整状态用于后续判断。
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
172
|
+
place_id: Roblox Place ID(云端 Place)
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
包含完整状态信息的 JSON 字典:
|
|
176
|
+
- active: Studio 是否运行中
|
|
177
|
+
- pid: 进程 ID
|
|
178
|
+
- hwnd: 主窗口句柄
|
|
179
|
+
- has_modal: 是否有模态弹窗
|
|
180
|
+
- modals: 模态弹窗列表
|
|
181
|
+
- ready: Studio 是否就绪(运行中且无模态弹窗)
|
|
182
|
+
"""
|
|
183
|
+
success, message, session = _get_session(place_path, place_id)
|
|
184
|
+
|
|
185
|
+
if not success:
|
|
186
|
+
return {
|
|
187
|
+
"active": False,
|
|
188
|
+
"ready": False,
|
|
189
|
+
"error": message,
|
|
190
|
+
"has_modal": False,
|
|
191
|
+
"modals": []
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# 检测模态弹窗
|
|
195
|
+
modals = get_modal_windows(session.hwnd, session.pid)
|
|
196
|
+
has_modal = len(modals) > 0
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
"active": True,
|
|
200
|
+
"ready": not has_modal, # 无模态弹窗时才算就绪
|
|
201
|
+
"pid": session.pid,
|
|
202
|
+
"hwnd": session.hwnd,
|
|
203
|
+
"has_modal": has_modal,
|
|
204
|
+
"modal_count": len(modals),
|
|
205
|
+
"modals": [
|
|
206
|
+
{
|
|
207
|
+
"hwnd": m['hwnd'],
|
|
208
|
+
"title": m['title'],
|
|
209
|
+
"size": f"{m['width']}x{m['height']}"
|
|
210
|
+
}
|
|
211
|
+
for m in modals
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@mcp.tool()
|
|
217
|
+
def modal_detect(place_path: str = None, place_id: int = None) -> dict:
|
|
218
|
+
"""
|
|
219
|
+
检测是否存在模态弹窗。
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
223
|
+
place_id: Roblox Place ID(云端 Place)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
包含模态弹窗信息的字典
|
|
227
|
+
"""
|
|
228
|
+
success, message, session = _get_session(place_path, place_id)
|
|
229
|
+
if not success:
|
|
230
|
+
return {"error": message}
|
|
231
|
+
|
|
232
|
+
modals = get_modal_windows(session.hwnd, session.pid)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"has_modal": len(modals) > 0,
|
|
236
|
+
"count": len(modals),
|
|
237
|
+
"modals": [
|
|
238
|
+
{
|
|
239
|
+
"hwnd": m['hwnd'],
|
|
240
|
+
"title": m['title'],
|
|
241
|
+
"size": f"{m['width']}x{m['height']}"
|
|
242
|
+
}
|
|
243
|
+
for m in modals
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@mcp.tool()
|
|
249
|
+
def modal_close(place_path: str = None, place_id: int = None) -> dict:
|
|
250
|
+
"""
|
|
251
|
+
关闭所有模态弹窗。
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
255
|
+
place_id: Roblox Place ID(云端 Place)
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
关闭结果
|
|
259
|
+
"""
|
|
260
|
+
success, message, session = _get_session(place_path, place_id)
|
|
261
|
+
if not success:
|
|
262
|
+
return {"error": message}
|
|
263
|
+
|
|
264
|
+
count, titles = close_all_modals(session.hwnd, session.pid)
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"closed_count": count,
|
|
268
|
+
"closed_titles": titles
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ============ 游戏控制 ============
|
|
273
|
+
|
|
274
|
+
@mcp.tool()
|
|
275
|
+
def game_start(place_path: str = None, place_id: int = None) -> str:
|
|
276
|
+
"""
|
|
277
|
+
开始游戏 (发送 F5)。
|
|
278
|
+
注意: 会短暂将 Studio 窗口置于前台。
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
282
|
+
place_id: Roblox Place ID(云端 Place)
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
操作结果信息
|
|
286
|
+
"""
|
|
287
|
+
success, message, session = _get_session(place_path, place_id)
|
|
288
|
+
if not success:
|
|
289
|
+
return f"错误: {message}"
|
|
290
|
+
|
|
291
|
+
result = send_key_to_window(session.hwnd, VK_F5)
|
|
292
|
+
return "已发送 F5 (开始游戏)" if result else "发送按键失败"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@mcp.tool()
|
|
296
|
+
def game_stop(place_path: str = None, place_id: int = None) -> str:
|
|
297
|
+
"""
|
|
298
|
+
停止游戏 (发送 Shift+F5)。
|
|
299
|
+
注意: 会短暂将 Studio 窗口置于前台。
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
303
|
+
place_id: Roblox Place ID(云端 Place)
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
操作结果信息
|
|
307
|
+
"""
|
|
308
|
+
success, message, session = _get_session(place_path, place_id)
|
|
309
|
+
if not success:
|
|
310
|
+
return f"错误: {message}"
|
|
311
|
+
|
|
312
|
+
result = send_key_combo_to_window(session.hwnd, [VK_SHIFT, VK_F5])
|
|
313
|
+
return "已发送 Shift+F5 (停止游戏)" if result else "发送按键失败"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@mcp.tool()
|
|
317
|
+
def game_pause(place_path: str = None, place_id: int = None) -> str:
|
|
318
|
+
"""
|
|
319
|
+
暂停/恢复游戏 (发送 F12)。
|
|
320
|
+
注意: 会短暂将 Studio 窗口置于前台。
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
324
|
+
place_id: Roblox Place ID(云端 Place)
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
操作结果信息
|
|
328
|
+
"""
|
|
329
|
+
success, message, session = _get_session(place_path, place_id)
|
|
330
|
+
if not success:
|
|
331
|
+
return f"错误: {message}"
|
|
332
|
+
|
|
333
|
+
result = send_key_to_window(session.hwnd, VK_F12)
|
|
334
|
+
return "已发送 F12 (暂停/恢复)" if result else "发送按键失败"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ============ 日志分析 ============
|
|
338
|
+
|
|
339
|
+
@mcp.tool()
|
|
340
|
+
def logs_get(
|
|
341
|
+
place_path: str = None,
|
|
342
|
+
place_id: int = None,
|
|
343
|
+
after_line: int = None,
|
|
344
|
+
before_line: int = None,
|
|
345
|
+
timestamps: bool = False
|
|
346
|
+
) -> dict:
|
|
347
|
+
"""
|
|
348
|
+
获取当前会话的日志 (仅 FLog::Output,已过滤 Studio 内部日志)。
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
352
|
+
place_id: Roblox Place ID(云端 Place)
|
|
353
|
+
after_line: 从哪一行之后开始读取,None 表示从头开始
|
|
354
|
+
before_line: 到哪一行之前结束,None 表示到末尾
|
|
355
|
+
timestamps: 是否附加时间戳 [HH:MM:SS],默认 False
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
{
|
|
359
|
+
"logs": "日志文本,每行一条",
|
|
360
|
+
"start_line": 起始行号,
|
|
361
|
+
"last_line": 最后行号(用于下次 after_line 参数),
|
|
362
|
+
"remaining": 剩余有效日志行数,
|
|
363
|
+
"has_more": 是否还有更多未返回
|
|
364
|
+
}
|
|
365
|
+
"""
|
|
366
|
+
success, message, session = _get_session(place_path, place_id)
|
|
367
|
+
if not success:
|
|
368
|
+
return {"error": message}
|
|
369
|
+
|
|
370
|
+
from .log_utils import get_logs_from_line
|
|
371
|
+
return get_logs_from_line(
|
|
372
|
+
session.log_path,
|
|
373
|
+
after_line=after_line,
|
|
374
|
+
before_line=before_line,
|
|
375
|
+
timestamps=timestamps
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@mcp.tool()
|
|
380
|
+
def logs_search(
|
|
381
|
+
place_path: str = None,
|
|
382
|
+
place_id: int = None,
|
|
383
|
+
pattern: str = "",
|
|
384
|
+
after_line: int = None,
|
|
385
|
+
before_line: int = None,
|
|
386
|
+
timestamps: bool = False
|
|
387
|
+
) -> dict:
|
|
388
|
+
"""
|
|
389
|
+
在当前会话日志中搜索匹配的条目。
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
393
|
+
place_id: Roblox Place ID(云端 Place)
|
|
394
|
+
pattern: 正则表达式模式
|
|
395
|
+
after_line: 从哪一行之后开始搜索
|
|
396
|
+
before_line: 到哪一行之前结束
|
|
397
|
+
timestamps: 是否附加时间戳
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
{
|
|
401
|
+
"logs": "行号|日志内容",
|
|
402
|
+
"start_line": 起始行号,
|
|
403
|
+
"last_line": 最后行号,
|
|
404
|
+
"match_count": 返回的匹配条数,
|
|
405
|
+
"remaining": 剩余匹配数,
|
|
406
|
+
"has_more": 是否还有更多
|
|
407
|
+
}
|
|
408
|
+
"""
|
|
409
|
+
success, message, session = _get_session(place_path, place_id)
|
|
410
|
+
if not success:
|
|
411
|
+
return {"error": message}
|
|
412
|
+
|
|
413
|
+
from .log_utils import search_logs_from_line
|
|
414
|
+
return search_logs_from_line(
|
|
415
|
+
session.log_path,
|
|
416
|
+
pattern,
|
|
417
|
+
after_line=after_line,
|
|
418
|
+
before_line=before_line,
|
|
419
|
+
timestamps=timestamps
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@mcp.tool()
|
|
424
|
+
def logs_clean(days: int = 7) -> str:
|
|
425
|
+
"""
|
|
426
|
+
清理超过指定天数的旧日志文件。
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
days: 保留最近多少天的日志,默认 7 天
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
清理结果信息
|
|
433
|
+
"""
|
|
434
|
+
count = clean_old_logs(days)
|
|
435
|
+
return f"已清理 {count} 个旧日志文件"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ============ 视觉捕获 ============
|
|
439
|
+
|
|
440
|
+
# ============ 状态检测 ============
|
|
441
|
+
|
|
442
|
+
@mcp.tool()
|
|
443
|
+
def toolbar_state(place_path: str = None, place_id: int = None) -> dict:
|
|
444
|
+
"""
|
|
445
|
+
检测 Roblox Studio 工具栏按钮状态。
|
|
446
|
+
|
|
447
|
+
通过分析工具栏截图,识别播放、暂停、停止、设备选择按钮的状态。
|
|
448
|
+
按钮状态通过灰度判断:灰色=不可用,彩色=可用/激活。
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
452
|
+
place_id: Roblox Place ID(云端 Place)
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
包含按钮状态的字典:
|
|
456
|
+
- play: 播放按钮状态 (disabled/enabled/active)
|
|
457
|
+
- pause: 暂停按钮状态
|
|
458
|
+
- stop: 停止按钮状态
|
|
459
|
+
- device: 设备选择按钮状态
|
|
460
|
+
- game_state: 推断的游戏状态 (stopped/running/paused)
|
|
461
|
+
"""
|
|
462
|
+
success, message, session = _get_session(place_path, place_id)
|
|
463
|
+
if not success:
|
|
464
|
+
return {"error": message}
|
|
465
|
+
|
|
466
|
+
state = detect_toolbar_state(session.hwnd)
|
|
467
|
+
if state is None:
|
|
468
|
+
return {"error": "无法检测工具栏状态"}
|
|
469
|
+
|
|
470
|
+
return state.to_dict()
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@mcp.tool()
|
|
474
|
+
def toolbar_state_debug(place_path: str = None, place_id: int = None, save_debug_image: bool = True) -> dict:
|
|
475
|
+
"""
|
|
476
|
+
检测工具栏状态(带调试信息)。
|
|
477
|
+
|
|
478
|
+
除了返回按钮状态外,还返回详细的调试信息,包括:
|
|
479
|
+
- 按钮区域坐标
|
|
480
|
+
- 每个按钮的饱和度和亮度值
|
|
481
|
+
- 可选保存标注了按钮位置的调试图像
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
485
|
+
place_id: Roblox Place ID(云端 Place)
|
|
486
|
+
save_debug_image: 是否保存调试图像,默认 True
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
包含状态和调试信息的字典
|
|
490
|
+
"""
|
|
491
|
+
success, message, session = _get_session(place_path, place_id)
|
|
492
|
+
if not success:
|
|
493
|
+
return {"error": message}
|
|
494
|
+
|
|
495
|
+
debug_output = None
|
|
496
|
+
if save_debug_image:
|
|
497
|
+
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
|
498
|
+
debug_output = os.path.join(
|
|
499
|
+
SCREENSHOT_DIR,
|
|
500
|
+
f"toolbar_debug_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
state, debug_info = detect_toolbar_state_with_debug(session.hwnd, debug_output)
|
|
504
|
+
|
|
505
|
+
result = {"debug": debug_info}
|
|
506
|
+
if state:
|
|
507
|
+
result["state"] = state.to_dict()
|
|
508
|
+
|
|
509
|
+
return result
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@mcp.tool()
|
|
513
|
+
def screenshot(place_path: str = None, place_id: int = None, filename: str = None) -> str:
|
|
514
|
+
"""
|
|
515
|
+
截取当前 Studio 窗口的截图。
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
519
|
+
place_id: Roblox Place ID(云端 Place)
|
|
520
|
+
filename: 保存的文件名 (不含路径),默认使用时间戳
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
截图文件的完整路径,或错误信息
|
|
524
|
+
"""
|
|
525
|
+
success, message, session = _get_session(place_path, place_id)
|
|
526
|
+
if not success:
|
|
527
|
+
return f"错误: {message}"
|
|
528
|
+
|
|
529
|
+
# 确保目录存在
|
|
530
|
+
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
|
531
|
+
|
|
532
|
+
# 生成文件名
|
|
533
|
+
if not filename:
|
|
534
|
+
filename = f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
|
535
|
+
|
|
536
|
+
output_path = os.path.join(SCREENSHOT_DIR, filename)
|
|
537
|
+
|
|
538
|
+
result = capture_window(session.hwnd, output_path)
|
|
539
|
+
if result:
|
|
540
|
+
return output_path
|
|
541
|
+
else:
|
|
542
|
+
return "错误: 截图失败"
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@mcp.tool()
|
|
546
|
+
def screenshot_full(place_path: str = None, place_id: int = None, filename: str = None) -> dict:
|
|
547
|
+
"""
|
|
548
|
+
截取 Studio 窗口及所有模态弹窗的完整截图。
|
|
549
|
+
|
|
550
|
+
此工具会查找同一进程的所有窗口(包括模态对话框),
|
|
551
|
+
计算它们的包围盒,然后截取整个屏幕区域。
|
|
552
|
+
适用于需要捕获登录框、确认框等模态弹窗的场景。
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
place_path: rbxl 文件的完整路径(本地文件)
|
|
556
|
+
place_id: Roblox Place ID(云端 Place)
|
|
557
|
+
filename: 保存的文件名 (不含路径),默认使用时间戳
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
包含截图路径和窗口信息的字典
|
|
561
|
+
"""
|
|
562
|
+
success, message, session = _get_session(place_path, place_id)
|
|
563
|
+
if not success:
|
|
564
|
+
return {"error": message}
|
|
565
|
+
|
|
566
|
+
# 确保目录存在
|
|
567
|
+
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
|
568
|
+
|
|
569
|
+
# 生成文件名
|
|
570
|
+
if not filename:
|
|
571
|
+
filename = f"screenshot_full_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
|
572
|
+
|
|
573
|
+
output_path = os.path.join(SCREENSHOT_DIR, filename)
|
|
574
|
+
|
|
575
|
+
# 使用新的截图函数
|
|
576
|
+
result, windows_info = capture_window_with_modals(session.hwnd, session.pid, output_path)
|
|
577
|
+
|
|
578
|
+
if result:
|
|
579
|
+
return {
|
|
580
|
+
"success": True,
|
|
581
|
+
"path": output_path,
|
|
582
|
+
"windows_count": len(windows_info),
|
|
583
|
+
"windows": [
|
|
584
|
+
{
|
|
585
|
+
"hwnd": w.get('hwnd'),
|
|
586
|
+
"title": w.get('title', ''),
|
|
587
|
+
"size": f"{w.get('width', 0)}x{w.get('height', 0)}"
|
|
588
|
+
}
|
|
589
|
+
for w in windows_info if 'hwnd' in w
|
|
590
|
+
]
|
|
591
|
+
}
|
|
592
|
+
else:
|
|
593
|
+
return {"error": "截图失败", "details": windows_info}
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def main():
|
|
597
|
+
"""启动 MCP 服务器"""
|
|
598
|
+
mcp.run()
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
if __name__ == "__main__":
|
|
602
|
+
main()
|