diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index 7ae5f8416..d4d40a42a 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -1576,7 +1576,15 @@ pub async fn start_channel_bridge_with_config( // DingTalk (webhook mode) if let Some(ref dt_config) = config.dingtalk { - if let Some(token) = read_token(&dt_config.access_token_env, "DingTalk") { + if dt_config.mode == "stream" { + if let Some(client_id) = read_token(&dt_config.client_id_env, "DingTalk (client_id)") { + let client_secret = + read_token(&dt_config.client_secret_env, "DingTalk (client_secret)") + .unwrap_or_default(); + let adapter = Arc::new(DingTalkAdapter::new_stream(client_id, client_secret)); + adapters.push((adapter, dt_config.default_agent.clone())); + } + } else if let Some(token) = read_token(&dt_config.access_token_env, "DingTalk") { let secret = read_token(&dt_config.secret_env, "DingTalk (secret)").unwrap_or_default(); let adapter = Arc::new(DingTalkAdapter::new(token, secret, dt_config.webhook_port)); adapters.push((adapter, dt_config.default_agent.clone())); diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 5a383d81d..18d8c334e 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -14,7 +14,7 @@ use openfang_kernel::OpenFangKernel; use openfang_runtime::kernel_handle::KernelHandle; use openfang_runtime::tool_runner::builtin_tool_definitions; use openfang_types::agent::{AgentId, AgentIdentity, AgentManifest}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, LazyLock}; use std::time::Instant; @@ -1893,18 +1893,21 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ }, ChannelMeta { name: "dingtalk", display_name: "DingTalk", icon: "DT", - description: "DingTalk Robot API adapter", + description: "DingTalk Robot API adapter (webhook or stream mode)", category: "enterprise", difficulty: "Easy", setup_time: "~3 min", - quick_setup: "Paste your webhook token and signing secret", + quick_setup: "Choose webhook or stream mode, then paste credentials", setup_type: "form", fields: &[ - ChannelField { key: "access_token_env", label: "Access Token", field_type: FieldType::Secret, env_var: Some("DINGTALK_ACCESS_TOKEN"), required: true, placeholder: "abc123...", advanced: false }, - ChannelField { key: "secret_env", label: "Signing Secret", field_type: FieldType::Secret, env_var: Some("DINGTALK_SECRET"), required: true, placeholder: "SEC...", advanced: false }, + ChannelField { key: "mode", label: "Mode", field_type: FieldType::Text, env_var: None, required: false, placeholder: "webhook or stream", advanced: false }, + ChannelField { key: "access_token_env", label: "Access Token (webhook)", field_type: FieldType::Secret, env_var: Some("DINGTALK_ACCESS_TOKEN"), required: false, placeholder: "abc123...", advanced: false }, + ChannelField { key: "secret_env", label: "Signing Secret (webhook)", field_type: FieldType::Secret, env_var: Some("DINGTALK_SECRET"), required: false, placeholder: "SEC...", advanced: false }, ChannelField { key: "webhook_port", label: "Webhook Port", field_type: FieldType::Number, env_var: None, required: false, placeholder: "8457", advanced: true }, + ChannelField { key: "client_id_env", label: "Client ID / AppKey (stream)", field_type: FieldType::Secret, env_var: Some("DINGTALK_CLIENT_ID"), required: false, placeholder: "dingxxx...", advanced: false }, + ChannelField { key: "client_secret_env", label: "Client Secret / AppSecret (stream)", field_type: FieldType::Secret, env_var: Some("DINGTALK_CLIENT_SECRET"), required: false, placeholder: "abc123...", advanced: false }, ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true }, ], - setup_steps: &["Create a robot in your DingTalk group", "Copy the token and signing secret", "Paste them below"], - config_template: "[channels.dingtalk]\naccess_token_env = \"DINGTALK_ACCESS_TOKEN\"\nsecret_env = \"DINGTALK_SECRET\"", + setup_steps: &["Create a robot in DingTalk", "Choose mode: webhook (needs public IP) or stream (no public IP needed)", "For webhook: copy token and signing secret", "For stream: copy Client ID and Client Secret from the app page"], + config_template: "[channels.dingtalk]\nmode = \"stream\"\nclient_id_env = \"DINGTALK_CLIENT_ID\"\nclient_secret_env = \"DINGTALK_CLIENT_SECRET\"", }, ChannelMeta { name: "dingtalk_stream", display_name: "DingTalk Stream", icon: "DS", @@ -2575,8 +2578,63 @@ pub async fn configure_channel( let secrets_path = home.join("secrets.env"); let config_path = home.join("config.toml"); let mut config_fields: HashMap = HashMap::new(); + let mut allowed_keys: Option> = None; + + if name == "dingtalk" { + let mode = match fields.get("mode").and_then(|v| v.as_str()) { + Some("webhook") | None => "webhook", + Some("stream") => "stream", + Some(_) => { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({"error": "DingTalk mode must be 'webhook' or 'stream'"})), + ) + } + }; + + let allowed: HashSet<&'static str> = if mode == "stream" { + ["mode", "default_agent", "client_id_env", "client_secret_env"] + .into_iter() + .collect() + } else { + [ + "mode", + "default_agent", + "access_token_env", + "secret_env", + "webhook_port", + ] + .into_iter() + .collect() + }; + allowed_keys = Some(allowed); + config_fields.insert("mode".to_string(), (mode.to_string(), FieldType::Text)); + + let inactive_secret_keys = if mode == "stream" { + ["DINGTALK_ACCESS_TOKEN", "DINGTALK_SECRET"] + } else { + ["DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_SECRET"] + }; + for env_key in inactive_secret_keys { + if let Err(e) = remove_secret_env(&secrets_path, env_key) { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": format!("Failed to remove stale secret: {e}")})), + ); + } + unsafe { + std::env::remove_var(env_key); + } + } + } for field_def in meta.fields { + if let Some(allowed) = &allowed_keys { + if !allowed.contains(field_def.key) { + continue; + } + } + let value = fields .get(field_def.key) .and_then(|v| v.as_str()) diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html index f9e5dffbf..b73f6b5dc 100644 --- a/crates/openfang-api/static/index_body.html +++ b/crates/openfang-api/static/index_body.html @@ -2107,6 +2107,21 @@

+ +
+ +
+ + +
+
+