mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(channels): improve Microsoft Teams config compatibility
This commit is contained in:
@@ -2472,7 +2472,11 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
normalized.allowedUserIds = csvToStringArray(normalized.allowedUserIds)
|
||||
}
|
||||
|
||||
for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'rateLimitPerMinute', 'httpPort']) {
|
||||
for (const key of ['promptStarters', 'delegatedAuthScopes']) {
|
||||
if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key])
|
||||
}
|
||||
|
||||
for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'rateLimitPerMinute', 'httpPort', 'webhookPort', 'feedbackReflectionCooldownMs']) {
|
||||
if (!Object.hasOwn(normalized, key)) continue
|
||||
const value = String(normalized[key] || '').trim()
|
||||
if (!value) {
|
||||
@@ -2485,7 +2489,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl', 'allowBots', 'blockStreaming']) {
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl', 'allowBots', 'blockStreaming', 'useManagedIdentity', 'typingIndicator', 'welcomeCard', 'groupWelcomeCard', 'feedbackEnabled', 'feedbackReflection', 'delegatedAuthEnabled', 'ssoEnabled']) {
|
||||
if (Object.hasOwn(normalized, key)) {
|
||||
const value = String(normalized[key] || '').trim()
|
||||
if (!value) {
|
||||
@@ -2612,6 +2616,33 @@ function hasConfiguredMessagingValue(value) {
|
||||
return value !== undefined && value !== null
|
||||
}
|
||||
|
||||
function isEnabledFormFlag(value) {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'number') return value !== 0
|
||||
if (typeof value === 'string') {
|
||||
return ['true', '1', 'yes', 'on', 'enabled'].includes(value.trim().toLowerCase())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function msteamsCredentialMissingLabels(form = {}) {
|
||||
const missing = []
|
||||
if (!hasConfiguredMessagingValue(form?.appId)) missing.push('App ID')
|
||||
if (missing.length) return missing
|
||||
|
||||
if (hasConfiguredMessagingValue(form?.appPassword)) return []
|
||||
if (isEnabledFormFlag(form?.useManagedIdentity)) return []
|
||||
|
||||
const authType = String(form?.authType || '').trim().toLowerCase()
|
||||
const hasFederatedCredential = hasConfiguredMessagingValue(form?.certificatePath) || hasConfiguredMessagingValue(form?.certificateThumbprint)
|
||||
if (authType === 'federated' && hasFederatedCredential) return []
|
||||
|
||||
if (authType === 'federated') {
|
||||
return ['Certificate Path / Certificate Thumbprint / Managed Identity / App Password']
|
||||
}
|
||||
return ['App Password']
|
||||
}
|
||||
|
||||
function channelRootHasMessagingCredential(root) {
|
||||
if (!root || typeof root !== 'object' || Array.isArray(root)) return false
|
||||
return MESSAGING_CREDENTIAL_FIELDS.some(key => hasConfiguredMessagingValue(root[key]))
|
||||
@@ -2649,7 +2680,6 @@ const CHANNEL_DIAG_REQUIRED_FIELDS = {
|
||||
feishu: [['appId', 'App ID'], ['appSecret', 'App Secret']],
|
||||
dingtalk: [['clientId', 'Client ID'], ['clientSecret', 'Client Secret']],
|
||||
'dingtalk-connector': [['clientId', 'Client ID'], ['clientSecret', 'Client Secret']],
|
||||
msteams: [['appId', 'App ID'], ['appPassword', 'App Password']],
|
||||
mattermost: [['botToken', 'Bot Token'], ['baseUrl', 'Base URL']],
|
||||
'synology-chat': [['token', 'Token'], ['incomingUrl', 'Incoming URL']],
|
||||
signal: [['account', 'Signal 账号']],
|
||||
@@ -2668,11 +2698,15 @@ function requiredChannelCredentialFields(platform, form = {}) {
|
||||
if (form.accessToken) return [['accessToken', 'Access Token']]
|
||||
return [['homeserver', 'Homeserver'], ['userId', 'User ID'], ['password', 'Password']]
|
||||
}
|
||||
if (storageKey === 'msteams') {
|
||||
return msteamsCredentialMissingLabels(form).map(label => [label === 'App ID' ? 'appId' : '__msteamsAuth', label])
|
||||
}
|
||||
return CHANNEL_DIAG_REQUIRED_FIELDS[storageKey] || []
|
||||
}
|
||||
|
||||
function channelDiagnosisCredentialsReady(platform, form = {}) {
|
||||
if (platformStorageKey(platform) === 'zalouser') return true
|
||||
if (platformStorageKey(platform) === 'msteams') return msteamsCredentialMissingLabels(form).length === 0
|
||||
const requiredFields = requiredChannelCredentialFields(platform, form)
|
||||
if (requiredFields.length) {
|
||||
return requiredFields.every(([key]) => hasConfiguredMessagingValue(form?.[key]))
|
||||
@@ -2726,9 +2760,11 @@ export function buildOpenClawChannelDiagnosis({
|
||||
const requiredFields = requiredChannelCredentialFields(storageKey, form)
|
||||
const anyFields = channelAnyCredentialFields(storageKey)
|
||||
const anyGroups = channelAnyCredentialGroups(storageKey)
|
||||
const missing = requiredFields
|
||||
.filter(([key]) => !hasConfiguredMessagingValue(form?.[key]))
|
||||
.map(([, label]) => label)
|
||||
const missing = storageKey === 'msteams'
|
||||
? msteamsCredentialMissingLabels(form)
|
||||
: requiredFields
|
||||
.filter(([key]) => !hasConfiguredMessagingValue(form?.[key]))
|
||||
.map(([, label]) => label)
|
||||
const missingGroups = anyGroups
|
||||
.filter(group => !group.fields.some(([key]) => hasConfiguredMessagingValue(form?.[key])))
|
||||
.map(group => group.label)
|
||||
@@ -2907,11 +2943,45 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options =
|
||||
}
|
||||
|
||||
if (storageKey === 'msteams') {
|
||||
for (const key of ['appId', 'appPassword', 'tenantId', 'botEndpoint', 'webhookPath']) {
|
||||
for (const key of ['appId', 'appPassword', 'tenantId', 'authType', 'certificatePath', 'certificateThumbprint', 'managedIdentityClientId', 'botEndpoint', 'replyStyle', 'sharePointSiteId', 'responsePrefix', 'ssoConnectionName']) {
|
||||
putSecretAwareFormValue(form, saved, key)
|
||||
}
|
||||
putAccessPolicyFormValues(form, saved)
|
||||
putStringFormValue(form, saved?.webhook, 'path')
|
||||
if (form.path) {
|
||||
form.webhookPath = form.path
|
||||
delete form.path
|
||||
}
|
||||
if (typeof saved?.webhook?.port === 'number') form.webhookPort = String(saved.webhook.port)
|
||||
putAccessPolicyFormValues(form, saved, { mentionCompat: true })
|
||||
putCsvFormValue(form, saved, 'groupAllowFrom')
|
||||
putBoolFormValue(form, saved, 'requireMention')
|
||||
for (const key of ['useManagedIdentity', 'blockStreaming', 'typingIndicator', 'welcomeCard', 'groupWelcomeCard', 'feedbackEnabled', 'feedbackReflection']) {
|
||||
putBoolFormValue(form, saved, key)
|
||||
}
|
||||
for (const key of ['historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'mediaMaxMb', 'feedbackReflectionCooldownMs']) {
|
||||
if (typeof saved[key] === 'number') form[key] = String(saved[key])
|
||||
}
|
||||
putCsvFormValue(form, saved, 'promptStarters')
|
||||
putBoolFormValue(form, saved?.delegatedAuth, 'enabled')
|
||||
if (form.enabled) {
|
||||
form.delegatedAuthEnabled = form.enabled
|
||||
delete form.enabled
|
||||
}
|
||||
putCsvFormValue(form, saved?.delegatedAuth, 'scopes')
|
||||
if (form.scopes) {
|
||||
form.delegatedAuthScopes = form.scopes
|
||||
delete form.scopes
|
||||
}
|
||||
putBoolFormValue(form, saved?.sso, 'enabled')
|
||||
if (form.enabled) {
|
||||
form.ssoEnabled = form.enabled
|
||||
delete form.enabled
|
||||
}
|
||||
putStringFormValue(form, saved?.sso, 'connectionName')
|
||||
if (form.connectionName) {
|
||||
form.ssoConnectionName = form.connectionName
|
||||
delete form.connectionName
|
||||
}
|
||||
return form
|
||||
}
|
||||
|
||||
@@ -3512,6 +3582,33 @@ function buildOpenClawMessagingPlatformEntry(platform, form, currentSaved = {})
|
||||
for (const key of ['historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'mediaMaxMb']) {
|
||||
if (typeof form[key] === 'number') entry[key] = form[key]
|
||||
}
|
||||
} else if (storageKey === 'msteams') {
|
||||
for (const key of ['appId', 'appPassword', 'tenantId', 'authType', 'certificatePath', 'certificateThumbprint', 'managedIdentityClientId', 'replyStyle', 'sharePointSiteId', 'responsePrefix']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
}
|
||||
const webhook = { ...(currentSaved?.webhook && typeof currentSaved.webhook === 'object' ? currentSaved.webhook : {}) }
|
||||
if (typeof form.webhookPort === 'number') webhook.port = form.webhookPort
|
||||
if (form.webhookPath) webhook.path = form.webhookPath
|
||||
if (Object.keys(webhook).length) entry.webhook = webhook
|
||||
entry.dmPolicy = form.dmPolicy
|
||||
entry.groupPolicy = form.groupPolicy
|
||||
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
|
||||
if (Array.isArray(form.groupAllowFrom) && form.groupAllowFrom.length) entry.groupAllowFrom = form.groupAllowFrom
|
||||
for (const key of ['useManagedIdentity', 'requireMention', 'blockStreaming', 'typingIndicator', 'welcomeCard', 'groupWelcomeCard', 'feedbackEnabled', 'feedbackReflection']) {
|
||||
if (typeof form[key] === 'boolean') entry[key] = form[key]
|
||||
}
|
||||
for (const key of ['historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'mediaMaxMb', 'feedbackReflectionCooldownMs']) {
|
||||
if (typeof form[key] === 'number') entry[key] = form[key]
|
||||
}
|
||||
if (Array.isArray(form.promptStarters) && form.promptStarters.length) entry.promptStarters = form.promptStarters
|
||||
const delegatedAuth = { ...(currentSaved?.delegatedAuth && typeof currentSaved.delegatedAuth === 'object' ? currentSaved.delegatedAuth : {}) }
|
||||
if (typeof form.delegatedAuthEnabled === 'boolean') delegatedAuth.enabled = form.delegatedAuthEnabled
|
||||
if (Array.isArray(form.delegatedAuthScopes) && form.delegatedAuthScopes.length) delegatedAuth.scopes = form.delegatedAuthScopes
|
||||
if (Object.keys(delegatedAuth).length) entry.delegatedAuth = delegatedAuth
|
||||
const sso = { ...(currentSaved?.sso && typeof currentSaved.sso === 'object' ? currentSaved.sso : {}) }
|
||||
if (typeof form.ssoEnabled === 'boolean') sso.enabled = form.ssoEnabled
|
||||
if (form.ssoConnectionName) sso.connectionName = form.ssoConnectionName
|
||||
if (Object.keys(sso).length) entry.sso = sso
|
||||
} else if (storageKey === 'zalouser') {
|
||||
for (const key of ['profile', 'messagePrefix', 'responsePrefix']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
@@ -3589,7 +3686,7 @@ export function mergeOpenClawMessagingPlatformConfig(cfg, { platform, form, acco
|
||||
const currentSaved = resolvePlatformConfigEntry(cfg.channels?.[storageKey], platform, normalizedAccountId) || {}
|
||||
const entry = buildOpenClawMessagingPlatformEntry(platform, normalizedForm, currentSaved)
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
if (['zalo', 'zalouser', 'line', 'mattermost', 'synology-chat', 'googlechat'].includes(storageKey)) {
|
||||
if (['zalo', 'zalouser', 'line', 'mattermost', 'synology-chat', 'googlechat', 'msteams'].includes(storageKey)) {
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
}
|
||||
return { entry, accountId: normalizedAccountId, storageKey }
|
||||
@@ -5059,7 +5156,7 @@ const handlers = {
|
||||
} else {
|
||||
setRootChannelEntry(entry)
|
||||
}
|
||||
} else if (['line', 'mattermost', 'synology-chat', 'googlechat'].includes(storageKey)) {
|
||||
} else if (['line', 'mattermost', 'synology-chat', 'googlechat', 'msteams'].includes(storageKey)) {
|
||||
const built = buildOpenClawMessagingPlatformEntry(platform, form, currentSaved)
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, built)
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
@@ -5068,7 +5165,7 @@ const handlers = {
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
}
|
||||
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && !['line', 'mattermost', 'synology-chat', 'googlechat'].includes(storageKey)) {
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && !['line', 'mattermost', 'synology-chat', 'googlechat', 'msteams'].includes(storageKey)) {
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
// 合并模式:保留用户通过 CLI 或手动编辑的自定义字段
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
@@ -5204,6 +5301,39 @@ const handlers = {
|
||||
return { valid: false, errors: [`Discord API 连接失败: ${e.message}`] }
|
||||
}
|
||||
}
|
||||
if (platform === 'msteams') {
|
||||
const missing = msteamsCredentialMissingLabels(form)
|
||||
if (missing.length) return { valid: false, errors: [`缺少 ${missing.join(' / ')}`] }
|
||||
if (!hasConfiguredMessagingValue(form.appPassword)) {
|
||||
return {
|
||||
valid: true,
|
||||
warnings: ['当前 Teams 认证模式不使用 Client Secret;面板已完成结构校验,实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证。'],
|
||||
details: [`App ID: ${String(form.appId || '').trim()}`],
|
||||
}
|
||||
}
|
||||
const tenantId = String(form.tenantId || 'botframework.com').trim() || 'botframework.com'
|
||||
try {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: String(form.appId || '').trim(),
|
||||
client_secret: String(form.appPassword || '').trim(),
|
||||
scope: 'https://api.botframework.com/.default',
|
||||
})
|
||||
const resp = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
})
|
||||
const result = await resp.json()
|
||||
if (result.access_token) {
|
||||
return { valid: true, errors: [], details: [`App ID: ${form.appId}`, `Tenant: ${tenantId}`, `Token 有效期: ${result.expires_in || 0}s`] }
|
||||
}
|
||||
return { valid: false, errors: [result.error_description || result.error || '凭证无效,请检查 App ID 和 App Password'] }
|
||||
} catch (e) {
|
||||
return { valid: false, errors: [`Azure AD 连接失败: ${e.message}`] }
|
||||
}
|
||||
}
|
||||
return { valid: true, warnings: ['该平台暂不支持在线校验'] }
|
||||
},
|
||||
|
||||
|
||||
@@ -179,6 +179,41 @@ fn has_configured_messaging_value(value: Option<&Value>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_enabled_form_flag(value: Option<&Value>) -> bool {
|
||||
match value {
|
||||
Some(Value::Bool(v)) => *v,
|
||||
Some(Value::Number(v)) => v.as_i64().map(|n| n != 0).unwrap_or(false),
|
||||
Some(Value::String(raw)) => matches!(
|
||||
raw.trim().to_ascii_lowercase().as_str(),
|
||||
"true" | "1" | "yes" | "on" | "enabled"
|
||||
),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn msteams_credential_missing_labels(form: &Map<String, Value>) -> Vec<&'static str> {
|
||||
if !has_configured_messaging_value(form.get("appId")) {
|
||||
return vec!["App ID"];
|
||||
}
|
||||
if has_configured_messaging_value(form.get("appPassword")) {
|
||||
return vec![];
|
||||
}
|
||||
if is_enabled_form_flag(form.get("useManagedIdentity")) {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let auth_type = form_string(form, "authType").to_ascii_lowercase();
|
||||
let has_federated_credential = has_configured_messaging_value(form.get("certificatePath"))
|
||||
|| has_configured_messaging_value(form.get("certificateThumbprint"));
|
||||
if auth_type == "federated" && has_federated_credential {
|
||||
return vec![];
|
||||
}
|
||||
if auth_type == "federated" {
|
||||
return vec!["Certificate Path / Certificate Thumbprint / Managed Identity / App Password"];
|
||||
}
|
||||
vec!["App Password"]
|
||||
}
|
||||
|
||||
fn channel_root_has_messaging_credential(root: &Map<String, Value>) -> bool {
|
||||
[
|
||||
"accessToken",
|
||||
@@ -223,7 +258,6 @@ fn required_channel_credential_fields(
|
||||
"discord" => vec![("token", "Bot Token")],
|
||||
"feishu" => vec![("appId", "App ID"), ("appSecret", "App Secret")],
|
||||
"dingtalk-connector" => vec![("clientId", "Client ID"), ("clientSecret", "Client Secret")],
|
||||
"msteams" => vec![("appId", "App ID"), ("appPassword", "App Password")],
|
||||
"mattermost" => vec![("botToken", "Bot Token"), ("baseUrl", "Base URL")],
|
||||
"synology-chat" => vec![("token", "Token"), ("incomingUrl", "Incoming URL")],
|
||||
"signal" => vec![("account", "Signal 账号")],
|
||||
@@ -249,6 +283,16 @@ fn required_channel_credential_fields(
|
||||
]
|
||||
}
|
||||
}
|
||||
"msteams" => msteams_credential_missing_labels(form)
|
||||
.into_iter()
|
||||
.map(|label| {
|
||||
if label == "App ID" {
|
||||
("appId", "App ID")
|
||||
} else {
|
||||
("__msteamsAuth", label)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
@@ -293,6 +337,9 @@ fn channel_diagnosis_credentials_ready(platform: &str, form: &Map<String, Value>
|
||||
if platform_storage_key(platform) == "zalouser" {
|
||||
return true;
|
||||
}
|
||||
if platform_storage_key(platform) == "msteams" {
|
||||
return msteams_credential_missing_labels(form).is_empty();
|
||||
}
|
||||
let required_fields = required_channel_credential_fields(platform, form);
|
||||
if !required_fields.is_empty() {
|
||||
return required_fields
|
||||
@@ -393,11 +440,15 @@ fn build_openclaw_channel_diagnosis(
|
||||
let required_fields = required_channel_credential_fields(storage_key, form);
|
||||
let any_fields = channel_any_credential_fields(storage_key);
|
||||
let any_groups = channel_any_credential_groups(storage_key);
|
||||
let missing: Vec<&str> = required_fields
|
||||
.iter()
|
||||
.filter(|(key, _)| !has_configured_messaging_value(form.get(*key)))
|
||||
.map(|(_, label)| *label)
|
||||
.collect();
|
||||
let missing: Vec<&str> = if storage_key == "msteams" {
|
||||
msteams_credential_missing_labels(form)
|
||||
} else {
|
||||
required_fields
|
||||
.iter()
|
||||
.filter(|(key, _)| !has_configured_messaging_value(form.get(*key)))
|
||||
.map(|(_, label)| *label)
|
||||
.collect()
|
||||
};
|
||||
let missing_groups: Vec<&str> = any_groups
|
||||
.iter()
|
||||
.filter(|(_, fields)| {
|
||||
@@ -816,6 +867,15 @@ fn normalize_messaging_platform_form(
|
||||
normalize_numeric_form_value(&mut normalized, "textChunkLimit");
|
||||
normalize_numeric_form_value(&mut normalized, "rateLimitPerMinute");
|
||||
normalize_numeric_form_value(&mut normalized, "httpPort");
|
||||
normalize_numeric_form_value(&mut normalized, "webhookPort");
|
||||
normalize_numeric_form_value(&mut normalized, "feedbackReflectionCooldownMs");
|
||||
|
||||
for key in ["promptStarters", "delegatedAuthScopes"] {
|
||||
if normalized.contains_key(key) {
|
||||
let items = json_array_from_csv_value(normalized.get(key));
|
||||
normalized.insert(key.into(), Value::Array(items));
|
||||
}
|
||||
}
|
||||
|
||||
for key in [
|
||||
"dangerouslyAllowNameMatching",
|
||||
@@ -824,6 +884,14 @@ fn normalize_messaging_platform_form(
|
||||
"allowInsecureSsl",
|
||||
"allowBots",
|
||||
"blockStreaming",
|
||||
"useManagedIdentity",
|
||||
"typingIndicator",
|
||||
"welcomeCard",
|
||||
"groupWelcomeCard",
|
||||
"feedbackEnabled",
|
||||
"feedbackReflection",
|
||||
"delegatedAuthEnabled",
|
||||
"ssoEnabled",
|
||||
] {
|
||||
if normalized.contains_key(key) {
|
||||
let value = match normalized.get(key) {
|
||||
@@ -1451,11 +1519,75 @@ pub async fn read_platform_config(
|
||||
"msteams" => {
|
||||
insert_secret_aware_form_value(&mut form, &saved, "appId");
|
||||
insert_secret_aware_form_value(&mut form, &saved, "appPassword");
|
||||
insert_string_if_present(&mut form, &saved, "tenantId");
|
||||
insert_string_if_present(&mut form, &saved, "botEndpoint");
|
||||
insert_string_if_present(&mut form, &saved, "webhookPath");
|
||||
for key in [
|
||||
"tenantId",
|
||||
"authType",
|
||||
"certificatePath",
|
||||
"certificateThumbprint",
|
||||
"managedIdentityClientId",
|
||||
"botEndpoint",
|
||||
"replyStyle",
|
||||
"sharePointSiteId",
|
||||
"responsePrefix",
|
||||
] {
|
||||
insert_string_if_present(&mut form, &saved, key);
|
||||
}
|
||||
if let Some(webhook) = saved.get("webhook") {
|
||||
insert_string_if_present(&mut form, webhook, "path");
|
||||
if let Some(v) = form.remove("path") {
|
||||
form.insert("webhookPath".into(), v);
|
||||
}
|
||||
insert_number_as_string(&mut form, webhook, "port");
|
||||
if let Some(v) = form.remove("port") {
|
||||
form.insert("webhookPort".into(), v);
|
||||
}
|
||||
} else {
|
||||
insert_string_if_present(&mut form, &saved, "webhookPath");
|
||||
}
|
||||
insert_access_policy_form_values(&mut form, &saved, false, true);
|
||||
insert_array_as_csv(&mut form, &saved, "groupAllowFrom");
|
||||
insert_bool_as_string(&mut form, &saved, "requireMention");
|
||||
for key in [
|
||||
"useManagedIdentity",
|
||||
"blockStreaming",
|
||||
"typingIndicator",
|
||||
"welcomeCard",
|
||||
"groupWelcomeCard",
|
||||
"feedbackEnabled",
|
||||
"feedbackReflection",
|
||||
] {
|
||||
insert_bool_as_string(&mut form, &saved, key);
|
||||
}
|
||||
for key in [
|
||||
"historyLimit",
|
||||
"dmHistoryLimit",
|
||||
"textChunkLimit",
|
||||
"mediaMaxMb",
|
||||
"feedbackReflectionCooldownMs",
|
||||
] {
|
||||
insert_number_as_string(&mut form, &saved, key);
|
||||
}
|
||||
insert_array_as_csv(&mut form, &saved, "promptStarters");
|
||||
if let Some(delegated_auth) = saved.get("delegatedAuth") {
|
||||
insert_bool_as_string(&mut form, delegated_auth, "enabled");
|
||||
if let Some(v) = form.remove("enabled") {
|
||||
form.insert("delegatedAuthEnabled".into(), v);
|
||||
}
|
||||
insert_array_as_csv(&mut form, delegated_auth, "scopes");
|
||||
if let Some(v) = form.remove("scopes") {
|
||||
form.insert("delegatedAuthScopes".into(), v);
|
||||
}
|
||||
}
|
||||
if let Some(sso) = saved.get("sso") {
|
||||
insert_bool_as_string(&mut form, sso, "enabled");
|
||||
if let Some(v) = form.remove("enabled") {
|
||||
form.insert("ssoEnabled".into(), v);
|
||||
}
|
||||
insert_string_if_present(&mut form, sso, "connectionName");
|
||||
if let Some(v) = form.remove("connectionName") {
|
||||
form.insert("ssoConnectionName".into(), v);
|
||||
}
|
||||
}
|
||||
}
|
||||
"line" => {
|
||||
for key in [
|
||||
@@ -2156,33 +2288,99 @@ pub async fn save_messaging_platform(
|
||||
"msteams" => {
|
||||
let app_id = form_string(form_obj, "appId");
|
||||
let app_password = form_string(form_obj, "appPassword");
|
||||
if app_id.is_empty() || app_password.is_empty() {
|
||||
return Err("App ID 和 App Password 不能为空".into());
|
||||
let missing_credentials = msteams_credential_missing_labels(form_obj);
|
||||
if !missing_credentials.is_empty() {
|
||||
return Err(format!("缺少 {}", missing_credentials.join(" / ")));
|
||||
}
|
||||
|
||||
let mut entry = Map::new();
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
put_string(&mut entry, "appId", app_id);
|
||||
put_string(&mut entry, "appPassword", app_password);
|
||||
put_string(&mut entry, "tenantId", form_string(form_obj, "tenantId"));
|
||||
put_string(
|
||||
&mut entry,
|
||||
"botEndpoint",
|
||||
form_string(form_obj, "botEndpoint"),
|
||||
);
|
||||
put_string(
|
||||
&mut entry,
|
||||
"webhookPath",
|
||||
form_string(form_obj, "webhookPath"),
|
||||
);
|
||||
for key in [
|
||||
"tenantId",
|
||||
"authType",
|
||||
"certificatePath",
|
||||
"certificateThumbprint",
|
||||
"managedIdentityClientId",
|
||||
"replyStyle",
|
||||
"sharePointSiteId",
|
||||
"responsePrefix",
|
||||
] {
|
||||
put_string(&mut entry, key, form_string(form_obj, key));
|
||||
}
|
||||
let mut webhook = current_saved
|
||||
.get("webhook")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
put_number_from_form(&mut webhook, "port", &form_string(form_obj, "webhookPort"));
|
||||
put_string(&mut webhook, "path", form_string(form_obj, "webhookPath"));
|
||||
if !webhook.is_empty() {
|
||||
entry.insert("webhook".into(), Value::Object(webhook));
|
||||
}
|
||||
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
|
||||
put_string(
|
||||
&mut entry,
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
put_array_from_form_value(&mut entry, "groupAllowFrom", form_obj.get("groupAllowFrom"));
|
||||
for key in [
|
||||
"useManagedIdentity",
|
||||
"requireMention",
|
||||
"blockStreaming",
|
||||
"typingIndicator",
|
||||
"welcomeCard",
|
||||
"groupWelcomeCard",
|
||||
"feedbackEnabled",
|
||||
"feedbackReflection",
|
||||
] {
|
||||
put_bool_value_if_present(&mut entry, key, form_obj.get(key));
|
||||
}
|
||||
for key in [
|
||||
"historyLimit",
|
||||
"dmHistoryLimit",
|
||||
"textChunkLimit",
|
||||
"mediaMaxMb",
|
||||
"feedbackReflectionCooldownMs",
|
||||
] {
|
||||
put_number_from_form(&mut entry, key, &form_string(form_obj, key));
|
||||
}
|
||||
put_array_from_form_value(&mut entry, "promptStarters", form_obj.get("promptStarters"));
|
||||
let mut delegated_auth = current_saved
|
||||
.get("delegatedAuth")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
put_bool_value_if_present(
|
||||
&mut delegated_auth,
|
||||
"enabled",
|
||||
form_obj.get("delegatedAuthEnabled"),
|
||||
);
|
||||
put_array_from_form_value(
|
||||
&mut delegated_auth,
|
||||
"scopes",
|
||||
form_obj.get("delegatedAuthScopes"),
|
||||
);
|
||||
if !delegated_auth.is_empty() {
|
||||
entry.insert("delegatedAuth".into(), Value::Object(delegated_auth));
|
||||
}
|
||||
let mut sso = current_saved
|
||||
.get("sso")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
put_bool_value_if_present(&mut sso, "enabled", form_obj.get("ssoEnabled"));
|
||||
put_string(
|
||||
&mut sso,
|
||||
"connectionName",
|
||||
form_string(form_obj, "ssoConnectionName"),
|
||||
);
|
||||
if !sso.is_empty() {
|
||||
entry.insert("sso".into(), Value::Object(sso));
|
||||
}
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
@@ -4000,8 +4198,18 @@ async fn verify_msteams(
|
||||
if app_id.is_empty() {
|
||||
return Ok(json!({ "valid": false, "errors": ["App ID 不能为空"] }));
|
||||
}
|
||||
let missing_credentials = msteams_credential_missing_labels(form);
|
||||
if !missing_credentials.is_empty() {
|
||||
return Ok(
|
||||
json!({ "valid": false, "errors": [format!("缺少 {}", missing_credentials.join(" / "))] }),
|
||||
);
|
||||
}
|
||||
if app_password.is_empty() {
|
||||
return Ok(json!({ "valid": false, "errors": ["App Password 不能为空"] }));
|
||||
return Ok(json!({
|
||||
"valid": true,
|
||||
"warnings": ["当前 Teams 认证模式不使用 Client Secret;面板已完成结构校验,实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证。"],
|
||||
"details": [format!("App ID: {}", app_id)]
|
||||
}));
|
||||
}
|
||||
|
||||
let token_url = format!(
|
||||
|
||||
@@ -216,8 +216,12 @@ export default {
|
||||
msteamsGuide3: _('在 Teams 中安装自定义 App', 'Install the custom App in Teams', '在 Teams 中安裝自定義 App'),
|
||||
msteamsGuide4: _('配置消息端点 URL', 'Configure the messaging endpoint URL', '設定訊息端点 URL'),
|
||||
msteamsGuideFooter: _('<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">需要 Azure AD 应用注册和 Teams 管理员权限</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">需要 Azure AD 应用注册和 Teams 管理员权限</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">需要 Azure AD 應用註冊和 Teams 管理員權限</div>'),
|
||||
msteamsAppPasswordHint: _('Client Secret 模式必填;Federated / Managed Identity 可留空', 'Required for Client Secret mode; leave empty for Federated / Managed Identity', 'Client Secret 模式必填;Federated / Managed Identity 可留空'),
|
||||
msteamsTenantIdPh: _('可选,单租户时填写', 'Optional, fill for single-tenant', '可選,單租戶時填写'),
|
||||
msteamsAllowFromPh: _('可选,逗号分隔', 'Optional, comma-separated', '可選,逗號分隔'),
|
||||
msteamsGroupAllowFromPh: _('可选,逗号分隔 Team 或 Channel ID', 'Optional, comma-separated Team or Channel IDs', '可選,逗號分隔 Team 或 Channel ID'),
|
||||
msteamsPromptStartersPh: _('可选,逗号分隔,例如 help, status', 'Optional, comma-separated, e.g. help, status', '可選,逗號分隔,例如 help, status'),
|
||||
msteamsDelegatedScopesPh: _('可选,逗号分隔 Microsoft Graph scopes', 'Optional, comma-separated Microsoft Graph scopes', '可選,逗號分隔 Microsoft Graph scopes'),
|
||||
signalDesc: _('接入 Signal Messenger', 'Connect to Signal Messenger', '', 'Signal に接続'),
|
||||
signalGuide1: _('安装 <a href="https://github.com/AsamK/signal-cli" target="_blank" rel="noopener">signal-cli</a> 并注册/链接账号', '安装 <a href="https://github.com/AsamK/signal-cli" target="_blank" rel="noopener">signal-cli</a> 并注册/链接账号', '安裝 <a href="https://github.com/AsamK/signal-cli" target="_blank" rel="noopener">signal-cli</a> 並註冊/連結账號'),
|
||||
signalGuide2: _('确保 signal-cli 可正常收发消息', 'Ensure signal-cli can send and receive messages', '確保 signal-cli 可正常收發訊息'),
|
||||
|
||||
@@ -430,13 +430,34 @@ const PLATFORM_REGISTRY = {
|
||||
guideFooter: t('channels.msteamsGuideFooter'),
|
||||
fields: [
|
||||
{ key: 'appId', label: 'App ID', placeholder: 'Azure AD Application ID', required: true },
|
||||
{ key: 'appPassword', label: 'App Password', placeholder: 'Azure AD Client Secret', secret: true, required: true },
|
||||
{ key: 'appPassword', label: 'App Password', placeholder: 'Azure AD Client Secret', secret: true, required: (form) => form.authType !== 'federated' && form.useManagedIdentity !== 'true', hint: t('channels.msteamsAppPasswordHint') },
|
||||
{ key: 'tenantId', label: 'Tenant ID', placeholder: t('channels.msteamsTenantIdPh'), required: false },
|
||||
{ key: 'botEndpoint', label: 'Bot Endpoint', placeholder: 'https://example.com/api/teams/messages', required: false },
|
||||
{ key: 'webhookPath', label: 'Webhook Path', placeholder: '/msteams/messages', required: false },
|
||||
{ key: 'authType', label: 'Auth Type', type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'secret', label: 'Client Secret' }, { value: 'federated', label: 'Federated / Certificate' }], required: false },
|
||||
{ key: 'certificatePath', label: 'Certificate Path', placeholder: t('channels.optionalEg', { example: '/run/secrets/teams.pem' }), required: false },
|
||||
{ key: 'certificateThumbprint', label: 'Certificate Thumbprint', placeholder: t('channels.optionalEg', { example: 'ABCD1234' }), required: false },
|
||||
{ key: 'useManagedIdentity', label: 'Managed Identity', type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'managedIdentityClientId', label: 'Managed Identity Client ID', placeholder: t('channels.optionalEg', { example: '00000000-0000-0000-0000-000000000000' }), required: false },
|
||||
{ key: 'webhookPort', label: 'Webhook Port', placeholder: '3978', required: false },
|
||||
{ key: 'webhookPath', label: 'Webhook Path', placeholder: '/api/teams/messages', required: false },
|
||||
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: DM_POLICY_OPTIONS, required: false },
|
||||
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: GROUP_POLICY_OPTIONS(t('channels.groupAllTeams'), { mention: true }), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.msteamsAllowFromPh'), required: false },
|
||||
{ key: 'groupAllowFrom', label: 'Group Allow From', placeholder: t('channels.msteamsGroupAllowFromPh'), required: false, hint: t('channels.groupAllowFromHint') },
|
||||
{ key: 'historyLimit', label: 'History Limit', placeholder: '80', required: false },
|
||||
{ key: 'dmHistoryLimit', label: 'DM History Limit', placeholder: '20', required: false },
|
||||
{ key: 'textChunkLimit', label: 'Text Chunk Limit', placeholder: '1800', required: false },
|
||||
{ key: 'mediaMaxMb', label: 'Media Max MB', placeholder: '100', required: false },
|
||||
{ key: 'blockStreaming', label: t('channels.signalBlockStreaming'), type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'typingIndicator', label: 'Typing Indicator', type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'replyStyle', label: 'Reply Style', type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'thread', label: 'Thread' }, { value: 'top-level', label: 'Top-level' }], required: false },
|
||||
{ key: 'sharePointSiteId', label: 'SharePoint Site ID', placeholder: t('channels.optionalEg', { example: 'contoso.sharepoint.com,guid1,guid2' }), required: false },
|
||||
{ key: 'responsePrefix', label: 'Response Prefix', placeholder: t('channels.optionalEg', { example: '[Teams]' }), required: false },
|
||||
{ key: 'welcomeCard', label: 'Welcome Card', type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'promptStarters', label: 'Prompt Starters', placeholder: t('channels.msteamsPromptStartersPh'), required: false },
|
||||
{ key: 'delegatedAuthEnabled', label: 'Delegated Auth', type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'delegatedAuthScopes', label: 'Delegated Auth Scopes', placeholder: t('channels.msteamsDelegatedScopesPh'), required: false },
|
||||
{ key: 'ssoEnabled', label: 'SSO', type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'ssoConnectionName', label: 'SSO Connection Name', placeholder: t('channels.optionalEg', { example: 'teams-oauth' }), required: false },
|
||||
],
|
||||
configKey: 'msteams',
|
||||
pluginRequired: '@openclaw/msteams@latest',
|
||||
@@ -2204,6 +2225,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
`
|
||||
|
||||
const isFieldRequired = (field, form) => {
|
||||
if (typeof field.required === 'function') return field.required(form || {})
|
||||
if (field.required) return true
|
||||
if (!field.requiredWhen) return false
|
||||
return Object.entries(field.requiredWhen).every(([k, expected]) => (form[k] || '') === expected)
|
||||
@@ -2221,6 +2243,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
const fieldsHtml = reg.fields.map((f, i) => {
|
||||
const val = existing[f.key] || ''
|
||||
const secretRefLocked = existing.__secretRefs?.[f.key]
|
||||
const fieldRequired = isFieldRequired(f, existing)
|
||||
const fieldHint = [
|
||||
f.hint,
|
||||
secretRefLocked ? t('channels.secretRefPreserveHint') : '',
|
||||
@@ -2228,7 +2251,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
if (f.type === 'select' && f.options) {
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label class="form-label">${labelWithHelp(f.label)}${f.required ? ' *' : ''}</label>
|
||||
<label class="form-label">${labelWithHelp(f.label)}<span class="required-marker" data-required-for="${escapeAttr(f.key)}">${fieldRequired ? ' *' : ''}</span></label>
|
||||
<select class="form-input" name="${f.key}" data-name="${f.key}">
|
||||
${f.options.map(o => `<option value="${o.value}" ${val === o.value ? 'selected' : ''}>${o.label}</option>`).join('')}
|
||||
</select>
|
||||
@@ -2239,7 +2262,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
if (f.multiline) {
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label class="form-label">${labelWithHelp(f.label)}${f.required ? ' *' : ''}</label>
|
||||
<label class="form-label">${labelWithHelp(f.label)}<span class="required-marker" data-required-for="${escapeAttr(f.key)}">${fieldRequired ? ' *' : ''}</span></label>
|
||||
<textarea class="form-input" name="${f.key}" rows="5" placeholder="${escapeAttr(f.placeholder || '')}" ${i === 0 ? 'autofocus' : ''} style="width:100%;min-height:112px;resize:vertical;font-family:var(--font-mono);line-height:1.5">${escapeAttr(val)}</textarea>
|
||||
${fieldHint ? `<div class="form-hint">${fieldHint}</div>` : ''}
|
||||
</div>
|
||||
@@ -2247,7 +2270,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
}
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label class="form-label">${labelWithHelp(f.label)}${f.required ? ' *' : ''}</label>
|
||||
<label class="form-label">${labelWithHelp(f.label)}<span class="required-marker" data-required-for="${escapeAttr(f.key)}">${fieldRequired ? ' *' : ''}</span></label>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input class="form-input" name="${f.key}" type="${f.secret ? 'password' : 'text'}"
|
||||
value="${escapeAttr(val)}" placeholder="${escapeAttr(f.placeholder || '')}"
|
||||
@@ -2379,6 +2402,19 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
return obj
|
||||
}
|
||||
|
||||
const updateRequiredMarkers = () => {
|
||||
const form = collectForm()
|
||||
reg.fields.forEach(f => {
|
||||
const marker = modal.querySelector(`.required-marker[data-required-for="${f.key}"]`)
|
||||
if (marker) marker.textContent = isFieldRequired(f, form) ? ' *' : ''
|
||||
})
|
||||
}
|
||||
modal.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
|
||||
el.addEventListener('input', updateRequiredMarkers)
|
||||
el.addEventListener('change', updateRequiredMarkers)
|
||||
})
|
||||
updateRequiredMarkers()
|
||||
|
||||
// 校验按钮
|
||||
const btnVerify = modal.querySelector('#btn-verify')
|
||||
const btnSave = modal.querySelector('#btn-save')
|
||||
|
||||
@@ -775,6 +775,151 @@ test('Google Chat 诊断要求 service account 文件或内联 JSON 其中一项
|
||||
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('Microsoft Teams 渠道保存会写入新版认证和运行字段', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'msteams',
|
||||
form: {
|
||||
appId: 'teams-app-id',
|
||||
appPassword: 'teams-secret',
|
||||
tenantId: 'tenant-1',
|
||||
authType: 'federated',
|
||||
certificatePath: '/run/secrets/teams.pem',
|
||||
certificateThumbprint: 'thumbprint-1',
|
||||
useManagedIdentity: 'true',
|
||||
managedIdentityClientId: 'identity-client-id',
|
||||
webhookPort: '3978',
|
||||
webhookPath: '/api/teams/messages',
|
||||
dmPolicy: 'allowlist',
|
||||
allowFrom: 'user-a, user-b',
|
||||
groupPolicy: 'mentioned',
|
||||
groupAllowFrom: 'team-1, team-2',
|
||||
textChunkLimit: '1800',
|
||||
historyLimit: '80',
|
||||
dmHistoryLimit: '20',
|
||||
mediaMaxMb: '100',
|
||||
blockStreaming: 'true',
|
||||
typingIndicator: 'true',
|
||||
replyStyle: 'thread',
|
||||
sharePointSiteId: 'contoso.sharepoint.com,guid1,guid2',
|
||||
responsePrefix: '[Teams]',
|
||||
welcomeCard: 'true',
|
||||
promptStarters: 'help, status',
|
||||
delegatedAuthEnabled: 'true',
|
||||
delegatedAuthScopes: 'User.Read, offline_access',
|
||||
ssoEnabled: 'true',
|
||||
ssoConnectionName: 'teams-oauth',
|
||||
},
|
||||
})
|
||||
|
||||
const entry = cfg.channels.msteams
|
||||
assert.equal(entry.appId, 'teams-app-id')
|
||||
assert.equal(entry.appPassword, 'teams-secret')
|
||||
assert.equal(entry.tenantId, 'tenant-1')
|
||||
assert.equal(entry.authType, 'federated')
|
||||
assert.equal(entry.certificatePath, '/run/secrets/teams.pem')
|
||||
assert.equal(entry.certificateThumbprint, 'thumbprint-1')
|
||||
assert.equal(entry.useManagedIdentity, true)
|
||||
assert.equal(entry.managedIdentityClientId, 'identity-client-id')
|
||||
assert.deepEqual(entry.webhook, { port: 3978, path: '/api/teams/messages' })
|
||||
assert.equal(entry.dmPolicy, 'allowlist')
|
||||
assert.deepEqual(entry.allowFrom, ['user-a', 'user-b'])
|
||||
assert.equal(entry.groupPolicy, 'open')
|
||||
assert.equal(entry.requireMention, true)
|
||||
assert.deepEqual(entry.groupAllowFrom, ['team-1', 'team-2'])
|
||||
assert.equal(entry.textChunkLimit, 1800)
|
||||
assert.equal(entry.historyLimit, 80)
|
||||
assert.equal(entry.dmHistoryLimit, 20)
|
||||
assert.equal(entry.mediaMaxMb, 100)
|
||||
assert.equal(entry.blockStreaming, true)
|
||||
assert.equal(entry.typingIndicator, true)
|
||||
assert.equal(entry.replyStyle, 'thread')
|
||||
assert.equal(entry.sharePointSiteId, 'contoso.sharepoint.com,guid1,guid2')
|
||||
assert.equal(entry.responsePrefix, '[Teams]')
|
||||
assert.equal(entry.welcomeCard, true)
|
||||
assert.deepEqual(entry.promptStarters, ['help', 'status'])
|
||||
assert.deepEqual(entry.delegatedAuth, { enabled: true, scopes: ['User.Read', 'offline_access'] })
|
||||
assert.deepEqual(entry.sso, { enabled: true, connectionName: 'teams-oauth' })
|
||||
assert.equal(cfg.plugins.entries.msteams.enabled, true)
|
||||
})
|
||||
|
||||
test('Microsoft Teams 渠道读取会回显嵌套 webhook 和运行字段', () => {
|
||||
const values = buildMessagingPlatformFormValues('msteams', {
|
||||
appId: 'teams-app-id',
|
||||
appPassword: 'teams-secret',
|
||||
authType: 'federated',
|
||||
webhook: { port: 3978, path: '/api/teams/messages' },
|
||||
dmPolicy: 'allowlist',
|
||||
allowFrom: ['user-a', 'user-b'],
|
||||
groupPolicy: 'open',
|
||||
requireMention: true,
|
||||
groupAllowFrom: ['team-1', 'team-2'],
|
||||
textChunkLimit: 1800,
|
||||
mediaMaxMb: 100,
|
||||
blockStreaming: true,
|
||||
typingIndicator: true,
|
||||
welcomeCard: true,
|
||||
promptStarters: ['help', 'status'],
|
||||
delegatedAuth: { enabled: true, scopes: ['User.Read', 'offline_access'] },
|
||||
sso: { enabled: true, connectionName: 'teams-oauth' },
|
||||
})
|
||||
|
||||
assert.equal(values.appId, 'teams-app-id')
|
||||
assert.equal(values.appPassword, 'teams-secret')
|
||||
assert.equal(values.authType, 'federated')
|
||||
assert.equal(values.webhookPort, '3978')
|
||||
assert.equal(values.webhookPath, '/api/teams/messages')
|
||||
assert.equal(values.groupPolicy, 'mentioned')
|
||||
assert.equal(values.groupAllowFrom, 'team-1, team-2')
|
||||
assert.equal(values.textChunkLimit, '1800')
|
||||
assert.equal(values.mediaMaxMb, '100')
|
||||
assert.equal(values.blockStreaming, 'true')
|
||||
assert.equal(values.typingIndicator, 'true')
|
||||
assert.equal(values.welcomeCard, 'true')
|
||||
assert.equal(values.promptStarters, 'help, status')
|
||||
assert.equal(values.delegatedAuthEnabled, 'true')
|
||||
assert.equal(values.delegatedAuthScopes, 'User.Read, offline_access')
|
||||
assert.equal(values.ssoEnabled, 'true')
|
||||
assert.equal(values.ssoConnectionName, 'teams-oauth')
|
||||
})
|
||||
|
||||
test('Microsoft Teams 诊断会按认证模式检查动态必填凭证', () => {
|
||||
const missingSecret = buildOpenClawChannelDiagnosis({
|
||||
platform: 'msteams',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
appId: 'teams-app-id',
|
||||
authType: 'secret',
|
||||
},
|
||||
})
|
||||
const federatedCert = buildOpenClawChannelDiagnosis({
|
||||
platform: 'msteams',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
appId: 'teams-app-id',
|
||||
authType: 'federated',
|
||||
certificatePath: '/run/secrets/teams.pem',
|
||||
},
|
||||
})
|
||||
const managedIdentity = buildOpenClawChannelDiagnosis({
|
||||
platform: 'msteams',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
appId: 'teams-app-id',
|
||||
useManagedIdentity: 'true',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(missingSecret.checks.find(item => item.id === 'credentials')?.ok, false)
|
||||
assert.match(missingSecret.checks.find(item => item.id === 'credentials')?.detail || '', /App Password/)
|
||||
assert.equal(federatedCert.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
assert.equal(managedIdentity.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('Discord 渠道保存会保留运行时需要的 applicationId', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user