@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,191 @@
|
|
|
1
|
+
"""命令分发。一条文本经触发器匹配后依次执行命中的 handler, 回复段按序收集。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from rover.bot import Bot
|
|
10
|
+
from rover.config import core_config
|
|
11
|
+
from rover.logger import logger
|
|
12
|
+
from rover.models import Event, Message, MessageReceive
|
|
13
|
+
from rover.sv import SL, SV, all_prefixes
|
|
14
|
+
from rover.trigger import Trigger
|
|
15
|
+
|
|
16
|
+
DEFAULT_BOT_ID = "dsh"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_user_pm(user_id: str, fallback: int = 6) -> int:
|
|
20
|
+
masters = core_config.get_config("masters") or []
|
|
21
|
+
if user_id in masters:
|
|
22
|
+
return 0
|
|
23
|
+
return fallback if fallback >= 1 else 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sv_authorized(_sv: SV, event: Event, user_pm: int) -> bool:
|
|
27
|
+
_plugins = _sv.plugins
|
|
28
|
+
if not _plugins.enabled:
|
|
29
|
+
return False
|
|
30
|
+
if user_pm > _plugins.pm:
|
|
31
|
+
return False
|
|
32
|
+
if event.group_id in _plugins.black_list or event.user_id in _plugins.black_list:
|
|
33
|
+
return False
|
|
34
|
+
# 只有本机一个使用者, 群聊/私聊之分没有意义: 限定场景的命令(如 获取ck)照样能直接下
|
|
35
|
+
if _plugins.white_list and _plugins.white_list != [""]:
|
|
36
|
+
if event.user_id not in _plugins.white_list and event.group_id not in _plugins.white_list:
|
|
37
|
+
return False
|
|
38
|
+
if not _sv.enabled:
|
|
39
|
+
return False
|
|
40
|
+
if user_pm > _sv.pm:
|
|
41
|
+
return False
|
|
42
|
+
if event.group_id in _sv.black_list or event.user_id in _sv.black_list:
|
|
43
|
+
return False
|
|
44
|
+
if _sv.white_list and _sv.white_list != [""]:
|
|
45
|
+
if event.user_id not in _sv.white_list and event.group_id not in _sv.white_list:
|
|
46
|
+
return False
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def msg_process(msg: MessageReceive) -> Event:
|
|
51
|
+
bot_id = msg.bot_id.split(":")[0] if ":" in msg.bot_id else msg.bot_id
|
|
52
|
+
event = Event(
|
|
53
|
+
bot_id,
|
|
54
|
+
msg.bot_self_id,
|
|
55
|
+
msg.msg_id,
|
|
56
|
+
msg.user_type,
|
|
57
|
+
msg.group_id,
|
|
58
|
+
msg.user_id,
|
|
59
|
+
msg.sender,
|
|
60
|
+
msg.user_pm,
|
|
61
|
+
real_bot_id=msg.bot_id,
|
|
62
|
+
)
|
|
63
|
+
event.WS_BOT_ID = msg.bot_id
|
|
64
|
+
if msg.user_type == "direct":
|
|
65
|
+
event.is_tome = True
|
|
66
|
+
|
|
67
|
+
content: List[Message] = []
|
|
68
|
+
for _msg in msg.content:
|
|
69
|
+
if _msg.type == "text":
|
|
70
|
+
if not _msg.data:
|
|
71
|
+
continue
|
|
72
|
+
text_part = str(_msg.data).strip()
|
|
73
|
+
event.raw_text += text_part
|
|
74
|
+
event.text += text_part
|
|
75
|
+
elif _msg.type == "at":
|
|
76
|
+
if str(_msg.data) == str(event.bot_self_id):
|
|
77
|
+
event.is_tome = True
|
|
78
|
+
continue
|
|
79
|
+
event.at = str(_msg.data)
|
|
80
|
+
event.at_list.append(str(_msg.data))
|
|
81
|
+
elif _msg.type == "image":
|
|
82
|
+
event.image = _msg.data
|
|
83
|
+
if _msg.data:
|
|
84
|
+
event.image_list.append(_msg.data)
|
|
85
|
+
elif _msg.type == "reply":
|
|
86
|
+
event.reply = _msg.data
|
|
87
|
+
elif _msg.type == "reply_id" and _msg.data is not None:
|
|
88
|
+
event.reply_id = str(_msg.data)
|
|
89
|
+
elif _msg.type == "file" and _msg.data:
|
|
90
|
+
data = str(_msg.data).split("|")
|
|
91
|
+
event.file_name = data[0]
|
|
92
|
+
event.file = data[1] if len(data) > 1 else ""
|
|
93
|
+
event.file_type = "url" if str(event.file).startswith("http") else "base64"
|
|
94
|
+
content.append(_msg)
|
|
95
|
+
event.content = content
|
|
96
|
+
return event
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def normalize_command(text: str) -> str:
|
|
100
|
+
"""无前缀时补一个默认前缀, 让前端/模型不必记住 ww。"""
|
|
101
|
+
text = text.strip()
|
|
102
|
+
if not text:
|
|
103
|
+
return text
|
|
104
|
+
for prefix in all_prefixes():
|
|
105
|
+
if text.startswith(prefix):
|
|
106
|
+
return text
|
|
107
|
+
default = all_prefixes()
|
|
108
|
+
return f"{default[0]}{text}" if default else text
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def collect_triggers(event: Event) -> List[Trigger]:
|
|
112
|
+
valid: Dict[Trigger, int] = {}
|
|
113
|
+
for sv in SL.lst.values():
|
|
114
|
+
if not _sv_authorized(sv, event, event.user_pm):
|
|
115
|
+
continue
|
|
116
|
+
for triggers in sv.TL.values():
|
|
117
|
+
for trigger in triggers.values():
|
|
118
|
+
if trigger.check_command(event):
|
|
119
|
+
valid[trigger] = sv.priority
|
|
120
|
+
return [t for t, _ in sorted(valid.items(), key=lambda x: (not x[0].prefix, x[1]))]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def dispatch(msg: MessageReceive, timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
124
|
+
started = time.perf_counter()
|
|
125
|
+
if timeout is None:
|
|
126
|
+
timeout = float(core_config.get_config("command_timeout") or 300)
|
|
127
|
+
event = msg_process(msg)
|
|
128
|
+
event.user_pm = get_user_pm(event.user_id, msg.user_pm)
|
|
129
|
+
event.task_id = uuid.uuid4().hex
|
|
130
|
+
|
|
131
|
+
triggers = collect_triggers(event)
|
|
132
|
+
sink: List[Message] = []
|
|
133
|
+
matched: List[str] = []
|
|
134
|
+
|
|
135
|
+
if not triggers:
|
|
136
|
+
return {
|
|
137
|
+
"matched": [],
|
|
138
|
+
"segments": sink,
|
|
139
|
+
"elapsed": time.perf_counter() - started,
|
|
140
|
+
"unmatched": True,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for trigger in triggers:
|
|
144
|
+
_event = deepcopy(event)
|
|
145
|
+
await trigger.get_command(_event)
|
|
146
|
+
bot = Bot(_event, sink)
|
|
147
|
+
matched.append(f"{trigger.type}:{trigger.prefix}{trigger.keyword}")
|
|
148
|
+
try:
|
|
149
|
+
await asyncio.wait_for(trigger.func(bot, _event), timeout=timeout)
|
|
150
|
+
except asyncio.TimeoutError:
|
|
151
|
+
logger.error(f"命令执行超时: {trigger.keyword}")
|
|
152
|
+
sink.append(Message("text", f"[执行超时] {trigger.keyword}"))
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.exception(e)
|
|
155
|
+
sink.append(Message("text", f"[执行出错] {e}"))
|
|
156
|
+
if trigger.block:
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"matched": matched,
|
|
161
|
+
"segments": sink,
|
|
162
|
+
"elapsed": time.perf_counter() - started,
|
|
163
|
+
"unmatched": False,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_message(
|
|
168
|
+
text: str,
|
|
169
|
+
user_id: str,
|
|
170
|
+
group_id: Optional[str] = "dsh",
|
|
171
|
+
user_type: str = "group",
|
|
172
|
+
images: Optional[List[str]] = None,
|
|
173
|
+
file_name: Optional[str] = None,
|
|
174
|
+
file_data: Optional[str] = None,
|
|
175
|
+
) -> MessageReceive:
|
|
176
|
+
content: List[Message] = [Message("text", text)]
|
|
177
|
+
for img in images or []:
|
|
178
|
+
content.append(Message("image", img))
|
|
179
|
+
if file_name and file_data:
|
|
180
|
+
content.append(Message("file", f"{file_name}|{file_data}"))
|
|
181
|
+
return MessageReceive(
|
|
182
|
+
bot_id=DEFAULT_BOT_ID,
|
|
183
|
+
bot_self_id=DEFAULT_BOT_ID,
|
|
184
|
+
msg_id=uuid.uuid4().hex[:12],
|
|
185
|
+
user_type=user_type, # type: ignore[arg-type]
|
|
186
|
+
group_id=group_id if user_type == "group" else None,
|
|
187
|
+
user_id=user_id,
|
|
188
|
+
sender={"nickname": "漫游者"},
|
|
189
|
+
user_pm=6,
|
|
190
|
+
content=content,
|
|
191
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""帮助渲染: 产出可点击的 HTML 卡片, 版式照画布版 1:1 摆放。
|
|
2
|
+
|
|
3
|
+
签名与旧图片实现一致, 插件零改动。条目对照当前已注册的触发器过滤,
|
|
4
|
+
被裁掉的命令不会出现在帮助里; 鼠标停在条目上给出该命令的说明。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import re
|
|
9
|
+
import zlib
|
|
10
|
+
from html import escape
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from rover.media import HtmlBytes
|
|
16
|
+
|
|
17
|
+
# 画布版的几何常量: 列宽/行高/分类条与条目在各自底图里的锚点
|
|
18
|
+
COLUMN = 5
|
|
19
|
+
CELL_W = 490
|
|
20
|
+
CELL_H = 175
|
|
21
|
+
ITEM_X0 = 45
|
|
22
|
+
SOFT = 10
|
|
23
|
+
BANNER_W = 1545
|
|
24
|
+
|
|
25
|
+
_CSS = """
|
|
26
|
+
* { box-sizing: border-box; }
|
|
27
|
+
html, body { margin: 0; padding: 0; background: #14161c; }
|
|
28
|
+
body { font-family: 'Source Han Sans CN', 'Noto Sans CJK JP', sans-serif; color: #fff; }
|
|
29
|
+
.container { position: relative; overflow: hidden; }
|
|
30
|
+
.bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
31
|
+
.layer { position: absolute; left: 0; top: 0; }
|
|
32
|
+
.layer img { display: block; }
|
|
33
|
+
.t { position: absolute; white-space: nowrap; line-height: 1; transform: translateY(-50%); }
|
|
34
|
+
.badge { position: absolute; display: flex; align-items: center; border-radius: 12px;
|
|
35
|
+
transform: translateY(-50%); overflow: hidden; }
|
|
36
|
+
.badge span { padding: 0 14px; line-height: 1; }
|
|
37
|
+
/* 点击桥接会给 [data-ww-cmd] 加 position:relative, 这里用更高特异性压回去 */
|
|
38
|
+
.container .item { position: absolute; width: 490px; height: 175px; cursor: pointer; }
|
|
39
|
+
.item > img { position: absolute; left: 0; top: 0; width: 490px; height: 175px; }
|
|
40
|
+
.item .icon { left: 6px; top: 12px; width: 150px; height: 150px; }
|
|
41
|
+
.item .nm { position: absolute; left: 156px; top: 67px; font-size: 38px; color: #fff;
|
|
42
|
+
transform: translateY(-50%); white-space: nowrap; }
|
|
43
|
+
.item .eg { position: absolute; left: 156px; top: 116px; font-size: 26px; color: #cecece;
|
|
44
|
+
transform: translateY(-50%); white-space: nowrap; }
|
|
45
|
+
.item:hover .hl { outline: 3px solid rgba(255, 214, 102, .95); outline-offset: -3px; border-radius: 12px; }
|
|
46
|
+
.tip { position: absolute; width: 900px; padding: 18px 22px; border-radius: 14px;
|
|
47
|
+
background: rgba(12, 14, 20, .96); border: 2px solid rgba(255, 255, 255, .16);
|
|
48
|
+
box-shadow: 0 12px 34px rgba(0, 0, 0, .55); font-size: 30px; line-height: 1.5;
|
|
49
|
+
color: #dfe3ec; white-space: normal; opacity: 0; pointer-events: none;
|
|
50
|
+
transition: opacity .12s; z-index: 9; }
|
|
51
|
+
.tip .k { display: block; margin-bottom: 8px; font-size: 26px; color: #ffd666;
|
|
52
|
+
font-family: ui-monospace, monospace; }
|
|
53
|
+
.item:hover .tip { opacity: 1; }
|
|
54
|
+
.tip.down { top: 160px; }
|
|
55
|
+
.tip.up { bottom: 160px; }
|
|
56
|
+
.tip.left { left: 20px; }
|
|
57
|
+
.tip.right { right: 20px; }
|
|
58
|
+
.hl { position: absolute; inset: 0; }
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _uri(image: Any, fmt: str = "PNG") -> str:
|
|
63
|
+
if image is None:
|
|
64
|
+
return ""
|
|
65
|
+
try:
|
|
66
|
+
buffer = BytesIO()
|
|
67
|
+
image.convert("RGBA" if fmt == "PNG" else "RGB").save(buffer, format=fmt)
|
|
68
|
+
mime = "png" if fmt == "PNG" else "jpeg"
|
|
69
|
+
return f"data:image/{mime};base64," + base64.b64encode(buffer.getvalue()).decode()
|
|
70
|
+
except Exception:
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _file_uri(path: Path, cache: Dict[str, str]) -> str:
|
|
75
|
+
key = str(path)
|
|
76
|
+
if key in cache:
|
|
77
|
+
return cache[key]
|
|
78
|
+
try:
|
|
79
|
+
raw = path.read_bytes()
|
|
80
|
+
except OSError:
|
|
81
|
+
cache[key] = ""
|
|
82
|
+
return ""
|
|
83
|
+
mime = "image/jpeg" if path.suffix.lower() in (".jpg", ".jpeg") else "image/png"
|
|
84
|
+
cache[key] = f"data:{mime};base64," + base64.b64encode(raw).decode()
|
|
85
|
+
return cache[key]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _find_icon(name: str, icon_path: Path) -> Optional[Path]:
|
|
89
|
+
"""与画布版同一套匹配: 全名 → 名字里含图标名 → 通用 → 兜底。
|
|
90
|
+
|
|
91
|
+
兜底按名字取模而不是随机: 同一条命令每次出图得是同一个图标。
|
|
92
|
+
"""
|
|
93
|
+
if not icon_path or not icon_path.is_dir():
|
|
94
|
+
return None
|
|
95
|
+
icons = sorted(icon_path.glob("*.png"))
|
|
96
|
+
for icon in icons:
|
|
97
|
+
if icon.stem == name:
|
|
98
|
+
return icon
|
|
99
|
+
for icon in icons:
|
|
100
|
+
if icon.stem in name:
|
|
101
|
+
return icon
|
|
102
|
+
generic = icon_path / "通用.png"
|
|
103
|
+
if generic.exists():
|
|
104
|
+
return generic
|
|
105
|
+
if not icons:
|
|
106
|
+
return None
|
|
107
|
+
return icons[zlib.crc32(name.encode("utf-8")) % len(icons)]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _dispatches(command: str) -> bool:
|
|
111
|
+
"""按分发时的同一套规则判定, 而不是宽松前缀匹配: 点了没反应的条目不该留在帮助里。"""
|
|
112
|
+
from rover.models import Event
|
|
113
|
+
from rover.sv import SL
|
|
114
|
+
|
|
115
|
+
event = Event(bot_id="dsh", user_id="local", raw_text=command, user_type="group")
|
|
116
|
+
for sv in SL.lst.values():
|
|
117
|
+
for table in sv.TL.values():
|
|
118
|
+
for trigger in table.values():
|
|
119
|
+
try:
|
|
120
|
+
if trigger.check_command(event):
|
|
121
|
+
return True
|
|
122
|
+
except Exception:
|
|
123
|
+
continue
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _candidates(command: str, prefix: str):
|
|
128
|
+
"""帮助里的示例常写成「a/b」这种并列写法, 原样点是跑不通的, 拆成真命令再试。"""
|
|
129
|
+
yield command
|
|
130
|
+
body = command[len(prefix) :] if prefix and command.startswith(prefix) else command
|
|
131
|
+
if "+" in body:
|
|
132
|
+
yield prefix + body.split("+", 1)[0].strip()
|
|
133
|
+
if "/" not in body:
|
|
134
|
+
return
|
|
135
|
+
head, _, tail = body.partition("/")
|
|
136
|
+
# 「深塔/海墟信息11」这类, 斜杠只替换前半段, 后面的尾巴是共用的
|
|
137
|
+
for cut in range(1, len(tail)):
|
|
138
|
+
if tail[cut:]:
|
|
139
|
+
yield prefix + head + tail[cut:]
|
|
140
|
+
yield prefix + head
|
|
141
|
+
yield prefix + tail
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _executable(command: str, prefix: str) -> Optional[str]:
|
|
145
|
+
for candidate in _candidates(command, prefix):
|
|
146
|
+
if candidate and _dispatches(candidate):
|
|
147
|
+
return candidate
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _clip_name(text: str) -> str:
|
|
152
|
+
"""画布版按字宽截断到 8 个中文宽度, 这里照做, 免得名字顶出卡片。"""
|
|
153
|
+
total = 0.0
|
|
154
|
+
out = ""
|
|
155
|
+
for char in text:
|
|
156
|
+
out += char
|
|
157
|
+
if "一" <= char <= "鿿":
|
|
158
|
+
total += 1
|
|
159
|
+
elif re.match(r"[A-Za-z0-9]", char):
|
|
160
|
+
total += 0.5
|
|
161
|
+
elif re.match(r"[^\w\s]", char):
|
|
162
|
+
total += 0.3
|
|
163
|
+
if total >= 8:
|
|
164
|
+
break
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _entries(cat: str, cat_data: dict, prefix: str, pm: int) -> List[dict]:
|
|
169
|
+
from rover.hooks import DROP_HELP_MARKERS
|
|
170
|
+
|
|
171
|
+
rows = []
|
|
172
|
+
for entry in cat_data.get("data", []):
|
|
173
|
+
if entry.get("need_admin") and pm > 3:
|
|
174
|
+
continue
|
|
175
|
+
eg = str(entry.get("eg") or entry.get("name") or "").strip()
|
|
176
|
+
shown = eg if cat == "插件帮助一览" else f"{prefix}{eg}"
|
|
177
|
+
name = str(entry.get("name", ""))
|
|
178
|
+
if any(marker in f"{name}{eg}" for marker in DROP_HELP_MARKERS):
|
|
179
|
+
continue
|
|
180
|
+
command = _executable(shown, "" if cat == "插件帮助一览" else prefix) if eg else shown
|
|
181
|
+
if eg and not command:
|
|
182
|
+
continue
|
|
183
|
+
rows.append(
|
|
184
|
+
{"name": name, "label": shown, "command": command, "desc": str(entry.get("desc", ""))}
|
|
185
|
+
)
|
|
186
|
+
return rows
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _banner(
|
|
190
|
+
width: int, scale: float, icon_uri: str, banner_uri: str, name: str, sub: str, version: str
|
|
191
|
+
) -> Tuple[str, int]:
|
|
192
|
+
height = int(550 * scale)
|
|
193
|
+
|
|
194
|
+
def at(x: float, y: float) -> str:
|
|
195
|
+
return f"left:{x * scale:.1f}px;top:{y * scale:.1f}px"
|
|
196
|
+
|
|
197
|
+
parts = [f'<div class="layer" style="left:0;top:0"><img src="{banner_uri}" '
|
|
198
|
+
f'style="width:{width}px;height:{height}px"></div>']
|
|
199
|
+
if icon_uri:
|
|
200
|
+
parts.append(
|
|
201
|
+
f'<img src="{icon_uri}" style="position:absolute;{at(89, 550 - 212)};'
|
|
202
|
+
f'width:{128 * scale:.1f}px;height:{128 * scale:.1f}px">'
|
|
203
|
+
)
|
|
204
|
+
parts.append(
|
|
205
|
+
f'<div class="t" style="{at(262, 550 - 172)};font-size:{50 * scale:.1f}px;'
|
|
206
|
+
f'font-weight:700">{escape(name)}帮助</div>'
|
|
207
|
+
)
|
|
208
|
+
parts.append(
|
|
209
|
+
f'<div class="t" style="{at(262, 550 - 117)};font-size:{30 * scale:.1f}px;'
|
|
210
|
+
f'color:#cecece">{escape(sub)}</div>'
|
|
211
|
+
)
|
|
212
|
+
if version:
|
|
213
|
+
# 画布版把版本号画成紧跟标题的色块, 这里按标题字宽估一个横向位置
|
|
214
|
+
title = f"{name}帮助"
|
|
215
|
+
em = sum(1.0 if "一" <= c <= "鿿" else 0.56 for c in title)
|
|
216
|
+
left = (262 + em * 50 + 16) * scale
|
|
217
|
+
parts.append(
|
|
218
|
+
f'<div class="badge" style="left:{left:.1f}px;top:{(550 - 172) * scale:.1f}px;'
|
|
219
|
+
f'height:{44 * scale:.1f}px;background:rgb(252,69,69);font-size:{30 * scale:.1f}px">'
|
|
220
|
+
f'<span>{escape(version)}</span></div>'
|
|
221
|
+
)
|
|
222
|
+
return "".join(parts), height
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def get_new_help(
|
|
226
|
+
plugin_name: str = "",
|
|
227
|
+
plugin_info: Optional[Dict[str, str]] = None,
|
|
228
|
+
plugin_icon: Any = None,
|
|
229
|
+
plugin_help: Optional[Dict[str, Any]] = None,
|
|
230
|
+
plugin_prefix: str = "",
|
|
231
|
+
pm: int = 6,
|
|
232
|
+
banner_sub_text: str = "",
|
|
233
|
+
banner_bg: Any = None,
|
|
234
|
+
help_bg: Any = None,
|
|
235
|
+
cag_bg: Any = None,
|
|
236
|
+
item_bg: Any = None,
|
|
237
|
+
icon_path: Any = None,
|
|
238
|
+
footer: Any = None,
|
|
239
|
+
**_ignored: Any,
|
|
240
|
+
) -> bytes:
|
|
241
|
+
from rover.html_actions import annotate_html
|
|
242
|
+
|
|
243
|
+
width = 120 + CELL_W * COLUMN
|
|
244
|
+
scale = width / BANNER_W
|
|
245
|
+
cag_h = int(100 * scale)
|
|
246
|
+
cache: Dict[str, str] = {}
|
|
247
|
+
icon_dir = Path(icon_path) if icon_path else None
|
|
248
|
+
|
|
249
|
+
groups = []
|
|
250
|
+
for cat, cat_data in (plugin_help or {}).items():
|
|
251
|
+
cat_pm = cat_data.get("pm")
|
|
252
|
+
if isinstance(cat_pm, int) and pm > cat_pm:
|
|
253
|
+
continue
|
|
254
|
+
rows = _entries(cat, cat_data, plugin_prefix, pm)
|
|
255
|
+
if rows:
|
|
256
|
+
groups.append((cat, str(cat_data.get("desc", "")), rows))
|
|
257
|
+
|
|
258
|
+
# 本机自带的运维命令上游帮助里没有, 并进同一张图, 不再单独发一段文字
|
|
259
|
+
from rover.builtin import ops_help_entries
|
|
260
|
+
|
|
261
|
+
ops = _entries("本机运维", {"data": ops_help_entries()}, plugin_prefix, pm)
|
|
262
|
+
if ops:
|
|
263
|
+
groups.append(("本机运维", "更新、重启与状态", ops))
|
|
264
|
+
|
|
265
|
+
footer_uri = _uri(footer)
|
|
266
|
+
footer_h = getattr(footer, "height", 0) or 0
|
|
267
|
+
footer_w = getattr(footer, "width", 0) or 0
|
|
268
|
+
|
|
269
|
+
banner_html, banner_h = _banner(
|
|
270
|
+
width,
|
|
271
|
+
scale,
|
|
272
|
+
_uri(plugin_icon),
|
|
273
|
+
_uri(banner_bg, "JPEG"),
|
|
274
|
+
plugin_name,
|
|
275
|
+
banner_sub_text,
|
|
276
|
+
next(iter(plugin_info or {}), ""),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
cag_uri = _uri(cag_bg)
|
|
280
|
+
item_uri = _uri(item_bg)
|
|
281
|
+
body: List[str] = [banner_html]
|
|
282
|
+
hs = 0
|
|
283
|
+
for cat, desc, rows in groups:
|
|
284
|
+
top = banner_h + hs - 14 * scale
|
|
285
|
+
body.append(
|
|
286
|
+
f'<div class="layer" style="top:{top:.1f}px"><img src="{cag_uri}" '
|
|
287
|
+
f'style="width:{width}px;height:{cag_h}px"></div>'
|
|
288
|
+
f'<div class="t" style="left:{136 * scale:.1f}px;top:{top + 50 * scale:.1f}px;'
|
|
289
|
+
f'font-size:{45 * scale:.1f}px;font-weight:700">{escape(cat)}</div>'
|
|
290
|
+
f'<div class="t" style="left:{(136 + len(cat) * 45 + 15) * scale:.1f}px;'
|
|
291
|
+
f'top:{top + 55 * scale:.1f}px;font-size:{30 * scale:.1f}px;color:#cecece">'
|
|
292
|
+
f"{escape(desc)}</div>"
|
|
293
|
+
)
|
|
294
|
+
last_row = (len(rows) - 1) // COLUMN
|
|
295
|
+
for index, row in enumerate(rows):
|
|
296
|
+
icon_file = _find_icon(row["name"], icon_dir) if icon_dir else None
|
|
297
|
+
icon_uri = _file_uri(icon_file, cache) if icon_file else ""
|
|
298
|
+
x = ITEM_X0 + (index % COLUMN) * CELL_W
|
|
299
|
+
y = int(banner_h + 70 * scale + (index // COLUMN) * CELL_H + hs + SOFT)
|
|
300
|
+
# 提示框贴着卡片弹出, 靠边的行/列翻向内侧, 免得被容器裁掉
|
|
301
|
+
column = index % COLUMN
|
|
302
|
+
tip_side = "up" if index // COLUMN == last_row and last_row else "down"
|
|
303
|
+
tip_side += " right" if column >= COLUMN - 2 else " left"
|
|
304
|
+
body.append(
|
|
305
|
+
f'<div class="item" style="left:{x}px;top:{y}px" '
|
|
306
|
+
f'data-ww-cmd="{escape(row["command"], quote=True)}">'
|
|
307
|
+
f'<img src="{item_uri}"><div class="hl"></div>'
|
|
308
|
+
+ (f'<img class="icon" src="{icon_uri}">' if icon_uri else "")
|
|
309
|
+
+ f'<div class="nm">{escape(_clip_name(row["name"]))}</div>'
|
|
310
|
+
f'<div class="eg">{escape(row["label"])}</div>'
|
|
311
|
+
f'<div class="tip {tip_side}"><span class="k">{escape(row["command"])}</span>'
|
|
312
|
+
f'{escape(row["desc"])}</div></div>'
|
|
313
|
+
)
|
|
314
|
+
hs += ((len(rows) - 1) // COLUMN + 1) * CELL_H + cag_h + SOFT
|
|
315
|
+
|
|
316
|
+
height = banner_h + hs + footer_h + 20
|
|
317
|
+
if footer_uri:
|
|
318
|
+
body.append(
|
|
319
|
+
f'<img src="{footer_uri}" style="position:absolute;left:{(width - footer_w) // 2}px;'
|
|
320
|
+
f'top:{height - footer_h - 20}px">'
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
bg_uri = _uri(help_bg, "JPEG")
|
|
324
|
+
html = (
|
|
325
|
+
'<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">'
|
|
326
|
+
f"<style>{_CSS}</style></head><body>"
|
|
327
|
+
f'<div class="container" style="width:{width}px;height:{height}px">'
|
|
328
|
+
+ (f'<img class="bg" src="{bg_uri}">' if bg_uri else "")
|
|
329
|
+
+ "".join(body)
|
|
330
|
+
+ "</div></body></html>"
|
|
331
|
+
)
|
|
332
|
+
return HtmlBytes(annotate_html(html, "help", {}), "help")
|