本次提交对 Mattermost 适配器进行了全面升级: 1. 重构为多账号架构,支持同时管理多个 Mattermost 机器人账号 2. 更新安全策略默认配置为配对模式和白名单模式 3. 新增 WebSocket 心跳、重连配置项与连接监控 4. 扩展 Agent 工具支持 pin/unpin、获取反应、搜索消息等操作 5. 重构交互按钮构建逻辑,新增分页与提供商筛选功能 6. 优化 SSRF 防护代码,复用公共工具库实现 7. 新增配置兼容性迁移与可变白名单项检测 8. 完善错误处理与日志输出,添加重复消息去重统计 9. 新增发送临时消息(ephemeral)支持 10. 修复提及检测逻辑,正确处理用户名大小写
239 lines
7.5 KiB
Python
239 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
|
|
MODEL_PICKER_PAGE_SIZE = 8
|
|
|
|
|
|
class PickerState(StrEnum):
|
|
PROVIDERS = "providers"
|
|
LIST = "list"
|
|
SELECT = "select"
|
|
BACK = "back"
|
|
|
|
|
|
@dataclass
|
|
class ModelOption:
|
|
model_id: str
|
|
display_name: str
|
|
provider: str = ""
|
|
description: str = ""
|
|
|
|
@property
|
|
def button_id(self) -> str:
|
|
return hashlib.sha256(self.model_id.encode()).hexdigest()[:8]
|
|
|
|
|
|
def get_default_model_options() -> list[ModelOption]:
|
|
return [
|
|
ModelOption("gpt-4o-mini", "GPT-4o Mini", "openai", "轻量快速模型"),
|
|
ModelOption("gpt-4o", "GPT-4o", "openai", "旗舰多模态模型"),
|
|
ModelOption("claude-sonnet-4-20250514", "Claude Sonnet 4", "anthropic", "高性能推理模型"),
|
|
ModelOption("deepseek-v3", "DeepSeek V3", "deepseek", "国产高性能模型"),
|
|
]
|
|
|
|
|
|
def get_available_providers(models: list[ModelOption] | None = None) -> list[str]:
|
|
if models is None:
|
|
models = get_default_model_options()
|
|
seen: dict[str, bool] = {}
|
|
result: list[str] = []
|
|
for m in models:
|
|
if m.provider and m.provider not in seen:
|
|
seen[m.provider] = True
|
|
result.append(m.provider)
|
|
return result
|
|
|
|
|
|
def build_model_picker_actions(
|
|
models: list[ModelOption] | None = None,
|
|
callback_id: str = "model_picker",
|
|
state: PickerState = PickerState.LIST,
|
|
page: int = 0,
|
|
) -> list[dict]:
|
|
if models is None:
|
|
models = get_default_model_options()
|
|
|
|
actions: list[dict] = []
|
|
|
|
if state == PickerState.PROVIDERS:
|
|
providers = get_available_providers(models)
|
|
for provider in providers:
|
|
actions.append(
|
|
{
|
|
"id": f"provider_{provider}",
|
|
"name": f"查看 {provider} 模型",
|
|
"integration": {
|
|
"url": "",
|
|
"context": {
|
|
"action": "list_provider",
|
|
"provider": provider,
|
|
"callback_id": callback_id,
|
|
"state": PickerState.LIST.value,
|
|
},
|
|
},
|
|
"type": "button",
|
|
"text": f"🤖 {provider.upper()}",
|
|
"style": "",
|
|
}
|
|
)
|
|
actions.append(
|
|
{
|
|
"id": "view_all",
|
|
"name": "查看全部模型",
|
|
"integration": {
|
|
"url": "",
|
|
"context": {
|
|
"action": "list_all",
|
|
"callback_id": callback_id,
|
|
"state": PickerState.LIST.value,
|
|
},
|
|
},
|
|
"type": "button",
|
|
"text": "📋 全部模型",
|
|
"style": "",
|
|
}
|
|
)
|
|
return actions
|
|
|
|
start = page * MODEL_PICKER_PAGE_SIZE
|
|
page_models = models[start : start + MODEL_PICKER_PAGE_SIZE]
|
|
total_pages = (len(models) + MODEL_PICKER_PAGE_SIZE - 1) // MODEL_PICKER_PAGE_SIZE
|
|
|
|
for m in page_models:
|
|
actions.append(
|
|
{
|
|
"id": f"model_{m.button_id}",
|
|
"name": f"选择 {m.display_name}",
|
|
"integration": {
|
|
"url": "",
|
|
"context": {
|
|
"action": "select_model",
|
|
"model_id": m.model_id,
|
|
"callback_id": callback_id,
|
|
"state": PickerState.SELECT.value,
|
|
},
|
|
},
|
|
"type": "button",
|
|
"text": f"🤖 {m.display_name}",
|
|
"style": "",
|
|
}
|
|
)
|
|
|
|
if total_pages > 1:
|
|
nav_actions: list[dict] = []
|
|
if page > 0:
|
|
nav_actions.append(
|
|
{
|
|
"id": "prev_page",
|
|
"name": "上一页",
|
|
"integration": {
|
|
"url": "",
|
|
"context": {
|
|
"action": "page",
|
|
"page": str(page - 1),
|
|
"callback_id": callback_id,
|
|
"state": PickerState.LIST.value,
|
|
},
|
|
},
|
|
"type": "button",
|
|
"text": "⬅️ 上一页",
|
|
"style": "",
|
|
}
|
|
)
|
|
if page < total_pages - 1:
|
|
nav_actions.append(
|
|
{
|
|
"id": "next_page",
|
|
"name": "下一页",
|
|
"integration": {
|
|
"url": "",
|
|
"context": {
|
|
"action": "page",
|
|
"page": str(page + 1),
|
|
"callback_id": callback_id,
|
|
"state": PickerState.LIST.value,
|
|
},
|
|
},
|
|
"type": "button",
|
|
"text": "➡️ 下一页",
|
|
"style": "",
|
|
}
|
|
)
|
|
actions.extend(nav_actions)
|
|
|
|
actions.append(
|
|
{
|
|
"id": "back_to_providers",
|
|
"name": "按提供商筛选",
|
|
"integration": {
|
|
"url": "",
|
|
"context": {
|
|
"action": "back",
|
|
"callback_id": callback_id,
|
|
"state": PickerState.PROVIDERS.value,
|
|
},
|
|
},
|
|
"type": "button",
|
|
"text": "🔙 按提供商筛选",
|
|
"style": "",
|
|
}
|
|
)
|
|
|
|
return actions
|
|
|
|
|
|
def build_model_picker_attachment(
|
|
current_model: str = "",
|
|
callback_id: str = "model_picker",
|
|
state: PickerState = PickerState.PROVIDERS,
|
|
page: int = 0,
|
|
provider_filter: str = "",
|
|
) -> dict:
|
|
models = get_default_model_options()
|
|
|
|
if provider_filter:
|
|
models = [m for m in models if m.provider == provider_filter]
|
|
|
|
if state == PickerState.PROVIDERS:
|
|
providers = get_available_providers(models)
|
|
lines = [f"• **{p.upper()}**" for p in providers]
|
|
text = "按 AI 提供商筛选模型:\n\n" + "\n".join(lines)
|
|
else:
|
|
start = page * MODEL_PICKER_PAGE_SIZE
|
|
page_models = models[start : start + MODEL_PICKER_PAGE_SIZE]
|
|
total_pages = (len(models) + MODEL_PICKER_PAGE_SIZE - 1) // MODEL_PICKER_PAGE_SIZE
|
|
|
|
lines = []
|
|
for m in page_models:
|
|
marker = " ✅" if m.model_id == current_model else ""
|
|
lines.append(f"• **{m.display_name}** ({m.provider}) — {m.description}{marker}")
|
|
|
|
text = f"当前可用模型 (页 {page + 1}/{total_pages}):\n\n" + "\n".join(lines)
|
|
|
|
actions = build_model_picker_actions(models, callback_id, state, page)
|
|
|
|
return {
|
|
"fallback": "模型选择器",
|
|
"title": "🤖 模型选择",
|
|
"text": text,
|
|
"callback_id": callback_id,
|
|
"actions": actions,
|
|
}
|
|
|
|
|
|
def build_model_select_attachment(provider: str, callback_id: str = "model_select") -> dict:
|
|
all_models = get_default_model_options()
|
|
provider_models = [m for m in all_models if m.provider == provider]
|
|
actions = build_model_picker_actions(provider_models, callback_id, PickerState.LIST, 0)
|
|
|
|
return {
|
|
"fallback": f"选择 {provider} 模型",
|
|
"title": f"🤖 {provider} 模型",
|
|
"text": f"选择 {provider} 下的模型:",
|
|
"callback_id": callback_id,
|
|
"actions": actions,
|
|
}
|