Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/version.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version_info": {
"v5.5.0-beta.3": {
"新增功能": [
"专项适配 提供可复制的 General 能力基线模板,覆盖后端任务、前端编辑页、注册清单与最小回归测试",
"MFW 项目支持在脚本运行前或运行后自动更新,可在项目配置中选择时机;升级后已有脚本默认为「运行前更新」,不需要可在项目配置中改为「不更新」 by [@qiyinxi](https://github.com/qiyinxi)",
"MAA专项 新增绿票商店开关:开启后每月单独启动一次 MAA 自动购买绿票商店,用户配置页可查看本月状态并手动重置 by [@qiyinxi](https://github.com/qiyinxi)",
"调度队列 新增循环队列:队列里的每个任务可单独设定固定时间或间隔重复运行,在调度台以「循环运行」启动后会按各自的周期一直跑下去,并显示接下来要运行的任务与时间 by [@qiyinxi](https://github.com/qiyinxi)",
Expand Down
78 changes: 78 additions & 0 deletions templates/specialized/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 专项适配模板

这是一套从 `General` 能力线整理出的专项适配骨架。它不是独立运行的脚本,也不依赖专用模板生成器;复制后按注册清单接入 AUTO-MAS 即可。接入新 schema 后,仍按项目流程生成 OpenAPI client。

## 占位符

- `Xxx`:Python/Vue 的 PascalCase 专项名,例如 `MaaDemo`。
- `xxx`:路径、测试和路由使用的 kebab-case 或 snake_case 名,例如 `maa_demo`。
- `专项显示名称`:用户界面和日志中的中文名称。

全局替换时请同时检查大小写和中文显示名。`Xxx` 不是最终的 `ScriptType`,最终类型名必须与注册表、路由、schema 和任务调度完全一致。

## 目录说明

```text
templates/specialized/
├─ README.md
├─ backend/
│ ├─ task/Xxx/
│ │ ├─ __init__.py
│ │ ├─ AutoProxy.py
│ │ ├─ manager.py
│ │ └─ ScriptConfig.py
│ ├─ config.py.template
│ ├─ schema.py.template
│ └─ registration-checklist.md
├─ frontend/
│ ├─ XxxScriptEdit.vue
│ ├─ XxxUserEdit.vue
│ └─ XxxUserEdit/
│ ├─ BasicInfoSection.vue
│ └─ NotifyConfigSection.vue
└─ tests/
└─ test_xxx_autoproxy.py
```

后端任务文件保留了 General 的启动、进程追踪、配置交换、游戏/模拟器启动、重试/超时、前后置脚本、日志判态、用户统计、历史记录、通知和 `final_task` / `on_crash` 生命周期。专项差异集中在 `TODO(specialized)` 处;不要把 TODO 留在可运行路径上。

## 五步使用流程

1. 复制 `backend/task/Xxx` 到 `app/task/新专项名`,并全局替换 `Xxx`、`xxx` 和显示名称。
2. 复制 `backend/config.py.template`、`backend/schema.py.template` 的片段,填写真实专项字段和验证器。
3. 复制两个前端编辑页及 `XxxUserEdit/`,保留基本信息、通知和通用配置会话,加入专项表单。
4. 按 `backend/registration-checklist.md` 补齐 `ScriptType`、`BOOK`、API/core/task 调度、路由、Hub 和前端类型分支。
5. 将 `tests/test_xxx_autoproxy.py` 移入 `tests/task/test_xxx_autoproxy.py`,替换夹具并运行最小专项测试。

前端通常放置为:`XxxScriptEdit.vue` → `frontend/src/views/EditView/Script/`,`XxxUserEdit.vue` → `frontend/src/views/EditView/User/`,`XxxUserEdit/` → `frontend/src/views/XxxUserEdit/`;页面中的相对 import 已按该目录关系书写。

## 设计边界

- `schema.py` 只描述 API 数据;文件、进程、日志和配置交换留在 task/core。
- `config.py` 中的所有 `ConfigItem` 必须在 `super().__init__()` 前声明,并配有注释。
- 用户配置来源默认沿用 General 的“用户独立 / 脚本直控”两态。若上游真实存在脚本共享或其他 owner,先在专项设计中确认,再同步 config、UI、AutoProxy 和 ScriptConfig;不要为了界面统一臆造第三种模式。
- 运行前备份脚本直控配置,运行中按用户配置原子替换,成功、失败、取消、超时和异常均恢复原配置。需要把运行结果写回用户配置时,必须先更新用户副本,再恢复直控配置。
- 日志监控使用 `LogMonitor(time_stamp_range, time_format, check_log)` 三参构造;每一轮创建 `LogRecord`,只写 `log_record.status`,不要给 `UserItem.result` 赋值。
- 生成的 OpenAPI 文件不得手改。schema 接入后由开发者从 `frontend/` 运行 `yarn openapi`。

## 必填专项决策

复制前先写下四个答案:

1. 脚本根目录、主程序和目标进程的真实哨兵文件是什么?自动发现和手动选择必须复用同一组哨兵。
2. 自动任务的启动参数如何构造?没有稳定 CLI 时,改为写入上游约定的运行配置,不要猜参数。
3. 哪些配置由上游脚本拥有,哪些由 MAS 用户副本拥有?配置会话、AutoProxy 和恢复逻辑必须一致。
4. 哪些日志明确代表成功、失败、运行中和提前退出?把失败/回退路径写入测试。

## 验证

```powershell
# 后端:在替换占位符并完成注册后
python -m pytest tests/task/test_xxx_autoproxy.py -q

# 前端:接入 schema 后由开发者执行生成器,再检查类型和 lint
yarn openapi
yarn lint
```

模板本身只提供最小纯逻辑回归测试;未完成上游契约、注册和夹具替换前,不要把模板测试当作专项已适配的证明。
248 changes: 248 additions & 0 deletions templates/specialized/backend/config.py.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
"""专项配置片段。

将本文件中的两个 ConfigBase 子类复制到 ``app/models/config.py``,并复用该文件
已有的 ConfigItem、Validator、MultipleConfig、Webhook 和 UTC4 imports。模板只放
真实运行时会被 AutoProxy/ScriptConfig 消费的字段;专项字段必须同时出现在 schema、
前端表单和任务逻辑中。
"""


class XxxUserConfig(ConfigBase):
"""专项用户配置:从 General 用户配置开始,再加入专项字段。"""

def __init__(self) -> None:
## Info ------------------------------------------------------------
## 用户名称
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
## 是否启用
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
## 剩余天数,-1 表示无限
self.Info_RemainedDay = ConfigItem(
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
)
## 是否使用用户独立脚本配置;关闭时直接使用脚本原配置
self.Info_IfUseMasConfig = ConfigItem(
"Info", "IfUseMasConfig", True, BoolValidator()
)
## 是否在任务前执行自定义脚本
self.Info_IfScriptBeforeTask = ConfigItem(
"Info", "IfScriptBeforeTask", False, BoolValidator()
)
## 任务前脚本路径
self.Info_ScriptBeforeTask = ConfigItem(
"Info", "ScriptBeforeTask", "", FileValidator()
)
## 是否在任务后执行自定义脚本
self.Info_IfScriptAfterTask = ConfigItem(
"Info", "IfScriptAfterTask", False, BoolValidator()
)
## 任务后脚本路径
self.Info_ScriptAfterTask = ConfigItem(
"Info", "ScriptAfterTask", "", FileValidator()
)
## 用户备注
self.Info_Notes = ConfigItem("Info", "Notes", "无")
## 用户列表展示标签
self.Info_Tag = ConfigItem(
"Info", "Tag", "[ ]", VirtualConfigValidator(self.getTags)
)

## Task ------------------------------------------------------------
# TODO(specialized): 增加专项任务字段,并在 AutoProxy/前端真正消费。
# self.Task_Example = ConfigItem(
# "Task", "Example", False, BoolValidator()
# )

## Data ------------------------------------------------------------
## 上次代理日期
self.Data_LastProxyDate = ConfigItem(
"Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
## 当日代理次数
self.Data_ProxyTimes = ConfigItem(
"Data", "ProxyTimes", 0, RangeValidator(0, 9999)
)

## Notify ----------------------------------------------------------
## 是否启用用户通知
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
## 是否发送统计信息
self.Notify_IfSendStatistic = ConfigItem(
"Notify", "IfSendStatistic", False, BoolValidator()
)
## 是否发送邮件
self.Notify_IfSendMail = ConfigItem(
"Notify", "IfSendMail", False, BoolValidator()
)
## 邮件收件地址
self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
## 是否发送 Server 酱
self.Notify_IfServerChan = ConfigItem(
"Notify", "IfServerChan", False, BoolValidator()
)
## Server 酱密钥
self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
## 自定义 Webhook
self.Notify_CustomWebhooks = MultipleConfig([Webhook])

super().__init__()

def getTags(self) -> str:
"""生成用户列表需要的简短状态标签。"""
tags = []
if (
datetime.strptime(self.get("Data", "LastProxyDate"), "%Y-%m-%d").date()
== datetime.now(tz=UTC4).date()
):
tags.append(
{"text": f"任务:已代理{self.get('Data', 'ProxyTimes')}次", "color": "green"}
)
else:
tags.append({"text": "任务:未代理", "color": "orange"})

remained_day = self.get("Info", "RemainedDay")
if remained_day == -1:
color = "gold"
elif remained_day == 0:
color = "red"
elif remained_day <= 3:
color = "orange"
elif remained_day <= 7:
color = "yellow"
elif remained_day <= 30:
color = "blue"
else:
color = "green"
tags.append(
{
"text": (
f"剩余天数:{remained_day}天"
if remained_day >= 0
else "剩余天数:无期限"
),
"color": color,
}
)

notes = self.get("Info", "Notes")
tags.append(
{"text": f"备注:{notes}" if len(notes) <= 20 else f"备注:{notes[:20]}...", "color": "pink"}
)
return json.dumps(tags, ensure_ascii=False)


class XxxConfig(ConfigBase):
"""专项脚本配置:保留 General 运行基线。"""

related_config: dict[str, MultipleConfig] = {}

def __init__(self) -> None:
## Info ------------------------------------------------------------
## 脚本显示名称
self.Info_Name = ConfigItem("Info", "Name", "新专项脚本")
## 脚本根目录
self.Info_RootPath = ConfigItem("Info", "RootPath", "", FileValidator())

## Script ----------------------------------------------------------
## 脚本主程序
self.Script_ScriptPath = ConfigItem(
"Script", "ScriptPath", "", FileValidator()
)
## 通用启动参数;专项参数在 AutoProxy 中构造
self.Script_Arguments = ConfigItem(
"Script", "Arguments", "", AdvancedArgumentValidator()
)
## 是否追踪目标进程
self.Script_IfTrackProcess = ConfigItem(
"Script", "IfTrackProcess", False, BoolValidator()
)
## 目标进程名称
self.Script_TrackProcessName = ConfigItem("Script", "TrackProcessName", "")
## 目标进程可执行文件
self.Script_TrackProcessExe = ConfigItem("Script", "TrackProcessExe", "")
## 目标进程命令行
self.Script_TrackProcessCmdline = ConfigItem(
"Script", "TrackProcessCmdline", "", ArgumentValidator()
)
## 脚本配置文件或目录
self.Script_ConfigPath = ConfigItem(
"Script", "ConfigPath", "", FileValidator()
)
## 配置路径类型
self.Script_ConfigPathMode = ConfigItem(
"Script", "ConfigPathMode", "File", OptionsValidator(["File", "Folder"])
)
## 脚本配置回写时机
self.Script_UpdateConfigMode = ConfigItem(
"Script",
"UpdateConfigMode",
"Never",
OptionsValidator(["Never", "Success", "Failure", "Always"]),
)
## 日志文件路径
self.Script_LogPath = ConfigItem("Script", "LogPath", "", FileValidator())
## 动态日志文件名格式;固定文件名可留空
self.Script_LogPathFormat = ConfigItem("Script", "LogPathFormat", "%Y-%m-%d")
## 日志时间戳在日志行中的起止位置
self.Script_LogTimeStart = ConfigItem(
"Script", "LogTimeStart", 1, RangeValidator(1, 9999)
)
self.Script_LogTimeEnd = ConfigItem(
"Script", "LogTimeEnd", 1, RangeValidator(1, 9999)
)
## 日志时间戳格式
self.Script_LogTimeFormat = ConfigItem(
"Script", "LogTimeFormat", "%Y-%m-%d %H:%M:%S"
)
## 成功日志关键词,多个关键词用 | 分隔
self.Script_SuccessLog = ConfigItem("Script", "SuccessLog", "")
## 失败日志关键词,多个关键词用 | 分隔
self.Script_ErrorLog = ConfigItem("Script", "ErrorLog", "")

## Game ------------------------------------------------------------
## 是否由 MAS 启动游戏或模拟器
self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator())
## Emulator / Client / URL
self.Game_Type = ConfigItem(
"Game", "Type", "Emulator", OptionsValidator(["Emulator", "Client", "URL"])
)
## PC 游戏或启动器路径
self.Game_Path = ConfigItem("Game", "Path", "", FileValidator())
## URL 协议
self.Game_URL = ConfigItem("Game", "URL", "")
## URL/客户端进程名称
self.Game_ProcessName = ConfigItem("Game", "ProcessName", "")
## 游戏启动参数
self.Game_Arguments = ConfigItem("Game", "Arguments", "", ArgumentValidator())
## 启动等待时间(秒)
self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999))
## 是否强制关闭游戏
self.Game_IfForceClose = ConfigItem(
"Game", "IfForceClose", False, BoolValidator()
)
## 模拟器实例关系
self.Game_EmulatorId = ConfigItem(
"Game", "EmulatorId", "-", MultipleUIDValidator("-", self.related_config, "EmulatorConfig")
)
self.Game_EmulatorIndex = ConfigItem("Game", "EmulatorIndex", "-")

