@sophiacyl/dsh-plugin-waves 0.0.0-stage → 0.1.0-beta.1

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 (140) hide show
  1. package/DSH-GO-VERIFICATION.md +57 -0
  2. package/ICON.png +0 -0
  3. package/LICENSE +674 -0
  4. package/README.md +251 -2
  5. package/cordis.patch.yml +8 -0
  6. package/install.ps1 +0 -0
  7. package/install.sh +209 -0
  8. package/lib/client.js +1511 -0
  9. package/lib/host/aliashint.js +70 -0
  10. package/lib/host/attachments.js +25 -0
  11. package/lib/host/command.js +34 -0
  12. package/lib/host/consts.js +16 -0
  13. package/lib/host/dataguide.js +35 -0
  14. package/lib/host/imagebridge.js +41 -0
  15. package/lib/host/native-result.js +91 -0
  16. package/lib/host/native.js +191 -0
  17. package/lib/host/proxy.js +59 -0
  18. package/lib/host/results.js +67 -0
  19. package/lib/host/service.js +263 -0
  20. package/lib/host/tool.js +185 -0
  21. package/lib/host/troubleshoot.js +31 -0
  22. package/lib/host/worldbook.js +80 -0
  23. package/lib/index.js +74 -0
  24. package/locale/en.json +6 -0
  25. package/locale/zh.json +6 -0
  26. package/package.json +136 -4
  27. package/service/ICON.png +0 -0
  28. package/service/ai/AI_DATA.md +221 -0
  29. package/service/knowledge/additional/manual.jsonl +5 -0
  30. package/service/knowledge/wiki/baike.jsonl +1231 -0
  31. package/service/knowledge/wiki/chengjiu.jsonl +36 -0
  32. package/service/knowledge/wiki/gonglve.jsonl +383 -0
  33. package/service/knowledge/wiki/huodong.jsonl +427 -0
  34. package/service/knowledge/wiki/renwu.jsonl +412 -0
  35. package/service/knowledge/wiki/tujian.jsonl +2889 -0
  36. package/service/knowledge/wiki/zixun.jsonl +47 -0
  37. package/service/plugins/__init__.py +0 -0
  38. package/service/pyproject.toml +40 -0
  39. package/service/rover/__init__.py +0 -0
  40. package/service/rover/ai_core/__init__.py +0 -0
  41. package/service/rover/ai_core/models.py +50 -0
  42. package/service/rover/ai_core/rag/__init__.py +5 -0
  43. package/service/rover/ai_core/rag/knowledge.py +23 -0
  44. package/service/rover/ai_core/register.py +86 -0
  45. package/service/rover/ai_core/resource.py +6 -0
  46. package/service/rover/ai_core/skills/__init__.py +0 -0
  47. package/service/rover/ai_core/skills/operations.py +8 -0
  48. package/service/rover/api.py +720 -0
  49. package/service/rover/api_render.py +161 -0
  50. package/service/rover/app_life.py +154 -0
  51. package/service/rover/aps.py +47 -0
  52. package/service/rover/avatar.py +85 -0
  53. package/service/rover/bot.py +133 -0
  54. package/service/rover/builtin.py +152 -0
  55. package/service/rover/cards/__init__.py +26 -0
  56. package/service/rover/cards/_shared.py +129 -0
  57. package/service/rover/cards/char_list.py +258 -0
  58. package/service/rover/cards/char_panel.py +1353 -0
  59. package/service/rover/cards/echo_list.py +206 -0
  60. package/service/rover/cards/matrix_roles.py +25 -0
  61. package/service/rover/cards/rank_boards.py +714 -0
  62. package/service/rover/cards/rank_total.py +664 -0
  63. package/service/rover/compat.py +215 -0
  64. package/service/rover/config.py +163 -0
  65. package/service/rover/custom_panel.py +220 -0
  66. package/service/rover/data_store.py +31 -0
  67. package/service/rover/gss.py +19 -0
  68. package/service/rover/handler.py +191 -0
  69. package/service/rover/help/__init__.py +0 -0
  70. package/service/rover/help/draw_new_plugin_help.py +332 -0
  71. package/service/rover/help/model.py +16 -0
  72. package/service/rover/help/utils.py +7 -0
  73. package/service/rover/hooks.py +420 -0
  74. package/service/rover/html_actions.py +236 -0
  75. package/service/rover/i18n.py +19 -0
  76. package/service/rover/knowledge.py +235 -0
  77. package/service/rover/logger.py +102 -0
  78. package/service/rover/main.py +33 -0
  79. package/service/rover/media.py +222 -0
  80. package/service/rover/message_models.py +23 -0
  81. package/service/rover/models.py +92 -0
  82. package/service/rover/native_media.py +64 -0
  83. package/service/rover/ocr.py +79 -0
  84. package/service/rover/pool.py +43 -0
  85. package/service/rover/restart.py +36 -0
  86. package/service/rover/segment.py +143 -0
  87. package/service/rover/server.py +131 -0
  88. package/service/rover/status/__init__.py +0 -0
  89. package/service/rover/status/models.py +10 -0
  90. package/service/rover/status/plugin_status.py +17 -0
  91. package/service/rover/subscribe.py +148 -0
  92. package/service/rover/sv.py +403 -0
  93. package/service/rover/templates/char_list.html +182 -0
  94. package/service/rover/templates/char_panel.html +465 -0
  95. package/service/rover/templates/echo_list.html +221 -0
  96. package/service/rover/templates/rank_all_total.html +193 -0
  97. package/service/rover/templates/rank_board.html +107 -0
  98. package/service/rover/templates/rank_level_total.html +57 -0
  99. package/service/rover/templates/rank_matrix_total.html +66 -0
  100. package/service/rover/templates/rank_phantom_total.html +180 -0
  101. package/service/rover/templates/rank_slash_total.html +38 -0
  102. package/service/rover/trigger.py +98 -0
  103. package/service/rover/utils/__init__.py +0 -0
  104. package/service/rover/utils/boardcast/__init__.py +0 -0
  105. package/service/rover/utils/boardcast/models.py +15 -0
  106. package/service/rover/utils/cookie_manager/__init__.py +0 -0
  107. package/service/rover/utils/cookie_manager/qrlogin.py +24 -0
  108. package/service/rover/utils/database/__init__.py +0 -0
  109. package/service/rover/utils/database/base_models.py +1771 -0
  110. package/service/rover/utils/database/models.py +80 -0
  111. package/service/rover/utils/database/startup.py +60 -0
  112. package/service/rover/utils/default_bg/bg.jpg +0 -0
  113. package/service/rover/utils/download_resource/__init__.py +0 -0
  114. package/service/rover/utils/download_resource/download_core.py +164 -0
  115. package/service/rover/utils/download_resource/download_file.py +46 -0
  116. package/service/rover/utils/fonts/__init__.py +0 -0
  117. package/service/rover/utils/fonts/fonts.py +33 -0
  118. package/service/rover/utils/image/__init__.py +0 -0
  119. package/service/rover/utils/image/convert.py +51 -0
  120. package/service/rover/utils/image/image_tools.py +30 -0
  121. package/service/rover/utils/image/utils.py +70 -0
  122. package/service/rover/utils/plugins_config/__init__.py +0 -0
  123. package/service/rover/utils/plugins_config/gs_config.py +506 -0
  124. package/service/rover/utils/plugins_config/models.py +182 -0
  125. package/service/rover/web_app.py +3 -0
  126. package/service/rover/webconsole/__init__.py +0 -0
  127. package/service/rover/webconsole/mount_app.py +26 -0
  128. package/service/tools/migrate.py +351 -0
  129. package/service/tools/regression.py +87 -0
  130. package/service/tools/smoke.py +63 -0
  131. package/service/tools/update_plugins.py +212 -0
  132. package/service/tools/verify_actions.py +104 -0
  133. package/service/tools/verify_card.py +82 -0
  134. package/service/tools/wwdata.py +665 -0
  135. package/tests/compat.test.js +181 -0
  136. package/tests/imagebridge.test.js +58 -0
  137. package/tests/media.test.js +68 -0
  138. package/tests/native.test.js +127 -0
  139. package/tests/runtime.test.js +69 -0
  140. package/tests/service.test.js +48 -0
