- 新增钉钉工具调用审计日志功能 - 新增参数校验装饰器与基础工具集(文档、审批、多维表格) - 完善事件类型映射与会话ID生成逻辑 - 新增消息去重、访问策略匹配、配置校验与UI提示 - 新增酷应用卡片、目录管理、表情反应支持 - 优化消息发送逻辑,添加401重试机制 - 新增配置化的权限控制与流式输出支持
266 lines
9.0 KiB
Python
266 lines
9.0 KiB
Python
"""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("未生成配置") |