## Task ------------------------------------------------------------
# TODO(specialized): 增加脚本实际需要的专项级配置。

## Run -------------------------------------------------------------
## 单用户每日代理次数上限,0 表示不限制
self.Run_ProxyTimesLimit = ConfigItem(
"Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
)
## 单用户尝试次数
self.Run_RunTimesLimit = ConfigItem(
"Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
)
## 无新日志的超时分钟数
self.Run_RunTimeLimit = ConfigItem(
"Run", "RunTimeLimit", 10, RangeValidator(1, 9999)
)

self.UserData = MultipleConfig([XxxUserConfig])

super().__init__()
52 changes: 52 additions & 0 deletions templates/specialized/backend/registration-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 专项注册清单

复制模板后逐项勾选。没有真实消费者的字段、按钮或模式不要注册。

## 1. 配置与 schema

- [ ] 将 `XxxConfig`、`XxxUserConfig` 复制进 `app/models/config.py`,所有 `ConfigItem` 位于 `super().__init__()` 前。
- [ ] 将 `Xxx*` schema 复制进 `app/models/schema.py`,补齐真实专项字段、`Literal` 和描述。
- [ ] `Config.ScriptConfig` / `GlobalConfig` / 相关 `MultipleConfig` 允许新类型。
- [ ] `app/models/config.py` 的类映射、序列化和默认配置分支包含新类型。
- [ ] `app/utils/constants.py` 的 `TYPE_BOOK["XxxConfig"]` 有用户可见文案。