@@ -0,0 +1,506 @@
1
+ import sys
2
+ import json
3
+ import datetime
4
+ from copy import deepcopy
5
+ from typing import Any, Dict, List, Union
6
+ from pathlib import Path
7
+
8
+ from msgspec import ValidationError, json as msgjson, to_builtins
9
+ from boltons.fileutils import atomic_save
10
+
11
+ from rover.logger import logger
12
+ from rover.data_store import CONFIGS_PATH, core_path
13
+
14
+ from .models import (
15
+ Divider,
16
+ IntConfig,
17
+ StrConfig,
18
+ BoolConfig,
19
+ DateConfig,
20
+ DictConfig,
21
+ ColorConfig,
22
+ ConfigItem,
23
+ FloatConfig,
24
+ TimeRConfig,
25
+ ListStrConfig,
26
+ TimeRangeConfig,
27
+ RepeatGroupConfig,
28
+ )
29
+
30
+
31
+ def reconcile_config(
32
+ stored: Dict[str, ConfigItem],
33
+ default: Dict[str, ConfigItem],
34
+ *,
35
+ config_name: str = "",
36
+ ) -> Dict[str, ConfigItem]:
37
+ """把已存配置向默认模板对齐(递归)。非破坏: 缺项补默认、类型不符仅该项重置、
38
+ 孤儿键保留; RepeatGroupConfig 的每个 data 项再对 template 递归调和。"""
39
+ result: Dict[str, ConfigItem] = {}
40
+ for key, dft in default.items():
41
+ if key not in stored:
42
+ result[key] = deepcopy(dft)
43
+ continue
44
+ cur = stored[key]
45
+ if type(cur) is not type(dft):
46
+ logger.warning(
47
+ f"[{config_name}] 配置项 {key} 类型不符({type(cur).__name__} != {type(dft).__name__}), 已重置为默认值"
48
+ )
49
+ result[key] = deepcopy(dft)
50
+ continue
51
+ # 同类型: 刷新代码侧元数据, 保留用户 data
52
+ if isinstance(cur, (StrConfig, ListStrConfig)) and isinstance(dft, (StrConfig, ListStrConfig)):
53
+ cur.options = dft.options
54
+ cur.title = dft.title
55
+ cur.desc = dft.desc
56
+ # secret 字段并非所有类型都有 (Divider / ColorConfig 无), 条件访问防 AttributeError
57
+ if not isinstance(cur, (Divider, ColorConfig)) and not isinstance(dft, (Divider, ColorConfig)):
58
+ cur.secret = dft.secret
59
+ # 递归分支: 组模板由代码所有(覆盖), data 每项对 template 再调和
60
+ if isinstance(cur, RepeatGroupConfig) and isinstance(dft, RepeatGroupConfig):
61
+ cur.template = deepcopy(dft.template)
62
+ cur.data = [reconcile_config(item, dft.template, config_name=config_name) for item in cur.data]
63
+ result[key] = cur
64
+ # 孤儿键(默认里没有)保留, 不静默丢用户数据
65
+ for key, val in stored.items():
66
+ if key not in default:
67
+ result[key] = val
68
+ return result
69
+
70
+
71
+ def _rebuild_group_item(raw: Dict[str, Any], template: Dict[str, ConfigItem]) -> Dict[str, ConfigItem]:
72
+ """用前端回传的值 raw(字段名->值) + template 重建一组 Dict[str, ConfigItem]。
73
+ 嵌套 RepeatGroupConfig 递归重建; 其余叶子把值写进 .data。"""
74
+ out: Dict[str, ConfigItem] = {}
75
+ for key, field in template.items():
76
+ proto = deepcopy(field)
77
+ if isinstance(proto, RepeatGroupConfig) and isinstance(field, RepeatGroupConfig):
78
+ sub_raw = raw[key] if key in raw and isinstance(raw[key], list) else []
79
+ proto.data = [_rebuild_group_item(r, field.template) for r in sub_raw if isinstance(r, dict)]
80
+ elif not isinstance(proto, Divider) and key in raw:
81
+ proto.data = raw[key]
82
+ out[key] = proto
83
+ return out
84
+
85
+
86
+ class StringConfig:
87
+ def __new__(cls, *args, **kwargs):
88
+ # 判断是否已经被初始化
89
+ if len(args) >= 1:
90
+ name = args[0]
91
+ else:
92
+ name = kwargs.get("config_name")
93
+
94
+ if name is None:
95
+ raise ValueError("Config.name is None!")
96
+
97
+ if name in all_config_list:
98
+ return all_config_list[name]
99
+ else:
100
+ _config = super().__new__(cls)
101
+ all_config_list[name] = _config
102
+ return _config
103
+
104
+ def __init__(
105
+ self,
106
+ config_name: str,
107
+ CONFIG_PATH: Union[Path, List[Path]],
108
+ config_list: Dict[str, ConfigItem],
109
+ ) -> None:
110
+ """
111
+ 初始化配置文件管理器。
112
+
113
+ Args:
114
+ config_name: 配置名称,用于标识配置项。
115
+ CONFIG_PATH: 配置文件路径,可以是单个路径或路径列表。
116
+ 当为列表时,将按照以下逻辑处理:
117
+ 1. 首先检查列表中除最后一个路径外的其他路径是否存在配置文件
118
+ 2. 如果在 [0:-1] 范围内的任何路径找到配置文件,
119
+ 会将其安全迁移到最后一个路径(index=-1)所指向的位置
120
+ 3. 最终加载和使用的配置文件始终是 index=-1 指向的路径
121
+ 4. 迁移过程中,原有配置文件会被读取后写入新位置,然后删除
122
+ 5. 如果目标路径已存在配置文件,则不会进行迁移操作
123
+ config_list: 配置项的默认字典,用于初始化或校验配置。
124
+
125
+ Example:
126
+ # 单路径方式
127
+ CONFIG_PATH = Path("config.json")
128
+
129
+ # 多路径方式 - 会从 path1 迁移到最终路径
130
+ CONFIG_PATH = [Path("old_config.json"), Path("new_config.json")]
131
+ """
132
+ self.config_list = config_list
133
+ self.config_default = config_list
134
+ self.config_name = config_name
135
+ self.CONFIG_PATH: Path = None # type: ignore
136
+
137
+ if isinstance(CONFIG_PATH, list):
138
+ # 按照 [0:-1] 的顺序查找是否存在配置文件
139
+ for old_path in CONFIG_PATH[:-1]:
140
+ if old_path.exists():
141
+ final_path = CONFIG_PATH[-1]
142
+ # 安全迁移:将找到的配置文件迁移到最终路径
143
+ self._migrate_config(old_path, final_path)
144
+ break
145
+ # 最终配置文件路径始终为列表的最后一个元素
146
+ CONFIG_PATH = CONFIG_PATH[-1]
147
+
148
+ if not CONFIG_PATH.exists():
149
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
150
+ with open(CONFIG_PATH, "wb") as file:
151
+ file.write(msgjson.encode(config_list))
152
+
153
+ self.CONFIG_PATH = CONFIG_PATH
154
+ self.config: Dict[str, ConfigItem] = {} # type: ignore
155
+ # 获取调用者的插件名
156
+ self.plugin_name = self._get_caller_plugin_name()
157
+ self.update_config()
158
+
159
+ def _migrate_config(self, old_path: Path, new_path: Path) -> None:
160
+ """
161
+ 安全地将配置文件从旧路径迁移到新路径。
162
+
163
+ 只有当新路径不存在配置文件时,才会进行迁移操作。
164
+ 迁移过程:读取旧配置 -> 写入新路径 -> 删除旧文件
165
+
166
+ Args:
167
+ old_path: 旧的配置文件路径。
168
+ new_path: 新的配置文件路径。
169
+ """
170
+ # 如果新路径已存在配置文件,则不进行迁移
171
+ if new_path.exists():
172
+ logger.info(f"[{self.config_name}] 目标配置文件已存在, 跳过迁移: {old_path}")
173
+ return
174
+
175
+ # 确保目标目录存在
176
+ new_path.parent.mkdir(parents=True, exist_ok=True)
177
+
178
+ try:
179
+ # 读取旧配置文件内容
180
+ with open(old_path, "r", encoding="UTF-8") as f:
181
+ content = f.read()
182
+
183
+ # 写入新路径
184
+ with open(new_path, "w", encoding="UTF-8") as f:
185
+ f.write(content)
186
+
187
+ # 删除旧文件
188
+ old_path.unlink()
189
+
190
+ logger.info(f"[{self.config_name}] 配置文件已迁移: {old_path} -> {new_path}")
191
+ except Exception as e:
192
+ logger.error(f"[{self.config_name}] 配置文件迁移失败: {e}")
193
+
194
+ def _get_caller_plugin_name(self):
195
+ """调用者所在的插件包名(service 下第一级目录), 运行时自身返回 None。"""
196
+ try:
197
+ frame = sys._getframe(2)
198
+ caller = Path(frame.f_code.co_filename).resolve()
199
+ rel = caller.relative_to(core_path.parent.resolve())
200
+ except (ValueError, AttributeError):
201
+ return None
202
+
203
+ if len(rel.parts) < 2 or rel.parts[0] == core_path.name:
204
+ return None
205
+ return rel.parts[0]
206
+
207
+ def __len__(self):
208
+ return len(self.config)
209
+
210
+ def __iter__(self):
211
+ return iter(self.config)
212
+
213
+ def __getitem__(self, key) -> ConfigItem:
214
+ return self.config[key]
215
+
216
+ def get_raw_config(self) -> Dict:
217
+ return to_builtins(self.config)
218
+
219
+ def sort_config(self):
220
+ _config = {}
221
+
222
+ for i in self.config_list:
223
+ if i in self.config:
224
+ _config[i] = self.config[i]
225
+
226
+ for i in self.config:
227
+ if i not in _config:
228
+ _config[i] = self.config[i]
229
+
230
+ self.config = _config
231
+
232
+ self.write_config()
233
+
234
+ def write_config(self):
235
+ payload = msgjson.format(msgjson.encode(self.config), indent=4)
236
+ # 内容没变就不落盘: 光是 import 一下配置也会走到这里, 白写一次会碰只读环境的沙箱
237
+ try:
238
+ if self.CONFIG_PATH.read_bytes() == payload:
239
+ return
240
+ except OSError:
241
+ pass
242
+ with atomic_save(
243
+ str(self.CONFIG_PATH),
244
+ text_mode=False,
245
+ overwrite=True,
246
+ overwrite_part=True,
247
+ file_perms=0o644,
248
+ ) as file:
249
+ if file:
250
+ file.write(payload)
251
+ else:
252
+ logger.error(f"[{self.config_name}] 配置文件写入失败!")
253
+
254
+ def repair_config(self):
255
+ with open(self.CONFIG_PATH, "r", encoding="UTF-8") as f:
256
+ logger.warning(f"[{self.config_name}] 配置文件格式已变动, 正在修复...")
257
+ temp_config: Dict[str, Any] = json.load(f)
258
+
259
+ for key in self.config_list:
260
+ defalut_dict = to_builtins(self.config_list[key])
261
+ if key not in temp_config:
262
+ continue
263
+ stored = temp_config[key]
264
+ # 容错:已知配置项的值若不是 dict(如被外部模块误写成字符串),
265
+ # 直接当作"格式异常"重置为默认值,避免后续 .keys() 抛 AttributeError。
266
+ if not isinstance(stored, dict):
267
+ temp_config[key] = defalut_dict
268
+ continue
269
+ # key 集合一致不代表值类型合法; 不实测一次 decode 会陷入
270
+ # repair <-> decode 死循环 (RecursionError), 故非法条目直接重置。
271
+ try:
272
+ msgjson.decode(msgjson.encode(stored), type=ConfigItem)
273
+ except ValidationError:
274
+ temp_config[key] = defalut_dict
275
+ continue
276
+ if list(stored.keys()) == list(defalut_dict.keys()):
277
+ continue
278
+ else:
279
+ temp_config[key] = defalut_dict
280
+
281
+ # 旁路字段清理: 非配置结构 (非 dict, 或 dict 但 decode 失败) 一律移除,
282
+ # 保证 repair 后必为合法 Dict[str, ConfigItem]; 合法历史条目仍保留待迁移。
283
+ foreign_keys = [k for k in temp_config if k not in self.config_list]
284
+ for k in foreign_keys:
285
+ v = temp_config[k]
286
+ if isinstance(v, dict):
287
+ try:
288
+ msgjson.decode(msgjson.encode(v), type=ConfigItem)
289
+ continue
290
+ except ValidationError:
291
+ pass
292
+ logger.warning(f"[{self.config_name}] 已移除不符合配置结构的旁路字段: {k} ({type(v).__name__})")
293
+ temp_config.pop(k)
294
+
295
+ with open(self.CONFIG_PATH, "w", encoding="UTF-8") as f:
296
+ json.dump(temp_config, f, indent=4, ensure_ascii=False)
297
+
298
+ def update_config(self):
299
+ # 最多修复一次后重试 decode; repair_config 已保证收敛, 失败兜底重置默认,
300
+ # 杜绝 update_config <-> repair_config 无限递归 (RecursionError)。
301
+ for attempt in range(2):
302
+ try:
303
+ with open(self.CONFIG_PATH, "r", encoding="UTF-8") as f:
304
+ d = f.read()
305
+ except UnicodeDecodeError:
306
+ with open(self.CONFIG_PATH, "r") as f:
307
+ d = f.read()
308
+
309
+ try:
310
+ self.config: Dict[str, ConfigItem] = msgjson.decode(
311
+ d,
312
+ type=Dict[str, ConfigItem],
313
+ )
314
+ break
315
+ except ValidationError:
316
+ if attempt == 0:
317
+ self.repair_config()
318
+ else:
319
+ logger.error(f"[{self.config_name}] 配置文件无法解析, 已回退默认值!")
320
+ self.config = dict(self.config_list)
321
+
322
+ # 逐键调和: 补默认 / 类型不符重置 / 刷新元数据; RepeatGroupConfig 递归(见函数)
323
+ self.config = reconcile_config(self.config, self.config_list, config_name=self.config_name)
324
+
325
+ # 重新写回
326
+ self.sort_config()
327
+
328
+ def get_config(self, key: str, default_value: Any = None) -> Any:
329
+ if key in self.config:
330
+ return self.config[key]
331
+ elif key in self.config_list:
332
+ logger.info(f"[{self.config_name}] 配置项 {key} 不存在, 正在更新配置文件...")
333
+ self.update_config()
334
+ return self.config[key]
335
+ else:
336
+ logger.warning(f"[{self.config_name}] 配置项 {key} 不存在!")
337
+ if default_value is None:
338
+ return BoolConfig("缺省值", "获取错误的配置项", False)
339
+
340
+ if isinstance(default_value, str):
341
+ return StrConfig("缺省值", "获取错误的配置项", default_value)
342
+ elif isinstance(default_value, bool):
343
+ return BoolConfig("缺省值", "获取错误的配置项", default_value)
344
+ elif isinstance(default_value, List):
345
+ return ListStrConfig("缺省值", "获取错误的配置项", default_value)
346
+ elif isinstance(default_value, Dict):
347
+ return DictConfig("缺省值", "获取错误的配置项", default_value)
348
+ else:
349
+ return BoolConfig("缺省值", "获取错误的配置项", False)
350
+
351
+ def set_config(self, key: str, value: Union[str, List, bool, Dict, int]) -> bool:
352
+ if key in self.config_list:
353
+ item = self.config[key]
354
+ temp = item.data
355
+ # 组配置: 前端回传 List[{字段->值}], 用 template 重建成 List[Dict[str, ConfigItem]]。
356
+ # 必须先于下方通用 list==list 分支, 否则会把裸值直接塞进 data 破坏结构。
357
+ if isinstance(item, RepeatGroupConfig) and isinstance(value, list):
358
+ item.data = [_rebuild_group_item(r, item.template) for r in value if isinstance(r, dict)]
359
+ self.write_config()
360
+ return True
361
+ if type(value) == type(temp): # noqa: E721
362
+ # 设置值
363
+ self.config[key].data = value # type: ignore
364
+ # 重新写回
365
+ self.write_config()
366
+ return True
367
+ elif isinstance(item, TimeRConfig) and isinstance(value, list):
368
+ # TimeRConfig 接受 list 类型并转换为 tuple
369
+ self.config[key].data = tuple(value) # type: ignore
370
+ self.write_config()
371
+ return True
372
+ elif isinstance(item, TimeRangeConfig) and isinstance(value, (list, tuple)) and len(value) == 2:
373
+ # 前端/命令传入嵌套 list, 落库需转换为嵌套 tuple
374
+ start, end = value
375
+ try:
376
+ item.data = ((int(start[0]), int(start[1])), (int(end[0]), int(end[1])))
377
+ except (ValueError, TypeError, IndexError):
378
+ logger.warning(f"[{self.config_name}] 配置项 {key} 时间范围格式非法: {value}")
379
+ return False
380
+ self.write_config()
381
+ return True
382
+ elif isinstance(item, FloatConfig) and isinstance(value, int) and not isinstance(value, bool):
383
+ # 前端可能以整数 JSON (如 10, 而非 10.0) 传入浮点配置, 归一化为 float
384
+ item.data = float(value)
385
+ self.write_config()
386
+ return True
387
+ elif isinstance(item, DateConfig) and isinstance(value, str):
388
+ # 前端/命令传入 ISO 日期字符串, 落库需转换为 datetime.date
389
+ try:
390
+ item.data = datetime.date.fromisoformat(value)
391
+ except ValueError:
392
+ logger.warning(f"[{self.config_name}] 配置项 {key} 日期格式非法: {value}")
393
+ return False
394
+ self.write_config()
395
+ return True
396
+ else:
397
+ logger.warning(f"[{self.config_name}] 配置项 {key} 写入类型不正确!")
398
+ return False
399
+ else:
400
+ return False
401
+
402
+ def migrate_from(self, old_configs: Union["StringConfig", List["StringConfig"]]):
403
+ """
404
+ 自动从旧配置实例中吸取同名键的数据(仅迁移用户设定的 data 值,保留新配置的 title/desc)。
405
+ 完成后自动从旧文件中清理该键。
406
+ """
407
+ if not isinstance(old_configs, list):
408
+ old_configs = [old_configs]
409
+
410
+ changed = False
411
+
412
+ for old_config in old_configs:
413
+ old_changed = False
414
+
415
+ for key in self.config_list:
416
+ if key in old_config.config:
417
+ # 仅转移最核心的 data 用户数据,这样就不会覆盖新配置的文本描述
418
+ self.config[key].data = old_config.config[key].data # type: ignore
419
+ changed = True
420
+
421
+ old_config.config.pop(key)
422
+ old_changed = True
423
+ logger.info(f"[配置迁移] 配置项 {key}: {old_config.config_name} -> {self.config_name}")
424
+
425
+ if old_changed:
426
+ old_config.sort_config()
427
+
428
+ if changed:
429
+ self.sort_config()
430
+
431
+
432
+ all_config_list: Dict[str, StringConfig] = {}
433
+
434
+
435
+ DATABASE_DEFAULT: Dict[str, ConfigItem] = {
436
+ "db_type": StrConfig(
437
+ "数据库类型",
438
+ "设置喜欢的数据库类型",
439
+ "SQLite",
440
+ ["SQLite", "MySql", "PostgreSQL", "自定义"],
441
+ ),
442
+ "db_driver": StrConfig(
443
+ "MySQL驱动",
444
+ "设置喜欢的MySQL驱动",
445
+ "aiomysql",
446
+ ["aiomysql", "asyncmy"],
447
+ ),
448
+ "db_custom_url": StrConfig(
449
+ "自定义数据库连接地址 (一般无需填写)",
450
+ "设置自定义数据库连接",
451
+ "",
452
+ ),
453
+ "db_host": StrConfig(
454
+ "数据库地址",
455
+ "设置数据库地址",
456
+ "localhost",
457
+ ["localhost"],
458
+ ),
459
+ "db_port": StrConfig(
460
+ "数据库端口",
461
+ "设置数据库端口",
462
+ "3306",
463
+ ["3306"],
464
+ ),
465
+ "db_user": StrConfig(
466
+ "数据库用户名",
467
+ "设置数据库用户名",
468
+ "root",
469
+ ["root", "admin", "postgres"],
470
+ ),
471
+ "db_password": StrConfig(
472
+ "数据库密码",
473
+ "设置数据库密码",
474
+ "root",
475
+ ["root", "123456"],
476
+ ),
477
+ "db_name": StrConfig(
478
+ "数据库名称",
479
+ "设置数据库名称",
480
+ "WavesData",
481
+ ["WavesData"],
482
+ ),
483
+ "db_pool_size": IntConfig(
484
+ "数据库连接池大小",
485
+ "设置数据库连接池大小",
486
+ 5,
487
+ options=[5, 10, 20, 30, 40, 50],
488
+ ),
489
+ "db_echo": BoolConfig(
490
+ "数据库调试模式",
491
+ "设置数据库调试模式",
492
+ False,
493
+ ),
494
+ "db_pool_recycle": IntConfig(
495
+ "数据库连接池回收时间",
496
+ "设置数据库连接池回收时间",
497
+ 1500,
498
+ options=[1500, 3600, 7200, 14400, 28800],
499
+ ),
500
+ }
501
+
502
+ database_config = StringConfig(
503
+ "数据库配置",
504
+ CONFIGS_PATH / "database_config.json",
505
+ DATABASE_DEFAULT,
506
+ )
@@ -0,0 +1,182 @@
1
+ import datetime
2
+ from typing import Dict, List, Tuple, Union, Optional
3
+ from typing_extensions import deprecated
4
+
5
+ import msgspec
6
+
7
+
8
+ class Config(msgspec.Struct, tag=True):
9
+ """基类, 禁止直接使用"""
10
+
11
+ title: str
12
+ desc: str
13
+
14
+
15
+ class StrConfig(Config, tag=True):
16
+ """字符串配置"""
17
+
18
+ data: str
19
+ options: List[str] = []
20
+ regex: Optional[str] = None
21
+ """仅用于前端正则校验的正则表达式"""
22
+ details: Optional[dict] = None
23
+ secret: bool = False
24
+
25
+
26
+ class BoolConfig(Config, tag=True):
27
+ """布尔开关配置"""
28
+
29
+ data: bool
30
+ secret: bool = False
31
+
32
+
33
+ class DictConfig(Config, tag=True):
34
+ """字典配置"""
35
+
36
+ data: Dict[str, List]
37
+ secret: bool = False
38
+
39
+
40
+ class ListStrConfig(Config, tag=True):
41
+ """字符串列表配置"""
42
+
43
+ data: List[str]
44
+ options: List[str] = []
45
+ secret: bool = False
46
+
47
+
48
+ class ListConfig(Config, tag=True):
49
+ """整数列表配置"""
50
+
51
+ data: List[int]
52
+ secret: bool = False
53
+
54
+
55
+ class IntConfig(Config, tag=True):
56
+ """整数配置"""
57
+
58
+ data: int
59
+ max_value: Optional[int] = None
60
+ options: List[int] = []
61
+ secret: bool = False
62
+
63
+
64
+ class FloatConfig(Config, tag=True):
65
+ """浮点数配置"""
66
+
67
+ data: float
68
+ min_value: Optional[float] = None
69
+ max_value: Optional[float] = None
70
+ secret: bool = False
71
+
72
+
73
+ class ImageConfig(Config, tag=True):
74
+ """图片配置
75
+
76
+ upload_to: 通过 get_res_path() 获取的绝对目录路径,不可写相对路径或跨插件路径
77
+ """
78
+
79
+ data: str
80
+ upload_to: str
81
+ filename: str
82
+ suffix: str = "jpg"
83
+ secret: bool = False
84
+
85
+
86
+ class TimeRConfig(Config, tag=True):
87
+ """时间点配置, data为时、分"""
88
+
89
+ data: Tuple[int, int]
90
+ secret: bool = False
91
+
92
+
93
+ class Divider(Config, tag=True):
94
+ """无实际作用, 在前端中用于分割配置, 便于用户UX"""
95
+
96
+ data: Optional[str] = None
97
+ """分割线标题, 为None时前端仅渲染分割线, 非None时渲染带标题的分割线"""
98
+
99
+
100
+ class FileUploadConfig(Config, tag=True):
101
+ """文件上传配置
102
+
103
+ upload_to: 通过 get_res_path() 获取的绝对目录路径,不可写相对路径或跨插件路径
104
+ """
105
+
106
+ data: str
107
+ upload_to: str
108
+ filename: str
109
+ suffix: str = "html"
110
+ secret: bool = False
111
+
112
+
113
+ class FilesUploadConfig(Config, tag=True):
114
+ """批量文件上传配置
115
+
116
+ data: 通过 get_res_path() 获取的绝对目录路径,同时也是上传目标目录,
117
+ 不可写相对路径或跨插件路径
118
+ """
119
+
120
+ data: str
121
+ suffix: str = "html"
122
+ secret: bool = False
123
+
124
+
125
+ class DateConfig(Config, tag=True):
126
+ """日期配置 (如 YYYY-MM-DD)"""
127
+
128
+ data: datetime.date
129
+ secret: bool = False
130
+
131
+
132
+ class TimeRangeConfig(Config, tag=True):
133
+ """时间范围配置 (如 允许访问的时间段: 08:00 - 20:00)"""
134
+
135
+ data: Tuple[Tuple[int, int], Tuple[int, int]]
136
+ secret: bool = False
137
+
138
+
139
+ class ColorConfig(Config, tag=True):
140
+ """颜色配置 (HEX格式如 #FFFFFF 或 RGBA)"""
141
+
142
+ data: str
143
+
144
+
145
+ class RepeatGroupConfig(Config, tag=True):
146
+ """可重复配置组:data 每项是一组子配置(Dict[str, ConfigItem], 与整份插件配置同构),
147
+ template 是新增一项的原型/默认。可再嵌套本类型。前端复用逐项渲染器递归渲染。
148
+ """
149
+
150
+ # "ConfigItem" 为字符串前向引用: 定义在本类之后, msgspec 加载时惰性解析。
151
+ data: List[Dict[str, "ConfigItem"]]
152
+ template: Dict[str, "ConfigItem"]
153
+ secret: bool = False
154
+
155
+
156
+ @deprecated("TimeConfig 已废弃,请使用 TimeRConfig 代替")
157
+ class TimeConfig(Config, tag=True):
158
+ """deprecated/已废弃"""
159
+
160
+ data: str
161
+ secret: bool = False
162
+
163
+
164
+ ConfigItem = Union[
165
+ DictConfig,
166
+ BoolConfig,
167
+ ListConfig,
168
+ ListStrConfig,
169
+ StrConfig,
170
+ IntConfig,
171
+ FloatConfig,
172
+ ImageConfig,
173
+ TimeRConfig,
174
+ Divider,
175
+ FileUploadConfig,
176
+ FilesUploadConfig,
177
+ DateConfig,
178
+ TimeRangeConfig,
179
+ ColorConfig,
180
+ RepeatGroupConfig,
181
+ TimeConfig,
182
+ ]
@@ -0,0 +1,3 @@
1
+ from rover.app_life import app
2
+
3
+ __all__ = ["app"]
File without changes