这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
257 lines
7.4 KiB
Python
257 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from .constants import TARGET_PREFIXES
|
|
from .normalize import (
|
|
implicit_mention,
|
|
was_explicitly_mentioned,
|
|
)
|
|
|
|
|
|
def resolve_agent_route(
|
|
channel_id: str,
|
|
chat_id: str,
|
|
user_id: str,
|
|
chat_type: str,
|
|
config: dict[str, Any],
|
|
) -> str:
|
|
default_agent_id = config.get("default_agent_id", "default")
|
|
|
|
if chat_type == "direct":
|
|
return f"agent:{default_agent_id}:{channel_id}:direct:{user_id}"
|
|
elif chat_type == "group":
|
|
agent_id = _resolve_group_agent(chat_id, config, default_agent_id)
|
|
return f"agent:{agent_id}:{channel_id}:group:{chat_id}"
|
|
else:
|
|
return f"agent:{default_agent_id}:{channel_id}:unknown:{chat_id}"
|
|
|
|
|
|
def _resolve_group_agent(chat_id: str, config: dict[str, Any], default_agent_id: str) -> str:
|
|
groups_config = config.get("groups", {})
|
|
chat_config = groups_config.get(chat_id, {})
|
|
return chat_config.get("agent_id", default_agent_id)
|
|
|
|
|
|
def check_dm_policy(user_id: str, config: dict[str, Any], paired_users: set[str]) -> bool:
|
|
dm_policy = config.get("dm_policy", "pairing")
|
|
|
|
match dm_policy:
|
|
case "open":
|
|
return True
|
|
case "disabled":
|
|
return False
|
|
case "pairing":
|
|
return f"zalo:{user_id}" in paired_users
|
|
case "allowlist":
|
|
allow_from = config.get("allow_from", [])
|
|
return f"zalo:{user_id}" in allow_from
|
|
case _:
|
|
return False
|
|
|
|
|
|
def check_group_policy(chat_id: str, user_id: str, config: dict[str, Any]) -> bool:
|
|
group_policy = config.get("group_policy", "allowlist")
|
|
|
|
match group_policy:
|
|
case "open":
|
|
return True
|
|
case "disabled":
|
|
return False
|
|
case "allowlist":
|
|
group_allow = config.get("group_allow_from", [])
|
|
if f"zalo:{user_id}" in group_allow:
|
|
return True
|
|
groups_config = config.get("groups", {})
|
|
chat_config = groups_config.get(chat_id, {})
|
|
per_group_allow = chat_config.get("allow_from", [])
|
|
return f"zalo:{user_id}" in per_group_allow
|
|
case _:
|
|
return False
|
|
|
|
|
|
def check_mention_required(
|
|
chat_id: str,
|
|
content: str,
|
|
config: dict[str, Any],
|
|
bot_names: str | list[str] = "",
|
|
) -> bool:
|
|
groups_config = config.get("groups", {})
|
|
chat_config = groups_config.get(chat_id, {})
|
|
wildcard_config = groups_config.get("*", {})
|
|
|
|
require_mention = chat_config.get(
|
|
"require_mention",
|
|
wildcard_config.get("require_mention", False),
|
|
)
|
|
|
|
if not require_mention:
|
|
return True
|
|
|
|
if isinstance(bot_names, str):
|
|
bot_names = [bot_names] if bot_names else []
|
|
|
|
if not bot_names:
|
|
return True
|
|
|
|
if was_explicitly_mentioned(content, bot_names):
|
|
return True
|
|
|
|
if implicit_mention(content, bot_names):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def resolve_chat_id(target: str, channel_id: str = "zalo_user") -> tuple[str | None, str | None]:
|
|
normalized = target.strip()
|
|
|
|
for prefix, chat_type in TARGET_PREFIXES.items():
|
|
if normalized.lower().startswith(prefix):
|
|
chat_id = normalized[len(prefix) :]
|
|
return chat_id, chat_type
|
|
|
|
if normalized.startswith("zalo:"):
|
|
chat_id = normalized[len("zalo:") :]
|
|
return chat_id, None
|
|
|
|
return normalized, None
|
|
|
|
|
|
def looks_like_id(target: str) -> bool:
|
|
if not target:
|
|
return False
|
|
stripped = target.strip()
|
|
if stripped.isdigit():
|
|
return True
|
|
for prefix in TARGET_PREFIXES:
|
|
if stripped.lower().startswith(prefix):
|
|
suffix = stripped[len(prefix) :]
|
|
if suffix.isdigit():
|
|
return True
|
|
return False
|
|
|
|
|
|
def parse_outbound_target(target: str) -> dict[str, Any]:
|
|
chat_id, chat_type = resolve_chat_id(target)
|
|
is_group = chat_type == "group" if chat_type is not None else False
|
|
return {
|
|
"thread_id": chat_id or target,
|
|
"is_group": is_group,
|
|
"chat_type": chat_type or ("group" if is_group else "direct"),
|
|
}
|
|
|
|
|
|
def resolve_zalo_allow_from_entries(
|
|
entries: list[str],
|
|
friends: list[dict[str, Any]],
|
|
) -> list[str]:
|
|
from .allow_from import resolve_allow_from_entries
|
|
|
|
return resolve_allow_from_entries(entries, friends)
|
|
|
|
|
|
def check_group_tool_policy(
|
|
chat_id: str,
|
|
tool_name: str,
|
|
config: dict[str, Any],
|
|
) -> bool:
|
|
groups_config = config.get("groups", {})
|
|
chat_config = groups_config.get(chat_id, {})
|
|
tool_policy = chat_config.get("tools", {})
|
|
|
|
if not tool_policy:
|
|
return True
|
|
|
|
deny_list = tool_policy.get("deny", [])
|
|
if tool_name in deny_list:
|
|
return False
|
|
|
|
allow_list = tool_policy.get("allow", [])
|
|
if allow_list and tool_name not in allow_list:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def _find_by_name(query: str, items: list[dict[str, Any]], *name_keys: str) -> dict[str, Any] | None:
|
|
query_lower = query.lower()
|
|
exact_matches: list[dict[str, Any]] = []
|
|
partial_matches: list[dict[str, Any]] = []
|
|
|
|
for item in items:
|
|
for key in name_keys:
|
|
name = str(item.get(key, "")).lower()
|
|
if not name:
|
|
continue
|
|
if name == query_lower:
|
|
exact_matches.append(item)
|
|
elif query_lower in name:
|
|
partial_matches.append(item)
|
|
break
|
|
|
|
if exact_matches:
|
|
return exact_matches[0]
|
|
if partial_matches:
|
|
return partial_matches[0]
|
|
return None
|
|
|
|
|
|
async def resolve_targets(
|
|
targets: list[str],
|
|
friends: list[dict[str, Any]],
|
|
groups: list[dict[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
results: list[dict[str, Any]] = []
|
|
for target in targets:
|
|
if not target:
|
|
results.append({"input": target, "resolved_id": None, "resolved_type": None})
|
|
continue
|
|
|
|
chat_id, chat_type = resolve_chat_id(target)
|
|
if chat_type is not None:
|
|
results.append({"input": target, "resolved_id": chat_id, "resolved_type": chat_type})
|
|
continue
|
|
|
|
if target.strip().isdigit():
|
|
results.append({"input": target, "resolved_id": target.strip(), "resolved_type": "direct"})
|
|
continue
|
|
|
|
target_lower = target.strip().lower()
|
|
matched_friend = _find_by_name(target_lower, friends, "display_name", "name")
|
|
if matched_friend:
|
|
uid = str(matched_friend.get("user_id") or matched_friend.get("id") or "")
|
|
results.append(
|
|
{
|
|
"input": target,
|
|
"resolved_id": uid,
|
|
"resolved_type": "user",
|
|
"display_name": matched_friend.get("display_name", target),
|
|
}
|
|
)
|
|
continue
|
|
|
|
matched_group = _find_by_name(target_lower, groups, "name", "group_name")
|
|
if matched_group:
|
|
gid = str(matched_group.get("group_id") or matched_group.get("id") or "")
|
|
results.append(
|
|
{
|
|
"input": target,
|
|
"resolved_id": gid,
|
|
"resolved_type": "group",
|
|
"display_name": matched_group.get("name", target),
|
|
}
|
|
)
|
|
continue
|
|
|
|
results.append(
|
|
{
|
|
"input": target,
|
|
"resolved_id": target.strip(),
|
|
"resolved_type": None,
|
|
"error": f"Could not resolve target '{target}'",
|
|
}
|
|
)
|
|
|
|
return results
|