"""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("未生成配置")