@sophiacyl/dsh-plugin-waves 0.0.0-stage → 0.1.0-beta.2
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.
- package/DSH-GO-VERIFICATION.md +57 -0
- package/ICON.png +0 -0
- package/LICENSE +674 -0
- package/README.md +251 -2
- package/cordis.patch.yml +8 -0
- package/install.ps1 +0 -0
- package/install.sh +209 -0
- package/lib/client.js +1511 -0
- package/lib/host/aliashint.js +70 -0
- package/lib/host/attachments.js +25 -0
- package/lib/host/command.js +34 -0
- package/lib/host/consts.js +16 -0
- package/lib/host/dataguide.js +35 -0
- package/lib/host/imagebridge.js +41 -0
- package/lib/host/native-result.js +91 -0
- package/lib/host/native.js +191 -0
- package/lib/host/proxy.js +59 -0
- package/lib/host/results.js +67 -0
- package/lib/host/service.js +263 -0
- package/lib/host/tool.js +185 -0
- package/lib/host/troubleshoot.js +31 -0
- package/lib/host/worldbook.js +80 -0
- package/lib/index.js +74 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +136 -4
- package/service/ICON.png +0 -0
- package/service/ai/AI_DATA.md +221 -0
- package/service/knowledge/additional/manual.jsonl +5 -0
- package/service/knowledge/wiki/baike.jsonl +1231 -0
- package/service/knowledge/wiki/chengjiu.jsonl +36 -0
- package/service/knowledge/wiki/gonglve.jsonl +383 -0
- package/service/knowledge/wiki/huodong.jsonl +427 -0
- package/service/knowledge/wiki/renwu.jsonl +412 -0
- package/service/knowledge/wiki/tujian.jsonl +2889 -0
- package/service/knowledge/wiki/zixun.jsonl +47 -0
- package/service/plugins/__init__.py +0 -0
- package/service/pyproject.toml +40 -0
- package/service/rover/__init__.py +0 -0
- package/service/rover/ai_core/__init__.py +0 -0
- package/service/rover/ai_core/models.py +50 -0
- package/service/rover/ai_core/rag/__init__.py +5 -0
- package/service/rover/ai_core/rag/knowledge.py +23 -0
- package/service/rover/ai_core/register.py +86 -0
- package/service/rover/ai_core/resource.py +6 -0
- package/service/rover/ai_core/skills/__init__.py +0 -0
- package/service/rover/ai_core/skills/operations.py +8 -0
- package/service/rover/api.py +720 -0
- package/service/rover/api_render.py +161 -0
- package/service/rover/app_life.py +154 -0
- package/service/rover/aps.py +47 -0
- package/service/rover/avatar.py +85 -0
- package/service/rover/bot.py +133 -0
- package/service/rover/builtin.py +152 -0
- package/service/rover/cards/__init__.py +26 -0
- package/service/rover/cards/_shared.py +129 -0
- package/service/rover/cards/char_list.py +258 -0
- package/service/rover/cards/char_panel.py +1353 -0
- package/service/rover/cards/echo_list.py +206 -0
- package/service/rover/cards/matrix_roles.py +25 -0
- package/service/rover/cards/rank_boards.py +714 -0
- package/service/rover/cards/rank_total.py +664 -0
- package/service/rover/compat.py +215 -0
- package/service/rover/config.py +163 -0
- package/service/rover/custom_panel.py +220 -0
- package/service/rover/data_store.py +31 -0
- package/service/rover/gss.py +19 -0
- package/service/rover/handler.py +191 -0
- package/service/rover/help/__init__.py +0 -0
- package/service/rover/help/draw_new_plugin_help.py +332 -0
- package/service/rover/help/model.py +16 -0
- package/service/rover/help/utils.py +7 -0
- package/service/rover/hooks.py +420 -0
- package/service/rover/html_actions.py +236 -0
- package/service/rover/i18n.py +19 -0
- package/service/rover/knowledge.py +235 -0
- package/service/rover/logger.py +102 -0
- package/service/rover/main.py +33 -0
- package/service/rover/media.py +222 -0
- package/service/rover/message_models.py +23 -0
- package/service/rover/models.py +92 -0
- package/service/rover/native_media.py +64 -0
- package/service/rover/ocr.py +79 -0
- package/service/rover/pool.py +43 -0
- package/service/rover/restart.py +36 -0
- package/service/rover/segment.py +143 -0
- package/service/rover/server.py +131 -0
- package/service/rover/status/__init__.py +0 -0
- package/service/rover/status/models.py +10 -0
- package/service/rover/status/plugin_status.py +17 -0
- package/service/rover/subscribe.py +148 -0
- package/service/rover/sv.py +403 -0
- package/service/rover/templates/char_list.html +182 -0
- package/service/rover/templates/char_panel.html +465 -0
- package/service/rover/templates/echo_list.html +221 -0
- package/service/rover/templates/rank_all_total.html +193 -0
- package/service/rover/templates/rank_board.html +107 -0
- package/service/rover/templates/rank_level_total.html +57 -0
- package/service/rover/templates/rank_matrix_total.html +66 -0
- package/service/rover/templates/rank_phantom_total.html +180 -0
- package/service/rover/templates/rank_slash_total.html +38 -0
- package/service/rover/trigger.py +98 -0
- package/service/rover/utils/__init__.py +0 -0
- package/service/rover/utils/boardcast/__init__.py +0 -0
- package/service/rover/utils/boardcast/models.py +15 -0
- package/service/rover/utils/cookie_manager/__init__.py +0 -0
- package/service/rover/utils/cookie_manager/qrlogin.py +24 -0
- package/service/rover/utils/database/__init__.py +0 -0
- package/service/rover/utils/database/base_models.py +1771 -0
- package/service/rover/utils/database/models.py +80 -0
- package/service/rover/utils/database/startup.py +60 -0
- package/service/rover/utils/default_bg/bg.jpg +0 -0
- package/service/rover/utils/download_resource/__init__.py +0 -0
- package/service/rover/utils/download_resource/download_core.py +164 -0
- package/service/rover/utils/download_resource/download_file.py +46 -0
- package/service/rover/utils/fonts/__init__.py +0 -0
- package/service/rover/utils/fonts/fonts.py +33 -0
- package/service/rover/utils/image/__init__.py +0 -0
- package/service/rover/utils/image/convert.py +51 -0
- package/service/rover/utils/image/image_tools.py +30 -0
- package/service/rover/utils/image/utils.py +70 -0
- package/service/rover/utils/plugins_config/__init__.py +0 -0
- package/service/rover/utils/plugins_config/gs_config.py +506 -0
- package/service/rover/utils/plugins_config/models.py +182 -0
- package/service/rover/web_app.py +3 -0
- package/service/rover/webconsole/__init__.py +0 -0
- package/service/rover/webconsole/mount_app.py +26 -0
- package/service/tools/migrate.py +351 -0
- package/service/tools/regression.py +87 -0
- package/service/tools/smoke.py +63 -0
- package/service/tools/update_plugins.py +212 -0
- package/service/tools/verify_actions.py +104 -0
- package/service/tools/verify_card.py +82 -0
- package/service/tools/wwdata.py +665 -0
- package/tests/compat.test.js +181 -0
- package/tests/imagebridge.test.js +58 -0
- package/tests/media.test.js +68 -0
- package/tests/native.test.js +127 -0
- package/tests/runtime.test.js +69 -0
- 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())
|