@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.
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,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")
@@ -0,0 +1,16 @@
1
+ from typing import List, TypedDict
2
+
3
+
4
+ class PluginSV(TypedDict):
5
+ name: str
6
+ desc: str
7
+ eg: str
8
+ highlight: int
9
+ need_ck: bool
10
+ need_sk: bool
11
+ need_admin: bool
12
+
13
+
14
+ class PluginHelp(TypedDict):
15
+ desc: str
16
+ data: List[PluginSV]
@@ -0,0 +1,7 @@
1
+ """帮助注册桩。帮助图由各插件自己的 get_help_data 渲染, 这里的登记无人消费。"""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def register_help(name: str, help: str, icon: Any = None) -> None:
7
+ return None