@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,420 @@
1
+ """对插件行为的运行时改造。
2
+
3
+ 插件目录保持上游原样以便随时更新, 本地需要的差异全部在这里施加:
4
+ 渲染改为直接产出 HTML、卡片内角色可点击、裁掉本地用不到的命令。
5
+ """
6
+
7
+ import importlib
8
+ from typing import Dict, List, Set, Tuple
9
+
10
+ from rover.logger import logger
11
+
12
+ # (SV 名, 触发词) —— 群聊/多用户场景才有意义的命令, 本地不注册
13
+ DROP_TRIGGERS: List[Tuple[str, str]] = [
14
+ ("鸣潮删除群成员", "^删除群成员"),
15
+ ("鸣潮删除群成员", "^(删除|清除|清理)不活跃"),
16
+ ("waves面板图权限提示和审核", "*"),
17
+ ("ww角色排行", "*"),
18
+ ("ww声骸排行", "*"),
19
+ ("ww练度排行", "*"),
20
+ ("waves无尽排行", "*"),
21
+ ("waves矩阵排行", "*"),
22
+ ("waves抽卡排行", "*"),
23
+ ("订阅鸣潮公告", "*"),
24
+ ("RoverSign-全部签到", "^(订阅|取消订阅)签到结果$"),
25
+ # 本机就是主人自己, 这条只是往订阅表里写一行
26
+ ("联系主人", "*"),
27
+ ]
28
+
29
+ # 帮助里的扩展模块条目按实际装了哪些插件给, 否则会列出根本没装的功能
30
+ HELP_EXTRA_MODULES = {
31
+ "roversign": ("RoverSign",),
32
+ "scoreecho": ("ScoreEcho",),
33
+ "roverreminder": ("RoverReminder",),
34
+ "todayecho": ("WavesTodayEcho", "TodayEcho"),
35
+ }
36
+
37
+ # 各卡片模板里的用户头像字段
38
+ AVATAR_CONTEXT_KEY = "avatar_url"
39
+
40
+ # 帮助条目里含这些词的均属已裁功能, 渲染时一并剔除
41
+ DROP_HELP_MARKERS = (
42
+ "群成员", "不活跃", "群排行", "群排名", "抽卡排行", "练度排行", "群持有率",
43
+ "订阅公告", "退订公告", "订阅签到结果", "联系主人",
44
+ )
45
+
46
+ # 命令里的这些关键词只在群里有意义, 从触发词元组中摘掉
47
+ DROP_KEYWORDS: Set[str] = {
48
+ "群角色持有率",
49
+ "群角色持有率列表",
50
+ "群持有率",
51
+ "群练度排行",
52
+ "练度群排行",
53
+ "练度群排行榜",
54
+ "群练度排名",
55
+ "练度群排名",
56
+ }
57
+
58
+
59
+ def _iter_triggers():
60
+ from rover.sv import SL
61
+
62
+ for sv in SL.lst.values():
63
+ for ttype, table in list(sv.TL.items()):
64
+ for key, trigger in list(table.items()):
65
+ yield sv, ttype, key, trigger
66
+
67
+
68
+ def _drop_buttons() -> int:
69
+ """快捷按钮里指向已裁命令的条目一并去掉, 免得点了没反应。"""
70
+ from rover.segment import MessageSegment
71
+
72
+ original = MessageSegment.buttons
73
+
74
+ @staticmethod
75
+ def buttons(items=None):
76
+ if items:
77
+ kept = []
78
+ for row in items:
79
+ if isinstance(row, list):
80
+ row = [b for b in row if getattr(b, "data", "") not in DROP_KEYWORDS]
81
+ if row:
82
+ kept.append(row)
83
+ elif getattr(row, "data", "") not in DROP_KEYWORDS:
84
+ kept.append(row)
85
+ items = kept
86
+ return original(items)
87
+
88
+ MessageSegment.buttons = buttons
89
+ return 0
90
+
91
+
92
+ def align_help_modules(installed=None) -> None:
93
+ """帮助里的扩展模块条目跟着已装插件走, 没装的模块不列。
94
+
95
+ 帮助表是在插件 import 时一次性建好的, 所以这一步必须赶在载入插件之前。
96
+ """
97
+ from rover.utils.plugins_config.gs_config import all_config_list
98
+
99
+ if installed is None:
100
+ from rover.sv import SL
101
+
102
+ installed = set(SL.plugins)
103
+ config = all_config_list.get("XutheringWavesUID")
104
+ if config is None:
105
+ return
106
+ item = config.get_config("HelpExtraModules")
107
+ if item is None:
108
+ return
109
+ want = sorted(
110
+ key for key, names in HELP_EXTRA_MODULES.items()
111
+ if any(name in installed for name in names)
112
+ )
113
+ if list(getattr(item, "data", []) or []) != want:
114
+ config.set_config("HelpExtraModules", want)
115
+ logger.info(f"[改造] 帮助扩展模块按已装插件对齐: {want}")
116
+
117
+
118
+ def drop_commands() -> int:
119
+ """按黑名单摘掉触发器。上游改名导致某条失配时告警, 不静默。"""
120
+ from rover.sv import SL
121
+
122
+ dropped = 0
123
+ matched_rules: Set[Tuple[str, str]] = set()
124
+
125
+ for sv_name, keyword in DROP_TRIGGERS:
126
+ sv = SL.lst.get(sv_name)
127
+ if sv is None:
128
+ continue
129
+ for ttype, table in list(sv.TL.items()):
130
+ for key, trigger in list(table.items()):
131
+ if keyword == "*" or trigger.keyword.startswith(keyword):
132
+ del table[key]
133
+ dropped += 1
134
+ matched_rules.add((sv_name, keyword))
135
+ if not table:
136
+ sv.TL.pop(ttype, None)
137
+
138
+ for sv, ttype, key, trigger in _iter_triggers():
139
+ if trigger.keyword in DROP_KEYWORDS:
140
+ sv.TL[ttype].pop(key, None)
141
+ dropped += 1
142
+
143
+ stale = [f"{n}:{k}" for n, k in DROP_TRIGGERS if (n, k) not in matched_rules]
144
+ if stale:
145
+ logger.warning(f"[改造] 命令黑名单有 {len(stale)} 条未命中(上游可能已改名): {stale}")
146
+ _drop_buttons()
147
+ logger.info(f"[改造] 已裁掉 {dropped} 个触发器")
148
+ return dropped
149
+
150
+
151
+ def _patch_render(module) -> None:
152
+ """把截图渲染换成直接产出 HTML, 并给卡片里的角色挂上点击命令。"""
153
+ from rover.html_actions import annotate_html
154
+ from rover.media import HtmlBytes
155
+
156
+ def apply_avatar(context: dict) -> None:
157
+ """深塔/海墟/矩阵/全息/资源/体力等模板的头像字段统一叫 avatar_url。"""
158
+ from rover.avatar import get_avatar_url
159
+
160
+ if AVATAR_CONTEXT_KEY not in context:
161
+ return
162
+ url = get_avatar_url()
163
+ if url:
164
+ context[AVATAR_CONTEXT_KEY] = url
165
+
166
+ def font_css_url() -> str:
167
+ """字体由本服务自己挂载, 不依赖外部部署。"""
168
+ from rover.config import core_config
169
+
170
+ if getattr(module, "_FONTS_DIR", None) is not None and module._FONTS_DIR.exists():
171
+ host = core_config.get_config("HOST") or "127.0.0.1"
172
+ if host in ("0.0.0.0", ""):
173
+ host = "127.0.0.1"
174
+ return f"http://{host}:{core_config.get_config('PORT')}/waves/fonts/fonts.css"
175
+ return module._get_font_css_url()
176
+
177
+ async def render_html(waves_templates, template_name: str, context: dict):
178
+ try:
179
+ template = waves_templates.get_template(template_name)
180
+ context["font_css_url"] = font_css_url()
181
+ try:
182
+ apply_avatar(context)
183
+ except Exception as e:
184
+ logger.warning(f"[渲染] 头像覆盖失败: {e}")
185
+ html = template.render(**context)
186
+ except Exception as e:
187
+ logger.error(f"[渲染] 模板失败 {template_name}: {e}")
188
+ return None
189
+ html = annotate_html(html, template_name, context)
190
+ logger.info(f"[渲染] {template_name} {len(html) / 1024 / 1024:.2f}MB")
191
+ return HtmlBytes(html, template_name)
192
+
193
+ module.render_html = render_html
194
+ module.PLAYWRIGHT_AVAILABLE = True
195
+
196
+
197
+ def _patch_stamina(module) -> None:
198
+ """体力卡多账号时拼接 HTML, 不再走 PIL 画布。"""
199
+ from rover.html_actions import merge_html_cards
200
+ from rover.media import HtmlBytes
201
+
202
+ original = module._assemble_stamina_canvas
203
+
204
+ def assemble(stamina_imgs: list, canvas_w: int, row_h: int):
205
+ html_parts = [i for i in stamina_imgs if isinstance(i, HtmlBytes)]
206
+ if html_parts and len(html_parts) == len(stamina_imgs):
207
+ return merge_html_cards(html_parts)
208
+ return original(stamina_imgs, canvas_w, row_h)
209
+
210
+ module._assemble_stamina_canvas = assemble
211
+
212
+ original_convert = module.convert_img
213
+
214
+ async def convert_img(img, *args, **kwargs):
215
+ if isinstance(img, HtmlBytes):
216
+ return img
217
+ return await original_convert(img, *args, **kwargs)
218
+
219
+ module.convert_img = convert_img
220
+
221
+ # 上游把渲染结果交给 Image.open 包装, HTML 要在这一步放行
222
+ image_module = module.Image
223
+
224
+ class _ImageProxy:
225
+ def __getattr__(self, name):
226
+ return getattr(image_module, name)
227
+
228
+ @staticmethod
229
+ def open(fp, *args, **kwargs):
230
+ # BytesIO 会拷贝字节丢掉类型, 只能按内容判断
231
+ raw = fp.getvalue() if hasattr(fp, "getvalue") else b""
232
+ if raw[:400].lstrip()[:15].lower().startswith((b"<!doctype", b"<html")):
233
+ return HtmlBytes(raw.decode("utf-8", "replace"), "stamina_card.html")
234
+ return image_module.open(fp, *args, **kwargs)
235
+
236
+ module.Image = _ImageProxy()
237
+
238
+
239
+ # 签到侧与查询侧同名的配置, 统一读查询侧的值
240
+ RS_SHARED_KEYS = {"LocalProxyUrl", "KuroUrlProxyUrl", "NeedProxyFunc", "HideUid"}
241
+
242
+
243
+ def _patch_rs_shared(module) -> None:
244
+ config = module.RoverSignConfig
245
+ original = config.get_config
246
+
247
+ def get_config(key: str, *args, **kwargs):
248
+ item = original(key, *args, **kwargs)
249
+ if key not in RS_SHARED_KEYS:
250
+ return item
251
+ from rover.utils.plugins_config.gs_config import all_config_list
252
+
253
+ waves = all_config_list.get("XutheringWavesUID")
254
+ if waves is None:
255
+ return item
256
+ shared = waves.get_config(key)
257
+ if shared is not None:
258
+ item.data = shared.data
259
+ return item
260
+
261
+ config.get_config = get_config
262
+
263
+
264
+ # 插件名 → {模块后缀: 补丁}
265
+ def _patch_skip_ai(module) -> None:
266
+ """本地由 dsh 自己的模型对话, 插件侧的知识库注册跳过。"""
267
+ import sys
268
+ from types import ModuleType
269
+
270
+ name = f"{module.__name__.rsplit('.', 3)[0]}.wutheringwaves_ai_rag"
271
+ if name in sys.modules:
272
+ return
273
+ stub = ModuleType(name)
274
+
275
+ async def reload_ai_rag(*args, **kwargs):
276
+ return None
277
+
278
+ stub.reload_ai_rag = reload_ai_rag
279
+ sys.modules[name] = stub
280
+
281
+
282
+ def _patch_score_token(module) -> None:
283
+ """评分与排行是同一套 token, 让评分侧直接读排行的配置项。"""
284
+ config = module.seconfig
285
+ original = config.get_config
286
+
287
+ def get_config(key: str, *args, **kwargs):
288
+ item = original(key, *args, **kwargs)
289
+ if key != "xwtoken":
290
+ return item
291
+ from rover.utils.plugins_config.gs_config import all_config_list
292
+
293
+ waves = all_config_list.get("XutheringWavesUID")
294
+ if waves is None:
295
+ return item
296
+ shared = waves.get_config("WavesToken")
297
+ if shared is not None and shared.data:
298
+ item.data = shared.data
299
+ return item
300
+
301
+ config.get_config = get_config
302
+
303
+
304
+ def _patch_help_period(module) -> None:
305
+ """帮助的深塔查询分组补一条挑战周期说明。"""
306
+ group = module.plugin_help.get("深塔查询")
307
+ data = group.get("data") if isinstance(group, dict) else None
308
+ if not isinstance(data, list) or any(i.get("name") == "挑战周期" for i in data):
309
+ return
310
+ data.append({
311
+ "name": "挑战周期",
312
+ "desc": "深塔/冥海每 28 天一期、矩阵约 42 天一期(实际以游戏内为准),当天 04:00 换期。"
313
+ "换期后接口只保留新一期数据:查询时挑战记录缺失或变空说明挑战已更新,打完新一期再查。",
314
+ "eg": "深塔",
315
+ "need_ck": False,
316
+ "need_sk": False,
317
+ "need_admin": False,
318
+ })
319
+
320
+
321
+ _PATCHES = {
322
+ "XutheringWavesUID": {
323
+ "utils.render_utils": _patch_render,
324
+ "utils.resource.download_all_resource": _patch_skip_ai,
325
+ "wutheringwaves_stamina.draw_waves_stamina": _patch_stamina,
326
+ "wutheringwaves_help.get_help": _patch_help_period,
327
+ },
328
+ "RoverSign": {
329
+ "roversign_config.roversign_config": _patch_rs_shared,
330
+ },
331
+ "ScoreEcho": {
332
+ "scoreecho_config.config": _patch_score_token,
333
+ },
334
+ }
335
+
336
+
337
+ _cards_patches = None
338
+
339
+
340
+ def _all_patches(plugin_name: str) -> Dict[str, object]:
341
+ """内置补丁 + rover/cards 下自动发现的模板补丁。"""
342
+ global _cards_patches
343
+ if _cards_patches is None:
344
+ from rover import cards
345
+
346
+ _cards_patches = cards.discover()
347
+ table = dict(_PATCHES.get(plugin_name, {}))
348
+ table.update(_cards_patches.get(plugin_name, {}))
349
+ return table
350
+
351
+
352
+ def patch_modules(plugin_name: str, package: str) -> None:
353
+ """在插件功能模块被 import 之前打补丁, 让它们按值绑定到新实现。"""
354
+ for suffix, patch in _all_patches(plugin_name).items():
355
+ name = f"{package}.{suffix}"
356
+ try:
357
+ module = importlib.import_module(name)
358
+ except Exception as e:
359
+ logger.warning(f"[改造] 跳过 {name}: {e}")
360
+ continue
361
+ try:
362
+ patch(module)
363
+ logger.debug(f"[改造] 已改造 {name}")
364
+ except Exception as e:
365
+ logger.error(f"[改造] {name} 失败: {e}")
366
+
367
+
368
+ def align_local_urls() -> None:
369
+ """配置里指向别处部署的地址改成本服务自己的。"""
370
+ from rover.config import core_config
371
+ from rover.utils.plugins_config.gs_config import all_config_list
372
+
373
+ waves = all_config_list.get("XutheringWavesUID")
374
+ if waves is None:
375
+ return
376
+ host = core_config.get_config("HOST") or "127.0.0.1"
377
+ if host in ("0.0.0.0", ""):
378
+ host = "127.0.0.1"
379
+ local = f"http://{host}:{core_config.get_config('PORT')}/waves/fonts/fonts.css"
380
+ item = waves.get_config("FontCssUrl")
381
+ if item is not None and item.data != local:
382
+ waves.set_config("FontCssUrl", local)
383
+ logger.info(f"[改造] 字体地址改为本服务 {local}")
384
+
385
+
386
+ def seed_defaults(configs: Dict[str, Dict[str, object]]) -> None:
387
+ """初值只写一次。写过就记在案, 之后用户在设置页改成什么就是什么。"""
388
+ from rover.config import core_config
389
+ from rover.utils.plugins_config.gs_config import all_config_list
390
+
391
+ seeded = list(core_config.get_config("seeded") or [])
392
+ for config_name, values in configs.items():
393
+ if config_name in seeded:
394
+ continue
395
+ config = all_config_list.get(config_name)
396
+ if config is None:
397
+ continue
398
+ for key, value in values.items():
399
+ item = config.get_config(key)
400
+ if item is not None and item.data != value:
401
+ config.set_config(key, value)
402
+ seeded.append(config_name)
403
+ logger.info(f"[改造] {config_name} 首次装载, 已写入初值")
404
+ if seeded != (core_config.get_config("seeded") or []):
405
+ core_config.set_config("seeded", seeded)
406
+
407
+
408
+ def apply_defaults(configs: Dict[str, Dict[str, object]]) -> None:
409
+ """运行时前提项, 每次启动都校正; 这些键不进设置页, 不会和用户的选择打架。"""
410
+ from rover.utils.plugins_config.gs_config import all_config_list
411
+
412
+ for config_name, values in configs.items():
413
+ config = all_config_list.get(config_name)
414
+ if config is None:
415
+ logger.warning(f"[改造] 未找到配置 {config_name}")
416
+ continue
417
+ for key, value in values.items():
418
+ item = config.get_config(key)
419
+ if item is not None and item.data != value:
420
+ config.set_config(key, value)
@@ -0,0 +1,236 @@
1
+ """给渲染好的 HTML 挂上点击动作: 点角色出面板。按渲染上下文的顺序与文档顺序一一对应。"""
2
+
3
+ import re
4
+ from typing import Any, Callable, Dict, Iterable, List, Optional
5
+
6
+ from rover.logger import logger
7
+
8
+
9
+
10
+ # 角色格子的开标签, 与各模板中的写法一致
11
+ _ROLE_TAG = re.compile(r'<div class="role-mini(?![-\w])(?P<rest>[^>]*)>')
12
+ # 模板用别的格子类名时在这里挂对应模式
13
+ _ROLE_TAGS: dict = {
14
+ "roleinfo/role_card.html": re.compile(r'<div class="role-card(?![-\w])(?P<rest>[^>]*)>'),
15
+ }
16
+ _BODY_END = re.compile(r"</body\s*>", re.I)
17
+
18
+ _BRIDGE = """
19
+ <style>
20
+ [data-ww-cmd]{cursor:pointer;position:relative}
21
+ [data-ww-cmd]:hover{outline:2px solid rgba(255,214,102,.95);outline-offset:2px}
22
+ html,body{margin:0;padding:0;overflow:hidden;background:transparent}
23
+ html{width:100%}
24
+ </style>
25
+ <script>
26
+ (function(){
27
+ var availW = 0;
28
+ var settled = false;
29
+ // 设计宽度只量一次: 模板多是 body 定宽 + 内层 width:100%, 一旦把 body 改窄,
30
+ // 内层跟着缩, 里面的定宽行就溢出被裁。量完就把 body 钉死在设计宽度上。
31
+ var designW = 0;
32
+ function root(){ return document.querySelector('.container') || document.body }
33
+ function post(msg){ try{ parent.postMessage(Object.assign({source:'ww-card'},msg),'*') }catch(e){} }
34
+ // scrollWidth/scrollHeight 是布局值, 不受 transform 影响
35
+ function apply(){
36
+ var el = root();
37
+ if(!designW){
38
+ designW = Math.max(el.scrollWidth, document.body.scrollWidth, 1);
39
+ document.body.style.width = designW + 'px';
40
+ }
41
+ var h = Math.max(el.scrollHeight, document.body.scrollHeight, 1);
42
+ var scale = availW ? Math.min(1, availW / designW) : 1;
43
+ // 缩放整个 body: 卡片外层的背景层之类不在 .container 里的元素也一起等比缩
44
+ document.body.style.transformOrigin = 'top left';
45
+ document.body.style.transform = scale < 1 ? 'scale(' + scale + ')' : '';
46
+ var w = Math.ceil(designW * scale), sh = Math.ceil(h * scale);
47
+ document.documentElement.style.width = w + 'px';
48
+ document.documentElement.style.height = sh + 'px';
49
+ if(settled) post({kind:'size', width: w, height: sh});
50
+ }
51
+ window.addEventListener('message', function(e){
52
+ var d = e.data;
53
+ if(d && d.source === 'ww-host' && d.kind === 'width'){ availW = d.width || 0; apply(); }
54
+ });
55
+ document.addEventListener('click', function(e){
56
+ var t = e.target && e.target.closest ? e.target.closest('[data-ww-cmd]') : null;
57
+ if(!t) return;
58
+ e.preventDefault();
59
+ post({kind:'command', command:t.getAttribute('data-ww-cmd')});
60
+ });
61
+ // 字体和图片没落定就报尺寸, 宿主会先按半成品高度撑开再跳一次;
62
+ // 所以量归量, 落定之后才往外报, 卡片一次到位。
63
+ function settle(){ settled = true; apply(); }
64
+ function whenLoaded(fn){
65
+ if(document.readyState === 'complete') fn();
66
+ else window.addEventListener('load', fn);
67
+ }
68
+ function ready(){ post({kind:'ready'}); apply(); }
69
+ document.addEventListener('DOMContentLoaded', ready);
70
+ whenLoaded(function(){
71
+ var fonts = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
72
+ fonts.then(settle, settle);
73
+ });
74
+ if(window.ResizeObserver){ new ResizeObserver(apply).observe(root()); }
75
+ setTimeout(ready, 300);
76
+ // 字体加载卡住也不能一直不出, 到点就认
77
+ setTimeout(settle, 2500);
78
+ })();
79
+ </script>
80
+ """
81
+
82
+
83
+ def _walk(node: Any, path: Iterable[str]) -> List[dict]:
84
+ """按 key 路径逐层展开嵌套列表, 结果保持模板遍历顺序。"""
85
+ keys = list(path)
86
+ current: List[Any] = [node]
87
+ for key in keys:
88
+ nxt: List[Any] = []
89
+ for item in current:
90
+ if isinstance(item, dict):
91
+ value = item.get(key)
92
+ if isinstance(value, list):
93
+ nxt.extend(value)
94
+ current = nxt
95
+ return [i for i in current if isinstance(i, dict)]
96
+
97
+
98
+ def _roles_from(context: dict, *paths: Iterable[str]) -> List[dict]:
99
+ out: List[dict] = []
100
+ for path in paths:
101
+ out.extend(_walk(context, path))
102
+ return out
103
+
104
+
105
+ # 矩阵模板的上下文里只有图标没有角色 id, 由 cards/matrix_roles 在构图时记下来补上
106
+ _RECORDED_ROLE_IDS: List[str] = []
107
+
108
+
109
+ def record_role_id(role_id: Any) -> None:
110
+ _RECORDED_ROLE_IDS.append(str(role_id))
111
+
112
+
113
+ def _matrix_roles(context: dict) -> List[dict]:
114
+ roles = _roles_from(context, ("modes", "teams", "roles"))
115
+ recorded = list(_RECORDED_ROLE_IDS)
116
+ _RECORDED_ROLE_IDS.clear()
117
+ real = [r for r in roles if not r.get("is_placeholder")]
118
+ # 条数对不上说明记录与实际渲染错位了, 宁可不挂也不能挂错人
119
+ if len(recorded) == len(real):
120
+ for role, role_id in zip(real, recorded):
121
+ role.setdefault("id", role_id)
122
+ elif recorded:
123
+ logger.debug(f"[鸣潮·交互] 矩阵角色 id 记录 {len(recorded)} 个, 实到 {len(real)} 个, 跳过")
124
+ return roles
125
+
126
+
127
+ _EXTRACTORS: Dict[str, Callable[[dict], List[dict]]] = {
128
+ "abyss/abyss_card.html": lambda c: _roles_from(c, ("towers", "floors", "roles")),
129
+ "abyss/slash_card.html": lambda c: _roles_from(c, ("challenges", "half_list", "roles")),
130
+ "abyss/challenge_card.html": lambda c: _roles_from(c, ("challenges", "roles")),
131
+ "abyss/matrix_detail_card.html": _matrix_roles,
132
+ "roleinfo/role_card.html": lambda c: [r for r in c.get("role_list") or [] if isinstance(r, dict)],
133
+ }
134
+
135
+
136
+ def _char_name(role_id: str) -> Optional[str]:
137
+ """角色名转换在插件侧, 延迟取用避免本模块依赖插件。"""
138
+ import importlib
139
+ import sys
140
+
141
+ suffix = "XutheringWavesUID.utils.name_convert"
142
+ for name, module in list(sys.modules.items()):
143
+ if name.endswith(suffix) and module is not None:
144
+ return module.char_id_to_char_name(str(role_id))
145
+ for package in ("plugins.XutheringWavesUID.XutheringWavesUID", "waves"):
146
+ try:
147
+ convert = importlib.import_module(f"{package}.utils.name_convert")
148
+ except ModuleNotFoundError:
149
+ continue
150
+ return convert.char_id_to_char_name(str(role_id))
151
+ return None
152
+
153
+
154
+ def _prefix() -> str:
155
+ from rover.sv import all_prefixes
156
+
157
+ items = all_prefixes()
158
+ return items[0] if items else ""
159
+
160
+
161
+ def _role_name(role: dict) -> Optional[str]:
162
+ role_id = role.get("id") or role.get("role_id") or role.get("roleId")
163
+ name = role.get("name") or role.get("role_name")
164
+ if not name and role_id:
165
+ name = _char_name(role_id)
166
+ return name or None
167
+
168
+
169
+ def _role_command(role: dict) -> Optional[str]:
170
+ name = _role_name(role)
171
+ return f"{_prefix()}{name}面板" if name else None
172
+
173
+
174
+ _CONTAINER = re.compile(
175
+ r'<div[^>]*class="[^"]*\bcontainer\b[^"]*"[^>]*>.*</div>', re.S | re.I
176
+ )
177
+
178
+
179
+ def merge_html_cards(parts: List[Any]) -> Any:
180
+ """多张同模板卡片竖排成一页。"""
181
+ from rover.media import HtmlBytes
182
+
183
+ if not parts:
184
+ return None
185
+ if len(parts) == 1:
186
+ return parts[0]
187
+ base = parts[0].html
188
+ extra: List[str] = []
189
+ for part in parts[1:]:
190
+ found = _CONTAINER.search(part.html)
191
+ if found:
192
+ extra.append(found.group(0))
193
+ if not extra:
194
+ return parts[0]
195
+ block = "\n".join(extra)
196
+ if "</body" in base:
197
+ merged = base.replace("</body", block + "\n</body", 1)
198
+ else:
199
+ merged = base + block
200
+ return HtmlBytes(merged, getattr(parts[0], "template_name", ""))
201
+
202
+
203
+ def annotate_html(html: str, template_name: str, context: dict) -> str:
204
+ """在角色格子上写入命令, 并注入点击/尺寸桥接脚本。"""
205
+ try:
206
+ extractor = _EXTRACTORS.get(template_name)
207
+ if extractor:
208
+ roles = extractor(context)
209
+ names = [_role_name(r) for r in roles]
210
+ commands = [_role_command(r) for r in roles]
211
+ if commands:
212
+ counter = {"i": 0}
213
+
214
+ def repl(match: re.Match) -> str:
215
+ idx = counter["i"]
216
+ counter["i"] += 1
217
+ command = commands[idx] if idx < len(commands) else None
218
+ tag = match.group(0)
219
+ if not command:
220
+ return tag
221
+ # 角色在卡面上只有头像, 名字另存一份: 给模型的文字摘要要认得出是谁
222
+ name = names[idx] if idx < len(names) else ""
223
+ extra = f' data-ww-name="{name}"' if name else ""
224
+ return f'{tag[:-1]} data-ww-cmd="{command}"{extra}>'
225
+
226
+ html = _ROLE_TAGS.get(template_name, _ROLE_TAG).sub(repl, html)
227
+ if counter["i"] != len(commands):
228
+ logger.debug(
229
+ f"[鸣潮·交互] {template_name} 角色格 {counter['i']} 个, 上下文 {len(commands)} 个"
230
+ )
231
+ if _BODY_END.search(html):
232
+ return _BODY_END.sub(_BRIDGE + "</body>", html, count=1)
233
+ return html + _BRIDGE
234
+ except Exception as e:
235
+ logger.warning(f"[鸣潮·交互] 注入失败 {template_name}: {e}")
236
+ return html
@@ -0,0 +1,19 @@
1
+ """文案取用。本项目不做多语言 UI, 词条表为空, t() 直接回落 key 本身。"""
2
+
3
+ from typing import Optional
4
+
5
+ DEFAULT_LANG: str = "zh-cn"
6
+
7
+
8
+ def t(key: str, /, lang: Optional[str] = None, **params: object) -> str:
9
+ """按 key 取词条; 无词条时回落 key 本身并填充具名占位符。"""
10
+ if not params:
11
+ return key
12
+ try:
13
+ return key.format(**params)
14
+ except (KeyError, IndexError, ValueError):
15
+ return key
16
+
17
+
18
+ def get_lang() -> str:
19
+ return DEFAULT_LANG