## 2. API 与核心调度

- [ ] `app/api/scripts.py` 的 `SCRIPT_BOOK` 增加 `XxxConfig`。
- [ ] `app/api/scripts.py` 的 `USER_BOOK` 增加 `XxxConfig: XxxUserConfig`。
- [ ] 任何专项 API 只做请求校验/响应整形;文件交换、任务循环和日志判态留在 core/task。
- [ ] `app/core/config.py` 的加载、创建、删除、用户增删和类型 union 分支包含新类型。
- [ ] `app/core/task_manager.py` 导入 `XxxManager`,在脚本类型 dispatch 中注册。
- [ ] 若接入计划表/任务队列,单独补 `PLAN_BOOK`、consumer、队列类型和对应前端表面;不要复制无关专项能力。

## 3. 任务模块

- [ ] 将 `task/Xxx/` 复制到 `app/task/Xxx/`,并全局替换类名、导入和 logger。
- [ ] `manager.py` 的 `METHOD_BOOK` 至少包含 `AutoProxy` 与 `ScriptConfig`,并核对任务模式是否真实支持。
- [ ] `AutoProxy.py` 的 `check()` 使用用户可操作的失败提示。
- [ ] `AutoProxy.py` 的 `LogMonitor` 使用时间范围、时间格式、回调三参构造。
- [ ] 每轮从 `log_record[start_time] = LogRecord()` 开始,只写 `log_record.status`。
- [ ] `final_task` 与 `on_crash` 都停止监控、停止/清理进程、恢复配置、释放锁,并在需要时写历史记录。
- [ ] 进程追踪至少有一个非空 `ProcessInfo` 字段;不要把空追踪条件交给运行时猜测。
- [ ] 配置目录/文件复制使用临时路径后替换;逐步清理失败分别记录日志。
- [ ] 多用户任务中单个用户检查失败只标记当前用户并继续后续用户。

