lockbot 2.8.0__tar.gz → 2.9.1__tar.gz
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.
- {lockbot-2.8.0/python/lockbot.egg-info → lockbot-2.9.1}/PKG-INFO +1 -1
- {lockbot-2.8.0 → lockbot-2.9.1}/pyproject.toml +1 -1
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/bots/router.py +34 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/bots/schemas.py +15 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/base_bot.py +80 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/device_bot.py +9 -4
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/device_usage_utils.py +3 -2
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/handler.py +2 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/i18n/en.py +6 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/i18n/zh.py +6 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/node_bot.py +14 -8
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/queue_bot.py +13 -7
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/utils.py +2 -1
- {lockbot-2.8.0 → lockbot-2.9.1/python/lockbot.egg-info}/PKG-INFO +1 -1
- {lockbot-2.8.0 → lockbot-2.9.1}/LICENSE +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/MANIFEST.in +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/README.md +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/admin/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/admin/router.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/audit/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/audit/models.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/audit/router.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/audit/service.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/auth/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/auth/dependencies.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/auth/models.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/auth/router.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/auth/schemas.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/backup/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/backup/router.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/backup/scheduler.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/backup/service.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/bots/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/bots/encryption.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/bots/manager.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/bots/models.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/bots/webhook_handler.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/config.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/database.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/logs/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/main.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/rate_limit.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/settings/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/settings/models.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/backend/app/settings/router.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/bot_instance.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/config.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/device_usage_alert.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/entry.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/env.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/i18n/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/io.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/message_adapter.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/msg_utils.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/platforms/__init__.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/platforms/infoflow.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/request.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot/core/scheduler.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot.egg-info/SOURCES.txt +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot.egg-info/dependency_links.txt +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot.egg-info/requires.txt +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/python/lockbot.egg-info/top_level.txt +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/setup.cfg +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/tools/create_super_admin.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/tools/gen_keys.py +0 -0
- {lockbot-2.8.0 → lockbot-2.9.1}/tools/reset_super_admin_password.py +0 -0
|
@@ -127,6 +127,33 @@ def _normalize_cluster_configs(cc):
|
|
|
127
127
|
return cc
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
def _get_node_keys(cc) -> set:
|
|
131
|
+
"""Extract node key set from cluster_configs (any format)."""
|
|
132
|
+
if _is_device_array_format(cc):
|
|
133
|
+
return {item["node_key"] for item in cc}
|
|
134
|
+
if isinstance(cc, dict):
|
|
135
|
+
return set(cc.keys())
|
|
136
|
+
if isinstance(cc, list):
|
|
137
|
+
return set(cc)
|
|
138
|
+
return set()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate_node_aliases(config_overrides, cluster_configs):
|
|
142
|
+
"""Raise HTTPException if NODE_ALIASES contains keys not in cluster_configs."""
|
|
143
|
+
if not config_overrides:
|
|
144
|
+
return
|
|
145
|
+
aliases = config_overrides.get("NODE_ALIASES")
|
|
146
|
+
if not aliases:
|
|
147
|
+
return
|
|
148
|
+
valid_keys = _get_node_keys(cluster_configs)
|
|
149
|
+
invalid = set(aliases.keys()) - valid_keys
|
|
150
|
+
if invalid:
|
|
151
|
+
raise HTTPException(
|
|
152
|
+
status_code=422,
|
|
153
|
+
detail=f"NODE_ALIASES contains keys not in cluster_configs: {sorted(invalid)}",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
130
157
|
def _device_dict_to_array(d: dict) -> list:
|
|
131
158
|
"""Convert DEVICE cluster_configs from dict to ordered array for API response.
|
|
132
159
|
|
|
@@ -214,6 +241,8 @@ def create_bot(
|
|
|
214
241
|
if body.bot_type.upper() not in VALID_BOT_TYPES:
|
|
215
242
|
raise HTTPException(status_code=422, detail=f"Invalid bot_type, must be one of {VALID_BOT_TYPES}")
|
|
216
243
|
|
|
244
|
+
_validate_node_aliases(body.config_overrides, body.cluster_configs)
|
|
245
|
+
|
|
217
246
|
exists = (
|
|
218
247
|
db.query(Bot)
|
|
219
248
|
.filter(
|
|
@@ -349,6 +378,11 @@ def update_bot(
|
|
|
349
378
|
if user.role != "super_admin" and bot.user_id != user.id:
|
|
350
379
|
raise HTTPException(status_code=403, detail="Cannot edit another user's bot")
|
|
351
380
|
|
|
381
|
+
# Validate NODE_ALIASES keys against cluster_configs
|
|
382
|
+
if body.config_overrides and body.config_overrides.get("NODE_ALIASES"):
|
|
383
|
+
cc = body.cluster_configs if body.cluster_configs is not None else json.loads(bot.cluster_configs)
|
|
384
|
+
_validate_node_aliases(body.config_overrides, cc)
|
|
385
|
+
|
|
352
386
|
changes = {}
|
|
353
387
|
if body.name is not None:
|
|
354
388
|
# Check for duplicate name (excluding current bot and deleted bots)
|
|
@@ -57,6 +57,21 @@ def _validate_config_overrides(v: dict | None) -> dict | None:
|
|
|
57
57
|
if whitelist is not None and (not isinstance(whitelist, list) or not all(isinstance(u, str) for u in whitelist)):
|
|
58
58
|
errors.append("DURATION_WHITELIST must be a list of strings")
|
|
59
59
|
|
|
60
|
+
node_aliases = v.get("NODE_ALIASES")
|
|
61
|
+
if node_aliases is not None and (
|
|
62
|
+
not isinstance(node_aliases, dict)
|
|
63
|
+
or not all(isinstance(k, str) and isinstance(a, str) and len(a) <= 15 for k, a in node_aliases.items())
|
|
64
|
+
):
|
|
65
|
+
errors.append("NODE_ALIASES must be a dict of {string: string} with alias length <= 15")
|
|
66
|
+
|
|
67
|
+
show_alias = v.get("SHOW_NODE_ALIAS")
|
|
68
|
+
if show_alias is not None and not isinstance(show_alias, bool):
|
|
69
|
+
errors.append("SHOW_NODE_ALIAS must be a boolean")
|
|
70
|
+
|
|
71
|
+
allow_alias = v.get("ALLOW_USER_ALIAS")
|
|
72
|
+
if allow_alias is not None and not isinstance(allow_alias, bool):
|
|
73
|
+
errors.append("ALLOW_USER_ALIAS must be a boolean")
|
|
74
|
+
|
|
60
75
|
max_dur = v.get("MAX_LOCK_DURATION")
|
|
61
76
|
default_dur = v.get("DEFAULT_DURATION")
|
|
62
77
|
if isinstance(max_dur, int) and isinstance(default_dur, int) and max_dur != -1 and default_dur > max_dur:
|
|
@@ -121,6 +121,86 @@ class BaseLockBot:
|
|
|
121
121
|
"""
|
|
122
122
|
return self.adapter.build_reply("\u274c" + error_msg, [user_id])
|
|
123
123
|
|
|
124
|
+
@property
|
|
125
|
+
def _duration_whitelist(self):
|
|
126
|
+
return self.config.get_val("DURATION_WHITELIST") or []
|
|
127
|
+
|
|
128
|
+
def _display_node(self, node_key):
|
|
129
|
+
"""Return node_key with alias suffix if SHOW_NODE_ALIAS is enabled."""
|
|
130
|
+
if not self.config.get_val("SHOW_NODE_ALIAS"):
|
|
131
|
+
return node_key
|
|
132
|
+
aliases = self.config.get_val("NODE_ALIASES") or {}
|
|
133
|
+
alias = aliases.get(node_key)
|
|
134
|
+
return f"{node_key}「{alias}」" if alias else node_key
|
|
135
|
+
|
|
136
|
+
def alias(self, user_id, command):
|
|
137
|
+
"""Set or clear a node alias. Usage: alias <node_key> [alias_value]"""
|
|
138
|
+
if not self.config.get_val("ALLOW_USER_ALIAS"):
|
|
139
|
+
return self.show_error(user_id, t("alias.disabled", config=self.config))
|
|
140
|
+
|
|
141
|
+
import re
|
|
142
|
+
|
|
143
|
+
m = re.match(r"^\s*alias\s+([\w\d_-]+)(?:\s+(.{1,15}))?\s*$", command)
|
|
144
|
+
if not m:
|
|
145
|
+
return self.show_error(user_id, t("alias.usage", config=self.config))
|
|
146
|
+
|
|
147
|
+
node_key = m.group(1)
|
|
148
|
+
alias_val = (m.group(2) or "").strip()
|
|
149
|
+
|
|
150
|
+
cluster_configs = self.config.get_val("CLUSTER_CONFIGS")
|
|
151
|
+
if node_key not in cluster_configs:
|
|
152
|
+
return self.show_error(
|
|
153
|
+
user_id,
|
|
154
|
+
t(
|
|
155
|
+
"alias.invalid_node",
|
|
156
|
+
config=self.config,
|
|
157
|
+
node_key=node_key,
|
|
158
|
+
valid_keys=str(list(cluster_configs.keys())),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
aliases = self.config.get_val("NODE_ALIASES") or {}
|
|
163
|
+
if alias_val:
|
|
164
|
+
aliases[node_key] = alias_val
|
|
165
|
+
else:
|
|
166
|
+
aliases.pop(node_key, None)
|
|
167
|
+
self.config.set_val("NODE_ALIASES", aliases)
|
|
168
|
+
|
|
169
|
+
# Persist to DB if running under platform mode
|
|
170
|
+
self._persist_config_override("NODE_ALIASES", aliases)
|
|
171
|
+
|
|
172
|
+
if alias_val:
|
|
173
|
+
self.logger.info("user=%s set alias %s -> %s", user_id, node_key, alias_val)
|
|
174
|
+
msg = t("alias.set_ok", config=self.config, node_key=node_key, alias=alias_val)
|
|
175
|
+
else:
|
|
176
|
+
self.logger.info("user=%s cleared alias %s", user_id, node_key)
|
|
177
|
+
msg = t("alias.clear_ok", config=self.config, node_key=node_key)
|
|
178
|
+
return self.adapter.build_reply(msg, [user_id])
|
|
179
|
+
|
|
180
|
+
def _persist_config_override(self, key, value):
|
|
181
|
+
"""Persist a single config_override key to DB (best-effort)."""
|
|
182
|
+
try:
|
|
183
|
+
import json
|
|
184
|
+
|
|
185
|
+
from lockbot.backend.app.bots.models import Bot
|
|
186
|
+
from lockbot.backend.app.database import SessionLocal
|
|
187
|
+
|
|
188
|
+
bot_id = self.config.get_val("BOT_ID")
|
|
189
|
+
if not bot_id:
|
|
190
|
+
return
|
|
191
|
+
db = SessionLocal()
|
|
192
|
+
try:
|
|
193
|
+
bot = db.query(Bot).filter(Bot.id == int(bot_id)).first()
|
|
194
|
+
if bot:
|
|
195
|
+
overrides = json.loads(bot.config_overrides) if bot.config_overrides else {}
|
|
196
|
+
overrides[key] = value
|
|
197
|
+
bot.config_overrides = json.dumps(overrides, ensure_ascii=False)
|
|
198
|
+
db.commit()
|
|
199
|
+
finally:
|
|
200
|
+
db.close()
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
124
204
|
def _whitelist_hint(self):
|
|
125
205
|
"""Return whitelist hint string if BOT_OWNER is configured, else empty string."""
|
|
126
206
|
owner = self.config.get_val("BOT_OWNER")
|
|
@@ -39,7 +39,7 @@ class DeviceBot(BaseLockBot):
|
|
|
39
39
|
_loader = staticmethod(create_or_load_device_state)
|
|
40
40
|
|
|
41
41
|
def supported_commands(self):
|
|
42
|
-
return ["lock", "slock", "unlock", "free", "kickout", "help", "h", "query"]
|
|
42
|
+
return ["lock", "slock", "unlock", "free", "kickout", "help", "h", "query", "alias"]
|
|
43
43
|
|
|
44
44
|
def parse_command(self, user_id, command_key, command, parsing_duration=False):
|
|
45
45
|
"""
|
|
@@ -177,7 +177,7 @@ class DeviceBot(BaseLockBot):
|
|
|
177
177
|
return error_reply
|
|
178
178
|
|
|
179
179
|
max_dur = self.config.get_val("MAX_LOCK_DURATION")
|
|
180
|
-
whitelist = self.
|
|
180
|
+
whitelist = self._duration_whitelist
|
|
181
181
|
with self._lock:
|
|
182
182
|
timestamp = int(time.time())
|
|
183
183
|
|
|
@@ -251,7 +251,7 @@ class DeviceBot(BaseLockBot):
|
|
|
251
251
|
return error_reply
|
|
252
252
|
|
|
253
253
|
max_dur = self.config.get_val("MAX_LOCK_DURATION")
|
|
254
|
-
whitelist = self.
|
|
254
|
+
whitelist = self._duration_whitelist
|
|
255
255
|
with self._lock:
|
|
256
256
|
timestamp = int(time.time())
|
|
257
257
|
for node_key, dev_ids in zip(node_key_list, dev_ids_list, strict=True):
|
|
@@ -493,6 +493,9 @@ class DeviceBot(BaseLockBot):
|
|
|
493
493
|
text += t("help.query_node_example", config=self.config, node=example_node)
|
|
494
494
|
text += "\n"
|
|
495
495
|
|
|
496
|
+
if self.config.get_val("ALLOW_USER_ALIAS"):
|
|
497
|
+
text += f"6. 备注: alias {example_node} <备注>\n\n"
|
|
498
|
+
|
|
496
499
|
return text
|
|
497
500
|
|
|
498
501
|
def _check_and_notify(self) -> float | None:
|
|
@@ -597,4 +600,6 @@ class DeviceBot(BaseLockBot):
|
|
|
597
600
|
"""
|
|
598
601
|
Get a text description of the current cluster device usage (supports merged display and heterogeneous hints).
|
|
599
602
|
"""
|
|
600
|
-
return get_current_usage(
|
|
603
|
+
return get_current_usage(
|
|
604
|
+
node_filter, self.state.bot_state, {}, config=self.config, display_node=self._display_node
|
|
605
|
+
)
|
|
@@ -157,7 +157,7 @@ def render_device_lines(node_status, grouped_usage, idle_groups, config=None):
|
|
|
157
157
|
return lines
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
def get_current_usage(node_filter, bot_state, monitor_status, config=None):
|
|
160
|
+
def get_current_usage(node_filter, bot_state, monitor_status, config=None, display_node=None):
|
|
161
161
|
"""
|
|
162
162
|
Get current device usage info including idle, locked, and in-use devices.
|
|
163
163
|
"""
|
|
@@ -169,7 +169,8 @@ def get_current_usage(node_filter, bot_state, monitor_status, config=None):
|
|
|
169
169
|
or node_key == node_filter
|
|
170
170
|
or (isinstance(node_filter, list) and node_key in node_filter)
|
|
171
171
|
):
|
|
172
|
-
|
|
172
|
+
shown_name = display_node(node_key) if display_node else node_key
|
|
173
|
+
usage_info += t("device_usage.node_header", config=config, node_key=shown_name)
|
|
173
174
|
|
|
174
175
|
grouped_usage = group_locked_devices(node_status)
|
|
175
176
|
shown_indices = set()
|
|
@@ -55,6 +55,8 @@ def execute_command(msg_data, bot):
|
|
|
55
55
|
return bot.query(user_id)
|
|
56
56
|
elif cmd in ("help", "h"):
|
|
57
57
|
return bot.print_help(user_id)
|
|
58
|
+
elif cmd == "alias":
|
|
59
|
+
return bot.alias(user_id, rcv_info)
|
|
58
60
|
else:
|
|
59
61
|
return bot.print_help(user_id, t("error.unknown_error", config=getattr(bot, "config", None), command=rcv_info))
|
|
60
62
|
return bot.print_help(user_id, t("error.unknown_error", config=getattr(bot, "config", None), command=rcv_info))
|
|
@@ -173,6 +173,12 @@ MESSAGES = {
|
|
|
173
173
|
"opkick.invalid_format": "Invalid opkick format. Usage: opkick <username> [node] [dev x,y]",
|
|
174
174
|
"opkick.notify_kicked": "⚠️ [Operator Release]\n\nNode: {node_key}\n",
|
|
175
175
|
"opkick.notify_kicked_reason": "Reason: {reason}\n\n",
|
|
176
|
+
# ── Alias command ──
|
|
177
|
+
"alias.usage": "Usage: alias <node_key> [label]\nExample: alias node0 10.10.10.56\nClear: alias node0",
|
|
178
|
+
"alias.invalid_node": "Node {node_key} does not exist. Available: {valid_keys}\n",
|
|
179
|
+
"alias.set_ok": "✅ Set {node_key} alias to「{alias}」\n\n",
|
|
180
|
+
"alias.clear_ok": "✅ Cleared alias for {node_key}\n\n",
|
|
181
|
+
"alias.disabled": "User alias command is not enabled for this bot",
|
|
176
182
|
# ── Webhook: bot not running ──
|
|
177
183
|
"webhook.bot_not_running": "⚠️ Bot {bot_name}(ID:{bot_id}) is not running. " # noqa: E501
|
|
178
184
|
"Please contact the owner @{owner_username} to start it.",
|
|
@@ -166,6 +166,12 @@ MESSAGES = {
|
|
|
166
166
|
"opkick.invalid_format": "opkick命令格式错误,正确格式: opkick <username> [node] [dev x,y]",
|
|
167
167
|
"opkick.notify_kicked": "⚠️【运维释放】\n\n节点: {node_key}\n",
|
|
168
168
|
"opkick.notify_kicked_reason": "原因: {reason}\n\n",
|
|
169
|
+
# ── Alias command ──
|
|
170
|
+
"alias.usage": "用法: alias <节点名> [备注]\n示例: alias node0 10.10.10.56\n清除: alias node0",
|
|
171
|
+
"alias.invalid_node": "节点 {node_key} 不存在,可用节点: {valid_keys}\n",
|
|
172
|
+
"alias.set_ok": "✅ 已设置 {node_key} 备注为「{alias}」\n\n",
|
|
173
|
+
"alias.clear_ok": "✅ 已清除 {node_key} 的备注\n\n",
|
|
174
|
+
"alias.disabled": "该机器人未开启用户自助设置备注功能",
|
|
169
175
|
# ── Webhook: bot not running ──
|
|
170
176
|
"webhook.bot_not_running": (
|
|
171
177
|
"⚠️ 机器人 {bot_name}(ID:{bot_id}) 尚未启动,请联系管理人 @{owner_username} 启动后再使用。"
|
|
@@ -37,7 +37,7 @@ class NodeBot(BaseLockBot):
|
|
|
37
37
|
_loader = staticmethod(create_or_load_node_state)
|
|
38
38
|
|
|
39
39
|
def supported_commands(self):
|
|
40
|
-
return ["lock", "slock", "unlock", "free", "kickout", "help", "h", "query"]
|
|
40
|
+
return ["lock", "slock", "unlock", "free", "kickout", "help", "h", "query", "alias"]
|
|
41
41
|
|
|
42
42
|
def parse_command(self, user_id, command_key, command, parsing_duration=False):
|
|
43
43
|
"""
|
|
@@ -111,7 +111,7 @@ class NodeBot(BaseLockBot):
|
|
|
111
111
|
return error_reply
|
|
112
112
|
|
|
113
113
|
max_dur = self.config.get_val("MAX_LOCK_DURATION")
|
|
114
|
-
whitelist = self.
|
|
114
|
+
whitelist = self._duration_whitelist
|
|
115
115
|
with self._lock:
|
|
116
116
|
nodes = [self.state.bot_state[node_key] for node_key in node_keys]
|
|
117
117
|
if not all(
|
|
@@ -171,7 +171,7 @@ class NodeBot(BaseLockBot):
|
|
|
171
171
|
return error_reply
|
|
172
172
|
|
|
173
173
|
max_dur = self.config.get_val("MAX_LOCK_DURATION")
|
|
174
|
-
whitelist = self.
|
|
174
|
+
whitelist = self._duration_whitelist
|
|
175
175
|
with self._lock:
|
|
176
176
|
nodes = [self.state.bot_state[node_key] for node_key in node_keys]
|
|
177
177
|
if not all(node["status"] != "exclusive" for node in nodes):
|
|
@@ -358,7 +358,12 @@ class NodeBot(BaseLockBot):
|
|
|
358
358
|
reply_info += t("help.section4_title", config=self.config)
|
|
359
359
|
reply_info += t("help.section5_title", config=self.config)
|
|
360
360
|
reply_info += t("help.query_at_bot", config=self.config)
|
|
361
|
-
reply_info += f" {example_node0}\n
|
|
361
|
+
reply_info += f" {example_node0}\n"
|
|
362
|
+
|
|
363
|
+
if self.config.get_val("ALLOW_USER_ALIAS"):
|
|
364
|
+
reply_info += f"6. 备注: alias {example_node0} <备注>\n"
|
|
365
|
+
|
|
366
|
+
reply_info += "\n"
|
|
362
367
|
return reply_info
|
|
363
368
|
|
|
364
369
|
def _check_and_notify(self) -> float | None:
|
|
@@ -397,7 +402,7 @@ class NodeBot(BaseLockBot):
|
|
|
397
402
|
|
|
398
403
|
uid = user_info["user_id"] + format_access_mode(node["status"], config=self.config)
|
|
399
404
|
duration = format_duration(remaining_time, config=self.config)
|
|
400
|
-
alert_info += f"{node_key} {uid} {duration}\n"
|
|
405
|
+
alert_info += f"{self._display_node(node_key)} {uid} {duration}\n"
|
|
401
406
|
|
|
402
407
|
if EARLY_NOTIFY and not user_info["is_notified"] and 0 < remaining_time <= TIME_ALERT:
|
|
403
408
|
trigger_time_alert = True
|
|
@@ -407,7 +412,7 @@ class NodeBot(BaseLockBot):
|
|
|
407
412
|
|
|
408
413
|
uid = user_info["user_id"] + format_access_mode(node["status"], config=self.config)
|
|
409
414
|
duration = format_duration(remaining_time, config=self.config)
|
|
410
|
-
alert_info += f"{node_key} {uid} {duration}\n"
|
|
415
|
+
alert_info += f"{self._display_node(node_key)} {uid} {duration}\n"
|
|
411
416
|
|
|
412
417
|
for user_id in removed_users_id:
|
|
413
418
|
remove_user_info(node["current_users"], user_id)
|
|
@@ -453,11 +458,12 @@ class NodeBot(BaseLockBot):
|
|
|
453
458
|
usage_info = ""
|
|
454
459
|
for node_key, node_status in nodes.items():
|
|
455
460
|
if node_filter is None or node_key == node_filter:
|
|
461
|
+
display_name = self._display_node(node_key)
|
|
456
462
|
if node_status["status"] == "idle":
|
|
457
|
-
usage_info += "{:} {}\n".format(
|
|
463
|
+
usage_info += "{:} {}\n".format(display_name, t("status.idle", config=self.config))
|
|
458
464
|
else:
|
|
459
465
|
for user_idx, user_info in enumerate(node_status["current_users"]):
|
|
460
|
-
node_name = f"{
|
|
466
|
+
node_name = f"{display_name}" if user_idx == 0 else ""
|
|
461
467
|
uid = user_info["user_id"] + format_access_mode(node_status["status"], config=self.config)
|
|
462
468
|
duration = format_duration(
|
|
463
469
|
remaining_duration(user_info["start_time"], user_info["duration"]), config=self.config
|
|
@@ -26,7 +26,7 @@ class QueueBot(NodeBot):
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
def supported_commands(self):
|
|
29
|
-
return ["lock", "unlock", "free", "kickout", "kicklock", "help", "h", "book", "take", "query"]
|
|
29
|
+
return ["lock", "unlock", "free", "kickout", "kicklock", "help", "h", "book", "take", "query", "alias"]
|
|
30
30
|
|
|
31
31
|
def lock(self, user_id, command):
|
|
32
32
|
"""
|
|
@@ -38,7 +38,7 @@ class QueueBot(NodeBot):
|
|
|
38
38
|
return error_reply
|
|
39
39
|
|
|
40
40
|
max_dur = self.config.get_val("MAX_LOCK_DURATION")
|
|
41
|
-
whitelist = self.
|
|
41
|
+
whitelist = self._duration_whitelist
|
|
42
42
|
with self._lock:
|
|
43
43
|
nodes = [self.state.bot_state[node_key] for node_key in node_keys]
|
|
44
44
|
if not all(
|
|
@@ -132,7 +132,7 @@ class QueueBot(NodeBot):
|
|
|
132
132
|
return error_reply
|
|
133
133
|
|
|
134
134
|
max_dur = self.config.get_val("MAX_LOCK_DURATION")
|
|
135
|
-
whitelist = self.
|
|
135
|
+
whitelist = self._duration_whitelist
|
|
136
136
|
with self._lock:
|
|
137
137
|
nodes = [self.state.bot_state[node_key] for node_key in node_keys]
|
|
138
138
|
if any(
|
|
@@ -174,7 +174,7 @@ class QueueBot(NodeBot):
|
|
|
174
174
|
content = t("success.take_success_by", config=self.config, user_id=user_id)
|
|
175
175
|
content += self._msg_with_usage("label.before_take")
|
|
176
176
|
max_dur = self.config.get_val("MAX_LOCK_DURATION")
|
|
177
|
-
whitelist = self.
|
|
177
|
+
whitelist = self._duration_whitelist
|
|
178
178
|
with self._lock:
|
|
179
179
|
nodes = [self.state.bot_state[node_key] for node_key in node_keys]
|
|
180
180
|
if any(find_user_info(node["current_users"], user_id) for node in nodes):
|
|
@@ -345,7 +345,12 @@ class QueueBot(NodeBot):
|
|
|
345
345
|
parts.append(t("help.section_help_title_queue", config=self.config))
|
|
346
346
|
parts.append(t("help.section_query_title_queue", config=self.config))
|
|
347
347
|
parts.append(t("help.query_at_bot", config=self.config))
|
|
348
|
-
parts.append(f" {example_node0}\n
|
|
348
|
+
parts.append(f" {example_node0}\n")
|
|
349
|
+
|
|
350
|
+
if self.config.get_val("ALLOW_USER_ALIAS"):
|
|
351
|
+
parts.append(f"8. 备注: alias {example_node0} <备注>\n")
|
|
352
|
+
|
|
353
|
+
parts.append("\n")
|
|
349
354
|
return "".join(parts)
|
|
350
355
|
|
|
351
356
|
def _check_and_notify(self) -> float | None:
|
|
@@ -522,11 +527,12 @@ class QueueBot(NodeBot):
|
|
|
522
527
|
usage_info = ""
|
|
523
528
|
for node_key, node_status in nodes.items():
|
|
524
529
|
if node_filter is None or node_key == node_filter:
|
|
530
|
+
display_name = self._display_node(node_key)
|
|
525
531
|
if node_status["status"] == "idle":
|
|
526
|
-
usage_info += "{:} {}\n".format(
|
|
532
|
+
usage_info += "{:} {}\n".format(display_name, t("status.idle", config=self.config))
|
|
527
533
|
else:
|
|
528
534
|
for user_idx, user_info in enumerate(node_status["current_users"]):
|
|
529
|
-
node_name = f"{
|
|
535
|
+
node_name = f"{display_name}" if user_idx == 0 else ""
|
|
530
536
|
uid = user_info["user_id"]
|
|
531
537
|
duration = format_duration(
|
|
532
538
|
remaining_duration(user_info["start_time"], user_info["duration"]),
|
|
@@ -77,7 +77,8 @@ def apply_max_duration_limit(user_list, max_duration, whitelist=None):
|
|
|
77
77
|
Returns a list of user_ids whose durations were clamped.
|
|
78
78
|
Whitelist users are skipped.
|
|
79
79
|
"""
|
|
80
|
-
|
|
80
|
+
if whitelist is None:
|
|
81
|
+
whitelist = []
|
|
81
82
|
clamped_users = []
|
|
82
83
|
for user_info in user_list:
|
|
83
84
|
if user_info["user_id"] in whitelist:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|