@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,26 @@
1
+ """后台管理注册桩。吃下插件的声明, 不提供 Web 界面。"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Type
5
+
6
+
7
+ @dataclass
8
+ class PageSchema:
9
+ label: str
10
+ icon: str = ""
11
+
12
+
13
+ class AdminModel:
14
+ model: Any = None
15
+ pk_name: str = "id"
16
+ page_schema: Any = None
17
+
18
+
19
+ class Site:
20
+ def register_admin(self, *admin_cls: Type[AdminModel], _ADD: bool = False) -> Type[AdminModel]:
21
+ return admin_cls[0]
22
+
23
+
24
+ site = Site()
25
+
26
+ __all__ = ["PageSchema", "AdminModel", "Site", "site"]
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env python3
2
+ """从既有部署迁入配置、凭据与素材。默认只做缺失项, --force 覆盖。"""
3
+
4
+ import argparse
5
+ import json
6
+ import shutil
7
+ import sqlite3
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ DST_DATA = Path(__file__).resolve().parents[1] / "data"
13
+
14
+ # 配置模型类名去掉了 Gs 前缀, 存量 json 的 type 标签要跟着换
15
+ TYPE_MAP = {
16
+ f"Gs{n}Config": f"{n}Config"
17
+ for n in ("Str", "Bool", "Dict", "ListStr", "List", "Int", "Float", "Image",
18
+ "TimeR", "FileUpload", "FilesUpload", "Date", "TimeRange", "Color",
19
+ "RepeatGroup", "Time")
20
+ }
21
+ TYPE_MAP["GsDivider"] = "Divider"
22
+
23
+ PLUGINS = ["XutheringWavesUID", "RoverSign", "ScoreEcho"]
24
+
25
+ SYMLINK_DIRS = ["guide_new", "custom_role_pile", "custom_mr_bg", "custom_orb", "custom_mr_role_pile"]
26
+ COPY_DIRS = ["alias", "show", "resource"]
27
+ COPY_FILES = ["ann_data.json", "guide_config.json", "gacha_config.json"]
28
+
29
+ # 订阅表不迁: 那是群聊场景的推送去向, 本地只有一个使用者, 迁过来只会留下外部群号
30
+ DB_TABLES = [
31
+ "wavesbind", "wavesuser", "WavesUserSdk", "WavesGachaCloud",
32
+ "WavesLangSettings", "WavesStaminaRecord", "RoverSign", "ScoreEcho",
33
+ "ScoreLangSettings",
34
+ ]
35
+
36
+
37
+ def _rewrite_paths(item: dict, src_root: Path, dst_root: Path) -> int:
38
+ """配置里存的是绝对路径, 迁过来要落到本地数据目录的同名位置。"""
39
+ changed = 0
40
+ prefix = str(src_root)
41
+ for field in ("data", "upload_to"):
42
+ value = item.get(field)
43
+ if not isinstance(value, str) or not value:
44
+ continue
45
+ if value.startswith(prefix):
46
+ item[field] = str(dst_root) + value[len(prefix) :]
47
+ elif "/XutheringWavesUID/" in value.replace("\\", "/"):
48
+ # 来源部署的目录结构不同时, 按插件目录之后的相对部分归位
49
+ tail = value.replace("\\", "/").split("/XutheringWavesUID/", 1)[1]
50
+ item[field] = str(dst_root / "XutheringWavesUID" / tail)
51
+ else:
52
+ continue
53
+ changed += 1
54
+ return changed
55
+
56
+
57
+ def convert_config(src: Path, dst: Path, force: bool, roots: Optional[tuple] = None) -> None:
58
+ if not src.is_file():
59
+ print(f"跳过 (源缺失): {src}")
60
+ return
61
+ if dst.exists() and not force:
62
+ print(f"跳过 (已存在): {dst}")
63
+ return
64
+ data = json.loads(src.read_text("utf-8"))
65
+ paths = 0
66
+ for item in data.values():
67
+ if not isinstance(item, dict):
68
+ continue
69
+ if item.get("type") in TYPE_MAP:
70
+ item["type"] = TYPE_MAP[item["type"]]
71
+ if roots:
72
+ paths += _rewrite_paths(item, *roots)
73
+ if paths:
74
+ print(f"路径改写: {paths} 处指向本地数据目录")
75
+ dst.parent.mkdir(parents=True, exist_ok=True)
76
+ dst.write_text(json.dumps(data, ensure_ascii=False, indent=2), "utf-8")
77
+ secrets = [k for k in ("WavesToken", "LocalProxyUrl", "xwtoken") if k in data]
78
+ print(f"配置: {dst.name} 项数={len(data)} 含凭据={secrets}")
79
+
80
+
81
+ def _prune_users(conn: sqlite3.Connection, masters: list) -> int:
82
+ """只留主人自己的行, 其余用户数据不进本地库。"""
83
+ if not masters:
84
+ return 0
85
+ placeholders = ",".join("?" for _ in masters)
86
+ removed = 0
87
+ for table in ("wavesbind", "wavesuser", "WavesUserSdk", "WavesGachaCloud",
88
+ "WavesLangSettings", "WavesStaminaRecord", "ScoreEcho",
89
+ "ScoreLangSettings"):
90
+ try:
91
+ cur = conn.execute(
92
+ f'DELETE FROM "{table}" WHERE user_id IS NOT NULL '
93
+ f"AND user_id NOT IN ({placeholders})",
94
+ masters,
95
+ )
96
+ removed += cur.rowcount or 0
97
+ except sqlite3.OperationalError:
98
+ continue
99
+ conn.commit()
100
+ return removed
101
+
102
+
103
+ def find_source_db(src: Path) -> Optional[Path]:
104
+ """来源库文件名随部署而异, 按是否含绑定表识别。"""
105
+ for path in sorted(src.glob("*.db")):
106
+ try:
107
+ conn = sqlite3.connect(f"file:{path}?mode=ro&immutable=1", uri=True)
108
+ except sqlite3.Error:
109
+ continue
110
+ try:
111
+ names = {
112
+ str(row[0]).lower()
113
+ for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
114
+ }
115
+ except sqlite3.Error:
116
+ names = set()
117
+ finally:
118
+ conn.close()
119
+ if "wavesbind" in names:
120
+ return path
121
+ return None
122
+
123
+
124
+ def snapshot_db(src: Optional[Path], dst: Path, force: bool, masters: list) -> None:
125
+ if src is None or not src.is_file():
126
+ print(f"跳过 (源库缺失): {src}")
127
+ return
128
+ if dst.exists() and not force:
129
+ print(f"跳过 (已存在): {dst}")
130
+ return
131
+ tmp = dst.with_suffix(".snapshot")
132
+ for path in (tmp, Path(str(tmp) + "-wal"), Path(str(tmp) + "-shm")):
133
+ path.unlink(missing_ok=True)
134
+ # 只读 + 快照, 不与在写进程共享连接
135
+ source = sqlite3.connect(f"file:{src}?mode=ro&immutable=1", uri=True)
136
+ target = sqlite3.connect(tmp)
137
+ try:
138
+ source.backup(target)
139
+ finally:
140
+ target.close()
141
+ source.close()
142
+
143
+ keep = sqlite3.connect(tmp)
144
+ try:
145
+ existing = {r[0] for r in keep.execute("SELECT name FROM sqlite_master WHERE type='table'")}
146
+ dropped = 0
147
+ for table in existing:
148
+ if table not in DB_TABLES and not table.startswith("sqlite_"):
149
+ keep.execute(f'DROP TABLE IF EXISTS "{table}"')
150
+ dropped += 1
151
+ keep.commit()
152
+ pruned = _prune_users(keep, masters)
153
+ rows = {t: keep.execute(f'SELECT COUNT(*) FROM "{t}"').fetchone()[0]
154
+ for t in DB_TABLES if t in existing}
155
+ keep.execute("VACUUM")
156
+ keep.commit()
157
+ print(f"数据库: 丢弃他人行={pruned}")
158
+ finally:
159
+ keep.close()
160
+ tmp.replace(dst)
161
+ print(f"数据库: {dst.name} 保留表={rows} 丢弃无关表={dropped}")
162
+
163
+
164
+ def rewrite_bot_id(db: Path, bot_id: str = "dsh") -> None:
165
+ """来源库按原平台标识分区, 本地只有一个会话来源, 统一改写。"""
166
+ if not db.is_file():
167
+ return
168
+ conn = sqlite3.connect(db)
169
+ try:
170
+ changed = 0
171
+ for table in DB_TABLES:
172
+ for column in ("bot_id", "bot_self_id", "WS_BOT_ID"):
173
+ try:
174
+ cur = conn.execute(
175
+ f'UPDATE "{table}" SET "{column}"=? WHERE "{column}" IS NOT NULL '
176
+ f'AND "{column}"!=?',
177
+ (bot_id, bot_id),
178
+ )
179
+ changed += cur.rowcount or 0
180
+ except sqlite3.OperationalError:
181
+ continue
182
+ conn.commit()
183
+ print(f"会话来源: {changed} 处改写为 {bot_id}")
184
+ finally:
185
+ conn.close()
186
+
187
+
188
+ def rewrite_user_id(db: Path, user_id: str) -> None:
189
+ """来源库按聊天平台的用户 id 分区, 本地只有一个使用者, 统一归到它名下。"""
190
+ if not db.is_file() or not user_id:
191
+ return
192
+ conn = sqlite3.connect(db)
193
+ try:
194
+ changed = 0
195
+ for table in DB_TABLES:
196
+ try:
197
+ cur = conn.execute(
198
+ f'UPDATE "{table}" SET user_id=? WHERE user_id IS NOT NULL AND user_id!=?',
199
+ (user_id, user_id),
200
+ )
201
+ changed += cur.rowcount or 0
202
+ except sqlite3.OperationalError:
203
+ continue
204
+ conn.commit()
205
+ print(f"归属: {changed} 处改写为 {user_id}")
206
+ finally:
207
+ conn.close()
208
+
209
+
210
+ def bind_group(db: Path, group: str = "dsh") -> None:
211
+ """把绑定记录挂到本地会话所属的组, 群维度的排行才有数据。"""
212
+ if not db.is_file():
213
+ return
214
+ conn = sqlite3.connect(db)
215
+ try:
216
+ rows = conn.execute("SELECT id, group_id FROM wavesbind").fetchall()
217
+ changed = 0
218
+ for row_id, group_id in rows:
219
+ parts = [g for g in (group_id or "").split("_") if g]
220
+ if group not in parts:
221
+ parts.append(group)
222
+ conn.execute("UPDATE wavesbind SET group_id=? WHERE id=?", ("_".join(parts), row_id))
223
+ changed += 1
224
+ conn.commit()
225
+ print(f"绑定: {changed}/{len(rows)} 行加入 {group}")
226
+ finally:
227
+ conn.close()
228
+
229
+
230
+ def master_uids(db: Path) -> list:
231
+ if not db.is_file():
232
+ return []
233
+ conn = sqlite3.connect(db)
234
+ try:
235
+ uids = set()
236
+ for (cell,) in conn.execute("SELECT uid FROM wavesbind WHERE uid IS NOT NULL"):
237
+ uids.update(u for u in str(cell).split("_") if u)
238
+ for (cell,) in conn.execute("SELECT uid FROM wavesuser WHERE uid IS NOT NULL"):
239
+ if cell:
240
+ uids.add(str(cell))
241
+ return sorted(uids)
242
+ finally:
243
+ conn.close()
244
+
245
+
246
+ def copy_players(src: Path, dst: Path, uids: list, force: bool) -> None:
247
+ if not src.is_dir() or not uids:
248
+ print("跳过 players (无绑定 uid)")
249
+ return
250
+ dst.mkdir(parents=True, exist_ok=True)
251
+ copied = 0
252
+ for uid in uids:
253
+ one_src, one_dst = src / uid, dst / uid
254
+ if not one_src.is_dir():
255
+ continue
256
+ if one_dst.exists():
257
+ if not force:
258
+ continue
259
+ shutil.rmtree(one_dst)
260
+ shutil.copytree(one_src, one_dst, symlinks=True)
261
+ copied += 1
262
+ print(f"玩家数据: {copied}/{len(uids)} 个 uid")
263
+
264
+
265
+ def link_or_copy(src: Path, dst: Path, mode: str, force: bool) -> None:
266
+ if not src.exists():
267
+ print(f"跳过 (源缺失): {src.name}")
268
+ return
269
+ if dst.exists() or dst.is_symlink():
270
+ if not force:
271
+ print(f"跳过 (已存在): {dst.name}")
272
+ return
273
+ if dst.is_symlink() or dst.is_file():
274
+ dst.unlink()
275
+ else:
276
+ shutil.rmtree(dst)
277
+ dst.parent.mkdir(parents=True, exist_ok=True)
278
+ if mode == "symlink":
279
+ try:
280
+ dst.symlink_to(src)
281
+ print(f"链接: {dst.name} -> {src}")
282
+ return
283
+ except OSError:
284
+ # Windows 未开开发者模式时创建符号链接会失败, 退回复制
285
+ print(f"链接失败, 改为复制: {dst.name}")
286
+ if src.is_dir():
287
+ shutil.copytree(src, dst, symlinks=True)
288
+ print(f"复制: {dst.name}/")
289
+ else:
290
+ shutil.copy2(src, dst)
291
+ print(f"复制: {dst.name}")
292
+
293
+
294
+ def main() -> None:
295
+ parser = argparse.ArgumentParser()
296
+ parser.add_argument("--src", type=Path, help="来源部署的 data 目录, 不给则跳过迁入")
297
+ parser.add_argument("--src-db", type=Path, help="来源数据库, 默认在 --src 下自动识别")
298
+ parser.add_argument("--force", action="store_true")
299
+ parser.add_argument("--skip-players", action="store_true")
300
+ parser.add_argument("--masters", nargs="*", help="要保留的用户 id, 默认取 data/config.json")
301
+ args = parser.parse_args()
302
+ src = args.src
303
+ if src is None:
304
+ print("未指定 --src, 跳过迁入 (全新部署无需迁移)")
305
+ return
306
+ if not src.is_dir():
307
+ sys.exit(f"源数据目录不存在: {src}")
308
+
309
+ DST_DATA.mkdir(parents=True, exist_ok=True)
310
+
311
+ for plugin in PLUGINS:
312
+ convert_config(
313
+ src / plugin / "config.json",
314
+ DST_DATA / plugin / "config.json",
315
+ args.force,
316
+ (src.resolve(), DST_DATA.resolve()),
317
+ )
318
+
319
+ masters = args.masters or json.loads(
320
+ (DST_DATA / "config.json").read_text("utf-8")
321
+ ).get("masters", [])
322
+ snapshot_db(
323
+ args.src_db or find_source_db(src),
324
+ DST_DATA / "WavesData.db",
325
+ args.force,
326
+ masters,
327
+ )
328
+ rewrite_bot_id(DST_DATA / "WavesData.db")
329
+ rewrite_user_id(DST_DATA / "WavesData.db", masters[0] if masters else "local")
330
+ bind_group(DST_DATA / "WavesData.db")
331
+
332
+ ww_src, ww_dst = src / "XutheringWavesUID", DST_DATA / "XutheringWavesUID"
333
+ for name in COPY_DIRS:
334
+ link_or_copy(ww_src / name, ww_dst / name, "copy", args.force)
335
+ for name in SYMLINK_DIRS:
336
+ link_or_copy(ww_src / name, ww_dst / name, "symlink", args.force)
337
+ for name in COPY_FILES:
338
+ link_or_copy(ww_src / name, ww_dst / name, "copy", args.force)
339
+ if not args.skip_players:
340
+ copy_players(
341
+ ww_src / "players",
342
+ ww_dst / "players",
343
+ master_uids(DST_DATA / "WavesData.db"),
344
+ args.force,
345
+ )
346
+
347
+ print("\n完成。若首次运行, 建议接着执行: .venv/bin/python tools/smoke.py ww查询")
348
+
349
+
350
+ if __name__ == "__main__":
351
+ main()
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """跑一批命令, 汇总命中与回复形态。默认打到本机服务。
3
+
4
+ 只跑只读命令: 删除/清理/压缩/上传类会改动素材与数据库, 一律拦下。
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ import sys
10
+ import urllib.error
11
+ import urllib.request
12
+
13
+ COMMANDS = [
14
+ "ww帮助", "ww查询", "ww体力", "ww探索", "ww库洛币", "ww积分", "ww日历", "ww签到日历",
15
+ "ww深塔", "ww海墟", "ww矩阵", "ww全息",
16
+ "ww练度", "ww声骸", "ww星声",
17
+ "ww椿面板", "ww椿伤害", "ww椿权重", "ww椿优化", "ww椿总排行", "ww椿声骸总排行",
18
+ "ww椿攻略", "ww椿图鉴", "ww椿养成", "ww武器列表", "ww套装列表",
19
+ "ww练度总排行", "ww无尽总排行", "ww矩阵总排行",
20
+ "ww抽卡记录", "ww抽卡帮助", "ww兑换码", "ww卡池倒计时",
21
+ "ww持有率", "ww深塔出场率", "ww矩阵配队",
22
+ "ww签到", "ww签到帮助", "ww分析帮助", "ww综合评分说明",
23
+ "ww查看特征码", "ww更新记录", "ww别名列表", "ww公告",
24
+ ]
25
+
26
+
27
+ # 会改动素材/数据库的命令, 不允许出现在自检里
28
+ DESTRUCTIVE = ("删除", "清理", "清除", "压缩", "上传", "重新计算", "强制下载", "导入")
29
+
30
+
31
+ def guard(commands: list) -> list:
32
+ blocked = [c for c in commands if any(word in c for word in DESTRUCTIVE)]
33
+ if blocked:
34
+ sys.exit("这些命令有副作用, 不能用于自检: " + " ".join(blocked))
35
+ return commands
36
+
37
+
38
+ def run(base: str, command: str, timeout: int) -> dict:
39
+ body = json.dumps({"command": command}).encode()
40
+ req = urllib.request.Request(
41
+ f"{base}/api/chat", data=body, headers={"content-type": "application/json"}
42
+ )
43
+ try:
44
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
45
+ return json.loads(resp.read())
46
+ except urllib.error.HTTPError as e:
47
+ return {"ok": False, "summary": f"HTTP {e.code}", "segments": [], "matched": []}
48
+ except Exception as e:
49
+ return {"ok": False, "summary": f"{type(e).__name__}: {e}", "segments": [], "matched": []}
50
+
51
+
52
+ def main() -> None:
53
+ parser = argparse.ArgumentParser()
54
+ parser.add_argument("--base", default="http://127.0.0.1:9777")
55
+ parser.add_argument("--timeout", type=int, default=180)
56
+ parser.add_argument("commands", nargs="*")
57
+ args = parser.parse_args()
58
+
59
+ commands = guard(args.commands or COMMANDS)
60
+ stats = {"html": 0, "image": 0, "text": 0, "node": 0, "other": 0}
61
+ unmatched, failed = [], []
62
+
63
+ for command in commands:
64
+ data = run(args.base, command, args.timeout)
65
+ segs = data.get("segments") or []
66
+ kinds = [s.get("type") for s in segs]
67
+ for kind in kinds:
68
+ stats[kind if kind in stats else "other"] += 1
69
+ summary = (data.get("summary") or "").replace("\n", " ")[:70]
70
+ flag = "√" if data.get("ok") else "×"
71
+ if not data.get("ok"):
72
+ unmatched.append(command)
73
+ elif not kinds:
74
+ failed.append(command)
75
+ print(f"{flag} {command:<16} {','.join(kinds) or '-':<22} {summary}")
76
+
77
+ print(f"\n段统计: {stats}")
78
+ if unmatched:
79
+ print(f"未命中/失败 ({len(unmatched)}): {' '.join(unmatched)}")
80
+ if failed:
81
+ print(f"命中但无回复 ({len(failed)}): {' '.join(failed)}")
82
+ print(f"总计 {len(commands)} 条, 正常 {len(commands) - len(unmatched) - len(failed)} 条")
83
+ sys.exit(1 if unmatched or failed else 0)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ main()
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """不起 HTTP 服务, 直接跑一遍启动流程与若干命令。"""
3
+
4
+ import argparse
5
+ import asyncio
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
+
11
+
12
+ def local_user() -> str:
13
+ """本机使用者标识, 从配置里取, 不写死。"""
14
+ from rover.config import core_config
15
+
16
+ masters = core_config.get_config("masters") or []
17
+ return str(masters[0]) if masters else "local"
18
+
19
+
20
+ async def boot():
21
+ from rover.app_life import load_plugins
22
+ from rover.server import core_start_before_execute
23
+ from rover.utils.database.base_models import init_database
24
+ from rover.utils.database.startup import init_all
25
+
26
+ await init_database()
27
+ load_plugins()
28
+ await init_all()
29
+ await core_start_before_execute()
30
+
31
+
32
+ async def run_commands(commands):
33
+ from rover.api_render import render_segments, summarize
34
+ from rover.handler import build_message, dispatch, normalize_command
35
+ from rover.sv import SL
36
+
37
+ print(f"\n注册: 插件={list(SL.plugins.keys())} SV={len(SL.lst)} "
38
+ f"触发器={sum(len(t) for sv in SL.lst.values() for t in sv.TL.values())}")
39
+
40
+ for raw in commands:
41
+ command = normalize_command(raw)
42
+ result = await dispatch(build_message(command, user_id=local_user()))
43
+ rendered = await render_segments(result["segments"])
44
+ kinds = [s["type"] for s in rendered]
45
+ print(f"\n[{command}] 命中={result['matched']} 段={kinds} 用时={result['elapsed']:.2f}s")
46
+ text = summarize(rendered)
47
+ if text:
48
+ print(" " + text.replace("\n", "\n ")[:600])
49
+
50
+
51
+ async def main():
52
+ parser = argparse.ArgumentParser()
53
+ parser.add_argument("commands", nargs="*", default=[])
54
+ args = parser.parse_args()
55
+ await boot()
56
+ if args.commands:
57
+ await run_commands(args.commands)
58
+ else:
59
+ await run_commands([])
60
+
61
+
62
+ if __name__ == "__main__":
63
+ asyncio.run(main())