新增飞书机器人适配器全套功能,包括: - 基础适配器入口与工具导出 - 消息格式化、卡片渲染、回复调度逻辑 - 会话ID生成、模型覆盖策略 - 消息发送缓存、顺序队列管理 - 飞书签名验证、加解密webhook请求 - 审批权限校验、机器人菜单事件处理 - 文档评论、钉消息、语音转码处理 - 静态/动态目录管理、子代理生命周期管理 - 各类工具集:聊天、云盘、文档、知识库API封装
256 lines
8.2 KiB
Python
256 lines
8.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
from .oauth import FeishuDeviceCodeClient
|
|
|
|
|
|
def run_feishu_setup() -> dict[str, Any] | None:
|
|
print("=== 飞书应用配置向导 ===\n")
|
|
|
|
has_existing = _detect_existing_config()
|
|
if has_existing:
|
|
choice = _prompt("检测到已有飞书配置,是否使用已有配置? [编辑(e)/新建(n)/取消(c)]", default="e").strip().lower()
|
|
if choice == "c":
|
|
print("❌ 配置已取消")
|
|
return None
|
|
if choice == "n":
|
|
return _run_new_app_flow()
|
|
|
|
return _run_edit_flow(has_existing)
|
|
|
|
return _run_new_app_flow()
|
|
|
|
|
|
def _run_new_app_flow() -> dict[str, Any] | None:
|
|
print("\n--- 新建应用 ---")
|
|
use_scan = _prompt("是否使用扫码注册? [y/N]", default="n").strip().lower()
|
|
if use_scan == "y":
|
|
config = _run_scan_to_create()
|
|
if config is None:
|
|
return None
|
|
else:
|
|
config = _prompt_manual_credentials()
|
|
if config is None:
|
|
return None
|
|
|
|
config = _prompt_policy_selection(config)
|
|
config = _apply_security_recommendations(config)
|
|
return _confirm_and_return_config(config)
|
|
|
|
|
|
def _run_edit_flow(existing: dict[str, Any]) -> dict[str, Any] | None:
|
|
print("\n--- 编辑已有应用 ---")
|
|
print(f"当前 App ID: {existing.get('app_id', '未配置')}")
|
|
|
|
config = dict(existing)
|
|
change_app = _prompt("是否更换应用凭据? [y/N]", default="n").strip().lower()
|
|
if change_app == "y":
|
|
new_config = _prompt_manual_credentials()
|
|
if new_config:
|
|
config.update(new_config)
|
|
|
|
config = _prompt_policy_selection(config)
|
|
config = _apply_security_recommendations(config)
|
|
return _confirm_and_return_config(config)
|
|
|
|
|
|
def _prompt_manual_credentials() -> dict[str, Any] | None:
|
|
app_id = _prompt("请输入 App ID").strip()
|
|
if not app_id:
|
|
print("❌ App ID 不能为空,配置取消")
|
|
return None
|
|
|
|
app_secret = _prompt("请输入 App Secret").strip()
|
|
if not app_secret:
|
|
print("❌ App Secret 不能为空,配置取消")
|
|
return None
|
|
|
|
platform = _prompt("平台类型 [feishu/lark]", default="feishu").strip()
|
|
domain = _prompt("私有部署域名 (留空使用默认)", default="").strip()
|
|
verify_token = _prompt("Verification Token (可选)", default="").strip()
|
|
encrypt_key = _prompt("Encrypt Key (可选)", default="").strip()
|
|
|
|
config: dict[str, Any] = {
|
|
"app_id": app_id,
|
|
"app_secret": app_secret,
|
|
"platform": platform or "feishu",
|
|
"verify_token": verify_token,
|
|
"encrypt_key": encrypt_key,
|
|
}
|
|
if domain:
|
|
config["domain"] = domain
|
|
return config
|
|
|
|
|
|
def _run_scan_to_create() -> dict[str, Any] | None:
|
|
print("\n--- 扫码注册模式 ---")
|
|
print("此模式将使用设备授权码流程自动获取应用凭据\n")
|
|
|
|
app_id = _prompt("App ID (从飞书开发者后台获取)").strip()
|
|
if not app_id:
|
|
print("❌ App ID 不能为空")
|
|
return None
|
|
|
|
app_secret = _prompt("App Secret").strip()
|
|
if not app_secret:
|
|
print("❌ App Secret 不能为空")
|
|
return None
|
|
|
|
client = FeishuDeviceCodeClient(app_id=app_id, app_secret=app_secret)
|
|
|
|
print("\n正在发起设备授权请求...")
|
|
device_resp = client.init_device_flow()
|
|
|
|
if device_resp is None:
|
|
print("❌ 设备授权请求失败,请检查 App ID 和 App Secret")
|
|
return None
|
|
|
|
verification_uri = device_resp.get("verification_uri", "")
|
|
user_code = device_resp.get("user_code", "")
|
|
device_code = device_resp.get("device_code", "")
|
|
interval = device_resp.get("interval", 5)
|
|
expires_in = device_resp.get("expires_in", 600)
|
|
|
|
if not verification_uri or not user_code:
|
|
print("❌ 获取设备授权信息失败")
|
|
return None
|
|
|
|
_print_qr_code(verification_uri)
|
|
print("\n请在浏览器中打开以下链接并输入验证码:")
|
|
print(f" URL: {verification_uri}")
|
|
print(f" 验证码: {user_code}")
|
|
print(f"\n有效期: {expires_in} 秒\n")
|
|
|
|
print("等待授权中...")
|
|
max_attempts = expires_in // interval + 1
|
|
for attempt in range(max_attempts):
|
|
time.sleep(interval)
|
|
token_resp = client.poll_device_token(device_code)
|
|
if token_resp is None:
|
|
continue
|
|
|
|
error = token_resp.get("error", "")
|
|
if error == "authorization_pending":
|
|
continue
|
|
elif error == "slow_down":
|
|
interval += 5
|
|
continue
|
|
elif error:
|
|
print(f"❌ 授权失败: {error}")
|
|
return None
|
|
|
|
access_token = token_resp.get("access_token", "")
|
|
refresh_token = token_resp.get("refresh_token", "")
|
|
|
|
print("\n✅ 授权成功!")
|
|
return {
|
|
"app_id": app_id,
|
|
"app_secret": app_secret,
|
|
"platform": "feishu",
|
|
"access_token": access_token,
|
|
"refresh_token": refresh_token,
|
|
}
|
|
|
|
print("❌ 授权超时")
|
|
return None
|
|
|
|
|
|
def _prompt_policy_selection(config: dict[str, Any]) -> dict[str, Any]:
|
|
print("\n--- 安全策略配置 ---")
|
|
print("请选择群聊策略:")
|
|
print(" 1) allowlist - 仅允许白名单中的群聊访问 (推荐)")
|
|
print(" 2) open - 所有群聊均可访问")
|
|
print(" 3) disabled - 禁用群聊功能")
|
|
|
|
choice = _prompt("请选择 [1/2/3]", default="1").strip()
|
|
policy_map = {"1": "allowlist", "2": "open", "3": "disabled"}
|
|
group_policy = policy_map.get(choice, "allowlist")
|
|
config["group_policy"] = group_policy
|
|
config["groupPolicy"] = group_policy
|
|
|
|
if group_policy == "allowlist":
|
|
print("\n请输入允许访问的群聊 ID (每行一个,空行结束):")
|
|
allowlist: list[str] = []
|
|
while True:
|
|
entry = _prompt(" chat_id (或直接回车结束)", default="").strip()
|
|
if not entry:
|
|
break
|
|
allowlist.append(entry)
|
|
if allowlist:
|
|
config["allowlist"] = allowlist
|
|
config["allowFrom"] = allowlist
|
|
|
|
print("\n请选择私聊策略:")
|
|
print(" 1) pairing - 用户需要先与机器人建立会话 (推荐)")
|
|
print(" 2) open - 所有用户均可发送私信")
|
|
print(" 3) allowlist - 仅白名单用户")
|
|
|
|
dm_choice = _prompt("请选择 [1/2/3]", default="1").strip()
|
|
dm_policy_map = {"1": "pairing", "2": "open", "3": "allowlist"}
|
|
dm_policy = dm_policy_map.get(dm_choice, "pairing")
|
|
config["dm_policy"] = dm_policy
|
|
config["dmPolicy"] = dm_policy
|
|
|
|
return config
|
|
|
|
|
|
def _apply_security_recommendations(config: dict[str, Any]) -> dict[str, Any]:
|
|
group_policy = config.get("group_policy", "allowlist")
|
|
if group_policy in ("open", "allowlist"):
|
|
if not config.get("allowlist") and not config.get("allowFrom"):
|
|
logger.info("[FeishuSetup] Adding security notice: no allowlist configured")
|
|
return config
|
|
|
|
|
|
def _confirm_and_return_config(config: dict[str, Any]) -> dict[str, Any] | None:
|
|
print("\n=== 配置摘要 ===")
|
|
for key, value in config.items():
|
|
if key in ("access_token", "refresh_token"):
|
|
continue
|
|
display = "****" if "secret" in key or "encrypt" in key else str(value)[:50]
|
|
print(f" {key}: {display}")
|
|
|
|
confirm = _prompt("\n是否保存配置? [y/N]", default="n").strip().lower()
|
|
if confirm != "y":
|
|
print("❌ 配置已取消")
|
|
return None
|
|
|
|
print("✅ 配置已保存")
|
|
return config
|
|
|
|
|
|
def _detect_existing_config() -> dict[str, Any] | None:
|
|
env_app_id = os.environ.get("FEISHU_APP_ID", "")
|
|
env_app_secret = os.environ.get("FEISHU_APP_SECRET", "")
|
|
if env_app_id and env_app_secret:
|
|
return {"app_id": env_app_id, "app_secret": env_app_secret, "platform": "feishu"}
|
|
return None
|
|
|
|
|
|
def _print_qr_code(url: str) -> None:
|
|
try:
|
|
import qrcode
|
|
|
|
qr = qrcode.QRCode(border=1)
|
|
qr.add_data(url)
|
|
qr.make(fit=True)
|
|
qr.print_ascii()
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
def _prompt(text: str, default: str = "") -> str:
|
|
if default:
|
|
prompt_text = f"{text} [{default}]: "
|
|
else:
|
|
prompt_text = f"{text}: "
|
|
try:
|
|
return input(prompt_text)
|
|
except (EOFError, KeyboardInterrupt):
|
|
return default
|