@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.
- 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,215 @@
|
|
|
1
|
+
"""让上游插件源码零改动运行。
|
|
2
|
+
|
|
3
|
+
插件按它们原本的框架命名空间 import; 这里把那些名字全部指向本运行时的实现,
|
|
4
|
+
因此插件目录可以直接 git pull 上游, 不需要改一行源码。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import sys
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
from typing import Dict, List
|
|
11
|
+
|
|
12
|
+
from rover.logger import logger
|
|
13
|
+
|
|
14
|
+
# 上游框架的顶层包名与插件命名空间
|
|
15
|
+
UPSTREAM = "gsuid_core"
|
|
16
|
+
UPSTREAM_PLUGINS = f"{UPSTREAM}.plugins"
|
|
17
|
+
|
|
18
|
+
# 上游模块路径 → 本运行时模块路径
|
|
19
|
+
MODULE_MAP: Dict[str, str] = {
|
|
20
|
+
"": "rover",
|
|
21
|
+
"bot": "rover.bot",
|
|
22
|
+
"models": "rover.models",
|
|
23
|
+
"segment": "rover.segment",
|
|
24
|
+
"sv": "rover.sv",
|
|
25
|
+
"trigger": "rover.trigger",
|
|
26
|
+
"handler": "rover.handler",
|
|
27
|
+
"logger": "rover.logger",
|
|
28
|
+
"config": "rover.config",
|
|
29
|
+
"data_store": "rover.data_store",
|
|
30
|
+
"aps": "rover.aps",
|
|
31
|
+
"pool": "rover.pool",
|
|
32
|
+
"gss": "rover.gss",
|
|
33
|
+
"server": "rover.server",
|
|
34
|
+
"subscribe": "rover.subscribe",
|
|
35
|
+
"i18n": "rover.i18n",
|
|
36
|
+
"message_models": "rover.message_models",
|
|
37
|
+
"web_app": "rover.web_app",
|
|
38
|
+
"app_life": "rover.app_life",
|
|
39
|
+
"utils": "rover.utils",
|
|
40
|
+
"utils.database": "rover.utils.database",
|
|
41
|
+
"utils.database.base_models": "rover.utils.database.base_models",
|
|
42
|
+
"utils.database.models": "rover.utils.database.models",
|
|
43
|
+
"utils.database.startup": "rover.utils.database.startup",
|
|
44
|
+
"utils.plugins_config": "rover.utils.plugins_config",
|
|
45
|
+
"utils.plugins_config.models": "rover.utils.plugins_config.models",
|
|
46
|
+
"utils.plugins_config.gs_config": "rover.utils.plugins_config.gs_config",
|
|
47
|
+
"utils.image": "rover.utils.image",
|
|
48
|
+
"utils.image.convert": "rover.utils.image.convert",
|
|
49
|
+
"utils.image.image_tools": "rover.utils.image.image_tools",
|
|
50
|
+
"utils.image.utils": "rover.utils.image.utils",
|
|
51
|
+
"utils.fonts": "rover.utils.fonts",
|
|
52
|
+
"utils.fonts.fonts": "rover.utils.fonts.fonts",
|
|
53
|
+
"utils.download_resource": "rover.utils.download_resource",
|
|
54
|
+
"utils.download_resource.download_file": "rover.utils.download_resource.download_file",
|
|
55
|
+
"utils.download_resource.download_core": "rover.utils.download_resource.download_core",
|
|
56
|
+
"utils.cookie_manager": "rover.utils.cookie_manager",
|
|
57
|
+
"utils.cookie_manager.qrlogin": "rover.utils.cookie_manager.qrlogin",
|
|
58
|
+
"utils.boardcast": "rover.utils.boardcast",
|
|
59
|
+
"utils.boardcast.models": "rover.utils.boardcast.models",
|
|
60
|
+
"help": "rover.help",
|
|
61
|
+
"help.model": "rover.help.model",
|
|
62
|
+
"help.utils": "rover.help.utils",
|
|
63
|
+
"help.draw_new_plugin_help": "rover.help.draw_new_plugin_help",
|
|
64
|
+
"status": "rover.status",
|
|
65
|
+
"status.plugin_status": "rover.status.plugin_status",
|
|
66
|
+
"webconsole": "rover.webconsole",
|
|
67
|
+
"webconsole.mount_app": "rover.webconsole.mount_app",
|
|
68
|
+
"ai_core": "rover.ai_core",
|
|
69
|
+
"ai_core.models": "rover.ai_core.models",
|
|
70
|
+
"ai_core.register": "rover.ai_core.register",
|
|
71
|
+
"ai_core.resource": "rover.ai_core.resource",
|
|
72
|
+
"ai_core.rag": "rover.ai_core.rag",
|
|
73
|
+
"ai_core.rag.knowledge": "rover.ai_core.rag.knowledge",
|
|
74
|
+
"ai_core.skills": "rover.ai_core.skills",
|
|
75
|
+
"ai_core.skills.operations": "rover.ai_core.skills.operations",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# 上游的重启入口在 buildin 插件里, 这里造一个同路径模块转发过去
|
|
79
|
+
RESTART_PATH = f"{UPSTREAM}.buildin_plugins.core_command.core_restart.restart"
|
|
80
|
+
|
|
81
|
+
# 上游配置模型的类名, 与本运行时的对应关系
|
|
82
|
+
CONFIG_CLASS_ALIASES = {
|
|
83
|
+
"GsStrConfig": "StrConfig",
|
|
84
|
+
"GsBoolConfig": "BoolConfig",
|
|
85
|
+
"GsDictConfig": "DictConfig",
|
|
86
|
+
"GsListStrConfig": "ListStrConfig",
|
|
87
|
+
"GsListConfig": "ListConfig",
|
|
88
|
+
"GsIntConfig": "IntConfig",
|
|
89
|
+
"GsFloatConfig": "FloatConfig",
|
|
90
|
+
"GsImageConfig": "ImageConfig",
|
|
91
|
+
"GsTimeRConfig": "TimeRConfig",
|
|
92
|
+
"GsDivider": "Divider",
|
|
93
|
+
"GsFileUploadConfig": "FileUploadConfig",
|
|
94
|
+
"GsFilesUploadConfig": "FilesUploadConfig",
|
|
95
|
+
"GsDateConfig": "DateConfig",
|
|
96
|
+
"GsTimeRangeConfig": "TimeRangeConfig",
|
|
97
|
+
"GsColorConfig": "ColorConfig",
|
|
98
|
+
"GsRepeatGroupConfig": "RepeatGroupConfig",
|
|
99
|
+
"GsTimeConfig": "TimeConfig",
|
|
100
|
+
"GSC": "ConfigItem",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# 其它沿用上游名字的符号: 运行时模块 → {上游名: 本地名}
|
|
104
|
+
SYMBOL_ALIASES = {
|
|
105
|
+
"rover.webconsole.mount_app": {"GsAdminModel": "AdminModel"},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_installed = False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _alias_module(upstream_name: str, target_name: str) -> None:
|
|
112
|
+
module = importlib.import_module(target_name)
|
|
113
|
+
sys.modules[upstream_name] = module
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _make_namespace(name: str, path: List[str]) -> ModuleType:
|
|
117
|
+
module = ModuleType(name)
|
|
118
|
+
module.__path__ = path # type: ignore[attr-defined]
|
|
119
|
+
sys.modules[name] = module
|
|
120
|
+
return module
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _install_config_aliases() -> None:
|
|
124
|
+
models = importlib.import_module("rover.utils.plugins_config.models")
|
|
125
|
+
for upstream_name, ours in CONFIG_CLASS_ALIASES.items():
|
|
126
|
+
target = getattr(models, ours, None)
|
|
127
|
+
if target is None:
|
|
128
|
+
logger.warning(f"[兼容层] 配置模型缺少 {ours}")
|
|
129
|
+
continue
|
|
130
|
+
setattr(models, upstream_name, target)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _install_symbol_aliases() -> None:
|
|
134
|
+
for module_name, mapping in SYMBOL_ALIASES.items():
|
|
135
|
+
try:
|
|
136
|
+
module = importlib.import_module(module_name)
|
|
137
|
+
except ModuleNotFoundError as e:
|
|
138
|
+
logger.warning(f"[兼容层] 跳过 {module_name}: {e}")
|
|
139
|
+
continue
|
|
140
|
+
for upstream_name, ours in mapping.items():
|
|
141
|
+
target = getattr(module, ours, None)
|
|
142
|
+
if target is None:
|
|
143
|
+
logger.warning(f"[兼容层] {module_name} 缺少 {ours}")
|
|
144
|
+
continue
|
|
145
|
+
setattr(module, upstream_name, target)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _install_restart_shim() -> None:
|
|
149
|
+
import rover.restart as restart
|
|
150
|
+
|
|
151
|
+
module = ModuleType(RESTART_PATH)
|
|
152
|
+
module.restart_genshinuid = restart.restart_service # type: ignore[attr-defined]
|
|
153
|
+
for attr in dir(restart):
|
|
154
|
+
if not attr.startswith("_"):
|
|
155
|
+
setattr(module, attr, getattr(restart, attr))
|
|
156
|
+
parts = RESTART_PATH.split(".")
|
|
157
|
+
for i in range(2, len(parts)):
|
|
158
|
+
parent = ".".join(parts[:i])
|
|
159
|
+
if parent not in sys.modules:
|
|
160
|
+
_make_namespace(parent, [])
|
|
161
|
+
sys.modules[RESTART_PATH] = module
|
|
162
|
+
setattr(sys.modules[".".join(parts[:-1])], parts[-1], module)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def install(plugins_path: List[str]) -> None:
|
|
166
|
+
"""建立命名空间映射。plugins_path 是存放上游插件包的目录列表。"""
|
|
167
|
+
global _installed
|
|
168
|
+
if _installed:
|
|
169
|
+
return
|
|
170
|
+
_installed = True
|
|
171
|
+
|
|
172
|
+
for suffix, target in MODULE_MAP.items():
|
|
173
|
+
upstream_name = UPSTREAM if not suffix else f"{UPSTREAM}.{suffix}"
|
|
174
|
+
try:
|
|
175
|
+
_alias_module(upstream_name, target)
|
|
176
|
+
except ModuleNotFoundError as e:
|
|
177
|
+
logger.warning(f"[兼容层] 跳过 {upstream_name}: {e}")
|
|
178
|
+
|
|
179
|
+
_make_namespace(UPSTREAM_PLUGINS, list(plugins_path))
|
|
180
|
+
setattr(sys.modules[UPSTREAM], "plugins", sys.modules[UPSTREAM_PLUGINS])
|
|
181
|
+
|
|
182
|
+
_install_config_aliases()
|
|
183
|
+
_install_symbol_aliases()
|
|
184
|
+
_install_restart_shim()
|
|
185
|
+
logger.debug(f"[兼容层] 已映射 {len(MODULE_MAP)} 个模块")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def missing_imports(source_root) -> List[str]:
|
|
189
|
+
"""扫描插件源码里本运行时尚未提供的框架模块, 供上游更新后自检。"""
|
|
190
|
+
import ast
|
|
191
|
+
from pathlib import Path
|
|
192
|
+
|
|
193
|
+
known = {UPSTREAM if not s else f"{UPSTREAM}.{s}" for s in MODULE_MAP}
|
|
194
|
+
known.add(UPSTREAM_PLUGINS)
|
|
195
|
+
known.add(RESTART_PATH)
|
|
196
|
+
missing = set()
|
|
197
|
+
|
|
198
|
+
for py in Path(source_root).rglob("*.py"):
|
|
199
|
+
try:
|
|
200
|
+
tree = ast.parse(py.read_text(encoding="utf-8"))
|
|
201
|
+
except (OSError, SyntaxError):
|
|
202
|
+
continue
|
|
203
|
+
for node in ast.walk(tree):
|
|
204
|
+
names = []
|
|
205
|
+
if isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
|
|
206
|
+
names = [node.module]
|
|
207
|
+
elif isinstance(node, ast.Import):
|
|
208
|
+
names = [a.name for a in node.names]
|
|
209
|
+
for name in names:
|
|
210
|
+
if name == UPSTREAM or name.startswith(f"{UPSTREAM}."):
|
|
211
|
+
if name.startswith(UPSTREAM_PLUGINS):
|
|
212
|
+
continue
|
|
213
|
+
if name not in known:
|
|
214
|
+
missing.add(name)
|
|
215
|
+
return sorted(missing)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""核心配置: 数据根下的 config.json。"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, List, Union, Literal, overload
|
|
6
|
+
|
|
7
|
+
from boltons.fileutils import atomic_save
|
|
8
|
+
|
|
9
|
+
from rover.data_store import get_res_path
|
|
10
|
+
|
|
11
|
+
CONFIG_PATH = get_res_path() / "config.json"
|
|
12
|
+
|
|
13
|
+
CONFIG_DEFAULT: Dict[str, Any] = {
|
|
14
|
+
# 把插件作为工具暴露给模型(占少量上下文); 关闭后只能用 /ww 命令
|
|
15
|
+
"expose_to_model": True,
|
|
16
|
+
# 用户消息命中游戏常识关键词时自动附带知识条目; 关闭只停自动注入, 主动检索不受影响
|
|
17
|
+
"knowledge_enhance": True,
|
|
18
|
+
# 图片卡片(评分/面板等)本地 OCR 抽文字进 summary, 纯文本模型才看得到卡上数值
|
|
19
|
+
"image_ocr": True,
|
|
20
|
+
# 卡片头部头像: 本地图片路径 / http(s) 链接 / QQ 号; 置空用插件自带的
|
|
21
|
+
"avatar": "",
|
|
22
|
+
# 服务监听地址; 登录页与字体链接也按它拼
|
|
23
|
+
"HOST": "127.0.0.1",
|
|
24
|
+
"PORT": "9777",
|
|
25
|
+
# 命令前缀, 多个用逗号分隔, 置空则不需要前缀
|
|
26
|
+
"command_prefix": "ww",
|
|
27
|
+
# 本机使用者标识, 数据都归属它, 一般不用改
|
|
28
|
+
"masters": ["local"],
|
|
29
|
+
# 定时任务允许的迟到秒数
|
|
30
|
+
"misfire_grace_time": 90,
|
|
31
|
+
# 单条命令最长执行秒数
|
|
32
|
+
"command_timeout": 300,
|
|
33
|
+
# 竖图(高>宽)在会话区里的最大宽度百分比
|
|
34
|
+
"image_portrait_max_percent": 50,
|
|
35
|
+
# 横图(宽>=高)的最大宽度百分比
|
|
36
|
+
"image_landscape_max_percent": 75,
|
|
37
|
+
# 渲染产物目录容量上限(MB), 超出时按最旧优先淘汰; 0 表示不限
|
|
38
|
+
"media_max_mb": 512,
|
|
39
|
+
# 日志级别; 输出固定为 stdout + data/logs 下的滚动文件(有上限, 不会涨爆)
|
|
40
|
+
"log_level": "INFO",
|
|
41
|
+
# 已写过初值的插件, 写过就不再覆盖用户的选择
|
|
42
|
+
"seeded": [],
|
|
43
|
+
# 单个功能项的覆盖: {"功能名": {"enabled": false, "pm": 0}}
|
|
44
|
+
"sv": {},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
STR_CONFIG = Literal["HOST", "PORT", "command_prefix", "avatar", "log_level"]
|
|
48
|
+
INT_CONFIG = Literal[
|
|
49
|
+
"misfire_grace_time",
|
|
50
|
+
"command_timeout",
|
|
51
|
+
"media_max_mb",
|
|
52
|
+
"image_portrait_max_percent",
|
|
53
|
+
"image_landscape_max_percent",
|
|
54
|
+
]
|
|
55
|
+
LIST_CONFIG = Literal["masters", "seeded"]
|
|
56
|
+
DICT_CONFIG = Literal["sv"]
|
|
57
|
+
BOOL_CONFIG = Literal["expose_to_model"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CoreConfig:
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
if not CONFIG_PATH.exists():
|
|
63
|
+
with open(CONFIG_PATH, "w", encoding="UTF-8") as file:
|
|
64
|
+
json.dump(CONFIG_DEFAULT, file, indent=4, ensure_ascii=False)
|
|
65
|
+
|
|
66
|
+
self.update_config()
|
|
67
|
+
|
|
68
|
+
def write_config(self):
|
|
69
|
+
max_retries = 3
|
|
70
|
+
retry_delay = 0.5
|
|
71
|
+
|
|
72
|
+
for attempt in range(max_retries):
|
|
73
|
+
try:
|
|
74
|
+
with atomic_save(
|
|
75
|
+
str(CONFIG_PATH),
|
|
76
|
+
text_mode=False,
|
|
77
|
+
overwrite=True,
|
|
78
|
+
overwrite_part=True,
|
|
79
|
+
file_perms=0o644,
|
|
80
|
+
) as file:
|
|
81
|
+
if file:
|
|
82
|
+
json_str = json.dumps(
|
|
83
|
+
self.config,
|
|
84
|
+
indent=4,
|
|
85
|
+
ensure_ascii=False,
|
|
86
|
+
)
|
|
87
|
+
file.write(json_str.encode("utf-8"))
|
|
88
|
+
else:
|
|
89
|
+
raise RuntimeError("写入配置文件失败!")
|
|
90
|
+
return
|
|
91
|
+
except OSError as e:
|
|
92
|
+
if attempt < max_retries - 1:
|
|
93
|
+
time.sleep(retry_delay)
|
|
94
|
+
else:
|
|
95
|
+
raise RuntimeError(f"写入配置文件失败,已重试 {max_retries} 次: {str(e)}")
|
|
96
|
+
|
|
97
|
+
def update_config(self):
|
|
98
|
+
try:
|
|
99
|
+
with open(CONFIG_PATH, "r", encoding="UTF-8") as f:
|
|
100
|
+
self.config: Dict[str, Any] = json.load(f)
|
|
101
|
+
except UnicodeDecodeError:
|
|
102
|
+
with open(CONFIG_PATH, "r") as f:
|
|
103
|
+
self.config = json.load(f)
|
|
104
|
+
|
|
105
|
+
# 旧版把日志写成 {level,output,module} 一整块, 只保留级别
|
|
106
|
+
legacy = self.config.pop("log", None)
|
|
107
|
+
if isinstance(legacy, dict) and "log_level" not in self.config:
|
|
108
|
+
self.config["log_level"] = legacy.get("level") or "INFO"
|
|
109
|
+
|
|
110
|
+
# 对没有的值,添加默认值
|
|
111
|
+
for key in CONFIG_DEFAULT:
|
|
112
|
+
if key not in self.config:
|
|
113
|
+
self.config[key] = CONFIG_DEFAULT[key]
|
|
114
|
+
if isinstance(CONFIG_DEFAULT[key], Dict):
|
|
115
|
+
for sub_key in CONFIG_DEFAULT[key]:
|
|
116
|
+
if sub_key not in self.config[key]:
|
|
117
|
+
self.config[key][sub_key] = CONFIG_DEFAULT[key][sub_key]
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def get_config(self, key: STR_CONFIG) -> str: ...
|
|
121
|
+
|
|
122
|
+
@overload
|
|
123
|
+
def get_config(self, key: LIST_CONFIG) -> List: ...
|
|
124
|
+
|
|
125
|
+
@overload
|
|
126
|
+
def get_config(self, key: INT_CONFIG) -> int: ...
|
|
127
|
+
|
|
128
|
+
def get_config(self, key: str) -> Union[str, Dict, List, int, bool]:
|
|
129
|
+
if key in self.config:
|
|
130
|
+
return self.config[key]
|
|
131
|
+
elif key in CONFIG_DEFAULT:
|
|
132
|
+
self.update_config()
|
|
133
|
+
return self.config[key]
|
|
134
|
+
else:
|
|
135
|
+
return {}
|
|
136
|
+
|
|
137
|
+
@overload
|
|
138
|
+
def set_config(self, key: STR_CONFIG, value: str) -> bool: ...
|
|
139
|
+
|
|
140
|
+
@overload
|
|
141
|
+
def set_config(self, key: LIST_CONFIG, value: List) -> bool: ...
|
|
142
|
+
|
|
143
|
+
@overload
|
|
144
|
+
def set_config(self, key: INT_CONFIG, value: int) -> bool: ...
|
|
145
|
+
|
|
146
|
+
def set_config(self, key: str, value: Union[str, List, Dict, int, bool]) -> bool:
|
|
147
|
+
if key in CONFIG_DEFAULT:
|
|
148
|
+
self.config[key] = value
|
|
149
|
+
self.write_config()
|
|
150
|
+
return True
|
|
151
|
+
else:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
def lazy_write_config(self):
|
|
155
|
+
self.write_config()
|
|
156
|
+
|
|
157
|
+
def lazy_set_config(self, key: str, value: Union[str, List, Dict, int, bool]):
|
|
158
|
+
"""只改内存, 由 lazy_write_config 统一落盘。"""
|
|
159
|
+
if key in CONFIG_DEFAULT:
|
|
160
|
+
self.config[key] = value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
core_config: CoreConfig = CoreConfig()
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""AI 自定义面板: 按传入 ctx 重组角色声骸后渲完整面板。
|
|
2
|
+
|
|
3
|
+
只渲染: 不写 players/、不进上传队列(总排行)、不改任何用户数据。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from rover.bot import Bot
|
|
10
|
+
from rover.handler import build_message, msg_process
|
|
11
|
+
from rover.logger import logger
|
|
12
|
+
from rover.models import Message
|
|
13
|
+
|
|
14
|
+
VALID_COSTS = {1, 3, 4}
|
|
15
|
+
MAX_TOTAL_COST = 12
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _props(pairs: Any) -> List[Dict[str, Any]]:
|
|
19
|
+
"""[["攻击","18.0%"], ...] 或 [{"attributeName":...}] → Props 列表。"""
|
|
20
|
+
out: List[Dict[str, Any]] = []
|
|
21
|
+
for item in pairs or []:
|
|
22
|
+
if isinstance(item, dict):
|
|
23
|
+
name = item.get("attributeName") or item.get("name")
|
|
24
|
+
value = item.get("attributeValue") or item.get("value")
|
|
25
|
+
elif isinstance(item, (list, tuple)) and len(item) >= 2:
|
|
26
|
+
name, value = item[0], item[1]
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError(f"词条格式不对: {item!r}")
|
|
29
|
+
if not name or value is None:
|
|
30
|
+
raise ValueError(f"词条缺名字或数值: {item!r}")
|
|
31
|
+
out.append({"attributeName": str(name), "attributeValue": str(value)})
|
|
32
|
+
return out
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_custom_echo(spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
36
|
+
from plugins.XutheringWavesUID.XutheringWavesUID.utils.name_convert import (
|
|
37
|
+
alias_to_echo_name,
|
|
38
|
+
alias_to_sonata_name,
|
|
39
|
+
echo_name_to_echo_id,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
raw_name = str(spec.get("name") or "").strip()
|
|
43
|
+
if not raw_name:
|
|
44
|
+
raise ValueError("自定义声骸缺 name")
|
|
45
|
+
cost = int(spec.get("cost") or 0)
|
|
46
|
+
if cost not in VALID_COSTS:
|
|
47
|
+
raise ValueError(f"声骸 cost 只能是 1/3/4: {raw_name} cost={cost}")
|
|
48
|
+
|
|
49
|
+
echo_name = alias_to_echo_name(raw_name) or raw_name
|
|
50
|
+
echo_id = echo_name_to_echo_id(echo_name)
|
|
51
|
+
sonata = alias_to_sonata_name(str(spec.get("sonata") or "")) or str(
|
|
52
|
+
spec.get("sonata") or ""
|
|
53
|
+
)
|
|
54
|
+
quality = int(spec.get("quality") or 5)
|
|
55
|
+
main = _props(spec.get("main"))
|
|
56
|
+
subs = _props(spec.get("subs"))
|
|
57
|
+
if not main:
|
|
58
|
+
raise ValueError(f"声骸 {echo_name} 缺主词条 main")
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"phantomProp": {
|
|
62
|
+
"phantomPropId": 0,
|
|
63
|
+
"name": echo_name,
|
|
64
|
+
"phantomId": int(echo_id) if echo_id else 0,
|
|
65
|
+
"quality": quality,
|
|
66
|
+
"cost": cost,
|
|
67
|
+
"iconUrl": "",
|
|
68
|
+
"skillDescription": "",
|
|
69
|
+
},
|
|
70
|
+
"cost": cost,
|
|
71
|
+
"quality": quality,
|
|
72
|
+
"level": int(spec.get("level") or 25),
|
|
73
|
+
"fetterDetail": {"groupId": 0, "name": sonata, "num": int(spec.get("num") or 5)},
|
|
74
|
+
"mainProps": main,
|
|
75
|
+
"subProps": subs,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _ref_echo(uid: str, ref: Dict[str, Any]):
|
|
80
|
+
"""{"char": "洛可可", "slot": 2, "uid": 可选} → 那个角色身上装着的那件。"""
|
|
81
|
+
from plugins.XutheringWavesUID.XutheringWavesUID.utils.char_info_utils import (
|
|
82
|
+
get_char_detail_for_id,
|
|
83
|
+
)
|
|
84
|
+
from plugins.XutheringWavesUID.XutheringWavesUID.utils.name_convert import (
|
|
85
|
+
alias_to_char_name,
|
|
86
|
+
char_name_to_char_id,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
char = str(ref.get("char") or "").strip()
|
|
90
|
+
slot = int(ref.get("slot") or 0)
|
|
91
|
+
if not char or slot < 1 or slot > 5:
|
|
92
|
+
raise ValueError(f"引用格式不对(需要 char + slot 1-5): {ref!r}")
|
|
93
|
+
char_name = alias_to_char_name(char)
|
|
94
|
+
char_id = char_name_to_char_id(char_name)
|
|
95
|
+
if not char_id:
|
|
96
|
+
raise ValueError(f"引用的角色不认识: {char}")
|
|
97
|
+
detail = await get_char_detail_for_id(str(ref.get("uid") or uid), char_id)
|
|
98
|
+
if detail is None:
|
|
99
|
+
raise ValueError(f"{char_name} 没有本地面板数据, 先 ww刷新{char_name}面板")
|
|
100
|
+
lst = detail.phantomData.equipPhantomList if detail.phantomData else None
|
|
101
|
+
echo = lst[slot - 1] if lst and len(lst) >= slot else None
|
|
102
|
+
if echo is None:
|
|
103
|
+
raise ValueError(f"{char_name} 的 {slot} 号位没有声骸")
|
|
104
|
+
return copy.deepcopy(echo)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def _apply_echoes(uid: str, role_detail, echoes: List[Dict[str, Any]]) -> None:
|
|
108
|
+
from plugins.XutheringWavesUID.XutheringWavesUID.utils.api.model import (
|
|
109
|
+
EquipPhantom,
|
|
110
|
+
EquipPhantomData,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if role_detail.phantomData is None:
|
|
114
|
+
role_detail.phantomData = EquipPhantomData(cost=0, equipPhantomList=None)
|
|
115
|
+
current = list(role_detail.phantomData.equipPhantomList or [None] * 5)
|
|
116
|
+
while len(current) < 5:
|
|
117
|
+
current.append(None)
|
|
118
|
+
|
|
119
|
+
for i, spec in enumerate(echoes):
|
|
120
|
+
if not isinstance(spec, dict):
|
|
121
|
+
raise ValueError(f"声骸项要是对象: {spec!r}")
|
|
122
|
+
slot = int(spec.get("slot") or i + 1)
|
|
123
|
+
if slot < 1 or slot > 5:
|
|
124
|
+
raise ValueError(f"slot 只能是 1-5: {slot}")
|
|
125
|
+
if spec.get("from"):
|
|
126
|
+
current[slot - 1] = await _ref_echo(uid, spec["from"])
|
|
127
|
+
else:
|
|
128
|
+
current[slot - 1] = EquipPhantom(**_build_custom_echo(spec))
|
|
129
|
+
|
|
130
|
+
total = sum(e.cost for e in current if e)
|
|
131
|
+
if total > MAX_TOTAL_COST:
|
|
132
|
+
raise ValueError(f"总 cost {total} 超过 {MAX_TOTAL_COST}, 检查各件 cost")
|
|
133
|
+
role_detail.phantomData.cost = total
|
|
134
|
+
role_detail.phantomData.equipPhantomList = current
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def render_custom_panel(
|
|
138
|
+
payload: Dict[str, Any],
|
|
139
|
+
) -> Tuple[Optional[str], List[Message]]:
|
|
140
|
+
"""(错误文案, 消息段)。成功时错误为 None。"""
|
|
141
|
+
from plugins.XutheringWavesUID.XutheringWavesUID.utils.char_info_utils import (
|
|
142
|
+
get_char_detail_for_id,
|
|
143
|
+
)
|
|
144
|
+
from plugins.XutheringWavesUID.XutheringWavesUID.utils.database.models import (
|
|
145
|
+
WavesBind,
|
|
146
|
+
)
|
|
147
|
+
from plugins.XutheringWavesUID.XutheringWavesUID.utils.name_convert import (
|
|
148
|
+
alias_to_char_name,
|
|
149
|
+
char_name_to_char_id,
|
|
150
|
+
)
|
|
151
|
+
from plugins.XutheringWavesUID.XutheringWavesUID.wutheringwaves_charinfo.draw_char_card import (
|
|
152
|
+
draw_char_detail_img,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
char = str(payload.get("char") or "").strip()
|
|
156
|
+
if not char:
|
|
157
|
+
return "缺角色名 char", []
|
|
158
|
+
char_name = alias_to_char_name(char)
|
|
159
|
+
char_id = char_name_to_char_id(char_name)
|
|
160
|
+
if not char_id or len(char_id) != 4 or not char_id.isdigit():
|
|
161
|
+
return f"未找到角色: {char}", []
|
|
162
|
+
|
|
163
|
+
from rover.config import core_config
|
|
164
|
+
|
|
165
|
+
masters = core_config.get_config("masters") or []
|
|
166
|
+
default_user = str(masters[0]) if masters else "dsh-user"
|
|
167
|
+
ev = msg_process(build_message(f"{char_name}面板", user_id=default_user))
|
|
168
|
+
ev.user_pm = 0
|
|
169
|
+
|
|
170
|
+
uid = str(payload.get("uid") or "").strip()
|
|
171
|
+
if not uid:
|
|
172
|
+
uid = await WavesBind.get_uid_by_game(ev.user_id, ev.bot_id) or ""
|
|
173
|
+
if not uid:
|
|
174
|
+
return "没有可用 UID: 传 uid, 或先 ww绑定<特征码>", []
|
|
175
|
+
|
|
176
|
+
base = await get_char_detail_for_id(uid, char_id)
|
|
177
|
+
if base is None:
|
|
178
|
+
return f"{char_name} 没有本地面板数据, 先跑一次 ww刷新{char_name}面板", []
|
|
179
|
+
role_detail = copy.deepcopy(base)
|
|
180
|
+
|
|
181
|
+
echoes = payload.get("echoes")
|
|
182
|
+
if echoes:
|
|
183
|
+
if not isinstance(echoes, list):
|
|
184
|
+
return "echoes 要是数组", []
|
|
185
|
+
try:
|
|
186
|
+
await _apply_echoes(uid, role_detail, echoes)
|
|
187
|
+
except ValueError as e:
|
|
188
|
+
return str(e), []
|
|
189
|
+
|
|
190
|
+
change = str(payload.get("change") or "").strip() or None
|
|
191
|
+
|
|
192
|
+
bot = Bot(ev)
|
|
193
|
+
try:
|
|
194
|
+
im = await draw_char_detail_img(
|
|
195
|
+
ev,
|
|
196
|
+
uid,
|
|
197
|
+
char_name,
|
|
198
|
+
ev.user_id,
|
|
199
|
+
change_list_regex=change,
|
|
200
|
+
role_detail_override=role_detail,
|
|
201
|
+
)
|
|
202
|
+
# 账号上下文取不到时降级为无账号渲染
|
|
203
|
+
if isinstance(im, str) and not change:
|
|
204
|
+
im2 = await draw_char_detail_img(
|
|
205
|
+
ev,
|
|
206
|
+
uid,
|
|
207
|
+
char_name,
|
|
208
|
+
ev.user_id,
|
|
209
|
+
is_limit_query=True,
|
|
210
|
+
role_detail_override=role_detail,
|
|
211
|
+
)
|
|
212
|
+
if not isinstance(im2, str):
|
|
213
|
+
im = im2
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.exception(f"[自定义面板] 渲染失败 {char_name}: {e}")
|
|
216
|
+
return f"渲染失败: {e}", []
|
|
217
|
+
if isinstance(im, str):
|
|
218
|
+
return im, []
|
|
219
|
+
await bot.send(im)
|
|
220
|
+
return None, bot.sink
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""数据目录。根为 service/data。"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional, Union
|
|
5
|
+
|
|
6
|
+
core_path = Path(__file__).parent
|
|
7
|
+
data_path = Path(__file__).parents[1] / "data"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_res_path(_path: Optional[Union[str, List[str], Path]] = None) -> Path:
|
|
11
|
+
if _path is None:
|
|
12
|
+
path = data_path
|
|
13
|
+
elif isinstance(_path, Path):
|
|
14
|
+
path = _path
|
|
15
|
+
elif isinstance(_path, str):
|
|
16
|
+
path = data_path / _path
|
|
17
|
+
else:
|
|
18
|
+
path = data_path.joinpath(*_path)
|
|
19
|
+
|
|
20
|
+
if not path.exists():
|
|
21
|
+
path.mkdir(parents=True)
|
|
22
|
+
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
RES = get_res_path()
|
|
27
|
+
|
|
28
|
+
# 下面这些按需创建: 取用时才落盘, 避免一上来就铺一堆空目录
|
|
29
|
+
image_res = data_path / "IMAGE_TEMP"
|
|
30
|
+
data_cache_path = data_path / "DATA_CACHE_PATH"
|
|
31
|
+
CONFIGS_PATH = data_path / "configs"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""连接注册表。定时推送与订阅回发都从这里取发送端。"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from rover.bot import PushBot
|
|
6
|
+
|
|
7
|
+
DEFAULT_BOT_ID = "dsh"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServerState:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.active_bot: Dict[str, PushBot] = {DEFAULT_BOT_ID: PushBot(DEFAULT_BOT_ID, DEFAULT_BOT_ID)}
|
|
13
|
+
self.bot_users: Dict[str, list] = {}
|
|
14
|
+
|
|
15
|
+
def get_bot(self, bot_id: str = DEFAULT_BOT_ID) -> PushBot:
|
|
16
|
+
return self.active_bot.get(bot_id) or self.active_bot[DEFAULT_BOT_ID]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
gss = ServerState()
|