## 4. 前端 Hub、路由和类型

- [ ] `frontend/src/types/script.ts` 增加 `ScriptType`、脚本/用户结构和默认值。
- [ ] `frontend/src/composables/useScriptApi.ts` 增加脚本类型映射、默认配置和 `XxxUserConfig -> users[]` 分支。
- [ ] `frontend/src/router/index.ts` 增加脚本编辑、用户新增、用户编辑路由;路径使用 lowercase kebab-case。
- [ ] `frontend/src/views/Scripts.vue` 的编辑、添加用户、编辑用户、创建/复制脚本分支全部补齐。
- [ ] `frontend/src/components/ScriptTable.vue` 增加图标、类型文案和专项操作(若确有专项动作)。
- [ ] `frontend/src/views/EditView/Script/XxxScriptEdit.vue` 与 `EditView/User/XxxUserEdit.vue` 接入真实 API。
- [ ] 新增用户流程先 `addUser`,再 `router.replace` 到带 `userId` 的编辑路由。
- [ ] 若使用 ScriptConfig 遮罩,启动、完成、错误、取消、超时、卸载都停止任务并清理 WebSocket 订阅。
- [ ] 自动发现与手动选择复用同一组哨兵文件;保存失败恢复旧值并显示原因。

## 5. 生成代码与验证

- [ ] 后端 schema 接入并重启后,确认 `openapi.json` 文本包含 `XxxConfig` / `XxxUserConfig`。
- [ ] 在 `frontend/` 运行 `yarn openapi`;禁止手改 `frontend/src/api/**`。
- [ ] 将测试移动到 `tests/task/test_xxx_autoproxy.py`,替换占位夹具,先运行该最小文件。
- [ ] 前端改动至少运行 `yarn lint`;路由/类型/构建改动再运行相关 build/type 命令。
- [ ] 版本记录在 `res/version.json` 下一个未发布版本中,说明新增专项模板或用户可见能力。
Loading