fix(agent): grant channel owners admin access

This commit is contained in:
jxxghp
2026-08-13 23:46:56 +08:00
parent 73df2aa331
commit 87b1caf3ff
16 changed files with 499 additions and 120 deletions

View File

@@ -5,7 +5,11 @@ from urllib.parse import quote, unquote
from app.core.context import MediaInfo, Context
from app.core.event import eventmanager
from app.helper.agent import matches_channel_admin
from app.helper.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.schemas import (
@@ -26,6 +30,12 @@ except Exception as err: # ImportError or other load issues
logger.error(f"Discord 模块未加载,缺少依赖或初始化错误:{err}")
register_channel_admin_resolver(
MessageChannel.Discord,
lambda config: resolve_config_principal_ids(config, "DISCORD_ADMINS"),
)
class DiscordModule(_ModuleBase, _MessageBase[Discord]):
_IMAGE_SUFFIXES = (
".png",
@@ -209,7 +219,7 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
userid=userid,
username=username,
is_channel_admin=matches_channel_admin(
client_config.config, "DISCORD_ADMINS", userid
MessageChannel.Discord, client_config.config, userid
),
text=f"CALLBACK:{callback_data}",
is_callback=True,
@@ -243,7 +253,7 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
userid=userid,
username=username,
is_channel_admin=matches_channel_admin(
client_config.config, "DISCORD_ADMINS", userid
MessageChannel.Discord, client_config.config, userid
),
text=text,
chat_id=str(chat_id) if chat_id else None,

View File

@@ -1,6 +1,7 @@
from typing import Any, List, Optional, Tuple, Union
from app.core.context import Context, MediaInfo
from app.helper.agent import register_channel_admin_resolver, resolve_config_principal_ids
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.feishu.feishu import Feishu
@@ -8,6 +9,14 @@ from app.schemas import CommingMessage, MessageChannel, MessageResponse, Notific
from app.schemas.types import ModuleType
register_channel_admin_resolver(
MessageChannel.Feishu,
lambda config: resolve_config_principal_ids(
config, "FEISHU_ADMINS", "FEISHU_OPEN_ID"
),
)
class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
def init_module(self) -> None:
super().init_service(service_name=Feishu.__name__.lower(), service_type=Feishu)

View File

@@ -115,12 +115,14 @@ class Feishu:
"""判断飞书命令或命令型按钮回调是否应因非管理员身份被拒绝。"""
if not self._admins:
return False
candidates = [
str(user_id).strip()
for user_id in user_ids
if user_id is not None and str(user_id).strip()
]
return not any(candidate in self._admins for candidate in candidates)
return not matches_channel_admin(
MessageChannel.Feishu,
{
"FEISHU_ADMINS": ",".join(self._admins),
"FEISHU_OPEN_ID": self._default_open_id,
},
*user_ids,
)
def _build_api_client(self) -> lark.Client:
"""构建飞书 OpenAPI client用于发送和编辑消息。"""
@@ -689,8 +691,11 @@ class Feishu:
userid=userid,
username=username,
is_channel_admin=matches_channel_admin(
{"FEISHU_ADMINS": ",".join(self._admins)},
"FEISHU_ADMINS",
MessageChannel.Feishu,
{
"FEISHU_ADMINS": ",".join(self._admins),
"FEISHU_OPEN_ID": self._default_open_id,
},
open_id,
user_id,
),
@@ -732,8 +737,11 @@ class Feishu:
userid=userid,
username=username,
is_channel_admin=matches_channel_admin(
{"FEISHU_ADMINS": ",".join(self._admins)},
"FEISHU_ADMINS",
MessageChannel.Feishu,
{
"FEISHU_ADMINS": ",".join(self._admins),
"FEISHU_OPEN_ID": self._default_open_id,
},
open_id,
user_id,
),

View File

@@ -9,7 +9,11 @@ from urllib.parse import quote, unquote
from typing import Optional, List, Tuple, Union, Any
from app.core.context import MediaInfo, Context
from app.helper.agent import matches_channel_admin
from app.helper.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.qqbot.qqbot import QQBot
@@ -18,6 +22,14 @@ from app.schemas.types import ModuleType
from app.utils.http import RequestUtils
register_channel_admin_resolver(
MessageChannel.QQ,
lambda config: resolve_config_principal_ids(
config, "QQBOT_ADMINS", "QQ_OPENID"
),
)
class QQBotModule(_ModuleBase, _MessageBase[QQBot]):
"""QQ Bot 通知模块"""
@@ -108,12 +120,11 @@ class QQBotModule(_ModuleBase, _MessageBase[QQBot]):
admins = cls._get_admins(config)
if not admins:
return False
candidates = [
str(user_id).strip()
for user_id in user_ids
if user_id is not None and str(user_id).strip()
]
return not any(candidate in admins for candidate in candidates)
return not matches_channel_admin(
MessageChannel.QQ,
config,
*user_ids,
)
@staticmethod
def _send_admin_denied(
@@ -176,7 +187,9 @@ class QQBotModule(_ModuleBase, _MessageBase[QQBot]):
userid=user_openid,
username=user_openid,
is_channel_admin=matches_channel_admin(
client_config.config, "QQBOT_ADMINS", user_openid
MessageChannel.QQ,
client_config.config,
user_openid,
),
text=content,
images=images,
@@ -205,7 +218,9 @@ class QQBotModule(_ModuleBase, _MessageBase[QQBot]):
userid=userid,
username=member_openid or group_openid,
is_channel_admin=matches_channel_admin(
client_config.config, "QQBOT_ADMINS", member_openid
MessageChannel.QQ,
client_config.config,
member_openid,
),
text=content,
images=images,

View File

@@ -6,7 +6,11 @@ from urllib.parse import quote, unquote
from app.core.context import MediaInfo, Context
from app.core.event import eventmanager
from app.helper.agent import matches_channel_admin
from app.helper.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.slack.slack import Slack
@@ -21,6 +25,12 @@ from app.schemas.types import ChainEventType, ModuleType
from app.utils.structures import DictUtils
register_channel_admin_resolver(
MessageChannel.Slack,
lambda config: resolve_config_principal_ids(config, "SLACK_ADMINS"),
)
class SlackModule(_ModuleBase, _MessageBase[Slack]):
PROCESSING_REACTION = "eyes"
_AUDIO_SUFFIXES = (
@@ -321,7 +331,7 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
userid=userid,
username=username,
is_channel_admin=matches_channel_admin(
client_config.config, "SLACK_ADMINS", userid
MessageChannel.Slack, client_config.config, userid
),
text=text,
is_callback=True,
@@ -378,7 +388,7 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
userid=userid,
username=username,
is_channel_admin=matches_channel_admin(
client_config.config, "SLACK_ADMINS", userid
MessageChannel.Slack, client_config.config, userid
),
text=text,
message_id=message_id,

View File

@@ -3,7 +3,11 @@ from typing import Optional, Union, List, Tuple, Any
from urllib.parse import quote, unquote
from app.core.context import MediaInfo, Context
from app.helper.agent import matches_channel_admin
from app.helper.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.synologychat.synologychat import SynologyChat
@@ -12,6 +16,12 @@ from app.schemas.types import ModuleType
from app.utils.http import RequestUtils
register_channel_admin_resolver(
MessageChannel.SynologyChat,
lambda config: resolve_config_principal_ids(config, "SYNOLOGYCHAT_ADMINS"),
)
class SynologyChatModule(_ModuleBase, _MessageBase[SynologyChat]):
_IMAGE_SUFFIXES = (
".png",
@@ -182,7 +192,9 @@ class SynologyChatModule(_ModuleBase, _MessageBase[SynologyChat]):
return CommingMessage(channel=MessageChannel.SynologyChat, source=client_config.name,
userid=user_id, username=user_name,
is_channel_admin=matches_channel_admin(
client_config.config, "SYNOLOGYCHAT_ADMINS", user_id
MessageChannel.SynologyChat,
client_config.config,
user_id,
), text=text or "",
images=images, audio_refs=audio_refs, files=files)
except Exception as err:

View File

@@ -5,7 +5,11 @@ from typing import Dict, Optional, Union, List, Tuple, Any
from app.core.context import MediaInfo, Context
from app.core.event import eventmanager
from app.helper.agent import matches_channel_admin
from app.helper.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.telegram.telegram import Telegram
@@ -21,6 +25,14 @@ from app.schemas.types import ModuleType, ChainEventType
from app.utils.structures import DictUtils
register_channel_admin_resolver(
MessageChannel.Telegram,
lambda config: resolve_config_principal_ids(
config, "TELEGRAM_ADMINS", "TELEGRAM_CHAT_ID"
),
)
class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
"""
Telegram 通知模块,负责模块生命周期、消息解析和通知发送。
@@ -112,12 +124,11 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
admins = cls._get_admins(config)
if not admins:
return False
candidates = [
str(user_id).strip()
for user_id in user_ids
if user_id is not None and str(user_id).strip()
]
return not any(candidate in admins for candidate in candidates)
return not matches_channel_admin(
MessageChannel.Telegram,
config,
*user_ids,
)
def message_parser(
self, source: str, body: Any, form: Any, args: Any
@@ -237,7 +248,9 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
userid=user_id,
username=user_name,
is_channel_admin=matches_channel_admin(
client_config.config, "TELEGRAM_ADMINS", user_id
MessageChannel.Telegram,
client_config.config,
user_id,
),
text=callback_text,
is_callback=True,
@@ -316,7 +329,9 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
userid=user_id,
username=user_name,
is_channel_admin=matches_channel_admin(
client_config.config, "TELEGRAM_ADMINS", user_id
MessageChannel.Telegram,
client_config.config,
user_id,
),
text=cleaned_text,
message_id=message_id,

View File

@@ -3,7 +3,11 @@ from urllib.parse import quote, unquote
from typing import Optional, Union, List, Tuple, Any, Dict
from app.core.context import Context, MediaInfo
from app.helper.agent import matches_channel_admin
from app.helper.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.vocechat.vocechat import VoceChat
@@ -11,6 +15,12 @@ from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
register_channel_admin_resolver(
MessageChannel.VoceChat,
lambda config: resolve_config_principal_ids(config, "VOCECHAT_ADMINS"),
)
class VoceChatModule(_ModuleBase, _MessageBase[VoceChat]):
_IMAGE_SUFFIXES = (
".png",
@@ -208,7 +218,7 @@ class VoceChatModule(_ModuleBase, _MessageBase[VoceChat]):
return CommingMessage(channel=MessageChannel.VoceChat, source=client_config.name,
userid=userid, username=userid,
is_channel_admin=matches_channel_admin(
client_config.config, "VOCECHAT_ADMINS",
MessageChannel.VoceChat, client_config.config,
from_uid, actor_userid,
), text=text or "",
images=images, audio_refs=audio_refs, files=files)

View File

@@ -7,7 +7,11 @@ from urllib.parse import quote
from app.core.context import Context, MediaInfo
from app.core.event import eventmanager
from app.helper.agent import matches_channel_admin
from app.helper.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
@@ -19,6 +23,17 @@ from app.utils.dom import DomUtils
from app.utils.structures import DictUtils
def _resolve_wechat_admin_ids(config: Optional[dict]) -> set[str]:
"""解析企业微信管理员及机器人模式下的主用户 ID。"""
config_keys = ["WECHAT_ADMINS"]
if (config or {}).get("WECHAT_MODE", "app") == "bot":
config_keys.append("WECHAT_BOT_CHAT_ID")
return resolve_config_principal_ids(config, *config_keys)
register_channel_admin_resolver(MessageChannel.Wechat, _resolve_wechat_admin_ids)
class WechatModule(_ModuleBase, _MessageBase[WeChat]):
def init_module(self) -> None:
@@ -88,7 +103,11 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
admins = cls._get_admins(config)
if not admins:
return False
return str(user_id or "").strip() not in admins
return not matches_channel_admin(
MessageChannel.Wechat,
config,
user_id,
)
@classmethod
def _create_client(cls, conf):
@@ -253,7 +272,9 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
return CommingMessage(channel=MessageChannel.Wechat, source=client_config.name,
userid=user_id, username=user_id,
is_channel_admin=matches_channel_admin(
client_config.config, "WECHAT_ADMINS", user_id
MessageChannel.Wechat,
client_config.config,
user_id,
), text=content or "",
images=images, audio_refs=audio_refs, files=files)
except Exception as err:
@@ -325,7 +346,9 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
userid=sender,
username=sender,
is_channel_admin=matches_channel_admin(
client_config.config, "WECHAT_ADMINS", sender
MessageChannel.Wechat,
client_config.config,
sender,
),
text=text or "",
images=images,

View File

@@ -15,8 +15,10 @@ from app.core.cache import FileCache
from app.core.config import settings
from app.core.context import MediaInfo, Context
from app.core.metainfo import MetaInfo
from app.helper.agent import matches_channel_admin
from app.log import logger
from app.schemas import CommingMessage
from app.schemas.types import MessageChannel
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
@@ -489,7 +491,16 @@ class WeChatBot:
self._remember_target(sender)
if text and text.startswith("/") and self._admins and sender not in self._admins:
is_channel_admin = matches_channel_admin(
MessageChannel.Wechat,
{
"WECHAT_ADMINS": ",".join(self._admins),
"WECHAT_BOT_CHAT_ID": getattr(self, "_default_chat_id", None),
"WECHAT_MODE": "bot",
},
sender,
)
if text and text.startswith("/") and self._admins and not is_channel_admin:
self.send_msg(title="只有管理员才有权限执行此命令", userid=sender)
return

View File

@@ -3,7 +3,11 @@ from typing import Any, List, Optional, Tuple, Union
from app.core.cache import TTLCache
from app.core.context import Context, MediaInfo
from app.helper.agent import matches_channel_admin
from app.helper.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.log import logger
from app.modules import _MessageBase, _ModuleBase
from app.modules.wechatclawbot.wechatclawbot import WechatClawBot
@@ -11,6 +15,14 @@ from app.schemas import CommingMessage, Notification
from app.schemas.types import MessageChannel, ModuleType
register_channel_admin_resolver(
MessageChannel.WechatClawBot,
lambda config: resolve_config_principal_ids(
config, "WECHATCLAWBOT_ADMINS", "WECHATCLAWBOT_DEFAULT_TARGET"
),
)
class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
def __init__(self):
"""初始化模块级去重缓存,拦截 iLink 偶发的重复回放消息。"""
@@ -182,7 +194,12 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
]
callback_data = text[9:].strip() if text.startswith("CALLBACK:") else ""
is_admin_command = text.startswith("/") or callback_data.startswith("/")
if is_admin_command and admins and user_id not in admins:
is_channel_admin = matches_channel_admin(
MessageChannel.WechatClawBot,
client_config.config,
user_id,
)
if is_admin_command and admins and not is_channel_admin:
client = self.get_instance(client_config.name)
if client:
client.send_msg(title="只有管理员才有权限执行此命令", userid=user_id)
@@ -199,9 +216,7 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
source=client_config.name,
userid=user_id,
username=username,
is_channel_admin=matches_channel_admin(
client_config.config, "WECHATCLAWBOT_ADMINS", user_id
),
is_channel_admin=is_channel_admin,
text=text,
message_id=message_id,
chat_id=str(message.get("chat_id") or "") or None,