automas-script-maafw-pack-m9a 0.1.1__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.
Files changed (18) hide show
  1. automas_script_maafw_pack_m9a-0.1.1/PKG-INFO +16 -0
  2. automas_script_maafw_pack_m9a-0.1.1/README.md +7 -0
  3. automas_script_maafw_pack_m9a-0.1.1/pyproject.toml +23 -0
  4. automas_script_maafw_pack_m9a-0.1.1/setup.cfg +4 -0
  5. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a/__init__.py +17 -0
  6. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a/assets/m9a.png +0 -0
  7. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a/migration.py +398 -0
  8. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a/models.py +43 -0
  9. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a/plugin.py +105 -0
  10. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a/py.typed +1 -0
  11. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a/schema.py +75 -0
  12. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a/service.py +185 -0
  13. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a.egg-info/PKG-INFO +16 -0
  14. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a.egg-info/SOURCES.txt +16 -0
  15. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a.egg-info/dependency_links.txt +1 -0
  16. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a.egg-info/entry_points.txt +2 -0
  17. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a.egg-info/requires.txt +2 -0
  18. automas_script_maafw_pack_m9a-0.1.1/src/automas_script_maafw_pack_m9a.egg-info/top_level.txt +1 -0
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: automas-script-maafw-pack-m9a
3
+ Version: 0.1.1
4
+ Summary: M9A project pack declarations for the AUTO-MAS MaaFW plugin
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: automas-script-maafw>=0.1.2
8
+ Requires-Dist: pydantic>=2
9
+
10
+ # automas-script-maafw-pack-m9a
11
+
12
+ M9A project pack declarations for the AUTO-MAS MaaFW plugin.
13
+
14
+ The package provides `maafw.pack.m9a.v1`. It does not declare default project
15
+ sources, controller, resource, preset, task queues, or task templates. Those are
16
+ loaded through the generic MaaFW project/interface flow.
@@ -0,0 +1,7 @@
1
+ # automas-script-maafw-pack-m9a
2
+
3
+ M9A project pack declarations for the AUTO-MAS MaaFW plugin.
4
+
5
+ The package provides `maafw.pack.m9a.v1`. It does not declare default project
6
+ sources, controller, resource, preset, task queues, or task templates. Those are
7
+ loaded through the generic MaaFW project/interface flow.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "automas-script-maafw-pack-m9a"
7
+ version = "0.1.1"
8
+ description = "M9A project pack declarations for the AUTO-MAS MaaFW plugin"
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "automas-script-maafw>=0.1.2",
13
+ "pydantic>=2",
14
+ ]
15
+
16
+ [project.entry-points."auto_mas.plugins"]
17
+ automas_script_maafw_pack_m9a = "automas_script_maafw_pack_m9a.plugin:Plugin"
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+
22
+ [tool.setuptools.package-data]
23
+ automas_script_maafw_pack_m9a = ["py.typed", "assets/*.png"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from .models import (
4
+ M9AMigrationDraft,
5
+ M9ANotificationContent,
6
+ M9APackDefinition,
7
+ M9APeriodRule,
8
+ )
9
+ from .service import M9APackService
10
+
11
+ __all__ = [
12
+ "M9AMigrationDraft",
13
+ "M9ANotificationContent",
14
+ "M9APackDefinition",
15
+ "M9APackService",
16
+ "M9APeriodRule",
17
+ ]
@@ -0,0 +1,398 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from automas_maafw_interface.models import MaaFWInterface, MaaFWOption, MaaFWTask
7
+ from automas_maafw_interface.service import MaaFWInterfaceService
8
+
9
+
10
+ _RESERVED_TASK_NAMES = {
11
+ "启动游戏",
12
+ "关闭游戏",
13
+ "切换账号",
14
+ "StartUp",
15
+ "CloseDown",
16
+ "Close1999",
17
+ "SwitchAccount",
18
+ }
19
+
20
+
21
+ async def migrate_legacy_m9a_config(legacy_script: Any, provider: Any) -> Any:
22
+ """Convert a persisted M9AConfig into the generated MaaFW plugin shape."""
23
+
24
+ raw_script = await legacy_script.toDict(if_decrypt=False)
25
+ project_path = str(_nested(raw_script, "Info", "Path") or "").strip()
26
+ if not project_path:
27
+ raise ValueError("旧 M9A 配置缺少项目路径,暂不自动迁移")
28
+
29
+ interface = MaaFWInterfaceService().load(project_path)
30
+ controller_name = _default_adb_controller(interface)
31
+ script_payload = _build_script_payload(raw_script, interface, controller_name)
32
+ script_payload["SubConfigsInfo"] = {
33
+ "UserData": await _build_user_collection(
34
+ legacy_script,
35
+ provider.user_config_class,
36
+ interface,
37
+ controller_name,
38
+ )
39
+ }
40
+
41
+ migrated = provider.script_config_class()
42
+ await migrated.load(script_payload)
43
+ return migrated
44
+
45
+
46
+ def _build_script_payload(
47
+ raw_script: dict[str, Any],
48
+ interface: MaaFWInterface,
49
+ controller_name: str,
50
+ ) -> dict[str, Any]:
51
+ daily_tasks = (
52
+ _task_names_for_entries(interface, "Psychube")
53
+ if bool(_nested(raw_script, "Run", "IfPsychubeDailyOnce"))
54
+ else []
55
+ )
56
+ monthly_tasks = (
57
+ _task_names_for_entries(interface, "Limbo", "Lucidscape", "SleepDream")
58
+ if bool(_nested(raw_script, "Run", "IfSleepDreamMonthlyOnce"))
59
+ else []
60
+ )
61
+ return {
62
+ "Info": {
63
+ "Name": _nested(raw_script, "Info", "Name"),
64
+ "ProjectLabel": interface.label or interface.name,
65
+ "Path": _nested(raw_script, "Info", "Path"),
66
+ "Controller": controller_name,
67
+ "Resource": "",
68
+ },
69
+ "Emulator": {
70
+ "Id": _nested(raw_script, "Emulator", "Id"),
71
+ "Index": _nested(raw_script, "Emulator", "Index"),
72
+ },
73
+ "Update": {
74
+ "IfAutoUpdate": bool(
75
+ _nested(raw_script, "Run", "IfAutoUpdateAfterQueue")
76
+ ),
77
+ },
78
+ "Run": {
79
+ "ProxyTimesLimit": _nested(raw_script, "Run", "ProxyTimesLimit"),
80
+ "RunTimesLimit": _nested(raw_script, "Run", "RunTimesLimit"),
81
+ "RunTimeLimit": _nested(raw_script, "Run", "RunTimeLimit"),
82
+ "DailyOnceTasks": _json(daily_tasks),
83
+ "WeeklyOnceTasks": _json([]),
84
+ "MonthlyOnceTasks": _json(monthly_tasks),
85
+ },
86
+ }
87
+
88
+
89
+ async def _build_user_collection(
90
+ legacy_script: Any,
91
+ user_config_class: type,
92
+ interface: MaaFWInterface,
93
+ controller_name: str,
94
+ ) -> dict[str, Any]:
95
+ result: dict[str, Any] = {"instances": []}
96
+ for uid, old_user in legacy_script.UserData.items():
97
+ raw_user = await old_user.toDict(if_decrypt=False)
98
+ result["instances"].append(
99
+ {"uid": str(uid), "type": user_config_class.__name__}
100
+ )
101
+ result[str(uid)] = _build_user_payload(
102
+ raw_user,
103
+ interface,
104
+ controller_name,
105
+ )
106
+ return result
107
+
108
+
109
+ def _build_user_payload(
110
+ raw_user: dict[str, Any],
111
+ interface: MaaFWInterface,
112
+ controller_name: str,
113
+ ) -> dict[str, Any]:
114
+ task_snapshot = _build_task_snapshot(
115
+ _load_json(_nested(raw_user, "Task", "Queue"), list),
116
+ interface,
117
+ )
118
+ period_records = {
119
+ "daily": _period_records(
120
+ interface,
121
+ str(_nested(raw_user, "Data", "LastPsychubeDate") or "2000-01-01"),
122
+ "Psychube",
123
+ ),
124
+ "weekly": {},
125
+ "monthly": {
126
+ **_period_records(
127
+ interface,
128
+ str(_nested(raw_user, "Data", "LastLimboMonth") or "2000-01"),
129
+ "Limbo",
130
+ ),
131
+ **_period_records(
132
+ interface,
133
+ str(
134
+ _nested(raw_user, "Data", "LastLucidscapeMonth")
135
+ or "2000-01"
136
+ ),
137
+ "Lucidscape",
138
+ ),
139
+ },
140
+ }
141
+ resource_name = _match_resource_name(
142
+ interface,
143
+ str(_nested(raw_user, "Info", "Resource") or ""),
144
+ controller_name,
145
+ )
146
+ payload = {
147
+ "Info": {
148
+ **_copy_group(
149
+ raw_user,
150
+ "Info",
151
+ (
152
+ "Name",
153
+ "Status",
154
+ "RemainedDay",
155
+ "IfScriptBeforeTask",
156
+ "ScriptBeforeTask",
157
+ "IfScriptAfterTask",
158
+ "ScriptAfterTask",
159
+ "Notes",
160
+ "Account",
161
+ ),
162
+ ),
163
+ "Controller": controller_name,
164
+ "Resource": resource_name,
165
+ },
166
+ "Task": {
167
+ "SelectedPreset": "",
168
+ "TaskSnapshot": _json(task_snapshot),
169
+ },
170
+ "Data": {
171
+ **_copy_group(
172
+ raw_user,
173
+ "Data",
174
+ ("LastProxyDate", "ProxyTimes", "IfPassCheck"),
175
+ ),
176
+ "PeriodTaskRecords": _json(period_records),
177
+ },
178
+ "Notify": _copy_group(
179
+ raw_user,
180
+ "Notify",
181
+ (
182
+ "Enabled",
183
+ "IfSendStatistic",
184
+ "IfSendMail",
185
+ "ToAddress",
186
+ "IfServerChan",
187
+ "ServerChanKey",
188
+ ),
189
+ ),
190
+ }
191
+ custom_webhooks = (
192
+ raw_user.get("SubConfigsInfo", {}).get("Notify_CustomWebhooks")
193
+ if isinstance(raw_user.get("SubConfigsInfo"), dict)
194
+ else None
195
+ )
196
+ if isinstance(custom_webhooks, dict):
197
+ payload["SubConfigsInfo"] = {
198
+ "Notify_CustomWebhooks": custom_webhooks,
199
+ }
200
+ return payload
201
+
202
+
203
+ def _build_task_snapshot(
204
+ queue: list[Any],
205
+ interface: MaaFWInterface,
206
+ ) -> dict[str, Any]:
207
+ task_order: list[str] = []
208
+ task_checked = {task.name: False for task in interface.task}
209
+ task_options: dict[str, dict[str, Any]] = {}
210
+ for item in queue:
211
+ if not isinstance(item, dict):
212
+ continue
213
+ raw_name = str(item.get("name") or item.get("entry") or "").strip()
214
+ if not raw_name or raw_name in _RESERVED_TASK_NAMES:
215
+ continue
216
+ task = _match_task(interface, raw_name, str(item.get("entry") or ""))
217
+ if task is None or task.name in task_order:
218
+ continue
219
+ task_order.append(task.name)
220
+ task_checked[task.name] = True
221
+ task_options[task.name] = _convert_task_options(
222
+ item.get("options"),
223
+ interface,
224
+ )
225
+
226
+ task_order.extend(task.name for task in interface.task if task.name not in task_order)
227
+ return {
228
+ "taskOrder": task_order,
229
+ "taskChecked": task_checked,
230
+ "taskOptions": task_options,
231
+ }
232
+
233
+
234
+ def _convert_task_options(
235
+ raw_options: Any,
236
+ interface: MaaFWInterface,
237
+ ) -> dict[str, Any]:
238
+ converted: dict[str, Any] = {}
239
+ if not isinstance(raw_options, list):
240
+ return converted
241
+ for raw_option in raw_options:
242
+ if not isinstance(raw_option, dict):
243
+ continue
244
+ option_name = str(raw_option.get("name") or "").strip()
245
+ option = interface.option.get(option_name)
246
+ if not option_name or option is None:
247
+ continue
248
+ value = _convert_option_value(raw_option, option)
249
+ if value is not None:
250
+ converted[option_name] = value
251
+ converted.update(
252
+ _convert_task_options(raw_option.get("sub_options"), interface)
253
+ )
254
+ return converted
255
+
256
+
257
+ def _convert_option_value(raw_option: dict[str, Any], option: MaaFWOption) -> Any:
258
+ if option.type == "input":
259
+ value = raw_option.get("input_values", raw_option.get("data"))
260
+ if isinstance(value, dict):
261
+ return {
262
+ str(key): str(item)
263
+ for key, item in value.items()
264
+ if isinstance(key, str)
265
+ }
266
+ return None
267
+ if option.type == "checkbox":
268
+ selected = raw_option.get("selected_cases")
269
+ if isinstance(selected, list):
270
+ return [str(item) for item in selected]
271
+ return []
272
+
273
+ cases = option.cases or []
274
+ selected = raw_option.get("selected_cases")
275
+ if isinstance(selected, list) and selected:
276
+ return str(selected[0])
277
+ try:
278
+ index = int(raw_option.get("index", 0))
279
+ except (TypeError, ValueError):
280
+ index = 0
281
+ if 0 <= index < len(cases):
282
+ return cases[index].name
283
+ return cases[0].name if cases else None
284
+
285
+
286
+ def _match_task(
287
+ interface: MaaFWInterface,
288
+ name: str,
289
+ entry: str = "",
290
+ ) -> MaaFWTask | None:
291
+ for candidate in (name, entry):
292
+ if not candidate:
293
+ continue
294
+ task = next(
295
+ (
296
+ item
297
+ for item in interface.task
298
+ if candidate in {item.name, item.entry, item.label}
299
+ ),
300
+ None,
301
+ )
302
+ if task is not None:
303
+ return task
304
+ return None
305
+
306
+
307
+ def _task_names_for_entries(
308
+ interface: MaaFWInterface,
309
+ *entries: str,
310
+ ) -> list[str]:
311
+ wanted = set(entries)
312
+ return [
313
+ task.name
314
+ for task in interface.task
315
+ if task.entry in wanted or task.name in wanted
316
+ ]
317
+
318
+
319
+ def _period_records(
320
+ interface: MaaFWInterface,
321
+ period_key: str,
322
+ *entries: str,
323
+ ) -> dict[str, str]:
324
+ return {
325
+ task_name: period_key
326
+ for task_name in _task_names_for_entries(interface, *entries)
327
+ }
328
+
329
+
330
+ def _default_adb_controller(interface: MaaFWInterface) -> str:
331
+ controller = next(
332
+ (item for item in interface.controller if item.type == "Adb"),
333
+ None,
334
+ )
335
+ return controller.name if controller is not None else ""
336
+
337
+
338
+ def _match_resource_name(
339
+ interface: MaaFWInterface,
340
+ legacy_value: str,
341
+ controller_name: str,
342
+ ) -> str:
343
+ if legacy_value:
344
+ resource = next(
345
+ (
346
+ item
347
+ for item in interface.resource
348
+ if legacy_value in {item.name, item.label}
349
+ ),
350
+ None,
351
+ )
352
+ if resource is not None:
353
+ return resource.name
354
+ resource = next(
355
+ (
356
+ item
357
+ for item in interface.resource
358
+ if not item.controller or controller_name in item.controller
359
+ ),
360
+ None,
361
+ )
362
+ return resource.name if resource is not None else ""
363
+
364
+
365
+ def _copy_group(
366
+ payload: dict[str, Any],
367
+ group_name: str,
368
+ field_names: tuple[str, ...],
369
+ ) -> dict[str, Any]:
370
+ group = payload.get(group_name)
371
+ if not isinstance(group, dict):
372
+ return {}
373
+ return {name: group[name] for name in field_names if name in group}
374
+
375
+
376
+ def _nested(payload: dict[str, Any], group: str, name: str) -> Any:
377
+ value = payload.get(group)
378
+ return value.get(name) if isinstance(value, dict) else None
379
+
380
+
381
+ def _load_json(value: Any, expected_type: type) -> Any:
382
+ if isinstance(value, expected_type):
383
+ return value
384
+ if isinstance(value, str):
385
+ try:
386
+ parsed = json.loads(value)
387
+ except json.JSONDecodeError:
388
+ return expected_type()
389
+ if isinstance(parsed, expected_type):
390
+ return parsed
391
+ return expected_type()
392
+
393
+
394
+ def _json(value: Any) -> str:
395
+ return json.dumps(value, ensure_ascii=False)
396
+
397
+
398
+ __all__ = ["migrate_legacy_m9a_config"]
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class M9APeriodRule(BaseModel):
9
+ task: str
10
+ period: Literal["daily", "weekly", "monthly"]
11
+
12
+
13
+ class M9APackDefinition(BaseModel):
14
+ """M9A project pack 定义,对应文档 6.3 MaaFWProjectPackDefinition。"""
15
+
16
+ key: str = "m9a"
17
+ display_name: str = "M9A"
18
+ project_repo: str | None = None
19
+ interface_path: str = "interface.json"
20
+ supported_controllers: list[str] = Field(default_factory=lambda: ["adb", "win32"])
21
+ default_controller: str = "adb"
22
+ default_resource: str | None = None
23
+ default_preset: str | None = None
24
+ default_task_queue: list[str] | None = None
25
+ period_rules: list[M9APeriodRule] = Field(default_factory=list)
26
+ reserved_task_semantics: dict[str, Any] = Field(default_factory=dict)
27
+ icon: str | None = "automas_script_maafw_pack_m9a:assets/m9a.png"
28
+ notes: str | None = None
29
+ # 额外能力声明(非 6.3 标准字段)
30
+ framework: str = "maafw"
31
+ capabilities: list[str] = Field(default_factory=list)
32
+
33
+
34
+ class M9ANotificationContent(BaseModel):
35
+ title: str
36
+ text: str
37
+ html: str | None = None
38
+
39
+
40
+ class M9AMigrationDraft(BaseModel):
41
+ script: dict[str, Any]
42
+ users: list[dict[str, Any]] = Field(default_factory=list)
43
+ warnings: list[str] = Field(default_factory=list)
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from app.plugins import ScriptAdapterDefinition, ScriptAdapterPlugin
6
+ from automas_script_maafw.adapter import MaaFWAdapterHooks
7
+
8
+ from .migration import migrate_legacy_m9a_config
9
+ from .schema import M9A_SCRIPT_GROUPS, M9A_USER_GROUPS
10
+ from .service import M9APackService
11
+
12
+ if TYPE_CHECKING:
13
+ from auto_mas_core import PluginContext
14
+
15
+
16
+ DEFAULT_INSTANCE = {
17
+ "name": "M9A MaaFW Pack",
18
+ "enabled": True,
19
+ "config": {},
20
+ }
21
+
22
+ schema = {
23
+ "__no_plugin_config__": {
24
+ "type": "boolean",
25
+ "default": True,
26
+ "hidden": True,
27
+ "configurable": False,
28
+ "title": "No plugin-level configuration",
29
+ },
30
+ }
31
+
32
+ SCRIPT_TYPE_BINDINGS = [
33
+ {
34
+ "type_key": "M9A",
35
+ "display_name": "M9A",
36
+ "script_config_class_name": "M9AConfig",
37
+ }
38
+ ]
39
+
40
+
41
+ class Plugin(ScriptAdapterPlugin):
42
+ provides = ["maafw.pack.m9a.v1"]
43
+ wants = [
44
+ "maafw.registry.v1",
45
+ "maafw.interface.v1",
46
+ "maafw.project_update.v1",
47
+ "maafw.runner.v1",
48
+ ]
49
+
50
+ def __init__(self, ctx: "PluginContext") -> None:
51
+ super().__init__(ctx)
52
+ self.ctx = ctx
53
+ self.service = M9APackService()
54
+
55
+ def build_script_adapters(self):
56
+ return [
57
+ ScriptAdapterDefinition(
58
+ type_key="M9A",
59
+ display_name="M9A",
60
+ hooks_factory=MaaFWAdapterHooks,
61
+ script_groups=M9A_SCRIPT_GROUPS,
62
+ user_groups=M9A_USER_GROUPS,
63
+ script_class_name="M9APluginConfig",
64
+ user_class_name="M9APluginUserConfig",
65
+ module="automas_script_maafw.schema",
66
+ related_bindings={"EmulatorConfig": "EmulatorConfig"},
67
+ supported_modes=("AutoProxy",),
68
+ icon="M9A",
69
+ icon_path="automas_script_maafw_pack_m9a:assets/m9a.png",
70
+ editor_kind="plugin:automas_script_maafw_pack_m9a",
71
+ legacy_config_class_name="M9AConfig",
72
+ legacy_user_config_class_name="M9AUserConfig",
73
+ is_builtin=False,
74
+ metadata={
75
+ "framework": "maafw",
76
+ "source": "automas_script_maafw_pack_m9a",
77
+ "project_pack": "m9a",
78
+ "m9a_standalone": True,
79
+ "theme_color": "cyan",
80
+ "legacy_config_migrator": migrate_legacy_m9a_config,
81
+ "script_edit_hint": {
82
+ "text": "遇到配置问题时,可以查看",
83
+ "link_text": "M9A 配置指南",
84
+ "url": "https://doc.auto-mas.top/docs/script-guide/m9a.html",
85
+ "suffix": ",其中整理了项目目录、运行环境和任务配置步骤。",
86
+ },
87
+ },
88
+ )
89
+ ]
90
+
91
+ async def on_start(self) -> None:
92
+ self.ctx.set("maafw.pack.m9a.v1", self.service)
93
+ registry = self.ctx.get("maafw.registry.v1")
94
+ if registry is not None and hasattr(registry, "register_project_pack"):
95
+ registry.register_project_pack(self.service.get_definition())
96
+ await super().on_start()
97
+ self.ctx.logger.info("maafw.pack.m9a.v1 ready")
98
+
99
+ async def on_stop(self, reason: str) -> None:
100
+ registry = self.ctx.get("maafw.registry.v1")
101
+ if registry is not None and hasattr(registry, "unregister_project_pack"):
102
+ registry.unregister_project_pack("m9a")
103
+ self.ctx.set("maafw.pack.m9a.v1", None)
104
+ await super().on_stop(reason)
105
+ self.ctx.logger.info(f"maafw.pack.m9a.v1 stopped, reason={reason}")
@@ -0,0 +1,75 @@
1
+ """M9A project-pack schema overrides for the shared MaaFW editor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import replace
7
+
8
+ from app.plugins.fields import PluginFieldGroup
9
+ from automas_script_maafw.schema import SCRIPT_GROUPS, USER_GROUPS, build_source_config
10
+
11
+
12
+ schema = {
13
+ "__no_plugin_config__": {
14
+ "type": "boolean",
15
+ "default": True,
16
+ "hidden": True,
17
+ "configurable": False,
18
+ "title": "No plugin-level configuration",
19
+ },
20
+ }
21
+
22
+
23
+ M9A_PERIOD_TASK_DEFAULTS = {
24
+ "DailyOnceTasks": ["Psychube"],
25
+ "WeeklyOnceTasks": [],
26
+ "MonthlyOnceTasks": ["SleepDream"],
27
+ }
28
+
29
+
30
+ def _build_m9a_info_group(group: PluginFieldGroup) -> PluginFieldGroup:
31
+ name_field = replace(group.fields[0], default="新M9A脚本")
32
+ path_field = replace(
33
+ group.fields[2],
34
+ label="M9A 项目路径",
35
+ placeholder="选择包含 interface.json 的 M9A 项目目录",
36
+ )
37
+ return replace(
38
+ group,
39
+ fields=(
40
+ name_field,
41
+ group.fields[1],
42
+ path_field,
43
+ *group.fields[3:],
44
+ ),
45
+ )
46
+
47
+
48
+ def _build_m9a_run_group(group: PluginFieldGroup) -> PluginFieldGroup:
49
+ return replace(
50
+ group,
51
+ fields=tuple(
52
+ replace(field, default=json.dumps(M9A_PERIOD_TASK_DEFAULTS[field.name]))
53
+ if field.name in M9A_PERIOD_TASK_DEFAULTS
54
+ else field
55
+ for field in group.fields
56
+ ),
57
+ )
58
+
59
+
60
+ def _build_m9a_group(group: PluginFieldGroup) -> PluginFieldGroup:
61
+ if group.key == "Info":
62
+ return _build_m9a_info_group(group)
63
+ if group.key == "Run":
64
+ return _build_m9a_run_group(group)
65
+ return group
66
+
67
+
68
+ M9A_SCRIPT_GROUPS: tuple[PluginFieldGroup, ...] = tuple(
69
+ _build_m9a_group(group) for group in SCRIPT_GROUPS
70
+ )
71
+
72
+ # User config intentionally reuses the MaaFW surface and storage shape.
73
+ M9A_USER_GROUPS = USER_GROUPS
74
+
75
+ __all__ = ["M9A_SCRIPT_GROUPS", "M9A_USER_GROUPS", "build_source_config"]
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from .models import (
7
+ M9AMigrationDraft,
8
+ M9ANotificationContent,
9
+ M9APackDefinition,
10
+ M9APeriodRule,
11
+ )
12
+
13
+
14
+ PERIOD_RULES = [
15
+ M9APeriodRule(task="Psychube", period="daily"),
16
+ M9APeriodRule(task="SleepDream", period="monthly"),
17
+ ]
18
+
19
+ DEFAULT_TASK_QUEUE = [
20
+ "StartUp",
21
+ "Psychube",
22
+ "SleepDream",
23
+ "Award",
24
+ "CloseDown",
25
+ ]
26
+
27
+ RESERVED_TASK_SEMANTICS = {
28
+ "Psychube": {"period": "daily", "label": "每日心相"},
29
+ "SleepDream": {"period": "monthly", "label": "深眠浅梦"},
30
+ }
31
+
32
+
33
+ class M9APackService:
34
+ """maafw.pack.m9a.v1 service."""
35
+
36
+ def get_definition(self) -> M9APackDefinition:
37
+ return M9APackDefinition(
38
+ key="m9a",
39
+ display_name="M9A",
40
+ project_repo="MAA1999/M9A",
41
+ interface_path="interface.json",
42
+ supported_controllers=["adb", "win32"],
43
+ default_controller="adb",
44
+ default_resource="resource",
45
+ default_preset="日常任务",
46
+ default_task_queue=DEFAULT_TASK_QUEUE,
47
+ period_rules=PERIOD_RULES,
48
+ reserved_task_semantics=RESERVED_TASK_SEMANTICS,
49
+ icon="automas_script_maafw_pack_m9a:assets/m9a.png",
50
+ notes="M9A 专项插件包 — 重返未来:1999 自动化",
51
+ framework="maafw",
52
+ capabilities=[
53
+ "period_rules",
54
+ "notification_translation",
55
+ "create_only_migration",
56
+ ],
57
+ )
58
+
59
+ def translate_notification(
60
+ self,
61
+ result: Any,
62
+ *,
63
+ script_name: str = "M9A",
64
+ user_name: str = "",
65
+ started_at: str | None = None,
66
+ ended_at: str | None = None,
67
+ ) -> M9ANotificationContent:
68
+ payload = _to_mapping(result)
69
+ success = bool(payload.get("success"))
70
+ completed_tasks = [
71
+ str(item)
72
+ for item in payload.get("completedTasks", [])
73
+ if str(item).strip()
74
+ ]
75
+ failed_task = str(payload.get("failedTask") or "").strip()
76
+ error_message = str(payload.get("errorMessage") or "").strip()
77
+
78
+ subject = user_name or script_name or "M9A"
79
+ title = f"{subject} 的 M9A 任务已完成" if success else f"{subject} 的 M9A 任务未完成"
80
+ lines = []
81
+ if started_at or ended_at:
82
+ lines.append(f"开始时间: {started_at or '-'}")
83
+ lines.append(f"结束时间: {ended_at or datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
84
+ if completed_tasks:
85
+ lines.append("已完成任务: " + ", ".join(completed_tasks))
86
+ else:
87
+ lines.append("已完成任务: 无")
88
+ if failed_task:
89
+ lines.append(f"失败任务: {failed_task}")
90
+ if error_message:
91
+ lines.append(f"失败原因: {error_message}")
92
+ if not error_message and not success:
93
+ lines.append("失败原因: MaaFW runner 未返回成功状态")
94
+ return M9ANotificationContent(title=title, text="\n".join(lines))
95
+
96
+ def create_migration_draft(
97
+ self,
98
+ old_script_config: dict[str, Any] | None,
99
+ old_user_configs: list[dict[str, Any]] | None = None,
100
+ *,
101
+ script_name: str = "M9A",
102
+ ) -> M9AMigrationDraft:
103
+ warnings = [
104
+ "迁移入口只创建新插件配置草稿,不覆盖旧 M9A 配置。",
105
+ "项目来源、controller、resource、preset、任务队列和模板必须通过通用 MaaFW 流程现拉。",
106
+ ]
107
+ script_payload = {
108
+ "type": "PluginScriptConfig",
109
+ "Meta": {"PluginTypeKey": "M9A"},
110
+ "Info": {"Name": script_name},
111
+ "PluginData": {
112
+ "Config": {
113
+ "pack": "m9a",
114
+ "legacy": _filter_legacy_script_fields(old_script_config or {}),
115
+ }
116
+ },
117
+ }
118
+
119
+ users: list[dict[str, Any]] = []
120
+ for index, old_user in enumerate(old_user_configs or [], start=1):
121
+ user_name = _extract_user_name(old_user) or f"M9A 用户 {index}"
122
+ users.append(
123
+ {
124
+ "type": "PluginUserConfig",
125
+ "Meta": {"PluginTypeKey": "M9A"},
126
+ "Info": {"Name": user_name},
127
+ "PluginData": {
128
+ "Config": {
129
+ "pack": "m9a",
130
+ "legacy": _filter_legacy_user_fields(old_user),
131
+ }
132
+ },
133
+ }
134
+ )
135
+
136
+ return M9AMigrationDraft(script=script_payload, users=users, warnings=warnings)
137
+
138
+
139
+ def _to_mapping(value: Any) -> dict[str, Any]:
140
+ if isinstance(value, dict):
141
+ return value
142
+ if hasattr(value, "model_dump"):
143
+ data = value.model_dump(mode="json")
144
+ return data if isinstance(data, dict) else {}
145
+ if hasattr(value, "__dict__"):
146
+ return {
147
+ key: item
148
+ for key, item in vars(value).items()
149
+ if not key.startswith("_")
150
+ }
151
+ return {}
152
+
153
+
154
+ def _filter_legacy_script_fields(value: dict[str, Any]) -> dict[str, Any]:
155
+ return {
156
+ "path": _nested_get(value, "Info", "Path"),
157
+ "autoUpdate": _nested_get(value, "Run", "IfAutoUpdateAfterQueue"),
158
+ "weeklyTasks": _nested_get(value, "Run", "WeeklyOnceTasks"),
159
+ "monthlyTasks": _nested_get(value, "Run", "MonthlyOnceTasks"),
160
+ }
161
+
162
+
163
+ def _filter_legacy_user_fields(value: dict[str, Any]) -> dict[str, Any]:
164
+ return {
165
+ "name": _extract_user_name(value),
166
+ "taskQueue": _nested_get(value, "Task", "Queue"),
167
+ "notifyEnabled": _nested_get(value, "Notify", "Enabled"),
168
+ }
169
+
170
+
171
+ def _nested_get(value: dict[str, Any], section: str, key: str) -> Any:
172
+ section_value = value.get(section)
173
+ if isinstance(section_value, dict):
174
+ return section_value.get(key)
175
+ return None
176
+
177
+
178
+ def _extract_user_name(value: dict[str, Any]) -> str:
179
+ info = value.get("Info")
180
+ if isinstance(info, dict):
181
+ name = info.get("Name")
182
+ if isinstance(name, str) and name.strip():
183
+ return name.strip()
184
+ name = value.get("name")
185
+ return name.strip() if isinstance(name, str) else ""
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: automas-script-maafw-pack-m9a
3
+ Version: 0.1.1
4
+ Summary: M9A project pack declarations for the AUTO-MAS MaaFW plugin
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: automas-script-maafw>=0.1.2
8
+ Requires-Dist: pydantic>=2
9
+
10
+ # automas-script-maafw-pack-m9a
11
+
12
+ M9A project pack declarations for the AUTO-MAS MaaFW plugin.
13
+
14
+ The package provides `maafw.pack.m9a.v1`. It does not declare default project
15
+ sources, controller, resource, preset, task queues, or task templates. Those are
16
+ loaded through the generic MaaFW project/interface flow.
@@ -0,0 +1,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/automas_script_maafw_pack_m9a/__init__.py
4
+ src/automas_script_maafw_pack_m9a/migration.py
5
+ src/automas_script_maafw_pack_m9a/models.py
6
+ src/automas_script_maafw_pack_m9a/plugin.py
7
+ src/automas_script_maafw_pack_m9a/py.typed
8
+ src/automas_script_maafw_pack_m9a/schema.py
9
+ src/automas_script_maafw_pack_m9a/service.py
10
+ src/automas_script_maafw_pack_m9a.egg-info/PKG-INFO
11
+ src/automas_script_maafw_pack_m9a.egg-info/SOURCES.txt
12
+ src/automas_script_maafw_pack_m9a.egg-info/dependency_links.txt
13
+ src/automas_script_maafw_pack_m9a.egg-info/entry_points.txt
14
+ src/automas_script_maafw_pack_m9a.egg-info/requires.txt
15
+ src/automas_script_maafw_pack_m9a.egg-info/top_level.txt
16
+ src/automas_script_maafw_pack_m9a/assets/m9a.png
@@ -0,0 +1,2 @@
1
+ [auto_mas.plugins]
2
+ automas_script_maafw_pack_m9a = automas_script_maafw_pack_m9a.plugin:Plugin
@@ -0,0 +1,2 @@
1
+ automas-script-maafw>=0.1.2
2
+ pydantic>=2