ForcePilot/backend/package/yuxi/channels/adapters/dingding/setup.py
Kris 9c01d480cc feat(dingding): 完整实现钉钉适配器功能模块
- 新增钉钉工具调用审计日志功能
- 新增参数校验装饰器与基础工具集(文档、审批、多维表格)
- 完善事件类型映射与会话ID生成逻辑
- 新增消息去重、访问策略匹配、配置校验与UI提示
- 新增酷应用卡片、目录管理、表情反应支持
- 优化消息发送逻辑,添加401重试机制
- 新增配置化的权限控制与流式输出支持
2026-05-13 16:06:10 +08:00

266 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""DingDing 渠道 CLI 配置向导。
交互式引导用户配置钉钉机器人,支持 Stream 和 Webhook 两种模式。
"""
from __future__ import annotations
import json
import os
from typing import Any
def run_dingding_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_config_flow()
return _run_edit_flow(has_existing)
return _run_new_config_flow()
def _run_new_config_flow() -> dict[str, Any] | None:
print("\n--- 新建钉钉应用 ---")
config = _prompt_credentials()
if config is None:
return None
config = _prompt_mode_selection(config)
config = _prompt_policy_selection(config)
config = _prompt_streaming_config(config)
config = _apply_recommendations(config)
return _confirm_and_return_config(config)
def _run_edit_flow(existing: dict[str, Any]) -> dict[str, Any] | None:
print("\n--- 编辑已有应用 ---")
accounts = existing.get("accounts", {}).get("default", {})
print(f"当前 App Key: {accounts.get('app_key', '未配置')}")
print(f"当前 Robot Code: {accounts.get('robot_code', '未配置')}")
print(f"当前模式: {existing.get('mode', 'stream')}")
config = dict(existing)
change_cred = _prompt("是否更换应用凭据?[y/N]", default="n").strip().lower()
if change_cred == "y":
new_creds = _prompt_credentials()
if new_creds:
config.update(new_creds)
config = _prompt_mode_selection(config)
config = _prompt_policy_selection(config)
config = _prompt_streaming_config(config)
config = _apply_recommendations(config)
return _confirm_and_return_config(config)
def _prompt_credentials() -> dict[str, Any] | None:
print("\n--- 应用凭据 ---")
print("凭据可在钉钉开放平台 (https://open.dingtalk.com) 获取")
print("路径: 应用开发 > 企业内部应用 > 应用详情 > 凭证与基础信息\n")
app_key = _prompt("App Key (Client ID)").strip()
if not app_key:
print("App Key 不能为空,配置取消")
return None
app_secret = _prompt("App Secret (Client Secret)").strip()
if not app_secret:
print("App Secret 不能为空,配置取消")
return None
robot_code = _prompt("Robot Code (机器人应用详情页获取,可选)", default="").strip()
return {
"accounts": {
"default": {
"app_key": app_key,
"app_secret": app_secret,
"robot_code": robot_code,
}
}
}
def _prompt_mode_selection(config: dict[str, Any]) -> dict[str, Any]:
print("\n--- 连接模式 ---")
print("1) Stream 模式 - 通过 WebSocket 长连接接收消息推荐无需公网IP")
print("2) Webhook 模式 - 通过 HTTP 回调接收消息(需要公网可达的服务器)")
choice = _prompt("请选择 [1/2]", default="1").strip()
if choice == "2":
config["mode"] = "webhook"
else:
config["mode"] = "stream"
return config
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["groupPolicy"] = group_policy
config["group_policy"] = group_policy
if group_policy == "allowlist":
print("\n请输入允许访问的群聊 ID每行一个空行结束")
print("提示: 群聊 ID 可从消息中获取,格式如 cidXXXXXXXXX")
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["dmPolicy"] = dm_policy
config["dm_policy"] = dm_policy
print("\n群聊中需要 @机器人 才触发回复吗?")
require = _prompt("[y/N]", default="y").strip().lower()
config["requireMention"] = require != "n"
return config
def _prompt_streaming_config(config: dict[str, Any]) -> dict[str, Any]:
print("\n--- 流式输出配置 ---")
print("1) block - 累积后一次性发送(推荐,兼容性好)")
print("2) partial - 实时编辑消息(需要机器人支持编辑消息权限)")
print("3) off - 关闭流式输出")
choice = _prompt("请选择 [1/2/3]", default="1").strip()
mode_map = {"1": "block", "2": "partial", "3": "off"}
stream_mode = mode_map.get(choice, "block")
config.setdefault("streaming", {})
config["streaming"]["mode"] = stream_mode
return config
def _apply_recommendations(config: dict[str, Any]) -> dict[str, Any]:
group_policy = config.get("groupPolicy", "allowlist")
if group_policy == "open":
print("\n⚠ 警告: 群聊策略设置为 'open',所有群聊均可访问机器人。")
print(" 建议在正式环境使用 'allowlist' 模式。")
if not config.get("robotCode", "") and not config.get("accounts", {}).get("default", {}).get("robot_code"):
print("\n⚠ 提示: 未配置 Robot Code表情回复和卡片功能可能不可用。")
print(" Robot Code 可在机器人应用详情页获取。")
return config
def _confirm_and_return_config(config: dict[str, Any]) -> dict[str, Any] | None:
print("\n=== 配置摘要 ===")
accounts = config.get("accounts", {}).get("default", {})
print(f" App Key: {accounts.get('app_key', '')[:10]}****")
print(f" Robot Code: {accounts.get('robot_code', '未配置')}")
print(f" 连接模式: {config.get('mode', 'stream')}")
print(f" 群聊策略: {config.get('groupPolicy', 'allowlist')}")
print(f" 私聊策略: {config.get('dmPolicy', 'pairing')}")
print(f" 需要 @机器人: {config.get('requireMention', True)}")
print(f" 流式模式: {config.get('streaming', {}).get('mode', 'block')}")
allowlist = config.get("allowlist", [])
if allowlist:
for item in allowlist[:5]:
print(f" 白名单: {item}")
if len(allowlist) > 5:
print(f" ... 共 {len(allowlist)}")
confirm = _prompt("\n是否保存配置?[y/N]", default="n").strip().lower()
if confirm != "y":
print("配置已取消")
return None
print("配置已保存")
return config
def save_config_to_file(config: dict[str, Any], filepath: str) -> None:
with open(filepath, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print(f"配置已写入: {filepath}")
def _detect_existing_config() -> dict[str, Any] | None:
env_app_key = os.environ.get("DINGDING_APP_KEY", "")
env_app_secret = os.environ.get("DINGDING_APP_SECRET", "")
env_robot_code = os.environ.get("DINGDING_ROBOT_CODE", "")
if env_app_key and env_app_secret:
return {
"accounts": {
"default": {
"app_key": env_app_key,
"app_secret": env_app_secret,
"robot_code": env_robot_code,
}
},
"mode": "stream",
}
config_paths = [
"dingding_config.json",
"config/dingding.json",
os.path.expanduser("~/.forcepilot/dingding.json"),
]
for path in config_paths:
if os.path.exists(path):
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
pass
return None
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
if __name__ == "__main__":
result = run_dingding_setup()
if result:
save_config_to_file(result, "dingding_config.json")
else:
print("未生成配置")