automas-script-maafw 0.1.2__tar.gz

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.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: automas-script-maafw
3
+ Version: 0.1.2
4
+ Summary: MaaFW script adapter plugin for AUTO-MAS
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: automas-maafw-agent-env>=0.1.0
8
+ Requires-Dist: automas-maafw-controller-adb>=0.1.0
9
+ Requires-Dist: automas-maafw-controller-win32>=0.1.1
10
+ Requires-Dist: automas-maafw-interface>=0.1.1
11
+ Requires-Dist: automas-maafw-project-update>=0.1.0
12
+ Requires-Dist: automas-maafw-runner>=0.1.1
13
+ Requires-Dist: pydantic>=2
14
+
15
+ # automas-script-maafw
16
+
17
+ MaaFW script adapter plugin for AUTO-MAS.
18
+
19
+ It registers `ScriptType=MaaFW` through the script adapter registry and stores
20
+ new MaaFW scripts in `PluginScriptConfig`. M9A is provided by the
21
+ `automas-script-maafw-pack-m9a` project pack, and that pack registers the
22
+ user-visible `ScriptType=M9A` entry while reusing this adapter's runtime hooks.
@@ -0,0 +1,8 @@
1
+ # automas-script-maafw
2
+
3
+ MaaFW script adapter plugin for AUTO-MAS.
4
+
5
+ It registers `ScriptType=MaaFW` through the script adapter registry and stores
6
+ new MaaFW scripts in `PluginScriptConfig`. M9A is provided by the
7
+ `automas-script-maafw-pack-m9a` project pack, and that pack registers the
8
+ user-visible `ScriptType=M9A` entry while reusing this adapter's runtime hooks.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "automas-script-maafw"
7
+ version = "0.1.2"
8
+ description = "MaaFW script adapter plugin for AUTO-MAS"
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "automas-maafw-agent-env>=0.1.0",
13
+ "automas-maafw-controller-adb>=0.1.0",
14
+ "automas-maafw-controller-win32>=0.1.1",
15
+ "automas-maafw-interface>=0.1.1",
16
+ "automas-maafw-project-update>=0.1.0",
17
+ "automas-maafw-runner>=0.1.1",
18
+ "pydantic>=2",
19
+ ]
20
+
21
+ [project.entry-points."auto_mas.plugins"]
22
+ automas_script_maafw = "automas_script_maafw.plugin:Plugin"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.setuptools.package-data]
28
+ automas_script_maafw = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,278 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import copy
5
+ import json
6
+ import uuid
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Any, TypeVar
10
+
11
+ from app.core import Config
12
+ from app.models.ConfigBase import ConfigBase, JSONValidator, MultipleConfig
13
+ from app.models.config import MaaFWConfig, MaaFWUserConfig
14
+ from app.models.task import ScriptItem, TaskExecuteBase, UserItem
15
+ from app.plugins import ScriptAdapterHooks, ScriptAdapterRuntime
16
+ from app.utils import get_logger
17
+ from automas_maafw_interface.loader import MaaFWInterfaceLoadError
18
+ from automas_maafw_interface.service import MaaFWInterfaceService
19
+ from automas_maafw_project_update.service import MaaFWProjectUpdateService
20
+
21
+ from .runner_task import MaaFWPluginAutoProxyTask
22
+ from .schema import build_source_config
23
+
24
+
25
+ LegacyConfigT = TypeVar("LegacyConfigT", bound=ConfigBase)
26
+
27
+
28
+ logger = get_logger("MaaFW 插件适配")
29
+
30
+
31
+ class MaaFWAdapterHooks(ScriptAdapterHooks):
32
+ """MaaFW script adapter backed by PluginScriptConfig."""
33
+
34
+ async def check(self, runtime: ScriptAdapterRuntime) -> str:
35
+ if runtime.mode != "AutoProxy":
36
+ return "MaaFW 插件当前仅支持 AutoProxy 模式"
37
+
38
+ script_data = await runtime.storage.read_script_data()
39
+ script_config = await _load_legacy_script_config(script_data)
40
+ raw_project_path = str(script_config.get("Info", "Path") or "").strip()
41
+ if not raw_project_path:
42
+ return "请设置 MaaFW 项目路径"
43
+
44
+ project_path = Path(raw_project_path)
45
+ if not project_path.exists():
46
+ return "请设置 MaaFW 项目路径"
47
+
48
+ try:
49
+ interface = MaaFWInterfaceService().load(project_path)
50
+ except MaaFWInterfaceLoadError as exc:
51
+ return f"无法读取 MaaFW interface,请检查项目路径: {exc}"
52
+
53
+ if not interface.controller:
54
+ return "MaaFW interface 未声明 controller,请检查项目目录"
55
+ if not interface.resource:
56
+ return "MaaFW interface 未声明 resource,请检查项目目录"
57
+ if not interface.task:
58
+ return "MaaFW interface 未声明 task,请检查项目目录"
59
+
60
+ emulator_id = script_config.get("Emulator", "Id")
61
+ emulator_index = script_config.get("Emulator", "Index")
62
+ if emulator_id != "-" and emulator_index in ("", "-"):
63
+ return "请在 MaaFW 脚本配置中选择模拟器实例"
64
+ return "Pass"
65
+
66
+ async def prepare(self, runtime: ScriptAdapterRuntime) -> None:
67
+ await runtime.storage.lock()
68
+
69
+ script_data = await runtime.storage.read_script_data()
70
+ script_config = await _load_legacy_script_config(script_data)
71
+ user_config = await _load_legacy_user_config(runtime)
72
+
73
+ runtime.script_config = script_config
74
+ runtime.user_config = user_config
75
+ runtime.extra["maafw_project_update_logs"] = []
76
+
77
+ await self._update_project_before_run(runtime, script_config)
78
+
79
+ emulator_manager = None
80
+ emulator_id = script_config.get("Emulator", "Id")
81
+ if emulator_id != "-":
82
+ emulator_manager = await runtime.initialize_emulator_manager(emulator_id)
83
+ runtime.emulator_manager = emulator_manager
84
+
85
+ runtime.script_info.user_list = [
86
+ UserItem(
87
+ user_id=str(uid),
88
+ name=config.get("Info", "Name"),
89
+ status="等待",
90
+ )
91
+ for uid, config in user_config.items()
92
+ if config.get("Info", "Status")
93
+ and config.get("Info", "RemainedDay") != 0
94
+ ]
95
+ self._emit_log(
96
+ runtime,
97
+ f"MaaFW 插件用户列表加载完成,已筛选用户数: {len(runtime.script_info.user_list)}",
98
+ )
99
+
100
+ def run_auto_proxy(self, runtime: ScriptAdapterRuntime) -> TaskExecuteBase:
101
+ if runtime.script_config is None or runtime.user_config is None:
102
+ raise RuntimeError("MaaFW 插件配置尚未准备")
103
+ return MaaFWPluginAutoProxyTask(
104
+ runtime.script_info,
105
+ runtime.script_config,
106
+ runtime.user_config,
107
+ runtime.emulator_manager,
108
+ list(runtime.extra.get("maafw_project_update_logs") or []),
109
+ )
110
+
111
+ async def finalize(self, runtime: ScriptAdapterRuntime) -> None:
112
+ previous_status = runtime.script_info.status
113
+ try:
114
+ if runtime.user_config is not None and runtime.mode == "AutoProxy":
115
+ # prepare() 锁定了整棵配置树(含 UserData),写回前必须先解锁,
116
+ # 否则 PluginUserConfig.set() 会抛出 "配置已锁定, 无法修改"
117
+ await runtime.storage.unlock()
118
+ await runtime.storage.save_user_models(runtime.user_config)
119
+ finally:
120
+ await runtime.storage.unlock()
121
+
122
+ if previous_status == "异常" or runtime.check_result not in ("-", "", "Pass"):
123
+ runtime.script_info.status = "异常"
124
+ return
125
+
126
+ error_user = [u.name for u in runtime.script_info.user_list if u.status == "异常"]
127
+ over_user = [u.name for u in runtime.script_info.user_list if u.status == "完成"]
128
+ if error_user:
129
+ runtime.script_info.status = "异常"
130
+ elif over_user:
131
+ runtime.script_info.status = "完成"
132
+ else:
133
+ runtime.script_info.status = "跳过"
134
+
135
+ async def on_crash(self, runtime: ScriptAdapterRuntime, error: Exception) -> None:
136
+ try:
137
+ await runtime.storage.unlock()
138
+ except Exception:
139
+ pass
140
+ await super().on_crash(runtime, error)
141
+
142
+ async def _update_project_before_run(
143
+ self,
144
+ runtime: ScriptAdapterRuntime,
145
+ script_config: MaaFWConfig,
146
+ ) -> None:
147
+ if not script_config.get("Update", "IfAutoUpdate"):
148
+ self._emit_log(runtime, "MaaFW 项目运行前自动更新已关闭")
149
+ return
150
+
151
+ project_path = Path(script_config.get("Info", "Path")).resolve()
152
+ try:
153
+ interface_model = MaaFWInterfaceService().load(project_path)
154
+ except MaaFWInterfaceLoadError as exc:
155
+ self._emit_log(runtime, f"MaaFW 项目更新跳过,interface 读取失败: {exc}")
156
+ return
157
+
158
+ script_data = await runtime.storage.read_script_data()
159
+ source_config = build_source_config(script_data)
160
+ mirror_cdk = (
161
+ script_config.get("Update", "MirrorChyanCDK")
162
+ or Config.get("Update", "MirrorChyanCDK")
163
+ )
164
+ channel = script_config.get("Update", "Channel") or Config.get("Update", "Channel")
165
+ try:
166
+ update_result = await MaaFWProjectUpdateService().update_if_needed(
167
+ project_path,
168
+ interface_model,
169
+ mirror_cdk=mirror_cdk,
170
+ channel=channel,
171
+ proxy=Config.proxy,
172
+ send_log=lambda message: self._emit_log(runtime, message),
173
+ source_config=source_config,
174
+ )
175
+ if update_result.updated:
176
+ refreshed_interface = MaaFWInterfaceService().load(
177
+ project_path,
178
+ force_reload=True,
179
+ )
180
+ self._emit_log(runtime, "MaaFW project updated, preparing agent Python env")
181
+ agent_prepare_logs: list[str] = []
182
+ try:
183
+ await asyncio.to_thread(
184
+ _prepare_maafw_agent_python_envs,
185
+ project_path,
186
+ refreshed_interface,
187
+ send_log=agent_prepare_logs.append,
188
+ )
189
+ finally:
190
+ for log_line in agent_prepare_logs:
191
+ self._emit_log(runtime, log_line)
192
+ except Exception as exc:
193
+ self._emit_log(runtime, f"MaaFW 项目更新失败,继续使用当前目录: {exc}")
194
+
195
+ @staticmethod
196
+ def _emit_log(runtime: ScriptAdapterRuntime, message: str) -> None:
197
+ """把 adapter 关键操作日志同时写入后端日志与 UI 通道。
198
+
199
+ UI 侧由 task_manager 广播 script_info.log(task.log 事件),因此这里
200
+ 除了 logger.info,还要追加到共享缓冲并刷新 script_info.log,
201
+ 否则用户在脚本管理页看不到插件适配阶段的进度。
202
+ """
203
+ logger.info(message)
204
+ logs = runtime.extra.setdefault("maafw_project_update_logs", [])
205
+ if isinstance(logs, list):
206
+ logs.extend(_format_update_log_lines(message))
207
+ runtime.script_info.log = "".join(logs[-80:])
208
+
209
+
210
+ async def _load_legacy_script_config(payload: dict[str, Any]) -> MaaFWConfig:
211
+ return await _load_legacy_config(MaaFWConfig(), payload)
212
+
213
+
214
+ async def _load_legacy_user_config(
215
+ runtime: ScriptAdapterRuntime,
216
+ ) -> MultipleConfig[MaaFWUserConfig]:
217
+ collection = MultipleConfig([MaaFWUserConfig])
218
+ source: dict[str, Any] = {"instances": []}
219
+ for user_id, payload in await runtime.storage.read_user_data_pairs():
220
+ uid = str(uuid.UUID(user_id))
221
+ source["instances"].append({"uid": uid, "type": "MaaFWUserConfig"})
222
+ source[uid] = _normalize_legacy_json_fields(MaaFWUserConfig(), payload)
223
+ await collection.load(source)
224
+ return collection
225
+
226
+
227
+ async def _load_legacy_config(
228
+ config: LegacyConfigT,
229
+ payload: dict[str, Any],
230
+ ) -> LegacyConfigT:
231
+ await config.load(_normalize_legacy_json_fields(config, payload))
232
+ return config
233
+
234
+
235
+ def _normalize_legacy_json_fields(
236
+ config: ConfigBase,
237
+ payload: dict[str, Any],
238
+ ) -> dict[str, Any]:
239
+ """Convert plugin form JSON values back to legacy ConfigBase strings."""
240
+
241
+ normalized = copy.deepcopy(payload)
242
+ for group, items in config._config_item_index.items():
243
+ group_data = normalized.get(group)
244
+ if not isinstance(group_data, dict):
245
+ continue
246
+ for name, item in items.items():
247
+ validator = item.validator
248
+ value = group_data.get(name)
249
+ if isinstance(validator, JSONValidator) and isinstance(
250
+ value,
251
+ validator.type,
252
+ ):
253
+ group_data[name] = json.dumps(value, ensure_ascii=False)
254
+ return normalized
255
+
256
+
257
+ def _format_update_log_lines(message: str) -> list[str]:
258
+ now = datetime.now().strftime("%H:%M:%S")
259
+ return [f"[{now}] {line}\n" for line in str(message).splitlines() or [""]]
260
+
261
+
262
+ def _prepare_maafw_agent_python_envs(
263
+ project_path: Path,
264
+ interface_model: Any,
265
+ *,
266
+ send_log: Any = None,
267
+ ) -> None:
268
+ from automas_maafw_agent_env.service import MaaFWAgentEnvService
269
+
270
+ MaaFWAgentEnvService().prepare_env(
271
+ project_path,
272
+ interface_model,
273
+ send_log=send_log,
274
+ )
275
+
276
+
277
+ def build_legacy_script_item(script_item: ScriptItem) -> ScriptItem:
278
+ return script_item
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from app.plugins import ScriptAdapterDefinition, ScriptAdapterPlugin
4
+
5
+ from .adapter import MaaFWAdapterHooks
6
+ from .registry import MaaFWRegistryService
7
+ from .schema import SCRIPT_GROUPS, USER_GROUPS
8
+
9
+
10
+ DEFAULT_INSTANCE = {
11
+ "name": "MaaFW 脚本适配",
12
+ "enabled": True,
13
+ "config": {},
14
+ }
15
+
16
+ schema = {
17
+ "__no_plugin_config__": {
18
+ "type": "boolean",
19
+ "default": True,
20
+ "hidden": True,
21
+ "configurable": False,
22
+ "title": "No plugin-level configuration",
23
+ },
24
+ }
25
+
26
+
27
+ class Plugin(ScriptAdapterPlugin):
28
+ """MaaFW script adapter plugin."""
29
+
30
+ provides = ["maafw.registry.v1"]
31
+ wants = [
32
+ "emulator",
33
+ "maafw.interface.v1",
34
+ "maafw.project_update.v1",
35
+ "maafw.agent_env.v1",
36
+ "maafw.runner.v1",
37
+ ]
38
+
39
+ def __init__(self, ctx):
40
+ super().__init__(ctx)
41
+ self.registry = MaaFWRegistryService()
42
+
43
+ def build_script_adapters(self):
44
+ return [
45
+ ScriptAdapterDefinition(
46
+ type_key="MaaFW",
47
+ display_name="MaaFramework 项目",
48
+ hooks_factory=MaaFWAdapterHooks,
49
+ script_groups=SCRIPT_GROUPS,
50
+ user_groups=USER_GROUPS,
51
+ script_class_name="MaaFWPluginConfig",
52
+ user_class_name="MaaFWPluginUserConfig",
53
+ module="automas_script_maafw.schema",
54
+ related_bindings={"EmulatorConfig": "EmulatorConfig"},
55
+ supported_modes=("AutoProxy",),
56
+ icon="MaaFW",
57
+ editor_kind="plugin:automas_script_maafw",
58
+ legacy_config_class_name="MaaFWConfig",
59
+ legacy_user_config_class_name="MaaFWUserConfig",
60
+ is_builtin=False,
61
+ metadata={
62
+ "framework": "maafw",
63
+ "source": "automas_script_maafw",
64
+ "m9a_standalone": False,
65
+ },
66
+ )
67
+ ]
68
+
69
+ async def on_start(self) -> None:
70
+ self.ctx.set("maafw.registry.v1", self.registry)
71
+ await super().on_start()
72
+
73
+ async def on_stop(self, reason: str) -> None:
74
+ self.ctx.set("maafw.registry.v1", None)
75
+ await super().on_stop(reason)
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class MaaFWRegistryService:
7
+ """Registry for MaaFW controller providers and project packs."""
8
+
9
+ def __init__(self) -> None:
10
+ self._controller_providers: dict[str, dict[str, Any]] = {}
11
+ self._project_packs: dict[str, dict[str, Any]] = {}
12
+
13
+ def register_controller_provider(self, definition: Any) -> None:
14
+ payload = _dump_definition(definition)
15
+ key = str(payload.get("key") or "").strip()
16
+ if not key:
17
+ raise ValueError("MaaFW controller provider key cannot be empty")
18
+ self._controller_providers[key] = payload
19
+
20
+ def unregister_controller_provider(self, key: str) -> None:
21
+ self._controller_providers.pop(str(key or "").strip(), None)
22
+
23
+ def list_controller_providers(self) -> list[dict[str, Any]]:
24
+ return list(self._controller_providers.values())
25
+
26
+ def get_controller_provider(self, key: str) -> dict[str, Any] | None:
27
+ return self._controller_providers.get(str(key or "").strip())
28
+
29
+ def register_project_pack(self, definition: Any) -> None:
30
+ payload = _dump_definition(definition)
31
+ key = str(payload.get("key") or "").strip()
32
+ if not key:
33
+ raise ValueError("MaaFW project pack key cannot be empty")
34
+ self._project_packs[key] = payload
35
+
36
+ def unregister_project_pack(self, key: str) -> None:
37
+ self._project_packs.pop(str(key or "").strip(), None)
38
+
39
+ def list_project_packs(self) -> list[dict[str, Any]]:
40
+ return list(self._project_packs.values())
41
+
42
+ def get_project_pack(self, key: str) -> dict[str, Any] | None:
43
+ return self._project_packs.get(str(key or "").strip())
44
+
45
+
46
+ def _dump_definition(definition: Any) -> dict[str, Any]:
47
+ if hasattr(definition, "model_dump"):
48
+ data = definition.model_dump(mode="json")
49
+ elif isinstance(definition, dict):
50
+ data = definition
51
+ else:
52
+ data = {
53
+ name: getattr(definition, name)
54
+ for name in dir(definition)
55
+ if not name.startswith("_") and not callable(getattr(definition, name))
56
+ }
57
+ if not isinstance(data, dict):
58
+ raise TypeError("MaaFW project pack definition must be a mapping")
59
+ return dict(data)